diff options
Diffstat (limited to 'src/VBox/Devices/Audio')
49 files changed, 59842 insertions, 0 deletions
diff --git a/src/VBox/Devices/Audio/AudioHlp.cpp b/src/VBox/Devices/Audio/AudioHlp.cpp new file mode 100644 index 00000000..180e5361 --- /dev/null +++ b/src/VBox/Devices/Audio/AudioHlp.cpp @@ -0,0 +1,656 @@ +/* $Id: AudioHlp.cpp $ */ +/** @file + * Audio helper routines. + * + * These are used with both drivers and devices. + */ + +/* + * Copyright (C) 2006-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 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/assert.h> +#include <iprt/dir.h> +#include <iprt/file.h> +#include <iprt/mem.h> +#include <iprt/string.h> +#include <iprt/uuid.h> +#include <iprt/formats/riff.h> + +#define LOG_GROUP LOG_GROUP_DRV_AUDIO +#include <VBox/log.h> + +#include <VBox/err.h> +#include <VBox/vmm/pdmdev.h> +#include <VBox/vmm/pdm.h> +#include <VBox/vmm/pdmaudioinline.h> + +#include "AudioHlp.h" + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +typedef struct AUDIOWAVEFILEHDR +{ + RTRIFFHDR Hdr; + RTRIFFWAVEFMTEXTCHUNK FmtExt; + RTRIFFCHUNK Data; +} AUDIOWAVEFILEHDR; + + +#if 0 /* unused, no header prototypes */ + +/** + * Retrieves the matching PDMAUDIOFMT for the given bits + signing flag. + * + * @return Matching PDMAUDIOFMT value. + * @retval PDMAUDIOFMT_INVALID if unsupported @a cBits value. + * + * @param cBits The number of bits in the audio format. + * @param fSigned Whether the audio format is signed @c true or not. + */ +PDMAUDIOFMT DrvAudioAudFmtBitsToFormat(uint8_t cBits, bool fSigned) +{ + if (fSigned) + { + switch (cBits) + { + case 8: return PDMAUDIOFMT_S8; + case 16: return PDMAUDIOFMT_S16; + case 32: return PDMAUDIOFMT_S32; + default: AssertMsgFailedReturn(("Bogus audio bits %RU8\n", cBits), PDMAUDIOFMT_INVALID); + } + } + else + { + switch (cBits) + { + case 8: return PDMAUDIOFMT_U8; + case 16: return PDMAUDIOFMT_U16; + case 32: return PDMAUDIOFMT_U32; + default: AssertMsgFailedReturn(("Bogus audio bits %RU8\n", cBits), PDMAUDIOFMT_INVALID); + } + } +} + +/** + * Returns an unique file name for this given audio connector instance. + * + * @return Allocated file name. Must be free'd using RTStrFree(). + * @param uInstance Driver / device instance. + * @param pszPath Path name of the file to delete. The path must exist. + * @param pszSuffix File name suffix to use. + */ +char *DrvAudioDbgGetFileNameA(uint8_t uInstance, const char *pszPath, const char *pszSuffix) +{ + char szFileName[64]; + RTStrPrintf(szFileName, sizeof(szFileName), "drvAudio%RU8-%s", uInstance, pszSuffix); + + char szFilePath[RTPATH_MAX]; + int rc2 = RTStrCopy(szFilePath, sizeof(szFilePath), pszPath); + AssertRC(rc2); + rc2 = RTPathAppend(szFilePath, sizeof(szFilePath), szFileName); + AssertRC(rc2); + + return RTStrDup(szFilePath); +} + +#endif /* unused */ + +/** + * Checks whether a given stream configuration is valid or not. + * + * @note See notes on AudioHlpPcmPropsAreValid(). + * + * Returns @c true if configuration is valid, @c false if not. + * @param pCfg Stream configuration to check. + */ +bool AudioHlpStreamCfgIsValid(PCPDMAUDIOSTREAMCFG pCfg) +{ + /* Ugly! HDA attach code calls us with uninitialized (all zero) config. */ + if (PDMAudioPropsHz(&pCfg->Props) != 0) + { + if (PDMAudioStrmCfgIsValid(pCfg)) + { + if ( pCfg->enmDir == PDMAUDIODIR_IN + || pCfg->enmDir == PDMAUDIODIR_OUT) + return AudioHlpPcmPropsAreValidAndSupported(&pCfg->Props); + } + } + return false; +} + +/** + * Calculates the audio bit rate of the given bits per sample, the Hz and the number + * of audio channels. + * + * Divide the result by 8 to get the byte rate. + * + * @returns Bitrate. + * @param cBits Number of bits per sample. + * @param uHz Hz (Hertz) rate. + * @param cChannels Number of audio channels. + */ +uint32_t AudioHlpCalcBitrate(uint8_t cBits, uint32_t uHz, uint8_t cChannels) +{ + return cBits * uHz * cChannels; +} + + +/** + * Checks whether given PCM properties are valid *and* supported by the audio stack or not. + * + * @returns @c true if the properties are valid and supported, @c false if not. + * @param pProps The PCM properties to check. + * + * @note Use PDMAudioPropsAreValid() to just check the validation bits. + */ +bool AudioHlpPcmPropsAreValidAndSupported(PCPDMAUDIOPCMPROPS pProps) +{ + AssertPtrReturn(pProps, false); + + if (!PDMAudioPropsAreValid(pProps)) + return false; + + /* Properties seem valid, now check if we actually support those. */ + switch (PDMAudioPropsSampleSize(pProps)) + { + case 1: /* 8 bit */ + /* Signed / unsigned. */ + break; + case 2: /* 16 bit */ + /* Signed / unsigned. */ + break; + /** @todo Do we need support for 24 bit samples? */ + case 4: /* 32 bit */ + /* Signed / unsigned. */ + break; + case 8: /* 64-bit raw */ + if ( !PDMAudioPropsIsSigned(pProps) + || !pProps->fRaw) + return false; + break; + default: + return false; + } + + if (!pProps->fSwapEndian) /** @todo Handling Big Endian audio data is not supported yet. */ + return true; + return false; +} + + +/********************************************************************************************************************************* +* Audio File Helpers * +*********************************************************************************************************************************/ + +/** + * Constructs an unique file name, based on the given path and the audio file type. + * + * @returns VBox status code. + * @param pszDst Where to store the constructed file name. + * @param cbDst Size of the destination buffer (bytes; incl terminator). + * @param pszPath Base path to use. If NULL or empty, the user's + * temporary directory will be used. + * @param pszNameFmt A name for better identifying the file. + * @param va Arguments for @a pszNameFmt. + * @param uInstance Device / driver instance which is using this file. + * @param enmType Audio file type to construct file name for. + * @param fFlags File naming flags, AUDIOHLPFILENAME_FLAGS_XXX. + * @param chTweak Retry tweak character. + */ +static int audioHlpConstructPathWorker(char *pszDst, size_t cbDst, const char *pszPath, const char *pszNameFmt, va_list va, + uint32_t uInstance, AUDIOHLPFILETYPE enmType, uint32_t fFlags, char chTweak) +{ + /* + * Validate input. + */ + AssertPtrNullReturn(pszPath, VERR_INVALID_POINTER); + AssertPtrReturn(pszNameFmt, VERR_INVALID_POINTER); + AssertReturn(!(fFlags & ~AUDIOHLPFILENAME_FLAGS_VALID_MASK), VERR_INVALID_FLAGS); + + /* Validate the type and translate it into a suffix. */ + const char *pszSuffix = NULL; + switch (enmType) + { + case AUDIOHLPFILETYPE_RAW: pszSuffix = ".pcm"; break; + case AUDIOHLPFILETYPE_WAV: pszSuffix = ".wav"; break; + case AUDIOHLPFILETYPE_INVALID: + case AUDIOHLPFILETYPE_32BIT_HACK: + break; /* no default */ + } + AssertMsgReturn(pszSuffix, ("enmType=%d\n", enmType), VERR_INVALID_PARAMETER); + + /* + * The directory. Make sure it exists and ends with a path separator. + */ + int rc; + if (!pszPath || !*pszPath) + rc = RTPathTemp(pszDst, cbDst); + else + { + AssertPtrReturn(pszDst, VERR_INVALID_POINTER); + rc = RTStrCopy(pszDst, cbDst, pszPath); + } + AssertRCReturn(rc, rc); + + if (!RTDirExists(pszDst)) + { + rc = RTDirCreateFullPath(pszDst, RTFS_UNIX_IRWXU); + AssertRCReturn(rc, rc); + } + + size_t offDst = RTPathEnsureTrailingSeparator(pszDst, cbDst); + AssertReturn(offDst > 0, VERR_BUFFER_OVERFLOW); + Assert(offDst < cbDst); + + /* + * The filename. + */ + /* Start with a ISO timestamp w/ colons replaced by dashes if requested. */ + if (fFlags & AUDIOHLPFILENAME_FLAGS_TS) + { + RTTIMESPEC NowTimeSpec; + RTTIME NowUtc; + AssertReturn(RTTimeToString(RTTimeExplode(&NowUtc, RTTimeNow(&NowTimeSpec)), &pszDst[offDst], cbDst - offDst), + VERR_BUFFER_OVERFLOW); + + /* Change the two colons in the time part to dashes. */ + char *pchColon = &pszDst[offDst]; + while ((pchColon = strchr(pchColon, ':')) != NULL) + *pchColon++ = '-'; + + offDst += strlen(&pszDst[offDst]); + Assert(pszDst[offDst - 1] == 'Z'); + + /* Append a dash to separate the timestamp from the name. */ + AssertReturn(offDst + 2 <= cbDst, VERR_BUFFER_OVERFLOW); + pszDst[offDst++] = '-'; + pszDst[offDst] = '\0'; + } + + /* Append the filename, instance, retry-tweak and suffix. */ + va_list vaCopy; + va_copy(vaCopy, va); + ssize_t cchTail; + if (chTweak == '\0') + cchTail = RTStrPrintf2(&pszDst[offDst], cbDst - offDst, "%N-%u%s", pszNameFmt, &vaCopy, uInstance, pszSuffix); + else + cchTail = RTStrPrintf2(&pszDst[offDst], cbDst - offDst, "%N-%u%c%s", pszNameFmt, &vaCopy, uInstance, chTweak, pszSuffix); + va_end(vaCopy); + AssertReturn(cchTail > 0, VERR_BUFFER_OVERFLOW); + + return VINF_SUCCESS; +} + + +/** + * Worker for AudioHlpFileCreateF and AudioHlpFileCreateAndOpenEx that allocates + * and initializes a AUDIOHLPFILE instance. + */ +static int audioHlpFileCreateWorker(PAUDIOHLPFILE *ppFile, uint32_t fFlags, AUDIOHLPFILETYPE enmType, const char *pszPath) +{ + AssertReturn(!(fFlags & ~AUDIOHLPFILE_FLAGS_VALID_MASK), VERR_INVALID_FLAGS); + + size_t const cbPath = strlen(pszPath) + 1; + PAUDIOHLPFILE pFile = (PAUDIOHLPFILE)RTMemAllocVar(RT_UOFFSETOF_DYN(AUDIOHLPFILE, szName[cbPath])); + AssertPtrReturn(pFile, VERR_NO_MEMORY); + + pFile->enmType = enmType; + pFile->fFlags = fFlags; + pFile->cbWaveData = 0; + pFile->hFile = NIL_RTFILE; + memcpy(pFile->szName, pszPath, cbPath); + + *ppFile = pFile; + return VINF_SUCCESS; +} + + +/** + * Creates an instance of AUDIOHLPFILE with the given filename and type. + * + * @note This does <b>NOT</b> create the file, see AudioHlpFileOpen for that. + * + * @returns VBox status code. + * @param ppFile Where to return the pointer to the audio debug file + * instance on success. + * @param fFlags AUDIOHLPFILE_FLAGS_XXX. + * @param enmType The audio file type to produce. + * @param pszPath The directory path. The temporary directory will be + * used if NULL or empty. + * @param fFilename AUDIOHLPFILENAME_FLAGS_XXX. + * @param uInstance The instance number (will be appended to the filename + * with a dash inbetween). + * @param pszNameFmt The filename format string. + * @param ... Arguments to the filename format string. + */ +int AudioHlpFileCreateF(PAUDIOHLPFILE *ppFile, uint32_t fFlags, AUDIOHLPFILETYPE enmType, + const char *pszPath, uint32_t fFilename, uint32_t uInstance, const char *pszNameFmt, ...) +{ + *ppFile = NULL; + + /* + * Construct the filename first. + */ + char szPath[RTPATH_MAX]; + va_list va; + va_start(va, pszNameFmt); + int rc = audioHlpConstructPathWorker(szPath, sizeof(szPath), pszPath, pszNameFmt, va, uInstance, enmType, fFilename, '\0'); + va_end(va); + AssertRCReturn(rc, rc); + + /* + * Allocate and initializes a debug file instance with that filename path. + */ + return audioHlpFileCreateWorker(ppFile, fFlags, enmType, szPath); +} + + +/** + * Destroys a formerly created audio file. + * + * @param pFile Audio file (object) to destroy. + */ +void AudioHlpFileDestroy(PAUDIOHLPFILE pFile) +{ + if (pFile) + { + AudioHlpFileClose(pFile); + RTMemFree(pFile); + } +} + + +/** + * Opens or creates an audio file. + * + * @returns VBox status code. + * @param pFile Pointer to audio file handle to use. + * @param fOpen Open flags. + * Use AUDIOHLPFILE_DEFAULT_OPEN_FLAGS for the default open flags. + * @param pProps PCM properties to use. + */ +int AudioHlpFileOpen(PAUDIOHLPFILE pFile, uint64_t fOpen, PCPDMAUDIOPCMPROPS pProps) +{ + int rc; + + AssertPtrReturn(pFile, VERR_INVALID_POINTER); + /** @todo Validate fOpen flags. */ + AssertPtrReturn(pProps, VERR_INVALID_POINTER); + Assert(PDMAudioPropsAreValid(pProps)); + + /* + * Raw files just needs to be opened. + */ + if (pFile->enmType == AUDIOHLPFILETYPE_RAW) + rc = RTFileOpen(&pFile->hFile, pFile->szName, fOpen); + /* + * Wave files needs a header to be constructed and we need to take note of where + * there are sizes to update later when closing the file. + */ + else if (pFile->enmType == AUDIOHLPFILETYPE_WAV) + { + /* Construct the header. */ + AUDIOWAVEFILEHDR FileHdr; + FileHdr.Hdr.uMagic = RTRIFFHDR_MAGIC; + FileHdr.Hdr.cbFile = 0; /* need to update this later */ + FileHdr.Hdr.uFileType = RTRIFF_FILE_TYPE_WAVE; + FileHdr.FmtExt.Chunk.uMagic = RTRIFFWAVEFMT_MAGIC; + FileHdr.FmtExt.Chunk.cbChunk = sizeof(RTRIFFWAVEFMTEXTCHUNK) - sizeof(RTRIFFCHUNK); + FileHdr.FmtExt.Data.Core.uFormatTag = RTRIFFWAVEFMT_TAG_EXTENSIBLE; + FileHdr.FmtExt.Data.Core.cChannels = PDMAudioPropsChannels(pProps); + FileHdr.FmtExt.Data.Core.uHz = PDMAudioPropsHz(pProps); + FileHdr.FmtExt.Data.Core.cbRate = PDMAudioPropsFramesToBytes(pProps, PDMAudioPropsHz(pProps)); + FileHdr.FmtExt.Data.Core.cbFrame = PDMAudioPropsFrameSize(pProps); + FileHdr.FmtExt.Data.Core.cBitsPerSample = PDMAudioPropsSampleBits(pProps); + FileHdr.FmtExt.Data.cbExtra = sizeof(FileHdr.FmtExt.Data) - sizeof(FileHdr.FmtExt.Data.Core); + FileHdr.FmtExt.Data.cValidBitsPerSample = PDMAudioPropsSampleBits(pProps); + FileHdr.FmtExt.Data.fChannelMask = 0; + for (uintptr_t idxCh = 0; idxCh < FileHdr.FmtExt.Data.Core.cChannels; idxCh++) + { + PDMAUDIOCHANNELID const idCh = (PDMAUDIOCHANNELID)pProps->aidChannels[idxCh]; + AssertLogRelMsgReturn(idCh >= PDMAUDIOCHANNELID_FIRST_STANDARD && idCh < PDMAUDIOCHANNELID_END_STANDARD, + ("Invalid channel ID %d for channel #%u", idCh, idxCh), VERR_INVALID_PARAMETER); + AssertLogRelMsgReturn(!(FileHdr.FmtExt.Data.fChannelMask & RT_BIT_32(idCh - PDMAUDIOCHANNELID_FIRST_STANDARD)), + ("Channel #%u repeats channel ID %d", idxCh, idCh), VERR_INVALID_PARAMETER); + FileHdr.FmtExt.Data.fChannelMask |= RT_BIT_32(idCh - PDMAUDIOCHANNELID_FIRST_STANDARD); + } + + RTUUID UuidTmp; + rc = RTUuidFromStr(&UuidTmp, RTRIFFWAVEFMTEXT_SUBTYPE_PCM); + AssertRCReturn(rc, rc); + FileHdr.FmtExt.Data.SubFormat = UuidTmp; /* (64-bit field maybe unaligned) */ + + FileHdr.Data.uMagic = RTRIFFWAVEDATACHUNK_MAGIC; + FileHdr.Data.cbChunk = 0; /* need to update this later */ + + /* Open the file and write out the header. */ + rc = RTFileOpen(&pFile->hFile, pFile->szName, fOpen); + if (RT_SUCCESS(rc)) + { + rc = RTFileWrite(pFile->hFile, &FileHdr, sizeof(FileHdr), NULL); + if (RT_FAILURE(rc)) + { + RTFileClose(pFile->hFile); + pFile->hFile = NIL_RTFILE; + } + } + } + else + AssertFailedStmt(rc = VERR_INTERNAL_ERROR_3); + if (RT_SUCCESS(rc)) + { + pFile->cbWaveData = 0; + LogRel2(("Audio: Opened file '%s'\n", pFile->szName)); + } + else + LogRel(("Audio: Failed opening file '%s': %Rrc\n", pFile->szName, rc)); + return rc; +} + + +/** + * Creates a debug file structure and opens a file for it, extended version. + * + * @returns VBox status code. + * @param ppFile Where to return the debug file instance on success. + * @param enmType The file type. + * @param pszDir The directory to open the file in. + * @param iInstance The device/driver instance. + * @param fFilename AUDIOHLPFILENAME_FLAGS_XXX. + * @param fCreate AUDIOHLPFILE_FLAGS_XXX. + * @param pProps PCM audio properties for the file. + * @param fOpen RTFILE_O_XXX or AUDIOHLPFILE_DEFAULT_OPEN_FLAGS. + * @param pszNameFmt The base filename. + * @param ... Filename format arguments. + */ +int AudioHlpFileCreateAndOpenEx(PAUDIOHLPFILE *ppFile, AUDIOHLPFILETYPE enmType, const char *pszDir, + uint32_t iInstance, uint32_t fFilename, uint32_t fCreate, + PCPDMAUDIOPCMPROPS pProps, uint64_t fOpen, const char *pszNameFmt, ...) +{ + *ppFile = NULL; + + for (uint32_t iTry = 0; ; iTry++) + { + /* Format the path to the filename. */ + char szFile[RTPATH_MAX]; + va_list va; + va_start(va, pszNameFmt); + int rc = audioHlpConstructPathWorker(szFile, sizeof(szFile), pszDir, pszNameFmt, va, iInstance, enmType, fFilename, + iTry == 0 ? '\0' : iTry + 'a'); + va_end(va); + AssertRCReturn(rc, rc); + + /* Create an debug audio file instance with the filename path. */ + PAUDIOHLPFILE pFile = NULL; + rc = audioHlpFileCreateWorker(&pFile, fCreate, enmType, szFile); + AssertRCReturn(rc, rc); + + /* Try open it. */ + rc = AudioHlpFileOpen(pFile, fOpen, pProps); + if (RT_SUCCESS(rc)) + { + *ppFile = pFile; + return rc; + } + AudioHlpFileDestroy(pFile); + + AssertReturn(iTry < 16, rc); + } +} + + +/** + * Creates a debug wav-file structure and opens a file for it, default flags. + * + * @returns VBox status code. + * @param ppFile Where to return the debug file instance on success. + * @param pszDir The directory to open the file in. + * @param pszName The base filename. + * @param iInstance The device/driver instance. + * @param pProps PCM audio properties for the file. + */ +int AudioHlpFileCreateAndOpen(PAUDIOHLPFILE *ppFile, const char *pszDir, const char *pszName, + uint32_t iInstance, PCPDMAUDIOPCMPROPS pProps) +{ + return AudioHlpFileCreateAndOpenEx(ppFile, AUDIOHLPFILETYPE_WAV, pszDir, iInstance, + AUDIOHLPFILENAME_FLAGS_NONE, AUDIOHLPFILE_FLAGS_NONE, + pProps, AUDIOHLPFILE_DEFAULT_OPEN_FLAGS, "%s", pszName); +} + + +/** + * Closes an audio file. + * + * @returns VBox status code. + * @param pFile Audio file handle to close. + */ +int AudioHlpFileClose(PAUDIOHLPFILE pFile) +{ + if (!pFile || pFile->hFile == NIL_RTFILE) + return VINF_SUCCESS; + + /* + * Wave files needs to update the data size and file size in the header. + */ + if (pFile->enmType == AUDIOHLPFILETYPE_WAV) + { + uint32_t const cbFile = sizeof(AUDIOWAVEFILEHDR) - sizeof(RTRIFFCHUNK) + (uint32_t)pFile->cbWaveData; + uint32_t const cbData = (uint32_t)pFile->cbWaveData; + + int rc2; + rc2 = RTFileWriteAt(pFile->hFile, RT_UOFFSETOF(AUDIOWAVEFILEHDR, Hdr.cbFile), &cbFile, sizeof(cbFile), NULL); + AssertRC(rc2); + rc2 = RTFileWriteAt(pFile->hFile, RT_UOFFSETOF(AUDIOWAVEFILEHDR, Data.cbChunk), &cbData, sizeof(cbData), NULL); + AssertRC(rc2); + } + + /* + * Do the closing. + */ + int rc = RTFileClose(pFile->hFile); + if (RT_SUCCESS(rc) || rc == VERR_INVALID_HANDLE) + pFile->hFile = NIL_RTFILE; + + if (RT_SUCCESS(rc)) + LogRel2(("Audio: Closed file '%s' (%'RU64 bytes PCM data)\n", pFile->szName, pFile->cbWaveData)); + else + LogRel(("Audio: Failed closing file '%s': %Rrc\n", pFile->szName, rc)); + + /* + * Delete empty file if requested. + */ + if ( !(pFile->fFlags & AUDIOHLPFILE_FLAGS_KEEP_IF_EMPTY) + && pFile->cbWaveData == 0 + && RT_SUCCESS(rc)) + AudioHlpFileDelete(pFile); + + return rc; +} + + +/** + * Deletes an audio file. + * + * @returns VBox status code. + * @param pFile Audio file to delete. + */ +int AudioHlpFileDelete(PAUDIOHLPFILE pFile) +{ + AssertPtrReturn(pFile, VERR_INVALID_POINTER); + + int rc = RTFileDelete(pFile->szName); + if (RT_SUCCESS(rc)) + LogRel2(("Audio: Deleted file '%s'\n", pFile->szName)); + else if (rc == VERR_FILE_NOT_FOUND) /* Don't bitch if the file is not around anymore. */ + rc = VINF_SUCCESS; + + if (RT_FAILURE(rc)) + LogRel(("Audio: Failed deleting file '%s', rc=%Rrc\n", pFile->szName, rc)); + + return rc; +} + + +/** + * Returns whether the given audio file is open and in use or not. + * + * @returns True if open, false if not. + * @param pFile Audio file to check open status for. + */ +bool AudioHlpFileIsOpen(PAUDIOHLPFILE pFile) +{ + if (!pFile || pFile->hFile == NIL_RTFILE) + return false; + + return RTFileIsValid(pFile->hFile); +} + + +/** + * Write PCM data to a wave (.WAV) file. + * + * @returns VBox status code. + * @param pFile Audio file to write PCM data to. + * @param pvBuf Audio data to write. + * @param cbBuf Size (in bytes) of audio data to write. + */ +int AudioHlpFileWrite(PAUDIOHLPFILE pFile, const void *pvBuf, size_t cbBuf) +{ + AssertPtrReturn(pFile, VERR_INVALID_POINTER); + AssertPtrReturn(pvBuf, VERR_INVALID_POINTER); + + if (!cbBuf) + return VINF_SUCCESS; + + int rc = RTFileWrite(pFile->hFile, pvBuf, cbBuf, NULL); + if (RT_SUCCESS(rc)) + pFile->cbWaveData += cbBuf; + + return rc; +} + diff --git a/src/VBox/Devices/Audio/AudioHlp.h b/src/VBox/Devices/Audio/AudioHlp.h new file mode 100644 index 00000000..73a37b57 --- /dev/null +++ b/src/VBox/Devices/Audio/AudioHlp.h @@ -0,0 +1,136 @@ +/* $Id: AudioHlp.h $ */ +/** @file + * Audio helper routines. + */ + +/* + * Copyright (C) 2006-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 + */ + +#ifndef VBOX_INCLUDED_SRC_Audio_AudioHlp_h +#define VBOX_INCLUDED_SRC_Audio_AudioHlp_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include <limits.h> + +#include <iprt/circbuf.h> +#include <iprt/critsect.h> +#include <iprt/file.h> +#include <iprt/path.h> + +#include <VBox/vmm/pdmaudioifs.h> + +/** @name Audio calculation helper methods. + * @{ */ +uint32_t AudioHlpCalcBitrate(uint8_t cBits, uint32_t uHz, uint8_t cChannels); +/** @} */ + +/** @name Audio PCM properties helper methods. + * @{ */ +bool AudioHlpPcmPropsAreValidAndSupported(PCPDMAUDIOPCMPROPS pProps); +/** @} */ + +/** @name Audio configuration helper methods. + * @{ */ +bool AudioHlpStreamCfgIsValid(PCPDMAUDIOSTREAMCFG pCfg); +/** @} */ + + +/** @name AUDIOHLPFILE_FLAGS_XXX + * @{ */ +/** No flags defined. */ +#define AUDIOHLPFILE_FLAGS_NONE UINT32_C(0) +/** Keep the audio file even if it contains no audio data. */ +#define AUDIOHLPFILE_FLAGS_KEEP_IF_EMPTY RT_BIT_32(0) +/** Audio file flag validation mask. */ +#define AUDIOHLPFILE_FLAGS_VALID_MASK UINT32_C(0x1) +/** @} */ + +/** Audio file default open flags. */ +#define AUDIOHLPFILE_DEFAULT_OPEN_FLAGS (RTFILE_O_OPEN_CREATE | RTFILE_O_APPEND | RTFILE_O_WRITE | RTFILE_O_DENY_WRITE) + +/** + * Audio file types. + */ +typedef enum AUDIOHLPFILETYPE +{ + /** The customary invalid zero value. */ + AUDIOHLPFILETYPE_INVALID = 0, + /** Raw (PCM) file. */ + AUDIOHLPFILETYPE_RAW, + /** Wave (.WAV) file. */ + AUDIOHLPFILETYPE_WAV, + /** Hack to blow the type up to 32-bit. */ + AUDIOHLPFILETYPE_32BIT_HACK = 0x7fffffff +} AUDIOHLPFILETYPE; + +/** @name AUDIOHLPFILENAME_FLAGS_XXX + * @{ */ +/** No flags defined. */ +#define AUDIOHLPFILENAME_FLAGS_NONE UINT32_C(0) +/** Adds an ISO timestamp to the file name. */ +#define AUDIOHLPFILENAME_FLAGS_TS RT_BIT_32(0) +/** Valid flag mask. */ +#define AUDIOHLPFILENAME_FLAGS_VALID_MASK AUDIOHLPFILENAME_FLAGS_TS +/** @} */ + +/** + * Audio file handle. + */ +typedef struct AUDIOHLPFILE +{ + /** Type of the audio file. */ + AUDIOHLPFILETYPE enmType; + /** Audio file flags, AUDIOHLPFILE_FLAGS_XXX. */ + uint32_t fFlags; + /** Amount of wave data written. */ + uint64_t cbWaveData; + /** Actual file handle. */ + RTFILE hFile; + /** File name and path. */ + RT_FLEXIBLE_ARRAY_EXTENSION + char szName[RT_FLEXIBLE_ARRAY]; +} AUDIOHLPFILE; +/** Pointer to an audio file handle. */ +typedef AUDIOHLPFILE *PAUDIOHLPFILE; + +/** @name Audio file methods. + * @{ */ +int AudioHlpFileCreateAndOpen(PAUDIOHLPFILE *ppFile, const char *pszDir, const char *pszName, + uint32_t iInstance, PCPDMAUDIOPCMPROPS pProps); +int AudioHlpFileCreateAndOpenEx(PAUDIOHLPFILE *ppFile, AUDIOHLPFILETYPE enmType, const char *pszDir, + uint32_t iInstance, uint32_t fFilename, uint32_t fCreate, + PCPDMAUDIOPCMPROPS pProps, uint64_t fOpen, const char *pszName, ...); +int AudioHlpFileCreateF(PAUDIOHLPFILE *ppFile, uint32_t fFlags, AUDIOHLPFILETYPE enmType, + const char *pszPath, uint32_t fFilename, uint32_t uInstance, const char *pszFileFmt, ...); + +void AudioHlpFileDestroy(PAUDIOHLPFILE pFile); +int AudioHlpFileOpen(PAUDIOHLPFILE pFile, uint64_t fOpen, PCPDMAUDIOPCMPROPS pProps); +int AudioHlpFileClose(PAUDIOHLPFILE pFile); +int AudioHlpFileDelete(PAUDIOHLPFILE pFile); +bool AudioHlpFileIsOpen(PAUDIOHLPFILE pFile); +int AudioHlpFileWrite(PAUDIOHLPFILE pFile, const void *pvBuf, size_t cbBuf); +/** @} */ + +#endif /* !VBOX_INCLUDED_SRC_Audio_AudioHlp_h */ + diff --git a/src/VBox/Devices/Audio/AudioMixBuffer-Convert.cpp.h b/src/VBox/Devices/Audio/AudioMixBuffer-Convert.cpp.h new file mode 100644 index 00000000..13aa3402 --- /dev/null +++ b/src/VBox/Devices/Audio/AudioMixBuffer-Convert.cpp.h @@ -0,0 +1,315 @@ +/* $Id: AudioMixBuffer-Convert.cpp.h $ */ +/** @file + * Audio mixing buffer - Format conversion template. + */ + +/* + * 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 + */ + + +/* used to be: #define AUDMIXBUF_CONVERT(a_Name, a_Type, a_Min, a_Max, a_fSigned, a_cShift) */ + +/* Clips a specific output value to a single sample value. */ +DECLINLINE(int32_t) RT_CONCAT(audioMixBufSampleFrom,a_Name)(a_Type aVal) +{ + /* left shifting of signed values is not defined, therefore the intermediate uint64_t cast */ + if (a_fSigned) + return (int32_t) (((uint32_t) ((int32_t) aVal )) << (32 - a_cShift)); + return (int32_t) (((uint32_t) ((int32_t) aVal - ((a_Max >> 1) + 1))) << (32 - a_cShift)); +} + +/* Clips a single sample value to a specific output value. */ +DECLINLINE(a_Type) RT_CONCAT(audioMixBufSampleTo,a_Name)(int32_t iVal) +{ + if (a_fSigned) + return (a_Type) (iVal >> (32 - a_cShift)); + return (a_Type) ((iVal >> (32 - a_cShift)) + ((a_Max >> 1) + 1)); +} + +/* Encoders for peek: */ + +/* Generic */ +static DECLCALLBACK(void) RT_CONCAT(audioMixBufEncodeGeneric,a_Name)(void *pvDst, int32_t const *pi32Src, uint32_t cFrames, + PAUDIOMIXBUFPEEKSTATE pState) +{ + RT_NOREF_PV(pState); + uintptr_t const cSrcChannels = pState->cSrcChannels; + uintptr_t const cDstChannels = pState->cDstChannels; + a_Type *pDst = (a_Type *)pvDst; + while (cFrames-- > 0) + { + uintptr_t idxDst = cDstChannels; + while (idxDst-- > 0) + { + intptr_t idxSrc = pState->aidxChannelMap[idxDst]; + if (idxSrc >= 0) + pDst[idxDst] = RT_CONCAT(audioMixBufSampleTo,a_Name)(pi32Src[idxSrc]); + else if (idxSrc != -2) + pDst[idxDst] = (a_fSigned) ? 0 : (a_Max >> 1); + else + pDst[idxDst] = 0; + } + pDst += cDstChannels; + pi32Src += cSrcChannels; + } +} + +/* 2ch -> 2ch */ +static DECLCALLBACK(void) RT_CONCAT(audioMixBufEncode2ChTo2Ch,a_Name)(void *pvDst, int32_t const *pi32Src, uint32_t cFrames, + PAUDIOMIXBUFPEEKSTATE pState) +{ + RT_NOREF_PV(pState); + a_Type *pDst = (a_Type *)pvDst; + while (cFrames-- > 0) + { + pDst[0] = RT_CONCAT(audioMixBufSampleTo,a_Name)(pi32Src[0]); + pDst[1] = RT_CONCAT(audioMixBufSampleTo,a_Name)(pi32Src[1]); + AUDMIXBUF_MACRO_LOG(("%p: %RI32 / %RI32 => %RI32 / %RI32\n", + &pi32Src[0], pi32Src[0], pi32Src[1], (int32_t)pDst[0], (int32_t)pDst[1])); + pDst += 2; + pi32Src += 2; + } +} + +/* 2ch -> 1ch */ +static DECLCALLBACK(void) RT_CONCAT(audioMixBufEncode2ChTo1Ch,a_Name)(void *pvDst, int32_t const *pi32Src, uint32_t cFrames, + PAUDIOMIXBUFPEEKSTATE pState) +{ + RT_NOREF_PV(pState); + a_Type *pDst = (a_Type *)pvDst; + while (cFrames-- > 0) + { + pDst[0] = RT_CONCAT(audioMixBufSampleTo,a_Name)(audioMixBufBlendSampleRet(pi32Src[0], pi32Src[1])); + pDst += 1; + pi32Src += 2; + } +} + +/* 1ch -> 2ch */ +static DECLCALLBACK(void) RT_CONCAT(audioMixBufEncode1ChTo2Ch,a_Name)(void *pvDst, int32_t const *pi32Src, uint32_t cFrames, + PAUDIOMIXBUFPEEKSTATE pState) +{ + RT_NOREF_PV(pState); + a_Type *pDst = (a_Type *)pvDst; + while (cFrames-- > 0) + { + pDst[0] = pDst[1] = RT_CONCAT(audioMixBufSampleTo,a_Name)(pi32Src[0]); + pDst += 2; + pi32Src += 1; + } +} +/* 1ch -> 1ch */ +static DECLCALLBACK(void) RT_CONCAT(audioMixBufEncode1ChTo1Ch,a_Name)(void *pvDst, int32_t const *pi32Src, uint32_t cFrames, + PAUDIOMIXBUFPEEKSTATE pState) +{ + RT_NOREF_PV(pState); + a_Type *pDst = (a_Type *)pvDst; + while (cFrames-- > 0) + { + pDst[0] = RT_CONCAT(audioMixBufSampleTo,a_Name)(pi32Src[0]); + pDst += 1; + pi32Src += 1; + } +} + +/* Decoders for write: */ + +/* Generic */ +static DECLCALLBACK(void) RT_CONCAT(audioMixBufDecodeGeneric,a_Name)(int32_t *pi32Dst, void const *pvSrc, uint32_t cFrames, + PAUDIOMIXBUFWRITESTATE pState) +{ + RT_NOREF_PV(pState); + uintptr_t const cSrcChannels = pState->cSrcChannels; + uintptr_t const cDstChannels = pState->cDstChannels; + a_Type const *pSrc = (a_Type const *)pvSrc; + while (cFrames-- > 0) + { + uintptr_t idxDst = cDstChannels; + while (idxDst-- > 0) + { + intptr_t idxSrc = pState->aidxChannelMap[idxDst]; + if (idxSrc >= 0) + pi32Dst[idxDst] = RT_CONCAT(audioMixBufSampleFrom,a_Name)(pSrc[idxSrc]); + else if (idxSrc != -2) + pi32Dst[idxDst] = (a_fSigned) ? 0 : (a_Max >> 1); + else + pi32Dst[idxDst] = 0; + } + pi32Dst += cDstChannels; + pSrc += cSrcChannels; + } +} + +/* 2ch -> 2ch */ +static DECLCALLBACK(void) RT_CONCAT(audioMixBufDecode2ChTo2Ch,a_Name)(int32_t *pi32Dst, void const *pvSrc, uint32_t cFrames, + PAUDIOMIXBUFWRITESTATE pState) +{ + RT_NOREF_PV(pState); + a_Type const *pSrc = (a_Type const *)pvSrc; + while (cFrames-- > 0) + { + pi32Dst[0] = RT_CONCAT(audioMixBufSampleFrom,a_Name)(pSrc[0]); + pi32Dst[1] = RT_CONCAT(audioMixBufSampleFrom,a_Name)(pSrc[1]); + AUDMIXBUF_MACRO_LOG(("%p: %RI32 / %RI32 => %RI32 / %RI32\n", + &pSrc[0], (int32_t)pSrc[0], (int32_t)pSrc[1], pi32Dst[0], pi32Dst[1])); + pi32Dst += 2; + pSrc += 2; + } +} + +/* 2ch -> 1ch */ +static DECLCALLBACK(void) RT_CONCAT(audioMixBufDecode2ChTo1Ch,a_Name)(int32_t *pi32Dst, void const *pvSrc, uint32_t cFrames, + PAUDIOMIXBUFWRITESTATE pState) +{ + RT_NOREF_PV(pState); + a_Type const *pSrc = (a_Type const *)pvSrc; + while (cFrames-- > 0) + { + pi32Dst[0] = audioMixBufBlendSampleRet(RT_CONCAT(audioMixBufSampleFrom,a_Name)(pSrc[0]), + RT_CONCAT(audioMixBufSampleFrom,a_Name)(pSrc[1])); + pi32Dst += 1; + pSrc += 2; + } +} + +/* 1ch -> 2ch */ +static DECLCALLBACK(void) RT_CONCAT(audioMixBufDecode1ChTo2Ch,a_Name)(int32_t *pi32Dst, void const *pvSrc, uint32_t cFrames, + PAUDIOMIXBUFWRITESTATE pState) +{ + RT_NOREF_PV(pState); + a_Type const *pSrc = (a_Type const *)pvSrc; + while (cFrames-- > 0) + { + pi32Dst[1] = pi32Dst[0] = RT_CONCAT(audioMixBufSampleFrom,a_Name)(pSrc[0]); + pi32Dst += 2; + pSrc += 1; + } +} + +/* 1ch -> 1ch */ +static DECLCALLBACK(void) RT_CONCAT(audioMixBufDecode1ChTo1Ch,a_Name)(int32_t *pi32Dst, void const *pvSrc, uint32_t cFrames, + PAUDIOMIXBUFWRITESTATE pState) +{ + RT_NOREF_PV(pState); + a_Type const *pSrc = (a_Type const *)pvSrc; + while (cFrames-- > 0) + { + pi32Dst[0] = RT_CONCAT(audioMixBufSampleFrom,a_Name)(pSrc[0]); + pi32Dst += 1; + pSrc += 1; + } +} + +/* Decoders for blending: */ + +/* Generic */ +static DECLCALLBACK(void) RT_CONCAT3(audioMixBufDecodeGeneric,a_Name,Blend)(int32_t *pi32Dst, void const *pvSrc, + uint32_t cFrames, PAUDIOMIXBUFWRITESTATE pState) +{ + RT_NOREF_PV(pState); + uintptr_t const cSrcChannels = pState->cSrcChannels; + uintptr_t const cDstChannels = pState->cDstChannels; + a_Type const *pSrc = (a_Type const *)pvSrc; + while (cFrames-- > 0) + { + uintptr_t idxDst = cDstChannels; + while (idxDst-- > 0) + { + intptr_t idxSrc = pState->aidxChannelMap[idxDst]; + if (idxSrc >= 0) + audioMixBufBlendSample(&pi32Dst[idxDst], RT_CONCAT(audioMixBufSampleFrom,a_Name)(pSrc[idxSrc])); + } + pi32Dst += cDstChannels; + pSrc += cSrcChannels; + } +} + +/* 2ch -> 2ch */ +static DECLCALLBACK(void) RT_CONCAT3(audioMixBufDecode2ChTo2Ch,a_Name,Blend)(int32_t *pi32Dst, void const *pvSrc, + uint32_t cFrames, PAUDIOMIXBUFWRITESTATE pState) +{ + RT_NOREF_PV(pState); + a_Type const *pSrc = (a_Type const *)pvSrc; + while (cFrames-- > 0) + { + audioMixBufBlendSample(&pi32Dst[0], RT_CONCAT(audioMixBufSampleFrom,a_Name)(pSrc[0])); + audioMixBufBlendSample(&pi32Dst[1], RT_CONCAT(audioMixBufSampleFrom,a_Name)(pSrc[1])); + AUDMIXBUF_MACRO_LOG(("%p: %RI32 / %RI32 => %RI32 / %RI32\n", + &pSrc[0], (int32_t)pSrc[0], (int32_t)pSrc[1], pi32Dst[0], pi32Dst[1])); + pi32Dst += 2; + pSrc += 2; + } +} + +/* 2ch -> 1ch */ +static DECLCALLBACK(void) RT_CONCAT3(audioMixBufDecode2ChTo1Ch,a_Name,Blend)(int32_t *pi32Dst, void const *pvSrc, + uint32_t cFrames, PAUDIOMIXBUFWRITESTATE pState) +{ + RT_NOREF_PV(pState); + a_Type const *pSrc = (a_Type const *)pvSrc; + while (cFrames-- > 0) + { + audioMixBufBlendSample(&pi32Dst[0], audioMixBufBlendSampleRet(RT_CONCAT(audioMixBufSampleFrom,a_Name)(pSrc[0]), + RT_CONCAT(audioMixBufSampleFrom,a_Name)(pSrc[1]))); + pi32Dst += 1; + pSrc += 2; + } +} + +/* 1ch -> 2ch */ +static DECLCALLBACK(void) RT_CONCAT3(audioMixBufDecode1ChTo2Ch,a_Name,Blend)(int32_t *pi32Dst, void const *pvSrc, + uint32_t cFrames, PAUDIOMIXBUFWRITESTATE pState) +{ + RT_NOREF_PV(pState); + a_Type const *pSrc = (a_Type const *)pvSrc; + while (cFrames-- > 0) + { + int32_t const i32Src = RT_CONCAT(audioMixBufSampleFrom,a_Name)(pSrc[0]); + audioMixBufBlendSample(&pi32Dst[0], i32Src); + audioMixBufBlendSample(&pi32Dst[1], i32Src); + pi32Dst += 2; + pSrc += 1; + } +} + +/* 1ch -> 1ch */ +static DECLCALLBACK(void) RT_CONCAT3(audioMixBufDecode1ChTo1Ch,a_Name,Blend)(int32_t *pi32Dst, void const *pvSrc, + uint32_t cFrames, PAUDIOMIXBUFWRITESTATE pState) +{ + RT_NOREF_PV(pState); + a_Type const *pSrc = (a_Type const *)pvSrc; + while (cFrames-- > 0) + { + audioMixBufBlendSample(&pi32Dst[0], RT_CONCAT(audioMixBufSampleFrom,a_Name)(pSrc[0])); + pi32Dst += 1; + pSrc += 1; + } +} + + +#undef a_Name +#undef a_Type +#undef a_Min +#undef a_Max +#undef a_fSigned +#undef a_cShift + diff --git a/src/VBox/Devices/Audio/AudioMixBuffer.cpp b/src/VBox/Devices/Audio/AudioMixBuffer.cpp new file mode 100644 index 00000000..e0c0438b --- /dev/null +++ b/src/VBox/Devices/Audio/AudioMixBuffer.cpp @@ -0,0 +1,2124 @@ +/* $Id: AudioMixBuffer.cpp $ */ +/** @file + * Audio mixing buffer for converting reading/writing audio data. + */ + +/* + * 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_mixing_buffers Audio Mixer Buffer + * + * @section sec_audio_mixing_buffers_volume Soft Volume Control + * + * The external code supplies an 8-bit volume (attenuation) value in the + * 0 .. 255 range. This represents 0 to -96dB attenuation where an input + * value of 0 corresponds to -96dB and 255 corresponds to 0dB (unchanged). + * + * Each step thus corresponds to 96 / 256 or 0.375dB. Every 6dB (16 steps) + * represents doubling the sample value. + * + * For internal use, the volume control needs to be converted to a 16-bit + * (sort of) exponential value between 1 and 65536. This is used with fixed + * point arithmetic such that 65536 means 1.0 and 1 means 1/65536. + * + * For actual volume calculation, 33.31 fixed point is used. Maximum (or + * unattenuated) volume is represented as 0x40000000; conveniently, this + * value fits into a uint32_t. + * + * To enable fast processing, the maximum volume must be a power of two + * and must not have a sign when converted to int32_t. While 0x80000000 + * violates these constraints, 0x40000000 does not. + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_AUDIO_MIXER_BUFFER +#if defined(VBOX_AUDIO_MIX_BUFFER_TESTCASE) && !defined(RT_STRICT) +# define RT_STRICT /* Run the testcase with assertions because the main functions doesn't return on invalid input. */ +#endif +#include <VBox/log.h> + +#if 0 +/* + * AUDIOMIXBUF_DEBUG_DUMP_PCM_DATA enables dumping the raw PCM data + * to a file on the host. Be sure to adjust AUDIOMIXBUF_DEBUG_DUMP_PCM_DATA_PATH + * to your needs before using this! + */ +# define AUDIOMIXBUF_DEBUG_DUMP_PCM_DATA +# ifdef RT_OS_WINDOWS +# define AUDIOMIXBUF_DEBUG_DUMP_PCM_DATA_PATH "c:\\temp\\" +# else +# define AUDIOMIXBUF_DEBUG_DUMP_PCM_DATA_PATH "/tmp/" +# endif +/* Warning: Enabling this will generate *huge* logs! */ +//# define AUDIOMIXBUF_DEBUG_MACROS +#endif + +#include <iprt/asm-math.h> +#include <iprt/assert.h> +#ifdef AUDIOMIXBUF_DEBUG_DUMP_PCM_DATA +# include <iprt/file.h> +#endif +#include <iprt/mem.h> +#include <iprt/string.h> /* For RT_BZERO. */ + +#ifdef VBOX_AUDIO_TESTCASE +# define LOG_ENABLED +# include <iprt/stream.h> +#endif +#include <iprt/errcore.h> +#include <VBox/vmm/pdmaudioinline.h> + +#include "AudioMixBuffer.h" + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +#ifndef VBOX_AUDIO_TESTCASE +# ifdef DEBUG +# define AUDMIXBUF_LOG(x) LogFlowFunc(x) +# define AUDMIXBUF_LOG_ENABLED +# else +# define AUDMIXBUF_LOG(x) do {} while (0) +# endif +#else /* VBOX_AUDIO_TESTCASE */ +# define AUDMIXBUF_LOG(x) RTPrintf x +# define AUDMIXBUF_LOG_ENABLED +#endif + + +/** Bit shift for fixed point conversion. + * @sa @ref sec_audio_mixing_buffers_volume */ +#define AUDIOMIXBUF_VOL_SHIFT 30 + +/** Internal representation of 0dB volume (1.0 in fixed point). + * @sa @ref sec_audio_mixing_buffers_volume */ +#define AUDIOMIXBUF_VOL_0DB (1 << AUDIOMIXBUF_VOL_SHIFT) +AssertCompile(AUDIOMIXBUF_VOL_0DB <= 0x40000000); /* Must always hold. */ +AssertCompile(AUDIOMIXBUF_VOL_0DB == 0x40000000); /* For now -- when only attenuation is used. */ + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** Logarithmic/exponential volume conversion table. + * @sa @ref sec_audio_mixing_buffers_volume + */ +static uint32_t const s_aVolumeConv[256] = +{ + 1, 1, 1, 1, 1, 1, 1, 1, /* 7 */ + 1, 2, 2, 2, 2, 2, 2, 2, /* 15 */ + 2, 2, 2, 2, 2, 3, 3, 3, /* 23 */ + 3, 3, 3, 3, 4, 4, 4, 4, /* 31 */ + 4, 4, 5, 5, 5, 5, 5, 6, /* 39 */ + 6, 6, 6, 7, 7, 7, 8, 8, /* 47 */ + 8, 9, 9, 10, 10, 10, 11, 11, /* 55 */ + 12, 12, 13, 13, 14, 15, 15, 16, /* 63 */ + 17, 17, 18, 19, 20, 21, 22, 23, /* 71 */ + 24, 25, 26, 27, 28, 29, 31, 32, /* 79 */ + 33, 35, 36, 38, 40, 41, 43, 45, /* 87 */ + 47, 49, 52, 54, 56, 59, 61, 64, /* 95 */ + 67, 70, 73, 76, 79, 83, 87, 91, /* 103 */ + 95, 99, 103, 108, 112, 117, 123, 128, /* 111 */ + 134, 140, 146, 152, 159, 166, 173, 181, /* 119 */ + 189, 197, 206, 215, 225, 235, 245, 256, /* 127 */ + 267, 279, 292, 304, 318, 332, 347, 362, /* 135 */ + 378, 395, 412, 431, 450, 470, 490, 512, /* 143 */ + 535, 558, 583, 609, 636, 664, 693, 724, /* 151 */ + 756, 790, 825, 861, 899, 939, 981, 1024, /* 159 */ + 1069, 1117, 1166, 1218, 1272, 1328, 1387, 1448, /* 167 */ + 1512, 1579, 1649, 1722, 1798, 1878, 1961, 2048, /* 175 */ + 2139, 2233, 2332, 2435, 2543, 2656, 2774, 2896, /* 183 */ + 3025, 3158, 3298, 3444, 3597, 3756, 3922, 4096, /* 191 */ + 4277, 4467, 4664, 4871, 5087, 5312, 5547, 5793, /* 199 */ + 6049, 6317, 6597, 6889, 7194, 7512, 7845, 8192, /* 207 */ + 8555, 8933, 9329, 9742, 10173, 10624, 11094, 11585, /* 215 */ + 12098, 12634, 13193, 13777, 14387, 15024, 15689, 16384, /* 223 */ + 17109, 17867, 18658, 19484, 20347, 21247, 22188, 23170, /* 231 */ + 24196, 25268, 26386, 27554, 28774, 30048, 31379, 32768, /* 239 */ + 34219, 35734, 37316, 38968, 40693, 42495, 44376, 46341, /* 247 */ + 48393, 50535, 52773, 55109, 57549, 60097, 62757, 65536, /* 255 */ +}; + + + +#ifdef VBOX_STRICT +# ifdef UNUSED + +/** + * Prints a single mixing buffer. + * Internal helper function for debugging. Do not use directly. + * + * @returns VBox status code. + * @param pMixBuf Mixing buffer to print. + * @param pszFunc Function name to log this for. + * @param uIdtLvl Indention level to use. + */ +static void audioMixBufDbgPrintSingle(PAUDIOMIXBUF pMixBuf, const char *pszFunc, uint16_t uIdtLvl) +{ + Log(("%s: %*s %s: offRead=%RU32, offWrite=%RU32 -> %RU32/%RU32\n", + pszFunc, uIdtLvl * 4, "", + pMixBuf->pszName, pMixBuf->offRead, pMixBuf->offWrite, pMixBuf->cUsed, pMixBuf->cFrames)); +} + +static void audioMixBufDbgPrintInternal(PAUDIOMIXBUF pMixBuf, const char *pszFunc) +{ + audioMixBufDbgPrintSingle(pMixBuf, pszFunc, 0 /* iIdtLevel */); +} + +/** + * Validates a single mixing buffer. + * + * @return @true if the buffer state is valid or @false if not. + * @param pMixBuf Mixing buffer to validate. + */ +static bool audioMixBufDbgValidate(PAUDIOMIXBUF pMixBuf) +{ + //const uint32_t offReadEnd = (pMixBuf->offRead + pMixBuf->cUsed) % pMixBuf->cFrames; + //const uint32_t offWriteEnd = (pMixBuf->offWrite + (pMixBuf->cFrames - pMixBuf->cUsed)) % pMixBuf->cFrames; + + bool fValid = true; + + AssertStmt(pMixBuf->offRead <= pMixBuf->cFrames, fValid = false); + AssertStmt(pMixBuf->offWrite <= pMixBuf->cFrames, fValid = false); + AssertStmt(pMixBuf->cUsed <= pMixBuf->cFrames, fValid = false); + + if (pMixBuf->offWrite > pMixBuf->offRead) + { + if (pMixBuf->offWrite - pMixBuf->offRead != pMixBuf->cUsed) + fValid = false; + } + else if (pMixBuf->offWrite < pMixBuf->offRead) + { + if (pMixBuf->offWrite + pMixBuf->cFrames - pMixBuf->offRead != pMixBuf->cUsed) + fValid = false; + } + + if (!fValid) + { + audioMixBufDbgPrintInternal(pMixBuf, __FUNCTION__); + AssertFailed(); + } + + return fValid; +} + +# endif /* UNUSED */ +#endif /* VBOX_STRICT */ + + +/** + * Merges @a i32Src into the value stored at @a pi32Dst. + * + * @param pi32Dst The value to merge @a i32Src into. + * @param i32Src The new value to add. + */ +DECL_FORCE_INLINE(void) audioMixBufBlendSample(int32_t *pi32Dst, int32_t i32Src) +{ + if (i32Src) + { + int64_t const i32Dst = *pi32Dst; + if (!i32Dst) + *pi32Dst = i32Src; + else + *pi32Dst = (int32_t)(((int64_t)i32Dst + i32Src) / 2); + } +} + + +/** + * Variant of audioMixBufBlendSample that returns the result rather than storing it. + * + * This is used for stereo -> mono. + */ +DECL_FORCE_INLINE(int32_t) audioMixBufBlendSampleRet(int32_t i32Sample1, int32_t i32Sample2) +{ + if (!i32Sample1) + return i32Sample2; + if (!i32Sample2) + return i32Sample1; + return (int32_t)(((int64_t)i32Sample1 + i32Sample2) / 2); +} + + +/** + * Blends (merges) the source buffer into the destination buffer. + * + * We're taking a very simple approach here, working sample by sample: + * - if one is silent, use the other one. + * - otherwise sum and divide by two. + * + * @param pi32Dst The destination stream buffer (input and output). + * @param pi32Src The source stream buffer. + * @param cFrames Number of frames to process. + * @param cChannels Number of channels. + */ +static void audioMixBufBlendBuffer(int32_t *pi32Dst, int32_t const *pi32Src, uint32_t cFrames, uint8_t cChannels) +{ + switch (cChannels) + { + case 2: + while (cFrames-- > 0) + { + audioMixBufBlendSample(&pi32Dst[0], pi32Src[0]); + audioMixBufBlendSample(&pi32Dst[1], pi32Src[1]); + pi32Dst += 2; + pi32Src += 2; + } + break; + + default: + cFrames *= cChannels; + RT_FALL_THROUGH(); + case 1: + while (cFrames-- > 0) + { + audioMixBufBlendSample(pi32Dst, pi32Src[0]); + pi32Dst++; + pi32Src++; + } + break; + } +} + + +#ifdef AUDIOMIXBUF_DEBUG_MACROS +# define AUDMIXBUF_MACRO_LOG(x) AUDMIXBUF_LOG(x) +#elif defined(VBOX_AUDIO_TESTCASE_VERBOSE) /* Warning: VBOX_AUDIO_TESTCASE_VERBOSE will generate huge logs! */ +# define AUDMIXBUF_MACRO_LOG(x) RTPrintf x +#else +# define AUDMIXBUF_MACRO_LOG(x) do {} while (0) +#endif + +/* + * Instantiate format conversion (in and out of the mixer buffer.) + */ +/** @todo Currently does not handle any endianness conversion yet! */ + +/* audioMixBufConvXXXS8: 8-bit, signed. */ +#define a_Name S8 +#define a_Type int8_t +#define a_Min INT8_MIN +#define a_Max INT8_MAX +#define a_fSigned 1 +#define a_cShift 8 +#include "AudioMixBuffer-Convert.cpp.h" + +/* audioMixBufConvXXXU8: 8-bit, unsigned. */ +#define a_Name U8 +#define a_Type uint8_t +#define a_Min 0 +#define a_Max UINT8_MAX +#define a_fSigned 0 +#define a_cShift 8 +#include "AudioMixBuffer-Convert.cpp.h" + +/* audioMixBufConvXXXS16: 16-bit, signed. */ +#define a_Name S16 +#define a_Type int16_t +#define a_Min INT16_MIN +#define a_Max INT16_MAX +#define a_fSigned 1 +#define a_cShift 16 +#include "AudioMixBuffer-Convert.cpp.h" + +/* audioMixBufConvXXXU16: 16-bit, unsigned. */ +#define a_Name U16 +#define a_Type uint16_t +#define a_Min 0 +#define a_Max UINT16_MAX +#define a_fSigned 0 +#define a_cShift 16 +#include "AudioMixBuffer-Convert.cpp.h" + +/* audioMixBufConvXXXS32: 32-bit, signed. */ +#define a_Name S32 +#define a_Type int32_t +#define a_Min INT32_MIN +#define a_Max INT32_MAX +#define a_fSigned 1 +#define a_cShift 32 +#include "AudioMixBuffer-Convert.cpp.h" + +/* audioMixBufConvXXXU32: 32-bit, unsigned. */ +#define a_Name U32 +#define a_Type uint32_t +#define a_Min 0 +#define a_Max UINT32_MAX +#define a_fSigned 0 +#define a_cShift 32 +#include "AudioMixBuffer-Convert.cpp.h" + +/* audioMixBufConvXXXRaw: 32-bit stored as 64-bit, signed. */ +#define a_Name Raw +#define a_Type int64_t +#define a_Min INT64_MIN +#define a_Max INT64_MAX +#define a_fSigned 1 +#define a_cShift 32 /* Yes, 32! */ +#include "AudioMixBuffer-Convert.cpp.h" + +#undef AUDMIXBUF_CONVERT +#undef AUDMIXBUF_MACRO_LOG + + +/* + * Resampling core. + */ +/** @todo Separate down- and up-sampling, borrow filter code from RDP. */ +#define COPY_LAST_FRAME_1CH(a_pi32Dst, a_pi32Src, a_cChannels) do { \ + (a_pi32Dst)[0] = (a_pi32Src)[0]; \ + } while (0) +#define COPY_LAST_FRAME_2CH(a_pi32Dst, a_pi32Src, a_cChannels) do { \ + (a_pi32Dst)[0] = (a_pi32Src)[0]; \ + (a_pi32Dst)[1] = (a_pi32Src)[1]; \ + } while (0) +#define COPY_LAST_FRAME_3CH(a_pi32Dst, a_pi32Src, a_cChannels) do { \ + (a_pi32Dst)[0] = (a_pi32Src)[0]; \ + (a_pi32Dst)[1] = (a_pi32Src)[1]; \ + (a_pi32Dst)[2] = (a_pi32Src)[2]; \ + } while (0) +#define COPY_LAST_FRAME_4CH(a_pi32Dst, a_pi32Src, a_cChannels) do { \ + (a_pi32Dst)[0] = (a_pi32Src)[0]; \ + (a_pi32Dst)[1] = (a_pi32Src)[1]; \ + (a_pi32Dst)[2] = (a_pi32Src)[2]; \ + (a_pi32Dst)[3] = (a_pi32Src)[3]; \ + } while (0) +#define COPY_LAST_FRAME_5CH(a_pi32Dst, a_pi32Src, a_cChannels) do { \ + (a_pi32Dst)[0] = (a_pi32Src)[0]; \ + (a_pi32Dst)[1] = (a_pi32Src)[1]; \ + (a_pi32Dst)[2] = (a_pi32Src)[2]; \ + (a_pi32Dst)[3] = (a_pi32Src)[3]; \ + (a_pi32Dst)[4] = (a_pi32Src)[4]; \ + } while (0) +#define COPY_LAST_FRAME_6CH(a_pi32Dst, a_pi32Src, a_cChannels) do { \ + (a_pi32Dst)[0] = (a_pi32Src)[0]; \ + (a_pi32Dst)[1] = (a_pi32Src)[1]; \ + (a_pi32Dst)[2] = (a_pi32Src)[2]; \ + (a_pi32Dst)[3] = (a_pi32Src)[3]; \ + (a_pi32Dst)[4] = (a_pi32Src)[4]; \ + (a_pi32Dst)[5] = (a_pi32Src)[5]; \ + } while (0) +#define COPY_LAST_FRAME_7CH(a_pi32Dst, a_pi32Src, a_cChannels) do { \ + (a_pi32Dst)[0] = (a_pi32Src)[0]; \ + (a_pi32Dst)[1] = (a_pi32Src)[1]; \ + (a_pi32Dst)[2] = (a_pi32Src)[2]; \ + (a_pi32Dst)[3] = (a_pi32Src)[3]; \ + (a_pi32Dst)[4] = (a_pi32Src)[4]; \ + (a_pi32Dst)[5] = (a_pi32Src)[5]; \ + (a_pi32Dst)[6] = (a_pi32Src)[6]; \ + } while (0) +#define COPY_LAST_FRAME_8CH(a_pi32Dst, a_pi32Src, a_cChannels) do { \ + (a_pi32Dst)[0] = (a_pi32Src)[0]; \ + (a_pi32Dst)[1] = (a_pi32Src)[1]; \ + (a_pi32Dst)[2] = (a_pi32Src)[2]; \ + (a_pi32Dst)[3] = (a_pi32Src)[3]; \ + (a_pi32Dst)[4] = (a_pi32Src)[4]; \ + (a_pi32Dst)[5] = (a_pi32Src)[5]; \ + (a_pi32Dst)[6] = (a_pi32Src)[6]; \ + (a_pi32Dst)[7] = (a_pi32Src)[7]; \ + } while (0) +#define COPY_LAST_FRAME_9CH(a_pi32Dst, a_pi32Src, a_cChannels) do { \ + (a_pi32Dst)[0] = (a_pi32Src)[0]; \ + (a_pi32Dst)[1] = (a_pi32Src)[1]; \ + (a_pi32Dst)[2] = (a_pi32Src)[2]; \ + (a_pi32Dst)[3] = (a_pi32Src)[3]; \ + (a_pi32Dst)[4] = (a_pi32Src)[4]; \ + (a_pi32Dst)[5] = (a_pi32Src)[5]; \ + (a_pi32Dst)[6] = (a_pi32Src)[6]; \ + (a_pi32Dst)[7] = (a_pi32Src)[7]; \ + (a_pi32Dst)[8] = (a_pi32Src)[8]; \ + } while (0) +#define COPY_LAST_FRAME_10CH(a_pi32Dst, a_pi32Src, a_cChannels) do { \ + (a_pi32Dst)[0] = (a_pi32Src)[0]; \ + (a_pi32Dst)[1] = (a_pi32Src)[1]; \ + (a_pi32Dst)[2] = (a_pi32Src)[2]; \ + (a_pi32Dst)[3] = (a_pi32Src)[3]; \ + (a_pi32Dst)[4] = (a_pi32Src)[4]; \ + (a_pi32Dst)[5] = (a_pi32Src)[5]; \ + (a_pi32Dst)[6] = (a_pi32Src)[6]; \ + (a_pi32Dst)[7] = (a_pi32Src)[7]; \ + (a_pi32Dst)[8] = (a_pi32Src)[8]; \ + (a_pi32Dst)[9] = (a_pi32Src)[9]; \ + } while (0) +#define COPY_LAST_FRAME_11CH(a_pi32Dst, a_pi32Src, a_cChannels) do { \ + (a_pi32Dst)[0] = (a_pi32Src)[0]; \ + (a_pi32Dst)[1] = (a_pi32Src)[1]; \ + (a_pi32Dst)[2] = (a_pi32Src)[2]; \ + (a_pi32Dst)[3] = (a_pi32Src)[3]; \ + (a_pi32Dst)[4] = (a_pi32Src)[4]; \ + (a_pi32Dst)[5] = (a_pi32Src)[5]; \ + (a_pi32Dst)[6] = (a_pi32Src)[6]; \ + (a_pi32Dst)[7] = (a_pi32Src)[7]; \ + (a_pi32Dst)[8] = (a_pi32Src)[8]; \ + (a_pi32Dst)[9] = (a_pi32Src)[9]; \ + (a_pi32Dst)[10] = (a_pi32Src)[10]; \ + } while (0) +#define COPY_LAST_FRAME_12CH(a_pi32Dst, a_pi32Src, a_cChannels) do { \ + (a_pi32Dst)[0] = (a_pi32Src)[0]; \ + (a_pi32Dst)[1] = (a_pi32Src)[1]; \ + (a_pi32Dst)[2] = (a_pi32Src)[2]; \ + (a_pi32Dst)[3] = (a_pi32Src)[3]; \ + (a_pi32Dst)[4] = (a_pi32Src)[4]; \ + (a_pi32Dst)[5] = (a_pi32Src)[5]; \ + (a_pi32Dst)[6] = (a_pi32Src)[6]; \ + (a_pi32Dst)[7] = (a_pi32Src)[7]; \ + (a_pi32Dst)[8] = (a_pi32Src)[8]; \ + (a_pi32Dst)[9] = (a_pi32Src)[9]; \ + (a_pi32Dst)[10] = (a_pi32Src)[10]; \ + (a_pi32Dst)[11] = (a_pi32Src)[11]; \ + } while (0) + +#define INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, a_iCh) \ + (a_pi32Dst)[a_iCh] = ((a_pi32Last)[a_iCh] * a_i64FactorLast + (a_pi32Src)[a_iCh] * a_i64FactorCur) >> 32 +#define INTERPOLATE_1CH(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, a_cChannels) do { \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 0); \ + } while (0) +#define INTERPOLATE_2CH(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, a_cChannels) do { \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 0); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 1); \ + } while (0) +#define INTERPOLATE_3CH(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, a_cChannels) do { \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 0); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 1); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 2); \ + } while (0) +#define INTERPOLATE_4CH(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, a_cChannels) do { \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 0); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 1); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 2); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 3); \ + } while (0) +#define INTERPOLATE_5CH(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, a_cChannels) do { \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 0); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 1); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 2); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 3); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 4); \ + } while (0) +#define INTERPOLATE_6CH(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, a_cChannels) do { \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 0); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 1); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 2); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 3); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 4); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 5); \ + } while (0) +#define INTERPOLATE_7CH(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, a_cChannels) do { \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 0); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 1); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 2); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 3); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 4); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 5); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 6); \ + } while (0) +#define INTERPOLATE_8CH(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, a_cChannels) do { \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 0); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 1); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 2); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 3); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 4); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 5); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 6); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 7); \ + } while (0) +#define INTERPOLATE_9CH(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, a_cChannels) do { \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 0); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 1); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 2); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 3); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 4); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 5); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 6); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 7); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 8); \ + } while (0) +#define INTERPOLATE_10CH(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, a_cChannels) do { \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 0); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 1); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 2); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 3); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 4); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 5); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 6); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 7); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 8); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 9); \ + } while (0) +#define INTERPOLATE_11CH(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, a_cChannels) do { \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 0); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 1); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 2); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 3); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 4); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 5); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 6); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 7); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 8); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 9); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 10); \ + } while (0) +#define INTERPOLATE_12CH(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, a_cChannels) do { \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 0); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 1); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 2); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 3); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 4); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 5); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 6); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 7); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 8); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 9); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 10); \ + INTERPOLATE_ONE(a_pi32Dst, a_pi32Src, a_pi32Last, a_i64FactorCur, a_i64FactorLast, 11); \ + } while (0) + +#define AUDIOMIXBUF_RESAMPLE(a_cChannels, a_Suffix) \ + /** @returns Number of destination frames written. */ \ + static DECLCALLBACK(uint32_t) \ + audioMixBufResample##a_cChannels##Ch##a_Suffix(int32_t *pi32Dst, uint32_t cDstFrames, \ + int32_t const *pi32Src, uint32_t cSrcFrames, uint32_t *pcSrcFramesRead, \ + PAUDIOSTREAMRATE pRate) \ + { \ + Log5(("Src: %RU32 L %RU32; Dst: %RU32 L%RU32; uDstInc=%#RX64\n", \ + pRate->offSrc, cSrcFrames, RT_HI_U32(pRate->offDst), cDstFrames, pRate->uDstInc)); \ + int32_t * const pi32DstStart = pi32Dst; \ + int32_t const * const pi32SrcStart = pi32Src; \ + \ + int32_t ai32LastFrame[a_cChannels]; \ + COPY_LAST_FRAME_##a_cChannels##CH(ai32LastFrame, pRate->SrcLast.ai32Samples, a_cChannels); \ + \ + while (cDstFrames > 0 && cSrcFrames > 0) \ + { \ + int32_t const cSrcNeeded = RT_HI_U32(pRate->offDst) - pRate->offSrc + 1; \ + if (cSrcNeeded > 0) \ + { \ + if ((uint32_t)cSrcNeeded + 1 < cSrcFrames) \ + { \ + pRate->offSrc += (uint32_t)cSrcNeeded; \ + cSrcFrames -= (uint32_t)cSrcNeeded; \ + pi32Src += (uint32_t)cSrcNeeded * a_cChannels; \ + COPY_LAST_FRAME_##a_cChannels##CH(ai32LastFrame, &pi32Src[-a_cChannels], a_cChannels); \ + } \ + else \ + { \ + pi32Src += cSrcFrames * a_cChannels; \ + pRate->offSrc += cSrcFrames; \ + COPY_LAST_FRAME_##a_cChannels##CH(pRate->SrcLast.ai32Samples, &pi32Src[-a_cChannels], a_cChannels); \ + *pcSrcFramesRead = (pi32Src - pi32SrcStart) / a_cChannels; \ + return (pi32Dst - pi32DstStart) / a_cChannels; \ + } \ + } \ + \ + /* Interpolate. */ \ + int64_t const offFactorCur = pRate->offDst & UINT32_MAX; \ + int64_t const offFactorLast = (int64_t)_4G - offFactorCur; \ + INTERPOLATE_##a_cChannels##CH(pi32Dst, pi32Src, ai32LastFrame, offFactorCur, offFactorLast, a_cChannels); \ + \ + /* Advance. */ \ + pRate->offDst += pRate->uDstInc; \ + pi32Dst += a_cChannels; \ + cDstFrames -= 1; \ + } \ + \ + COPY_LAST_FRAME_##a_cChannels##CH(pRate->SrcLast.ai32Samples, ai32LastFrame, a_cChannels); \ + *pcSrcFramesRead = (pi32Src - pi32SrcStart) / a_cChannels; \ + return (pi32Dst - pi32DstStart) / a_cChannels; \ + } + +AUDIOMIXBUF_RESAMPLE(1,Generic) +AUDIOMIXBUF_RESAMPLE(2,Generic) +AUDIOMIXBUF_RESAMPLE(3,Generic) +AUDIOMIXBUF_RESAMPLE(4,Generic) +AUDIOMIXBUF_RESAMPLE(5,Generic) +AUDIOMIXBUF_RESAMPLE(6,Generic) +AUDIOMIXBUF_RESAMPLE(7,Generic) +AUDIOMIXBUF_RESAMPLE(8,Generic) +AUDIOMIXBUF_RESAMPLE(9,Generic) +AUDIOMIXBUF_RESAMPLE(10,Generic) +AUDIOMIXBUF_RESAMPLE(11,Generic) +AUDIOMIXBUF_RESAMPLE(12,Generic) + + +/** + * Resets the resampling state unconditionally. + * + * @param pRate The state to reset. + */ +static void audioMixBufRateResetAlways(PAUDIOSTREAMRATE pRate) +{ + pRate->offDst = 0; + pRate->offSrc = 0; + for (uintptr_t i = 0; i < RT_ELEMENTS(pRate->SrcLast.ai32Samples); i++) + pRate->SrcLast.ai32Samples[0] = 0; +} + + +/** + * Resets the resampling state. + * + * @param pRate The state to reset. + */ +DECLINLINE(void) audioMixBufRateReset(PAUDIOSTREAMRATE pRate) +{ + if (pRate->offDst == 0) + { /* likely */ } + else + { + Assert(!pRate->fNoConversionNeeded); + audioMixBufRateResetAlways(pRate); + } +} + + +/** + * Initializes the frame rate converter state. + * + * @returns VBox status code. + * @param pRate The state to initialize. + * @param uSrcHz The source frame rate. + * @param uDstHz The destination frame rate. + * @param cChannels The number of channels in a frame. + */ +DECLINLINE(int) audioMixBufRateInit(PAUDIOSTREAMRATE pRate, uint32_t uSrcHz, uint32_t uDstHz, uint8_t cChannels) +{ + /* + * Do we need to set up frequency conversion? + * + * Some examples to get an idea of what uDstInc holds: + * 44100 to 44100 -> (44100<<32) / 44100 = 0x01'00000000 (4294967296) + * 22050 to 44100 -> (22050<<32) / 44100 = 0x00'80000000 (2147483648) + * 44100 to 22050 -> (44100<<32) / 22050 = 0x02'00000000 (8589934592) + * 44100 to 48000 -> (44100<<32) / 48000 = 0x00'EB333333 (3946001203.2) + * 48000 to 44100 -> (48000<<32) / 44100 = 0x01'16A3B35F (4674794335.7823129251700680272109) + */ + audioMixBufRateResetAlways(pRate); + if (uSrcHz == uDstHz) + { + pRate->fNoConversionNeeded = true; + pRate->uDstInc = RT_BIT_64(32); + pRate->pfnResample = NULL; + } + else + { + pRate->fNoConversionNeeded = false; + pRate->uDstInc = ((uint64_t)uSrcHz << 32) / uDstHz; + AssertReturn(uSrcHz != 0, VERR_INVALID_PARAMETER); + switch (cChannels) + { + case 1: pRate->pfnResample = audioMixBufResample1ChGeneric; break; + case 2: pRate->pfnResample = audioMixBufResample2ChGeneric; break; + case 3: pRate->pfnResample = audioMixBufResample3ChGeneric; break; + case 4: pRate->pfnResample = audioMixBufResample4ChGeneric; break; + case 5: pRate->pfnResample = audioMixBufResample5ChGeneric; break; + case 6: pRate->pfnResample = audioMixBufResample6ChGeneric; break; + case 7: pRate->pfnResample = audioMixBufResample7ChGeneric; break; + case 8: pRate->pfnResample = audioMixBufResample8ChGeneric; break; + case 9: pRate->pfnResample = audioMixBufResample9ChGeneric; break; + case 10: pRate->pfnResample = audioMixBufResample10ChGeneric; break; + case 11: pRate->pfnResample = audioMixBufResample11ChGeneric; break; + case 12: pRate->pfnResample = audioMixBufResample12ChGeneric; break; + default: + AssertMsgFailedReturn(("resampling %u changes is not implemented yet\n", cChannels), VERR_OUT_OF_RANGE); + } + } + return VINF_SUCCESS; +} + + +/** + * Initializes a mixing buffer. + * + * @returns VBox status code. + * @param pMixBuf Mixing buffer to initialize. + * @param pszName Name of mixing buffer for easier identification. Optional. + * @param pProps PCM audio properties to use for the mixing buffer. + * @param cFrames Maximum number of audio frames the mixing buffer can hold. + */ +int AudioMixBufInit(PAUDIOMIXBUF pMixBuf, const char *pszName, PCPDMAUDIOPCMPROPS pProps, uint32_t cFrames) +{ + AssertPtrReturn(pMixBuf, VERR_INVALID_POINTER); + AssertPtrReturn(pszName, VERR_INVALID_POINTER); + AssertPtrReturn(pProps, VERR_INVALID_POINTER); + Assert(PDMAudioPropsAreValid(pProps)); + + /* + * Initialize all members, setting the volume to max (0dB). + */ + pMixBuf->cFrames = 0; + pMixBuf->pi32Samples = NULL; + pMixBuf->cChannels = 0; + pMixBuf->cbFrame = 0; + pMixBuf->offRead = 0; + pMixBuf->offWrite = 0; + pMixBuf->cUsed = 0; + pMixBuf->Props = *pProps; + pMixBuf->Volume.fMuted = false; + pMixBuf->Volume.fAllMax = true; + for (uintptr_t i = 0; i < RT_ELEMENTS(pMixBuf->Volume.auChannels); i++) + pMixBuf->Volume.auChannels[i] = AUDIOMIXBUF_VOL_0DB; + + int rc; + uint8_t const cChannels = PDMAudioPropsChannels(pProps); + if (cChannels >= 1 && cChannels <= PDMAUDIO_MAX_CHANNELS) + { + pMixBuf->pszName = RTStrDup(pszName); + if (pMixBuf->pszName) + { + pMixBuf->pi32Samples = (int32_t *)RTMemAllocZ(cFrames * cChannels * sizeof(pMixBuf->pi32Samples[0])); + if (pMixBuf->pi32Samples) + { + pMixBuf->cFrames = cFrames; + pMixBuf->cChannels = cChannels; + pMixBuf->cbFrame = cChannels * sizeof(pMixBuf->pi32Samples[0]); + pMixBuf->uMagic = AUDIOMIXBUF_MAGIC; +#ifdef AUDMIXBUF_LOG_ENABLED + char szTmp[PDMAUDIOPROPSTOSTRING_MAX]; + AUDMIXBUF_LOG(("%s: %s - cFrames=%#x (%d)\n", + pMixBuf->pszName, PDMAudioPropsToString(pProps, szTmp, sizeof(szTmp)), cFrames, cFrames)); +#endif + return VINF_SUCCESS; + } + RTStrFree(pMixBuf->pszName); + pMixBuf->pszName = NULL; + rc = VERR_NO_MEMORY; + } + else + rc = VERR_NO_STR_MEMORY; + } + else + { + LogRelMaxFunc(64, ("cChannels=%d pszName=%s\n", cChannels, pszName)); + rc = VERR_OUT_OF_RANGE; + } + pMixBuf->uMagic = AUDIOMIXBUF_MAGIC_DEAD; + return rc; +} + +/** + * Terminates (uninitializes) a mixing buffer. + * + * @param pMixBuf The mixing buffer. Uninitialized mixer buffers will be + * quietly ignored. As will NULL. + */ +void AudioMixBufTerm(PAUDIOMIXBUF pMixBuf) +{ + if (!pMixBuf) + return; + + /* Ignore calls for an uninitialized (zeroed) or already destroyed instance. Happens a lot. */ + if ( pMixBuf->uMagic == 0 + || pMixBuf->uMagic == AUDIOMIXBUF_MAGIC_DEAD) + { + Assert(!pMixBuf->pszName); + Assert(!pMixBuf->pi32Samples); + Assert(!pMixBuf->cFrames); + return; + } + + Assert(pMixBuf->uMagic == AUDIOMIXBUF_MAGIC); + pMixBuf->uMagic = ~AUDIOMIXBUF_MAGIC; + + if (pMixBuf->pszName) + { + AUDMIXBUF_LOG(("%s\n", pMixBuf->pszName)); + + RTStrFree(pMixBuf->pszName); + pMixBuf->pszName = NULL; + } + + if (pMixBuf->pi32Samples) + { + Assert(pMixBuf->cFrames); + RTMemFree(pMixBuf->pi32Samples); + pMixBuf->pi32Samples = NULL; + } + + pMixBuf->cFrames = 0; + pMixBuf->cChannels = 0; +} + + +/** + * Drops all the frames in the given mixing buffer + * + * This will reset the read and write offsets to zero. + * + * @param pMixBuf The mixing buffer. Uninitialized mixer buffers will be + * quietly ignored. + */ +void AudioMixBufDrop(PAUDIOMIXBUF pMixBuf) +{ + AssertPtrReturnVoid(pMixBuf); + + /* Ignore uninitialized (zeroed) mixer sink buffers (happens with AC'97 during VM construction). */ + if ( pMixBuf->uMagic == 0 + || pMixBuf->uMagic == AUDIOMIXBUF_MAGIC_DEAD) + return; + + AUDMIXBUF_LOG(("%s\n", pMixBuf->pszName)); + + pMixBuf->offRead = 0; + pMixBuf->offWrite = 0; + pMixBuf->cUsed = 0; +} + + +/** + * Gets the maximum number of audio frames this buffer can hold. + * + * @returns Number of frames. + * @param pMixBuf The mixing buffer. + */ +uint32_t AudioMixBufSize(PCAUDIOMIXBUF pMixBuf) +{ + AssertPtrReturn(pMixBuf, 0); + Assert(pMixBuf->uMagic == AUDIOMIXBUF_MAGIC); + return pMixBuf->cFrames; +} + + +/** + * Gets the maximum number of bytes this buffer can hold. + * + * @returns Number of bytes. + * @param pMixBuf The mixing buffer. + */ +uint32_t AudioMixBufSizeBytes(PCAUDIOMIXBUF pMixBuf) +{ + AssertPtrReturn(pMixBuf, 0); + AssertReturn(pMixBuf->uMagic == AUDIOMIXBUF_MAGIC, 0); + return AUDIOMIXBUF_F2B(pMixBuf, pMixBuf->cFrames); +} + + +/** + * Worker for AudioMixBufUsed and AudioMixBufUsedBytes. + */ +DECLINLINE(uint32_t) audioMixBufUsedInternal(PCAUDIOMIXBUF pMixBuf) +{ + uint32_t const cFrames = pMixBuf->cFrames; + uint32_t cUsed = pMixBuf->cUsed; + AssertStmt(cUsed <= cFrames, cUsed = cFrames); + return cUsed; +} + + +/** + * Get the number of used (readable) frames in the buffer. + * + * @returns Number of frames. + * @param pMixBuf The mixing buffer. + */ +uint32_t AudioMixBufUsed(PCAUDIOMIXBUF pMixBuf) +{ + AssertPtrReturn(pMixBuf, 0); + Assert(pMixBuf->uMagic == AUDIOMIXBUF_MAGIC); + return audioMixBufUsedInternal(pMixBuf); +} + + +/** + * Get the number of (readable) bytes in the buffer. + * + * @returns Number of bytes. + * @param pMixBuf The mixing buffer. + */ +uint32_t AudioMixBufUsedBytes(PCAUDIOMIXBUF pMixBuf) +{ + AssertPtrReturn(pMixBuf, 0); + Assert(pMixBuf->uMagic == AUDIOMIXBUF_MAGIC); + return AUDIOMIXBUF_F2B(pMixBuf, audioMixBufUsedInternal(pMixBuf)); +} + + +/** + * Worker for AudioMixBufFree and AudioMixBufFreeBytes. + */ +DECLINLINE(uint32_t) audioMixBufFreeInternal(PCAUDIOMIXBUF pMixBuf) +{ + uint32_t const cFrames = pMixBuf->cFrames; + uint32_t cUsed = pMixBuf->cUsed; + AssertStmt(cUsed <= cFrames, cUsed = cFrames); + uint32_t const cFramesFree = cFrames - cUsed; + + AUDMIXBUF_LOG(("%s: %RU32 of %RU32\n", pMixBuf->pszName, cFramesFree, cFrames)); + return cFramesFree; +} + + +/** + * Gets the free buffer space in frames. + * + * @return Number of frames. + * @param pMixBuf The mixing buffer. + */ +uint32_t AudioMixBufFree(PCAUDIOMIXBUF pMixBuf) +{ + AssertPtrReturn(pMixBuf, 0); + Assert(pMixBuf->uMagic == AUDIOMIXBUF_MAGIC); + return audioMixBufFreeInternal(pMixBuf); +} + + +/** + * Gets the free buffer space in bytes. + * + * @return Number of bytes. + * @param pMixBuf The mixing buffer. + */ +uint32_t AudioMixBufFreeBytes(PCAUDIOMIXBUF pMixBuf) +{ + AssertPtrReturn(pMixBuf, 0); + Assert(pMixBuf->uMagic == AUDIOMIXBUF_MAGIC); + return AUDIOMIXBUF_F2B(pMixBuf, audioMixBufFreeInternal(pMixBuf)); +} + + +/** + * Checks if the buffer is empty. + * + * @retval true if empty buffer. + * @retval false if not empty and there are frames to be processed. + * @param pMixBuf The mixing buffer. + */ +bool AudioMixBufIsEmpty(PCAUDIOMIXBUF pMixBuf) +{ + AssertPtrReturn(pMixBuf, true); + Assert(pMixBuf->uMagic == AUDIOMIXBUF_MAGIC); + return pMixBuf->cUsed == 0; +} + + +/** + * Get the current read position. + * + * This is for the testcase. + * + * @returns Frame number. + * @param pMixBuf The mixing buffer. + */ +uint32_t AudioMixBufReadPos(PCAUDIOMIXBUF pMixBuf) +{ + AssertPtrReturn(pMixBuf, 0); + Assert(pMixBuf->uMagic == AUDIOMIXBUF_MAGIC); + return pMixBuf->offRead; +} + + +/** + * Gets the current write position. + * + * This is for the testcase. + * + * @returns Frame number. + * @param pMixBuf The mixing buffer. + */ +uint32_t AudioMixBufWritePos(PCAUDIOMIXBUF pMixBuf) +{ + AssertPtrReturn(pMixBuf, 0); + Assert(pMixBuf->uMagic == AUDIOMIXBUF_MAGIC); + return pMixBuf->offWrite; +} + + +/** + * Creates a mapping between desination channels and source source channels. + * + * @param paidxChannelMap Where to store the mapping. Indexed by + * destination channel. Entry is either source + * channel index or -1 for zero and -2 for silence. + * @param pSrcProps The source properties. + * @param pDstProps The desination properties. + */ +static void audioMixBufInitChannelMap(int8_t paidxChannelMap[PDMAUDIO_MAX_CHANNELS], + PCPDMAUDIOPCMPROPS pSrcProps, PCPDMAUDIOPCMPROPS pDstProps) +{ + uintptr_t const cDstChannels = PDMAudioPropsChannels(pDstProps); + uintptr_t const cSrcChannels = PDMAudioPropsChannels(pSrcProps); + uintptr_t idxDst; + for (idxDst = 0; idxDst < cDstChannels; idxDst++) + { + uint8_t const idDstCh = pDstProps->aidChannels[idxDst]; + if (idDstCh >= PDMAUDIOCHANNELID_FRONT_LEFT && idDstCh < PDMAUDIOCHANNELID_END) + { + uintptr_t idxSrc; + for (idxSrc = 0; idxSrc < cSrcChannels; idxSrc++) + if (idDstCh == pSrcProps->aidChannels[idxSrc]) + { + paidxChannelMap[idxDst] = idxSrc; + break; + } + if (idxSrc >= cSrcChannels) + { + /** @todo deal with mono. */ + paidxChannelMap[idxDst] = -2; + } + } + else if (idDstCh == PDMAUDIOCHANNELID_UNKNOWN) + { + /** @todo What to do here? Pick unused source channels in order? */ + paidxChannelMap[idxDst] = -2; + } + else + { + AssertMsg(idDstCh == PDMAUDIOCHANNELID_UNUSED_SILENCE || idDstCh == PDMAUDIOCHANNELID_UNUSED_ZERO, + ("idxDst=%u idDstCh=%u\n", idxDst, idDstCh)); + paidxChannelMap[idxDst] = idDstCh == PDMAUDIOCHANNELID_UNUSED_SILENCE ? -2 : -1; + } + } + + /* Set the remainder to -1 just to be sure their are safe. */ + for (; idxDst < PDMAUDIO_MAX_CHANNELS; idxDst++) + paidxChannelMap[idxDst] = -1; +} + + +/** + * Initializes the peek state, setting up encoder and (if necessary) resampling. + * + * @returns VBox status code. + */ +int AudioMixBufInitPeekState(PCAUDIOMIXBUF pMixBuf, PAUDIOMIXBUFPEEKSTATE pState, PCPDMAUDIOPCMPROPS pProps) +{ + AssertPtr(pMixBuf); + AssertPtr(pState); + AssertPtr(pProps); + + /* + * Pick the encoding function first. + */ + uint8_t const cbSample = PDMAudioPropsSampleSize(pProps); + uint8_t const cSrcCh = PDMAudioPropsChannels(&pMixBuf->Props); + uint8_t const cDstCh = PDMAudioPropsChannels(pProps); + pState->cSrcChannels = cSrcCh; + pState->cDstChannels = cDstCh; + pState->cbDstFrame = PDMAudioPropsFrameSize(pProps); + audioMixBufInitChannelMap(pState->aidxChannelMap, &pMixBuf->Props, pProps); + AssertReturn(cDstCh > 0 && cDstCh <= PDMAUDIO_MAX_CHANNELS, VERR_OUT_OF_RANGE); + AssertReturn(cSrcCh > 0 && cSrcCh <= PDMAUDIO_MAX_CHANNELS, VERR_OUT_OF_RANGE); + + if (PDMAudioPropsIsSigned(pProps)) + { + /* Assign generic encoder first. */ + switch (cbSample) + { + case 1: pState->pfnEncode = audioMixBufEncodeGenericS8; break; + case 2: pState->pfnEncode = audioMixBufEncodeGenericS16; break; + case 4: pState->pfnEncode = audioMixBufEncodeGenericS32; break; + case 8: + AssertReturn(pProps->fRaw, VERR_DISK_INVALID_FORMAT); + pState->pfnEncode = audioMixBufEncodeGenericRaw; + break; + default: + AssertMsgFailedReturn(("%u bytes\n", cbSample), VERR_OUT_OF_RANGE); + } + + /* Any specializations available? */ + switch (cDstCh) + { + case 1: + if (cSrcCh == 1) + switch (cbSample) + { + case 1: pState->pfnEncode = audioMixBufEncode1ChTo1ChS8; break; + case 2: pState->pfnEncode = audioMixBufEncode1ChTo1ChS16; break; + case 4: pState->pfnEncode = audioMixBufEncode1ChTo1ChS32; break; + case 8: pState->pfnEncode = audioMixBufEncode1ChTo1ChRaw; break; + } + else if (cSrcCh == 2) + switch (cbSample) + { + case 1: pState->pfnEncode = audioMixBufEncode2ChTo1ChS8; break; + case 2: pState->pfnEncode = audioMixBufEncode2ChTo1ChS16; break; + case 4: pState->pfnEncode = audioMixBufEncode2ChTo1ChS32; break; + case 8: pState->pfnEncode = audioMixBufEncode2ChTo1ChRaw; break; + } + break; + + case 2: + if (cSrcCh == 1) + switch (cbSample) + { + case 1: pState->pfnEncode = audioMixBufEncode1ChTo2ChS8; break; + case 2: pState->pfnEncode = audioMixBufEncode1ChTo2ChS16; break; + case 4: pState->pfnEncode = audioMixBufEncode1ChTo2ChS32; break; + case 8: pState->pfnEncode = audioMixBufEncode1ChTo2ChRaw; break; + } + else if (cSrcCh == 2) + switch (cbSample) + { + case 1: pState->pfnEncode = audioMixBufEncode2ChTo2ChS8; break; + case 2: pState->pfnEncode = audioMixBufEncode2ChTo2ChS16; break; + case 4: pState->pfnEncode = audioMixBufEncode2ChTo2ChS32; break; + case 8: pState->pfnEncode = audioMixBufEncode2ChTo2ChRaw; break; + } + break; + } + } + else + { + /* Assign generic encoder first. */ + switch (cbSample) + { + case 1: pState->pfnEncode = audioMixBufEncodeGenericU8; break; + case 2: pState->pfnEncode = audioMixBufEncodeGenericU16; break; + case 4: pState->pfnEncode = audioMixBufEncodeGenericU32; break; + default: + AssertMsgFailedReturn(("%u bytes\n", cbSample), VERR_OUT_OF_RANGE); + } + + /* Any specializations available? */ + switch (cDstCh) + { + case 1: + if (cSrcCh == 1) + switch (cbSample) + { + case 1: pState->pfnEncode = audioMixBufEncode1ChTo1ChU8; break; + case 2: pState->pfnEncode = audioMixBufEncode1ChTo1ChU16; break; + case 4: pState->pfnEncode = audioMixBufEncode1ChTo1ChU32; break; + } + else if (cSrcCh == 2) + switch (cbSample) + { + case 1: pState->pfnEncode = audioMixBufEncode2ChTo1ChU8; break; + case 2: pState->pfnEncode = audioMixBufEncode2ChTo1ChU16; break; + case 4: pState->pfnEncode = audioMixBufEncode2ChTo1ChU32; break; + } + break; + + case 2: + if (cSrcCh == 1) + switch (cbSample) + { + case 1: pState->pfnEncode = audioMixBufEncode1ChTo2ChU8; break; + case 2: pState->pfnEncode = audioMixBufEncode1ChTo2ChU16; break; + case 4: pState->pfnEncode = audioMixBufEncode1ChTo2ChU32; break; + } + else if (cSrcCh == 2) + switch (cbSample) + { + case 1: pState->pfnEncode = audioMixBufEncode2ChTo2ChU8; break; + case 2: pState->pfnEncode = audioMixBufEncode2ChTo2ChU16; break; + case 4: pState->pfnEncode = audioMixBufEncode2ChTo2ChU32; break; + } + break; + } + } + + int rc = audioMixBufRateInit(&pState->Rate, PDMAudioPropsHz(&pMixBuf->Props), PDMAudioPropsHz(pProps), cSrcCh); + AUDMIXBUF_LOG(("%s: %RU32 Hz to %RU32 Hz => uDstInc=0x%'RX64\n", pMixBuf->pszName, PDMAudioPropsHz(&pMixBuf->Props), + PDMAudioPropsHz(pProps), pState->Rate.uDstInc)); + return rc; +} + + +/** + * Initializes the write/blend state, setting up decoders and (if necessary) + * resampling. + * + * @returns VBox status code. + */ +int AudioMixBufInitWriteState(PCAUDIOMIXBUF pMixBuf, PAUDIOMIXBUFWRITESTATE pState, PCPDMAUDIOPCMPROPS pProps) +{ + AssertPtr(pMixBuf); + AssertPtr(pState); + AssertPtr(pProps); + + /* + * Pick the encoding function first. + */ + uint8_t const cbSample = PDMAudioPropsSampleSize(pProps); + uint8_t const cSrcCh = PDMAudioPropsChannels(pProps); + uint8_t const cDstCh = PDMAudioPropsChannels(&pMixBuf->Props); + pState->cSrcChannels = cSrcCh; + pState->cDstChannels = cDstCh; + pState->cbSrcFrame = PDMAudioPropsFrameSize(pProps); + audioMixBufInitChannelMap(pState->aidxChannelMap, pProps, &pMixBuf->Props); + + if (PDMAudioPropsIsSigned(pProps)) + { + /* Assign generic decoders first. */ + switch (cbSample) + { + case 1: + pState->pfnDecode = audioMixBufDecodeGenericS8; + pState->pfnDecodeBlend = audioMixBufDecodeGenericS8Blend; + break; + case 2: + pState->pfnDecode = audioMixBufDecodeGenericS16; + pState->pfnDecodeBlend = audioMixBufDecodeGenericS16Blend; + break; + case 4: + pState->pfnDecode = audioMixBufDecodeGenericS32; + pState->pfnDecodeBlend = audioMixBufDecodeGenericS32Blend; + break; + case 8: + AssertReturn(pProps->fRaw, VERR_DISK_INVALID_FORMAT); + pState->pfnDecode = audioMixBufDecodeGenericRaw; + pState->pfnDecodeBlend = audioMixBufDecodeGenericRawBlend; + break; + default: + AssertMsgFailedReturn(("%u bytes\n", cbSample), VERR_OUT_OF_RANGE); + } + + /* Any specializations available? */ + switch (cDstCh) + { + case 1: + if (cSrcCh == 1) + switch (cbSample) + { + case 1: + pState->pfnDecode = audioMixBufDecode1ChTo1ChS8; + pState->pfnDecodeBlend = audioMixBufDecode1ChTo1ChS8Blend; + break; + case 2: + pState->pfnDecode = audioMixBufDecode1ChTo1ChS16; + pState->pfnDecodeBlend = audioMixBufDecode1ChTo1ChS16Blend; + break; + case 4: + pState->pfnDecode = audioMixBufDecode1ChTo1ChS32; + pState->pfnDecodeBlend = audioMixBufDecode1ChTo1ChS32Blend; + break; + case 8: + pState->pfnDecode = audioMixBufDecode1ChTo1ChRaw; + pState->pfnDecodeBlend = audioMixBufDecode1ChTo1ChRawBlend; + break; + } + else if (cSrcCh == 2) + switch (cbSample) + { + case 1: + pState->pfnDecode = audioMixBufDecode2ChTo1ChS8; + pState->pfnDecodeBlend = audioMixBufDecode2ChTo1ChS8Blend; + break; + case 2: + pState->pfnDecode = audioMixBufDecode2ChTo1ChS16; + pState->pfnDecodeBlend = audioMixBufDecode2ChTo1ChS16Blend; + break; + case 4: + pState->pfnDecode = audioMixBufDecode2ChTo1ChS32; + pState->pfnDecodeBlend = audioMixBufDecode2ChTo1ChS32Blend; + break; + case 8: + pState->pfnDecode = audioMixBufDecode2ChTo1ChRaw; + pState->pfnDecodeBlend = audioMixBufDecode2ChTo1ChRawBlend; + break; + } + break; + + case 2: + if (cSrcCh == 1) + switch (cbSample) + { + case 1: + pState->pfnDecode = audioMixBufDecode1ChTo2ChS8; + pState->pfnDecodeBlend = audioMixBufDecode1ChTo2ChS8Blend; + break; + case 2: + pState->pfnDecode = audioMixBufDecode1ChTo2ChS16; + pState->pfnDecodeBlend = audioMixBufDecode1ChTo2ChS16Blend; + break; + case 4: + pState->pfnDecode = audioMixBufDecode1ChTo2ChS32; + pState->pfnDecodeBlend = audioMixBufDecode1ChTo2ChS32Blend; + break; + case 8: + pState->pfnDecode = audioMixBufDecode1ChTo2ChRaw; + pState->pfnDecodeBlend = audioMixBufDecode1ChTo2ChRawBlend; + break; + } + else if (cSrcCh == 2) + switch (cbSample) + { + case 1: + pState->pfnDecode = audioMixBufDecode2ChTo2ChS8; + pState->pfnDecodeBlend = audioMixBufDecode2ChTo2ChS8Blend; + break; + case 2: + pState->pfnDecode = audioMixBufDecode2ChTo2ChS16; + pState->pfnDecodeBlend = audioMixBufDecode2ChTo2ChS16Blend; + break; + case 4: + pState->pfnDecode = audioMixBufDecode2ChTo2ChS32; + pState->pfnDecodeBlend = audioMixBufDecode2ChTo2ChS32Blend; + break; + case 8: + pState->pfnDecode = audioMixBufDecode2ChTo2ChRaw; + pState->pfnDecodeBlend = audioMixBufDecode2ChTo2ChRawBlend; + break; + } + break; + } + } + else + { + /* Assign generic decoders first. */ + switch (cbSample) + { + case 1: + pState->pfnDecode = audioMixBufDecodeGenericU8; + pState->pfnDecodeBlend = audioMixBufDecodeGenericU8Blend; + break; + case 2: + pState->pfnDecode = audioMixBufDecodeGenericU16; + pState->pfnDecodeBlend = audioMixBufDecodeGenericU16Blend; + break; + case 4: + pState->pfnDecode = audioMixBufDecodeGenericU32; + pState->pfnDecodeBlend = audioMixBufDecodeGenericU32Blend; + break; + default: + AssertMsgFailedReturn(("%u bytes\n", cbSample), VERR_OUT_OF_RANGE); + } + + /* Any specializations available? */ + switch (cDstCh) + { + case 1: + if (cSrcCh == 1) + switch (cbSample) + { + case 1: + pState->pfnDecode = audioMixBufDecode1ChTo1ChU8; + pState->pfnDecodeBlend = audioMixBufDecode1ChTo1ChU8Blend; + break; + case 2: + pState->pfnDecode = audioMixBufDecode1ChTo1ChU16; + pState->pfnDecodeBlend = audioMixBufDecode1ChTo1ChU16Blend; + break; + case 4: + pState->pfnDecode = audioMixBufDecode1ChTo1ChU32; + pState->pfnDecodeBlend = audioMixBufDecode1ChTo1ChU32Blend; + break; + } + else if (cSrcCh == 2) + switch (cbSample) + { + case 1: + pState->pfnDecode = audioMixBufDecode2ChTo1ChU8; + pState->pfnDecodeBlend = audioMixBufDecode2ChTo1ChU8Blend; + break; + case 2: + pState->pfnDecode = audioMixBufDecode2ChTo1ChU16; + pState->pfnDecodeBlend = audioMixBufDecode2ChTo1ChU16Blend; + break; + case 4: + pState->pfnDecode = audioMixBufDecode2ChTo1ChU32; + pState->pfnDecodeBlend = audioMixBufDecode2ChTo1ChU32Blend; + break; + } + break; + + case 2: + if (cSrcCh == 1) + switch (cbSample) + { + case 1: + pState->pfnDecode = audioMixBufDecode1ChTo2ChU8; + pState->pfnDecodeBlend = audioMixBufDecode1ChTo2ChU8Blend; + break; + case 2: + pState->pfnDecode = audioMixBufDecode1ChTo2ChU16; + pState->pfnDecodeBlend = audioMixBufDecode1ChTo2ChU16Blend; + break; + case 4: + pState->pfnDecode = audioMixBufDecode1ChTo2ChU32; + pState->pfnDecodeBlend = audioMixBufDecode1ChTo2ChU32Blend; + break; + } + else if (cSrcCh == 2) + switch (cbSample) + { + case 1: + pState->pfnDecode = audioMixBufDecode2ChTo2ChU8; + pState->pfnDecodeBlend = audioMixBufDecode2ChTo2ChU8Blend; + break; + case 2: + pState->pfnDecode = audioMixBufDecode2ChTo2ChU16; + pState->pfnDecodeBlend = audioMixBufDecode2ChTo2ChU16Blend; + break; + case 4: + pState->pfnDecode = audioMixBufDecode2ChTo2ChU32; + pState->pfnDecodeBlend = audioMixBufDecode2ChTo2ChU32Blend; + break; + } + break; + } + } + + int rc = audioMixBufRateInit(&pState->Rate, PDMAudioPropsHz(pProps), PDMAudioPropsHz(&pMixBuf->Props), cDstCh); + AUDMIXBUF_LOG(("%s: %RU32 Hz to %RU32 Hz => uDstInc=0x%'RX64\n", pMixBuf->pszName, PDMAudioPropsHz(pProps), + PDMAudioPropsHz(&pMixBuf->Props), pState->Rate.uDstInc)); + return rc; +} + + +/** + * Worker for AudioMixBufPeek that handles the rate conversion case. + */ +DECL_NO_INLINE(static, void) +audioMixBufPeekResampling(PCAUDIOMIXBUF pMixBuf, uint32_t offSrcFrame, uint32_t cMaxSrcFrames, uint32_t *pcSrcFramesPeeked, + PAUDIOMIXBUFPEEKSTATE pState, void *pvDst, uint32_t cbDst, uint32_t *pcbDstPeeked) +{ + *pcSrcFramesPeeked = 0; + *pcbDstPeeked = 0; + while (cMaxSrcFrames > 0 && cbDst >= pState->cbDstFrame) + { + /* Rate conversion into temporary buffer. */ + int32_t ai32DstRate[1024]; + uint32_t cSrcFrames = RT_MIN(pMixBuf->cFrames - offSrcFrame, cMaxSrcFrames); + uint32_t cDstMaxFrames = RT_MIN(RT_ELEMENTS(ai32DstRate) / pState->cSrcChannels, cbDst / pState->cbDstFrame); + uint32_t const cDstFrames = pState->Rate.pfnResample(ai32DstRate, cDstMaxFrames, + &pMixBuf->pi32Samples[offSrcFrame * pMixBuf->cChannels], + cSrcFrames, &cSrcFrames, &pState->Rate); + *pcSrcFramesPeeked += cSrcFrames; + cMaxSrcFrames -= cSrcFrames; + offSrcFrame = (offSrcFrame + cSrcFrames) % pMixBuf->cFrames; + + /* Encode the converted frames. */ + uint32_t const cbDstEncoded = cDstFrames * pState->cbDstFrame; + pState->pfnEncode(pvDst, ai32DstRate, cDstFrames, pState); + *pcbDstPeeked += cbDstEncoded; + cbDst -= cbDstEncoded; + pvDst = (uint8_t *)pvDst + cbDstEncoded; + } +} + + +/** + * Copies data out of the mixing buffer, converting it if needed, but leaves the + * read offset untouched. + * + * @param pMixBuf The mixing buffer. + * @param offSrcFrame The offset to start reading at relative to + * current read position (offRead). The caller has + * made sure there is at least this number of + * frames available in the buffer before calling. + * @param cMaxSrcFrames Maximum number of frames to read. + * @param pcSrcFramesPeeked Where to return the actual number of frames read + * from the mixing buffer. + * @param pState Output configuration & conversion state. + * @param pvDst The destination buffer. + * @param cbDst The size of the destination buffer in bytes. + * @param pcbDstPeeked Where to put the actual number of bytes + * returned. + */ +void AudioMixBufPeek(PCAUDIOMIXBUF pMixBuf, uint32_t offSrcFrame, uint32_t cMaxSrcFrames, uint32_t *pcSrcFramesPeeked, + PAUDIOMIXBUFPEEKSTATE pState, void *pvDst, uint32_t cbDst, uint32_t *pcbDstPeeked) +{ + /* + * Check inputs. + */ + AssertPtr(pMixBuf); + Assert(pMixBuf->uMagic == AUDIOMIXBUF_MAGIC); + AssertPtr(pState); + AssertPtr(pState->pfnEncode); + Assert(pState->cSrcChannels == PDMAudioPropsChannels(&pMixBuf->Props)); + Assert(cMaxSrcFrames > 0); + Assert(cMaxSrcFrames <= pMixBuf->cFrames); + Assert(offSrcFrame <= pMixBuf->cFrames); + Assert(offSrcFrame + cMaxSrcFrames <= pMixBuf->cUsed); + AssertPtr(pcSrcFramesPeeked); + AssertPtr(pvDst); + Assert(cbDst >= pState->cbDstFrame); + AssertPtr(pcbDstPeeked); + + /* + * Make start frame absolute. + */ + offSrcFrame = (pMixBuf->offRead + offSrcFrame) % pMixBuf->cFrames; + + /* + * Hopefully no sample rate conversion is necessary... + */ + if (pState->Rate.fNoConversionNeeded) + { + /* Figure out how much we should convert. */ + cMaxSrcFrames = RT_MIN(cMaxSrcFrames, cbDst / pState->cbDstFrame); + *pcSrcFramesPeeked = cMaxSrcFrames; + *pcbDstPeeked = cMaxSrcFrames * pState->cbDstFrame; + + /* First chunk. */ + uint32_t const cSrcFrames1 = RT_MIN(pMixBuf->cFrames - offSrcFrame, cMaxSrcFrames); + pState->pfnEncode(pvDst, &pMixBuf->pi32Samples[offSrcFrame * pMixBuf->cChannels], cSrcFrames1, pState); + + /* Another chunk from the start of the mixing buffer? */ + if (cMaxSrcFrames > cSrcFrames1) + pState->pfnEncode((uint8_t *)pvDst + cSrcFrames1 * pState->cbDstFrame, + &pMixBuf->pi32Samples[0], cMaxSrcFrames - cSrcFrames1, pState); + + //Log9Func(("*pcbDstPeeked=%#x\n%32.*Rhxd\n", *pcbDstPeeked, *pcbDstPeeked, pvDst)); + } + else + audioMixBufPeekResampling(pMixBuf, offSrcFrame, cMaxSrcFrames, pcSrcFramesPeeked, pState, pvDst, cbDst, pcbDstPeeked); +} + + +/** + * Worker for AudioMixBufWrite that handles the rate conversion case. + */ +DECL_NO_INLINE(static, void) +audioMixBufWriteResampling(PAUDIOMIXBUF pMixBuf, PAUDIOMIXBUFWRITESTATE pState, const void *pvSrcBuf, uint32_t cbSrcBuf, + uint32_t offDstFrame, uint32_t cDstMaxFrames, uint32_t *pcDstFramesWritten) +{ + *pcDstFramesWritten = 0; + while (cDstMaxFrames > 0 && cbSrcBuf >= pState->cbSrcFrame) + { + /* Decode into temporary buffer. */ + int32_t ai32Decoded[1024]; + uint32_t cFramesDecoded = RT_MIN(RT_ELEMENTS(ai32Decoded) / pState->cDstChannels, cbSrcBuf / pState->cbSrcFrame); + pState->pfnDecode(ai32Decoded, pvSrcBuf, cFramesDecoded, pState); + cbSrcBuf -= cFramesDecoded * pState->cbSrcFrame; + pvSrcBuf = (uint8_t const *)pvSrcBuf + cFramesDecoded * pState->cbSrcFrame; + + /* Rate convert that into the mixer. */ + uint32_t iFrameDecoded = 0; + while (iFrameDecoded < cFramesDecoded) + { + uint32_t cDstMaxFramesNow = RT_MIN(pMixBuf->cFrames - offDstFrame, cDstMaxFrames); + uint32_t cSrcFrames = cFramesDecoded - iFrameDecoded; + uint32_t const cDstFrames = pState->Rate.pfnResample(&pMixBuf->pi32Samples[offDstFrame * pMixBuf->cChannels], + cDstMaxFramesNow, + &ai32Decoded[iFrameDecoded * pState->cDstChannels], + cSrcFrames, &cSrcFrames, &pState->Rate); + + iFrameDecoded += cSrcFrames; + *pcDstFramesWritten += cDstFrames; + offDstFrame = (offDstFrame + cDstFrames) % pMixBuf->cFrames; + } + } + + /** @todo How to squeeze odd frames out of 22050 => 44100 conversion? */ +} + + +/** + * Writes @a cbSrcBuf bytes to the mixer buffer starting at @a offDstFrame, + * converting it as needed, leaving the write offset untouched. + * + * @param pMixBuf The mixing buffer. + * @param pState Source configuration & conversion state. + * @param pvSrcBuf The source frames. + * @param cbSrcBuf Number of bytes of source frames. This will be + * convered in full. + * @param offDstFrame Mixing buffer offset relative to the write + * position. + * @param cDstMaxFrames Max number of frames to write. + * @param pcDstFramesWritten Where to return the number of frames actually + * written. + * + * @note Does not advance the write position, please call AudioMixBufCommit() + * to do that. + */ +void AudioMixBufWrite(PAUDIOMIXBUF pMixBuf, PAUDIOMIXBUFWRITESTATE pState, const void *pvSrcBuf, uint32_t cbSrcBuf, + uint32_t offDstFrame, uint32_t cDstMaxFrames, uint32_t *pcDstFramesWritten) +{ + /* + * Check inputs. + */ + AssertPtr(pMixBuf); + Assert(pMixBuf->uMagic == AUDIOMIXBUF_MAGIC); + AssertPtr(pState); + AssertPtr(pState->pfnDecode); + AssertPtr(pState->pfnDecodeBlend); + Assert(pState->cDstChannels == PDMAudioPropsChannels(&pMixBuf->Props)); + Assert(cDstMaxFrames > 0); + Assert(cDstMaxFrames <= pMixBuf->cFrames - pMixBuf->cUsed); + Assert(offDstFrame <= pMixBuf->cFrames); + AssertPtr(pvSrcBuf); + Assert(!(cbSrcBuf % pState->cbSrcFrame)); + AssertPtr(pcDstFramesWritten); + + /* + * Make start frame absolute. + */ + offDstFrame = (pMixBuf->offWrite + offDstFrame) % pMixBuf->cFrames; + + /* + * Hopefully no sample rate conversion is necessary... + */ + if (pState->Rate.fNoConversionNeeded) + { + /* Figure out how much we should convert. */ + Assert(cDstMaxFrames >= cbSrcBuf / pState->cbSrcFrame); + cDstMaxFrames = RT_MIN(cDstMaxFrames, cbSrcBuf / pState->cbSrcFrame); + *pcDstFramesWritten = cDstMaxFrames; + + //Log10Func(("cbSrc=%#x\n%32.*Rhxd\n", pState->cbSrcFrame * cDstMaxFrames, pState->cbSrcFrame * cDstMaxFrames, pvSrcBuf)); + + /* First chunk. */ + uint32_t const cDstFrames1 = RT_MIN(pMixBuf->cFrames - offDstFrame, cDstMaxFrames); + pState->pfnDecode(&pMixBuf->pi32Samples[offDstFrame * pMixBuf->cChannels], pvSrcBuf, cDstFrames1, pState); + //Log8Func(("offDstFrame=%#x cDstFrames1=%#x\n%32.*Rhxd\n", offDstFrame, cDstFrames1, + // cDstFrames1 * pMixBuf->cbFrame, &pMixBuf->pi32Samples[offDstFrame * pMixBuf->cChannels])); + + /* Another chunk from the start of the mixing buffer? */ + if (cDstMaxFrames > cDstFrames1) + { + pState->pfnDecode(&pMixBuf->pi32Samples[0], (uint8_t *)pvSrcBuf + cDstFrames1 * pState->cbSrcFrame, + cDstMaxFrames - cDstFrames1, pState); + //Log8Func(("cDstFrames2=%#x\n%32.*Rhxd\n", cDstMaxFrames - cDstFrames1, + // (cDstMaxFrames - cDstFrames1) * pMixBuf->cbFrame, &pMixBuf->pi32Samples[0])); + } + } + else + audioMixBufWriteResampling(pMixBuf, pState, pvSrcBuf, cbSrcBuf, offDstFrame, cDstMaxFrames, pcDstFramesWritten); +} + + +/** + * Worker for AudioMixBufBlend that handles the rate conversion case. + */ +DECL_NO_INLINE(static, void) +audioMixBufBlendResampling(PAUDIOMIXBUF pMixBuf, PAUDIOMIXBUFWRITESTATE pState, const void *pvSrcBuf, uint32_t cbSrcBuf, + uint32_t offDstFrame, uint32_t cDstMaxFrames, uint32_t *pcDstFramesBlended) +{ + *pcDstFramesBlended = 0; + while (cDstMaxFrames > 0 && cbSrcBuf >= pState->cbSrcFrame) + { + /* Decode into temporary buffer. This then has the destination channel count. */ + int32_t ai32Decoded[1024]; + uint32_t cFramesDecoded = RT_MIN(RT_ELEMENTS(ai32Decoded) / pState->cDstChannels, cbSrcBuf / pState->cbSrcFrame); + pState->pfnDecode(ai32Decoded, pvSrcBuf, cFramesDecoded, pState); + cbSrcBuf -= cFramesDecoded * pState->cbSrcFrame; + pvSrcBuf = (uint8_t const *)pvSrcBuf + cFramesDecoded * pState->cbSrcFrame; + + /* Rate convert that into another temporary buffer and then blend that into the mixer. */ + uint32_t iFrameDecoded = 0; + while (iFrameDecoded < cFramesDecoded) + { + int32_t ai32Rate[1024]; + uint32_t cDstMaxFramesNow = RT_MIN(RT_ELEMENTS(ai32Rate) / pState->cDstChannels, cDstMaxFrames); + uint32_t cSrcFrames = cFramesDecoded - iFrameDecoded; + uint32_t const cDstFrames = pState->Rate.pfnResample(&ai32Rate[0], cDstMaxFramesNow, + &ai32Decoded[iFrameDecoded * pState->cDstChannels], + cSrcFrames, &cSrcFrames, &pState->Rate); + + /* First chunk.*/ + uint32_t const cDstFrames1 = RT_MIN(pMixBuf->cFrames - offDstFrame, cDstFrames); + audioMixBufBlendBuffer(&pMixBuf->pi32Samples[offDstFrame * pMixBuf->cChannels], + ai32Rate, cDstFrames1, pState->cDstChannels); + + /* Another chunk from the start of the mixing buffer? */ + if (cDstFrames > cDstFrames1) + audioMixBufBlendBuffer(&pMixBuf->pi32Samples[0], &ai32Rate[cDstFrames1 * pState->cDstChannels], + cDstFrames - cDstFrames1, pState->cDstChannels); + + /* Advance */ + iFrameDecoded += cSrcFrames; + *pcDstFramesBlended += cDstFrames; + offDstFrame = (offDstFrame + cDstFrames) % pMixBuf->cFrames; + } + } + + /** @todo How to squeeze odd frames out of 22050 => 44100 conversion? */ +} + + +/** + * @todo not sure if 'blend' is the appropriate term here, but you know what + * we mean. + */ +void AudioMixBufBlend(PAUDIOMIXBUF pMixBuf, PAUDIOMIXBUFWRITESTATE pState, const void *pvSrcBuf, uint32_t cbSrcBuf, + uint32_t offDstFrame, uint32_t cDstMaxFrames, uint32_t *pcDstFramesBlended) +{ + /* + * Check inputs. + */ + AssertPtr(pMixBuf); + Assert(pMixBuf->uMagic == AUDIOMIXBUF_MAGIC); + AssertPtr(pState); + AssertPtr(pState->pfnDecode); + AssertPtr(pState->pfnDecodeBlend); + Assert(pState->cDstChannels == PDMAudioPropsChannels(&pMixBuf->Props)); + Assert(cDstMaxFrames > 0); + Assert(cDstMaxFrames <= pMixBuf->cFrames - pMixBuf->cUsed); + Assert(offDstFrame <= pMixBuf->cFrames); + AssertPtr(pvSrcBuf); + Assert(!(cbSrcBuf % pState->cbSrcFrame)); + AssertPtr(pcDstFramesBlended); + + /* + * Make start frame absolute. + */ + offDstFrame = (pMixBuf->offWrite + offDstFrame) % pMixBuf->cFrames; + + /* + * Hopefully no sample rate conversion is necessary... + */ + if (pState->Rate.fNoConversionNeeded) + { + /* Figure out how much we should convert. */ + Assert(cDstMaxFrames >= cbSrcBuf / pState->cbSrcFrame); + cDstMaxFrames = RT_MIN(cDstMaxFrames, cbSrcBuf / pState->cbSrcFrame); + *pcDstFramesBlended = cDstMaxFrames; + + /* First chunk. */ + uint32_t const cDstFrames1 = RT_MIN(pMixBuf->cFrames - offDstFrame, cDstMaxFrames); + pState->pfnDecodeBlend(&pMixBuf->pi32Samples[offDstFrame * pMixBuf->cChannels], pvSrcBuf, cDstFrames1, pState); + + /* Another chunk from the start of the mixing buffer? */ + if (cDstMaxFrames > cDstFrames1) + pState->pfnDecodeBlend(&pMixBuf->pi32Samples[0], (uint8_t *)pvSrcBuf + cDstFrames1 * pState->cbSrcFrame, + cDstMaxFrames - cDstFrames1, pState); + } + else + audioMixBufBlendResampling(pMixBuf, pState, pvSrcBuf, cbSrcBuf, offDstFrame, cDstMaxFrames, pcDstFramesBlended); +} + + +/** + * Writes @a cFrames of silence at @a offFrame relative to current write pos. + * + * This will also adjust the resampling state. + * + * @param pMixBuf The mixing buffer. + * @param pState The write state. + * @param offFrame Where to start writing silence relative to the current + * write position. + * @param cFrames Number of frames of silence. + * @sa AudioMixBufWrite + * + * @note Does not advance the write position, please call AudioMixBufCommit() + * to do that. + */ +void AudioMixBufSilence(PAUDIOMIXBUF pMixBuf, PAUDIOMIXBUFWRITESTATE pState, uint32_t offFrame, uint32_t cFrames) +{ + /* + * Check inputs. + */ + AssertPtr(pMixBuf); + Assert(pMixBuf->uMagic == AUDIOMIXBUF_MAGIC); + AssertPtr(pState); + AssertPtr(pState->pfnDecode); + AssertPtr(pState->pfnDecodeBlend); + Assert(pState->cDstChannels == PDMAudioPropsChannels(&pMixBuf->Props)); + Assert(cFrames > 0); +#ifdef VBOX_STRICT + uint32_t const cMixBufFree = pMixBuf->cFrames - pMixBuf->cUsed; +#endif + Assert(cFrames <= cMixBufFree); + Assert(offFrame < cMixBufFree); + Assert(offFrame + cFrames <= cMixBufFree); + + /* + * Make start frame absolute. + */ + offFrame = (pMixBuf->offWrite + offFrame) % pMixBuf->cFrames; + + /* + * First chunk. + */ + uint32_t const cFramesChunk1 = RT_MIN(pMixBuf->cFrames - offFrame, cFrames); + RT_BZERO(&pMixBuf->pi32Samples[offFrame * pMixBuf->cChannels], cFramesChunk1 * pMixBuf->cbFrame); + + /* + * Second chunk, if needed. + */ + if (cFrames > cFramesChunk1) + { + cFrames -= cFramesChunk1; + AssertStmt(cFrames <= pMixBuf->cFrames, cFrames = pMixBuf->cFrames); + RT_BZERO(&pMixBuf->pi32Samples[0], cFrames * pMixBuf->cbFrame); + } + + /* + * Reset the resampling state. + */ + audioMixBufRateReset(&pState->Rate); +} + + +/** + * Records a blending gap (silence) of @a cFrames. + * + * This is used to adjust or reset the resampling state so we start from a + * silence state the next time we need to blend or write using @a pState. + * + * @param pMixBuf The mixing buffer. + * @param pState The write state. + * @param cFrames Number of frames of silence. + * @sa AudioMixBufSilence + */ +void AudioMixBufBlendGap(PAUDIOMIXBUF pMixBuf, PAUDIOMIXBUFWRITESTATE pState, uint32_t cFrames) +{ + /* + * For now we'll just reset the resampling state regardless of how many + * frames of silence there is. + */ + audioMixBufRateReset(&pState->Rate); + RT_NOREF(pMixBuf, cFrames); +} + + +/** + * Advances the read position of the buffer. + * + * For use after done peeking with AudioMixBufPeek(). + * + * @param pMixBuf The mixing buffer. + * @param cFrames Number of frames to advance. + * @sa AudioMixBufCommit + */ +void AudioMixBufAdvance(PAUDIOMIXBUF pMixBuf, uint32_t cFrames) +{ + AssertPtrReturnVoid(pMixBuf); + AssertReturnVoid(pMixBuf->uMagic == AUDIOMIXBUF_MAGIC); + + AssertStmt(cFrames <= pMixBuf->cUsed, cFrames = pMixBuf->cUsed); + pMixBuf->cUsed -= cFrames; + pMixBuf->offRead = (pMixBuf->offRead + cFrames) % pMixBuf->cFrames; + LogFlowFunc(("%s: Advanced %u frames: offRead=%u cUsed=%u\n", pMixBuf->pszName, cFrames, pMixBuf->offRead, pMixBuf->cUsed)); +} + + +/** + * Worker for audioMixAdjustVolume that adjust one contiguous chunk. + */ +static void audioMixAdjustVolumeWorker(PAUDIOMIXBUF pMixBuf, uint32_t off, uint32_t cFrames) +{ + int32_t *pi32Samples = &pMixBuf->pi32Samples[off * pMixBuf->cChannels]; + switch (pMixBuf->cChannels) + { + case 1: + { + uint32_t const uFactorCh0 = pMixBuf->Volume.auChannels[0]; + while (cFrames-- > 0) + { + *pi32Samples = (int32_t)(ASMMult2xS32RetS64(*pi32Samples, uFactorCh0) >> AUDIOMIXBUF_VOL_SHIFT); + pi32Samples++; + } + break; + } + + case 2: + { + uint32_t const uFactorCh0 = pMixBuf->Volume.auChannels[0]; + uint32_t const uFactorCh1 = pMixBuf->Volume.auChannels[1]; + while (cFrames-- > 0) + { + pi32Samples[0] = (int32_t)(ASMMult2xS32RetS64(pi32Samples[0], uFactorCh0) >> AUDIOMIXBUF_VOL_SHIFT); + pi32Samples[1] = (int32_t)(ASMMult2xS32RetS64(pi32Samples[1], uFactorCh1) >> AUDIOMIXBUF_VOL_SHIFT); + pi32Samples += 2; + } + break; + } + + case 3: + { + uint32_t const uFactorCh0 = pMixBuf->Volume.auChannels[0]; + uint32_t const uFactorCh1 = pMixBuf->Volume.auChannels[1]; + uint32_t const uFactorCh2 = pMixBuf->Volume.auChannels[2]; + while (cFrames-- > 0) + { + pi32Samples[0] = (int32_t)(ASMMult2xS32RetS64(pi32Samples[0], uFactorCh0) >> AUDIOMIXBUF_VOL_SHIFT); + pi32Samples[1] = (int32_t)(ASMMult2xS32RetS64(pi32Samples[1], uFactorCh1) >> AUDIOMIXBUF_VOL_SHIFT); + pi32Samples[2] = (int32_t)(ASMMult2xS32RetS64(pi32Samples[2], uFactorCh2) >> AUDIOMIXBUF_VOL_SHIFT); + pi32Samples += 3; + } + break; + } + + case 4: + { + uint32_t const uFactorCh0 = pMixBuf->Volume.auChannels[0]; + uint32_t const uFactorCh1 = pMixBuf->Volume.auChannels[1]; + uint32_t const uFactorCh2 = pMixBuf->Volume.auChannels[2]; + uint32_t const uFactorCh3 = pMixBuf->Volume.auChannels[3]; + while (cFrames-- > 0) + { + pi32Samples[0] = (int32_t)(ASMMult2xS32RetS64(pi32Samples[0], uFactorCh0) >> AUDIOMIXBUF_VOL_SHIFT); + pi32Samples[1] = (int32_t)(ASMMult2xS32RetS64(pi32Samples[1], uFactorCh1) >> AUDIOMIXBUF_VOL_SHIFT); + pi32Samples[2] = (int32_t)(ASMMult2xS32RetS64(pi32Samples[2], uFactorCh2) >> AUDIOMIXBUF_VOL_SHIFT); + pi32Samples[3] = (int32_t)(ASMMult2xS32RetS64(pi32Samples[3], uFactorCh3) >> AUDIOMIXBUF_VOL_SHIFT); + pi32Samples += 4; + } + break; + } + + case 5: + { + uint32_t const uFactorCh0 = pMixBuf->Volume.auChannels[0]; + uint32_t const uFactorCh1 = pMixBuf->Volume.auChannels[1]; + uint32_t const uFactorCh2 = pMixBuf->Volume.auChannels[2]; + uint32_t const uFactorCh3 = pMixBuf->Volume.auChannels[3]; + uint32_t const uFactorCh4 = pMixBuf->Volume.auChannels[4]; + while (cFrames-- > 0) + { + pi32Samples[0] = (int32_t)(ASMMult2xS32RetS64(pi32Samples[0], uFactorCh0) >> AUDIOMIXBUF_VOL_SHIFT); + pi32Samples[1] = (int32_t)(ASMMult2xS32RetS64(pi32Samples[1], uFactorCh1) >> AUDIOMIXBUF_VOL_SHIFT); + pi32Samples[2] = (int32_t)(ASMMult2xS32RetS64(pi32Samples[2], uFactorCh2) >> AUDIOMIXBUF_VOL_SHIFT); + pi32Samples[3] = (int32_t)(ASMMult2xS32RetS64(pi32Samples[3], uFactorCh3) >> AUDIOMIXBUF_VOL_SHIFT); + pi32Samples[4] = (int32_t)(ASMMult2xS32RetS64(pi32Samples[4], uFactorCh4) >> AUDIOMIXBUF_VOL_SHIFT); + pi32Samples += 5; + } + break; + } + + case 6: + { + uint32_t const uFactorCh0 = pMixBuf->Volume.auChannels[0]; + uint32_t const uFactorCh1 = pMixBuf->Volume.auChannels[1]; + uint32_t const uFactorCh2 = pMixBuf->Volume.auChannels[2]; + uint32_t const uFactorCh3 = pMixBuf->Volume.auChannels[3]; + uint32_t const uFactorCh4 = pMixBuf->Volume.auChannels[4]; + uint32_t const uFactorCh5 = pMixBuf->Volume.auChannels[5]; + while (cFrames-- > 0) + { + pi32Samples[0] = (int32_t)(ASMMult2xS32RetS64(pi32Samples[0], uFactorCh0) >> AUDIOMIXBUF_VOL_SHIFT); + pi32Samples[1] = (int32_t)(ASMMult2xS32RetS64(pi32Samples[1], uFactorCh1) >> AUDIOMIXBUF_VOL_SHIFT); + pi32Samples[2] = (int32_t)(ASMMult2xS32RetS64(pi32Samples[2], uFactorCh2) >> AUDIOMIXBUF_VOL_SHIFT); + pi32Samples[3] = (int32_t)(ASMMult2xS32RetS64(pi32Samples[3], uFactorCh3) >> AUDIOMIXBUF_VOL_SHIFT); + pi32Samples[4] = (int32_t)(ASMMult2xS32RetS64(pi32Samples[4], uFactorCh4) >> AUDIOMIXBUF_VOL_SHIFT); + pi32Samples[5] = (int32_t)(ASMMult2xS32RetS64(pi32Samples[5], uFactorCh5) >> AUDIOMIXBUF_VOL_SHIFT); + pi32Samples += 6; + } + break; + } + + default: + while (cFrames-- > 0) + for (uint32_t iCh = 0; iCh < pMixBuf->cChannels; iCh++, pi32Samples++) + *pi32Samples = ASMMult2xS32RetS64(*pi32Samples, pMixBuf->Volume.auChannels[iCh]) >> AUDIOMIXBUF_VOL_SHIFT; + break; + } +} + + +/** + * Does volume adjustments for the given stretch of the buffer. + * + * @param pMixBuf The mixing buffer. + * @param offFirst Where to start (validated). + * @param cFrames How many frames (validated). + */ +static void audioMixAdjustVolume(PAUDIOMIXBUF pMixBuf, uint32_t offFirst, uint32_t cFrames) +{ + /* Caller has already validated these, so we don't need to repeat that in non-strict builds. */ + Assert(offFirst < pMixBuf->cFrames); + Assert(cFrames <= pMixBuf->cFrames); + + /* + * Muted? + */ + if (pMixBuf->Volume.fMuted) + { + /* first chunk */ + uint32_t const cFramesChunk1 = RT_MIN(pMixBuf->cFrames - offFirst, cFrames); + RT_BZERO(&pMixBuf->pi32Samples[offFirst * pMixBuf->cChannels], pMixBuf->cbFrame * cFramesChunk1); + + /* second chunk */ + if (cFramesChunk1 < cFrames) + RT_BZERO(&pMixBuf->pi32Samples[0], pMixBuf->cbFrame * (cFrames - cFramesChunk1)); + } + /* + * Less than max volume? + */ + else if (!pMixBuf->Volume.fAllMax) + { + /* first chunk */ + uint32_t const cFramesChunk1 = RT_MIN(pMixBuf->cFrames - offFirst, cFrames); + audioMixAdjustVolumeWorker(pMixBuf, offFirst, cFramesChunk1); + + /* second chunk */ + if (cFramesChunk1 < cFrames) + audioMixAdjustVolumeWorker(pMixBuf, 0, cFrames - cFramesChunk1); + } +} + + +/** + * Adjust for volume settings and advances the write position of the buffer. + * + * For use after done peeking with AudioMixBufWrite(), AudioMixBufSilence(), + * AudioMixBufBlend() and AudioMixBufBlendGap(). + * + * @param pMixBuf The mixing buffer. + * @param cFrames Number of frames to advance. + * @sa AudioMixBufAdvance, AudioMixBufSetVolume + */ +void AudioMixBufCommit(PAUDIOMIXBUF pMixBuf, uint32_t cFrames) +{ + AssertPtrReturnVoid(pMixBuf); + AssertReturnVoid(pMixBuf->uMagic == AUDIOMIXBUF_MAGIC); + + AssertStmt(cFrames <= pMixBuf->cFrames - pMixBuf->cUsed, cFrames = pMixBuf->cFrames - pMixBuf->cUsed); + + audioMixAdjustVolume(pMixBuf, pMixBuf->offWrite, cFrames); + + pMixBuf->cUsed += cFrames; + pMixBuf->offWrite = (pMixBuf->offWrite + cFrames) % pMixBuf->cFrames; + LogFlowFunc(("%s: Advanced %u frames: offWrite=%u cUsed=%u\n", pMixBuf->pszName, cFrames, pMixBuf->offWrite, pMixBuf->cUsed)); +} + + +/** + * Sets the volume. + * + * The volume adjustments are applied by AudioMixBufCommit(). + * + * @param pMixBuf Mixing buffer to set volume for. + * @param pVol Pointer to volume structure to set. + */ +void AudioMixBufSetVolume(PAUDIOMIXBUF pMixBuf, PCPDMAUDIOVOLUME pVol) +{ + AssertPtrReturnVoid(pMixBuf); + AssertPtrReturnVoid(pVol); + + LogFlowFunc(("%s: fMuted=%RTbool auChannels=%.*Rhxs\n", + pMixBuf->pszName, pVol->fMuted, sizeof(pVol->auChannels), pVol->auChannels)); + + /* + * Convert PDM audio volume to the internal format. + */ + if (!pVol->fMuted) + { + pMixBuf->Volume.fMuted = false; + + AssertCompileSize(pVol->auChannels[0], sizeof(uint8_t)); + for (uintptr_t i = 0; i < pMixBuf->cChannels; i++) + pMixBuf->Volume.auChannels[i] = s_aVolumeConv[pVol->auChannels[i]] * (AUDIOMIXBUF_VOL_0DB >> 16); + + pMixBuf->Volume.fAllMax = true; + for (uintptr_t i = 0; i < pMixBuf->cChannels; i++) + if (pMixBuf->Volume.auChannels[i] != AUDIOMIXBUF_VOL_0DB) + { + pMixBuf->Volume.fAllMax = false; + break; + } + } + else + { + pMixBuf->Volume.fMuted = true; + pMixBuf->Volume.fAllMax = false; + for (uintptr_t i = 0; i < RT_ELEMENTS(pMixBuf->Volume.auChannels); i++) + pMixBuf->Volume.auChannels[i] = 0; + } +} + diff --git a/src/VBox/Devices/Audio/AudioMixBuffer.h b/src/VBox/Devices/Audio/AudioMixBuffer.h new file mode 100644 index 00000000..339366eb --- /dev/null +++ b/src/VBox/Devices/Audio/AudioMixBuffer.h @@ -0,0 +1,252 @@ +/* $Id: AudioMixBuffer.h $ */ +/** @file + * Audio Mixing bufer convert audio samples to/from different rates / formats. + */ + +/* + * 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 + */ + +#ifndef VBOX_INCLUDED_SRC_Audio_AudioMixBuffer_h +#define VBOX_INCLUDED_SRC_Audio_AudioMixBuffer_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include <iprt/cdefs.h> +#include <VBox/vmm/pdmaudioifs.h> + +/** @defgroup grp_pdm_ifs_audio_mixing_buffers Audio Mixing Buffers + * @ingroup grp_pdm_ifs_audio_mixing + * + * @note This is currently placed under PDM Audio Interface as that seemed like + * the best place for it. + * + * @{ + */ + + +/** + * Rate processing information of a source & destination audio stream. + * + * This is needed because both streams can differ regarding their rates and + * therefore need to be treated accordingly. + */ +typedef struct AUDIOSTREAMRATE +{ + /** Current (absolute) offset in the output (destination) stream. + * @todo r=bird: Please reveal which unit these members are given in. */ + uint64_t offDst; + /** Increment for moving offDst for the destination stream. + * This is needed because the source <-> destination rate might be different. */ + uint64_t uDstInc; + /** Current (absolute) offset in the input stream. */ + uint32_t offSrc; + /** Set if no conversion is necessary. */ + bool fNoConversionNeeded; + bool afPadding[3]; + + /** Last processed frame of the input stream. + * Needed for interpolation. */ + union + { + int32_t ai32Samples[PDMAUDIO_MAX_CHANNELS]; + } SrcLast; + + /** + * Resampling function. + * @returns Number of destination frames written. + */ + DECLR3CALLBACKMEMBER(uint32_t, pfnResample, (int32_t *pi32Dst, uint32_t cDstFrames, + int32_t const *pi32Src, uint32_t cSrcFrames, uint32_t *pcSrcFramesRead, + struct AUDIOSTREAMRATE *pRate)); + +} AUDIOSTREAMRATE; +/** Pointer to rate processing information of a stream. */ +typedef AUDIOSTREAMRATE *PAUDIOSTREAMRATE; + +/** + * Mixing buffer volume parameters. + * + * The volume values are in fixed point style and must be converted to/from + * before using with e.g. PDMAUDIOVOLUME. + */ +typedef struct AUDMIXBUFVOL +{ + /** Set to @c true if this stream is muted, @c false if not. */ + bool fMuted; + /** Set if all (relevant) channels are at max. */ + bool fAllMax; + /** The per-channels values. */ + uint32_t auChannels[PDMAUDIO_MAX_CHANNELS]; +} AUDMIXBUFVOL; +/** Pointer to mixing buffer volument parameters. */ +typedef AUDMIXBUFVOL *PAUDMIXBUFVOL; + + +/** Pointer to audio mixing buffer. */ +typedef struct AUDIOMIXBUF *PAUDIOMIXBUF; +/** Pointer to a const audio mixing buffer. */ +typedef struct AUDIOMIXBUF const *PCAUDIOMIXBUF; + + +/** + * State & config for AudioMixBufPeek created by AudioMixBufInitPeekState. + */ +typedef struct AUDIOMIXBUFPEEKSTATE +{ + /** Encodes @a cFrames from @a paSrc to @a pvDst. */ + DECLR3CALLBACKMEMBER(void, pfnEncode,(void *pvDst, int32_t const *paSrc, uint32_t cFrames, struct AUDIOMIXBUFPEEKSTATE *pState)); + /** Sample rate conversion state (only used when needed). */ + AUDIOSTREAMRATE Rate; + /** Source (mixer) channels. */ + uint8_t cSrcChannels; + /** Destination channels. */ + uint8_t cDstChannels; + /** Destination frame size. */ + uint8_t cbDstFrame; + /** The destination frame layout described as indexes into the source frame. + * This ASSUMES that all channels uses the same sample size, so one sample per + * channel if you like. + * Negative values are special: -1 for zero, -2 for silence. + * @note Blending stereo into mono is not really expressible here. */ + int8_t aidxChannelMap[PDMAUDIO_MAX_CHANNELS]; +} AUDIOMIXBUFPEEKSTATE; +/** Pointer to peek state & config. */ +typedef AUDIOMIXBUFPEEKSTATE *PAUDIOMIXBUFPEEKSTATE; + + +/** + * State & config for AudioMixBufWrite, AudioMixBufSilence, AudioMixBufBlend and + * AudioMixBufBlendGap, created by AudioMixBufInitWriteState. + */ +typedef struct AUDIOMIXBUFWRITESTATE +{ + /** Encodes @a cFrames from @a pvSrc to @a paDst. */ + DECLR3CALLBACKMEMBER(void, pfnDecode,(int32_t *paDst, const void *pvSrc, uint32_t cFrames, struct AUDIOMIXBUFWRITESTATE *pState)); + /** Encodes @a cFrames from @a pvSrc blending into @a paDst. */ + DECLR3CALLBACKMEMBER(void, pfnDecodeBlend,(int32_t *paDst, const void *pvSrc, uint32_t cFrames, struct AUDIOMIXBUFWRITESTATE *pState)); + /** Sample rate conversion state (only used when needed). */ + AUDIOSTREAMRATE Rate; + /** Destination (mixer) channels. */ + uint8_t cDstChannels; + /** Source hannels. */ + uint8_t cSrcChannels; + /** Source frame size. */ + uint8_t cbSrcFrame; + /** The destination frame layout described as indexes into the source frame. + * This ASSUMES that all channels uses the same sample size, so one sample per + * channel if you like. + * Negative values are special: -1 for zero, -2 for silence. + * @note Blending stereo into mono is not really expressible here. */ + int8_t aidxChannelMap[PDMAUDIO_MAX_CHANNELS]; +} AUDIOMIXBUFWRITESTATE; +/** Pointer to write state & config. */ +typedef AUDIOMIXBUFWRITESTATE *PAUDIOMIXBUFWRITESTATE; + + +/** + * Audio mixing buffer. + */ +typedef struct AUDIOMIXBUF +{ + /** Magic value (AUDIOMIXBUF_MAGIC). */ + uint32_t uMagic; + /** Size of the frame buffer (in audio frames). */ + uint32_t cFrames; + /** The frame buffer. + * This is a two dimensional array consisting of cFrames rows and + * cChannels columns. */ + int32_t *pi32Samples; + /** The number of channels. */ + uint8_t cChannels; + /** The frame size (row size if you like). */ + uint8_t cbFrame; + uint8_t abPadding[2]; + /** The current read position (in frames). */ + uint32_t offRead; + /** The current write position (in frames). */ + uint32_t offWrite; + /** How much audio frames are currently being used in this buffer. + * @note This also is known as the distance in ring buffer terms. */ + uint32_t cUsed; + /** Audio properties for the buffer content - for frequency and channel count. + * (This is the guest side PCM properties.) */ + PDMAUDIOPCMPROPS Props; + /** Internal representation of current volume used for mixing. */ + AUDMIXBUFVOL Volume; + /** Name of the buffer. */ + char *pszName; +} AUDIOMIXBUF; + +/** Magic value for AUDIOMIXBUF (Antonio Lucio Vivaldi). */ +#define AUDIOMIXBUF_MAGIC UINT32_C(0x16780304) +/** Dead mixer buffer magic. */ +#define AUDIOMIXBUF_MAGIC_DEAD UINT32_C(0x17410728) + +/** Converts (audio) frames to bytes. */ +#define AUDIOMIXBUF_F2B(a_pMixBuf, a_cFrames) PDMAUDIOPCMPROPS_F2B(&(a_pMixBuf)->Props, a_cFrames) +/** Converts bytes to (audio) frames. + * @note Does *not* take the conversion ratio into account. */ +#define AUDIOMIXBUF_B2F(a_pMixBuf, a_cb) PDMAUDIOPCMPROPS_B2F(&(a_pMixBuf)->Props, a_cb) + + +int AudioMixBufInit(PAUDIOMIXBUF pMixBuf, const char *pszName, PCPDMAUDIOPCMPROPS pProps, uint32_t cFrames); +void AudioMixBufTerm(PAUDIOMIXBUF pMixBuf); +void AudioMixBufDrop(PAUDIOMIXBUF pMixBuf); +void AudioMixBufSetVolume(PAUDIOMIXBUF pMixBuf, PCPDMAUDIOVOLUME pVol); + +/** @name Mixer buffer getters + * @{ */ +uint32_t AudioMixBufSize(PCAUDIOMIXBUF pMixBuf); +uint32_t AudioMixBufSizeBytes(PCAUDIOMIXBUF pMixBuf); +uint32_t AudioMixBufUsed(PCAUDIOMIXBUF pMixBuf); +uint32_t AudioMixBufUsedBytes(PCAUDIOMIXBUF pMixBuf); +uint32_t AudioMixBufFree(PCAUDIOMIXBUF pMixBuf); +uint32_t AudioMixBufFreeBytes(PCAUDIOMIXBUF pMixBuf); +bool AudioMixBufIsEmpty(PCAUDIOMIXBUF pMixBuf); +uint32_t AudioMixBufReadPos(PCAUDIOMIXBUF pMixBuf); +uint32_t AudioMixBufWritePos(PCAUDIOMIXBUF pMixBuf); +/** @} */ + +/** @name Mixer buffer reading + * @{ */ +int AudioMixBufInitPeekState(PCAUDIOMIXBUF pMixBuf, PAUDIOMIXBUFPEEKSTATE pState, PCPDMAUDIOPCMPROPS pDstProps); +void AudioMixBufPeek(PCAUDIOMIXBUF pMixBuf, uint32_t offSrcFrame, uint32_t cMaxSrcFrames, uint32_t *pcSrcFramesPeeked, + PAUDIOMIXBUFPEEKSTATE pState, void *pvDst, uint32_t cbDst, uint32_t *pcbDstPeeked); +void AudioMixBufAdvance(PAUDIOMIXBUF pMixBuf, uint32_t cFrames); +/** @} */ + +/** @name Mixer buffer writing + * @{ */ +int AudioMixBufInitWriteState(PCAUDIOMIXBUF pMixBuf, PAUDIOMIXBUFWRITESTATE pState, PCPDMAUDIOPCMPROPS pSrcProps); +void AudioMixBufWrite(PAUDIOMIXBUF pMixBuf, PAUDIOMIXBUFWRITESTATE pState, const void *pvSrcBuf, uint32_t cbSrcBuf, + uint32_t offDstFrame, uint32_t cMaxDstFrames, uint32_t *pcDstFramesWritten); +void AudioMixBufSilence(PAUDIOMIXBUF pMixBuf, PAUDIOMIXBUFWRITESTATE pState, uint32_t offFrame, uint32_t cFrames); +void AudioMixBufBlend(PAUDIOMIXBUF pMixBuf, PAUDIOMIXBUFWRITESTATE pState, const void *pvSrcBuf, uint32_t cbSrcBuf, + uint32_t offDstFrame, uint32_t cMaxDstFrames, uint32_t *pcDstFramesBlended); +void AudioMixBufBlendGap(PAUDIOMIXBUF pMixBuf, PAUDIOMIXBUFWRITESTATE pState, uint32_t cFrames); +void AudioMixBufCommit(PAUDIOMIXBUF pMixBuf, uint32_t cFrames); +/** @} */ + +/** @} */ +#endif /* !VBOX_INCLUDED_SRC_Audio_AudioMixBuffer_h */ + 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")); +} + diff --git a/src/VBox/Devices/Audio/AudioMixer.h b/src/VBox/Devices/Audio/AudioMixer.h new file mode 100644 index 00000000..3611787f --- /dev/null +++ b/src/VBox/Devices/Audio/AudioMixer.h @@ -0,0 +1,349 @@ +/* $Id: AudioMixer.h $ */ +/** @file + * VBox audio - Mixing routines. + * + * The mixing routines are mainly used by the various audio device emulations + * to achieve proper multiplexing from/to attached devices LUNs. + */ + +/* + * 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 + */ + +#ifndef VBOX_INCLUDED_SRC_Audio_AudioMixer_h +#define VBOX_INCLUDED_SRC_Audio_AudioMixer_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include <iprt/cdefs.h> +#include <iprt/critsect.h> + +#include <VBox/vmm/pdmaudioifs.h> +#include "AudioMixBuffer.h" +#include "AudioHlp.h" + + +/** @defgroup grp_pdm_ifs_audio_mixing Audio Mixing + * @ingroup grp_pdm_ifs_audio + * + * @note This is currently placed under PDM Audio Interface as that seemed like + * the best place for it. + * + * @{ + */ + +/** Pointer to an audio mixer sink. */ +typedef struct AUDMIXSINK *PAUDMIXSINK; +/** Pointer to a const audio mixer sink. */ +typedef struct AUDMIXSINK const *PCAUDMIXSINK; + + +/** + * Audio mixer instance. + */ +typedef struct AUDIOMIXER +{ + /** Magic value (AUDIOMIXER_MAGIC). */ + uintptr_t uMagic; + /** The mixer's name (allocated after this structure). */ + char const *pszName; + /** The master volume of this mixer. */ + PDMAUDIOVOLUME VolMaster; + /** List of audio mixer sinks (AUDMIXSINK). */ + RTLISTANCHOR lstSinks; + /** Number of used audio sinks. */ + uint8_t cSinks; + /** Mixer flags. See AUDMIXER_FLAGS_XXX. */ + uint32_t fFlags; + /** The mixer's critical section. */ + RTCRITSECT CritSect; +} AUDIOMIXER; +/** Pointer to an audio mixer instance. */ +typedef AUDIOMIXER *PAUDIOMIXER; + +/** Value for AUDIOMIXER::uMagic. (Attilio Joseph "Teo" Macero) */ +#define AUDIOMIXER_MAGIC UINT32_C(0x19251030) +/** Value for AUDIOMIXER::uMagic after destruction. */ +#define AUDIOMIXER_MAGIC_DEAD UINT32_C(0x20080219) + +/** @name AUDMIXER_FLAGS_XXX - For AudioMixerCreate(). + * @{ */ +/** No mixer flags specified. */ +#define AUDMIXER_FLAGS_NONE 0 +/** Debug mode enabled. + * This writes .WAV file to the host, usually to the temporary directory. */ +#define AUDMIXER_FLAGS_DEBUG RT_BIT(0) +/** Validation mask. */ +#define AUDMIXER_FLAGS_VALID_MASK UINT32_C(0x00000001) +/** @} */ + + +/** + * Audio mixer stream. + */ +typedef struct AUDMIXSTREAM +{ + /** List entry on AUDMIXSINK::lstStreams. */ + RTLISTNODE Node; + /** Magic value (AUDMIXSTREAM_MAGIC). */ + uint32_t uMagic; + /** The backend buffer size in frames (for draining deadline calc). */ + uint32_t cFramesBackendBuffer; + /** Stream status of type AUDMIXSTREAM_STATUS_. */ + uint32_t fStatus; + /** Number of writable/readable frames the last time we checked. */ + uint32_t cFramesLastAvail; + /** Set if the stream has been found unreliable wrt. consuming/producing + * samples, and that we shouldn't consider it when deciding how much to move + * from the mixer buffer and to the drivers. */ + bool fUnreliable; + /** Name of this stream. */ + char *pszName; + /** The statistics prefix. */ + char *pszStatPrefix; + /** Sink this stream is attached to. */ + PAUDMIXSINK pSink; + /** Pointer to audio connector being used. */ + PPDMIAUDIOCONNECTOR pConn; + /** Pointer to PDM audio stream this mixer stream handles. */ + PPDMAUDIOSTREAM pStream; + union + { + /** Output: Mixing buffer peeking state & config. */ + AUDIOMIXBUFPEEKSTATE PeekState; + /** Input: Mixing buffer writing state & config. */ + AUDIOMIXBUFWRITESTATE WriteState; + }; + /** Last read (recording) / written (playback) timestamp (in ns). */ + uint64_t tsLastReadWrittenNs; + /** The streams's critical section. */ + RTCRITSECT CritSect; +} AUDMIXSTREAM; +/** Pointer to an audio mixer stream. */ +typedef AUDMIXSTREAM *PAUDMIXSTREAM; + +/** Value for AUDMIXSTREAM::uMagic. (Jan Erik Kongshaug) */ +#define AUDMIXSTREAM_MAGIC UINT32_C(0x19440704) +/** Value for AUDMIXSTREAM::uMagic after destruction. */ +#define AUDMIXSTREAM_MAGIC_DEAD UINT32_C(0x20191105) + + +/** @name AUDMIXSTREAM_STATUS_XXX - mixer stream status. + * (This is a destilled version of PDMAUDIOSTREAM_STS_XXX.) + * @{ */ +/** No status set. */ +#define AUDMIXSTREAM_STATUS_NONE UINT32_C(0) +/** The mixing stream is enabled (active). */ +#define AUDMIXSTREAM_STATUS_ENABLED RT_BIT_32(0) +/** The mixing stream can be read from. + * Always set together with AUDMIXSTREAM_STATUS_ENABLED. */ +#define AUDMIXSTREAM_STATUS_CAN_READ RT_BIT_32(1) +/** The mixing stream can be written to. + * Always set together with AUDMIXSTREAM_STATUS_ENABLED. */ +#define AUDMIXSTREAM_STATUS_CAN_WRITE RT_BIT_32(2) +/** @} */ + + +/** Callback for an asynchronous I/O update job. */ +typedef DECLCALLBACKTYPE(void, FNAUDMIXSINKUPDATE,(PPDMDEVINS pDevIns, PAUDMIXSINK pSink, void *pvUser)); +/** Pointer to a callback for an asynchronous I/O update job. */ +typedef FNAUDMIXSINKUPDATE *PFNAUDMIXSINKUPDATE; + +/** + * Audio mixer sink. + */ +typedef struct AUDMIXSINK +{ + /** List entry on AUDIOMIXER::lstSinks. */ + RTLISTNODE Node; + /** Magic value (AUDMIXSINK_MAGIC). */ + uint32_t uMagic; + /** The sink direction (either PDMAUDIODIR_IN or PDMAUDIODIR_OUT). */ + PDMAUDIODIR enmDir; + /** Pointer to mixer object this sink is bound to. */ + PAUDIOMIXER pParent; + /** Name of this sink (allocated after this structure). */ + char const *pszName; + /** The sink's PCM format (i.e. the guest device side). */ + PDMAUDIOPCMPROPS PCMProps; + /** Sink status bits - AUDMIXSINK_STS_XXX. */ + uint32_t fStatus; + /** Number of bytes to be transferred from the device DMA buffer before the + * streams will be put into draining mode. */ + uint32_t cbDmaLeftToDrain; + /** The deadline for draining if it's pending. */ + uint64_t nsDrainDeadline; + /** When the draining startet (for logging). */ + uint64_t nsDrainStarted; + /** Number of streams assigned. */ + uint8_t cStreams; + /** List of assigned streams (AUDMIXSTREAM). + * @note All streams have the same PCM properties, so the mixer does not do + * any conversion. bird: That is *NOT* true any more, the mixer has + * encoders/decoder states for each stream (well, input is still a todo). + * + * @todo Use something faster -- vector maybe? bird: It won't be faster. You + * will have a vector of stream pointers (because you cannot have a vector + * of full AUDMIXSTREAM structures since they'll move when the vector is + * reallocated and we need pointers to them to give out to devices), which + * is the same cost as going via Node.pNext/pPrev. */ + RTLISTANCHOR lstStreams; + /** The volume of this sink. The volume always will + * be combined with the mixer's master volume. */ + PDMAUDIOVOLUME Volume; + /** The volume of this sink, combined with the last set master volume. */ + PDMAUDIOVOLUME VolumeCombined; + /** Timestamp since last update (in ms). */ + uint64_t tsLastUpdatedMs; + /** Last read (recording) / written (playback) timestamp (in ns). */ + uint64_t tsLastReadWrittenNs; + /** Union for input/output specifics. */ + union + { + struct + { + /** The sink's peek state. */ + AUDIOMIXBUFPEEKSTATE State; + } In; + struct + { + /** The sink's write state. */ + AUDIOMIXBUFWRITESTATE State; + } Out; + }; + struct + { + PAUDIOHLPFILE pFile; + } Dbg; + /** This sink's mixing buffer. */ + AUDIOMIXBUF MixBuf; + /** Asynchronous I/O thread related stuff. */ + struct + { + /** The thread handle, NIL_RTTHREAD if not active. */ + RTTHREAD hThread; + /** Event for letting the thread know there is some data to process. */ + RTSEMEVENT hEvent; + /** The device instance (same for all update jobs). */ + PPDMDEVINS pDevIns; + /** Started indicator. */ + volatile bool fStarted; + /** Shutdown indicator. */ + volatile bool fShutdown; + /** Number of update jobs this sink has (usually zero or one). */ + uint8_t cUpdateJobs; + /** The minimum typical interval for all jobs. */ + uint32_t cMsMinTypicalInterval; + /** Update jobs for this sink. */ + struct + { + /** User specific argument. */ + void *pvUser; + /** The callback. */ + PFNAUDMIXSINKUPDATE pfnUpdate; + /** Typical interval in milliseconds. */ + uint32_t cMsTypicalInterval; + } aUpdateJobs[8]; + } AIO; + /** The sink's critical section. */ + RTCRITSECT CritSect; +} AUDMIXSINK; + +/** Value for AUDMIXSINK::uMagic. (Sir George Martin) */ +#define AUDMIXSINK_MAGIC UINT32_C(0x19260103) +/** Value for AUDMIXSINK::uMagic after destruction. */ +#define AUDMIXSINK_MAGIC_DEAD UINT32_C(0x20160308) + + +/** @name AUDMIXSINK_STS_XXX - Sink status bits. + * @{ */ +/** No status specified. */ +#define AUDMIXSINK_STS_NONE 0 +/** The sink is active and running. */ +#define AUDMIXSINK_STS_RUNNING RT_BIT(0) +/** Draining the buffers and pending stop - output only. */ +#define AUDMIXSINK_STS_DRAINING RT_BIT(1) +/** Drained the DMA buffer. */ +#define AUDMIXSINK_STS_DRAINED_DMA RT_BIT(2) +/** Drained the mixer buffer, only waiting for streams (drivers) now. */ +#define AUDMIXSINK_STS_DRAINED_MIXBUF RT_BIT(3) +/** Dirty flag. + * - For output sinks this means that there is data in the sink which has not + * been played yet. + * - For input sinks this means that there is data in the sink which has been + * recorded but not transferred to the destination yet. + * @todo This isn't used for *anything* at the moment. Remove? */ +#define AUDMIXSINK_STS_DIRTY RT_BIT(4) +/** @} */ + + +/** @name Audio mixer methods + * @{ */ +int AudioMixerCreate(const char *pszName, uint32_t fFlags, PAUDIOMIXER *ppMixer); +void AudioMixerDestroy(PAUDIOMIXER pMixer, PPDMDEVINS pDevIns); +void AudioMixerDebug(PAUDIOMIXER pMixer, PCDBGFINFOHLP pHlp, const char *pszArgs); +int AudioMixerSetMasterVolume(PAUDIOMIXER pMixer, PCPDMAUDIOVOLUME pVol); +int AudioMixerCreateSink(PAUDIOMIXER pMixer, const char *pszName, PDMAUDIODIR enmDir, PPDMDEVINS pDevIns, PAUDMIXSINK *ppSink); +/** @} */ + +/** @name Audio mixer sink methods + * @{ */ +int AudioMixerSinkStart(PAUDMIXSINK pSink); +int AudioMixerSinkDrainAndStop(PAUDMIXSINK pSink, uint32_t cbComming); +void AudioMixerSinkDestroy(PAUDMIXSINK pSink, PPDMDEVINS pDevIns); +uint32_t AudioMixerSinkGetReadable(PAUDMIXSINK pSink); +uint32_t AudioMixerSinkGetWritable(PAUDMIXSINK pSink); +PDMAUDIODIR AudioMixerSinkGetDir(PCAUDMIXSINK pSink); +uint32_t AudioMixerSinkGetStatus(PAUDMIXSINK pSink); +bool AudioMixerSinkIsActive(PAUDMIXSINK pSink); +void AudioMixerSinkReset(PAUDMIXSINK pSink); +int AudioMixerSinkSetFormat(PAUDMIXSINK pSink, PCPDMAUDIOPCMPROPS pPCMProps, uint32_t cMsSchedulingHint); +int AudioMixerSinkSetVolume(PAUDMIXSINK pSink, PCPDMAUDIOVOLUME pVol); +int AudioMixerSinkUpdate(PAUDMIXSINK pSink, uint32_t cbDmaUsed, uint32_t cbDmaPeriod); + +int AudioMixerSinkAddUpdateJob(PAUDMIXSINK pSink, PFNAUDMIXSINKUPDATE pfnUpdate, void *pvUser, uint32_t cMsTypicalInterval); +int AudioMixerSinkRemoveUpdateJob(PAUDMIXSINK pSink, PFNAUDMIXSINKUPDATE pfnUpdate, void *pvUser); +int AudioMixerSinkSignalUpdateJob(PAUDMIXSINK pSink); +uint64_t AudioMixerSinkTransferFromCircBuf(PAUDMIXSINK pSink, PRTCIRCBUF pCircBuf, uint64_t offStream, + uint32_t idStream, PAUDIOHLPFILE pDbgFile); +uint64_t AudioMixerSinkTransferToCircBuf(PAUDMIXSINK pSink, PRTCIRCBUF pCircBuf, uint64_t offStream, + uint32_t idStream, PAUDIOHLPFILE pDbgFile); +int AudioMixerSinkLock(PAUDMIXSINK pSink); +int AudioMixerSinkTryLock(PAUDMIXSINK pSink); +int AudioMixerSinkUnlock(PAUDMIXSINK pSink); + +int AudioMixerSinkCreateStream(PAUDMIXSINK pSink, PPDMIAUDIOCONNECTOR pConnector, PCPDMAUDIOSTREAMCFG pCfg, + PPDMDEVINS pDevIns, PAUDMIXSTREAM *ppStream); +int AudioMixerSinkAddStream(PAUDMIXSINK pSink, PAUDMIXSTREAM pStream); +void AudioMixerSinkRemoveStream(PAUDMIXSINK pSink, PAUDMIXSTREAM pStream); +void AudioMixerSinkRemoveAllStreams(PAUDMIXSINK pSink); +/** @} */ + +/** @name Audio mixer stream methods + * @{ */ +void AudioMixerStreamDestroy(PAUDMIXSTREAM pStream, PPDMDEVINS pDevIns, bool fImmediate); +/** @} */ + +/** @} */ + +#endif /* !VBOX_INCLUDED_SRC_Audio_AudioMixer_h */ + diff --git a/src/VBox/Devices/Audio/AudioTest.cpp b/src/VBox/Devices/Audio/AudioTest.cpp new file mode 100644 index 00000000..8694d154 --- /dev/null +++ b/src/VBox/Devices/Audio/AudioTest.cpp @@ -0,0 +1,3581 @@ +/* $Id: AudioTest.cpp $ */ +/** @file + * Audio testing routines. + * + * Common code which is being used by the ValidationKit and the + * debug / ValdikationKit audio driver(s). + */ + +/* + * Copyright (C) 2021-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 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <package-generated.h> +#include "product-generated.h" + +#include <iprt/buildconfig.h> +#include <iprt/cdefs.h> +#include <iprt/dir.h> +#include <iprt/env.h> +#include <iprt/file.h> +#include <iprt/formats/riff.h> +#include <iprt/inifile.h> +#include <iprt/list.h> +#include <iprt/message.h> /** @todo Get rid of this once we have own log hooks. */ +#include <iprt/rand.h> +#include <iprt/stream.h> +#include <iprt/system.h> +#include <iprt/uuid.h> +#include <iprt/vfs.h> +#include <iprt/zip.h> + +#define _USE_MATH_DEFINES +#include <math.h> /* sin, M_PI */ + +#define LOG_GROUP LOG_GROUP_AUDIO_TEST +#include <VBox/log.h> + +#include <VBox/version.h> +#include <VBox/vmm/pdmaudioifs.h> +#include <VBox/vmm/pdmaudioinline.h> + +#include "AudioTest.h" + + +/********************************************************************************************************************************* +* Defines * +*********************************************************************************************************************************/ +/** The test manifest file name. */ +#define AUDIOTEST_MANIFEST_FILE_STR "vkat_manifest.ini" +/** The current test manifest version. */ +#define AUDIOTEST_MANIFEST_VER 1 +/** Audio test archive default suffix. + * According to IPRT terminology this always contains the dot. */ +#define AUDIOTEST_ARCHIVE_SUFF_STR ".tar.gz" + +/** Test manifest header name. */ +#define AUDIOTEST_SEC_HDR_STR "header" +/** Maximum section name length (in UTF-8 characters). */ +#define AUDIOTEST_MAX_SEC_LEN 128 +/** Maximum object name length (in UTF-8 characters). */ +#define AUDIOTEST_MAX_OBJ_LEN 128 + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +/** + * Enumeration for an audio test object type. + */ +typedef enum AUDIOTESTOBJTYPE +{ + /** Unknown / invalid, do not use. */ + AUDIOTESTOBJTYPE_UNKNOWN = 0, + /** The test object is a file. */ + AUDIOTESTOBJTYPE_FILE, + /** The usual 32-bit hack. */ + AUDIOTESTOBJTYPE_32BIT_HACK = 0x7fffffff +} AUDIOTESTOBJTYPE; + +/** + * Structure for keeping an audio test object file. + */ +typedef struct AUDIOTESTOBJFILE +{ + /** File handle. */ + RTFILE hFile; + /** Total size (in bytes). */ + size_t cbSize; +} AUDIOTESTOBJFILE; +/** Pointer to an audio test object file. */ +typedef AUDIOTESTOBJFILE *PAUDIOTESTOBJFILE; + +/** + * Enumeration for an audio test object meta data type. + */ +typedef enum AUDIOTESTOBJMETADATATYPE +{ + /** Unknown / invalid, do not use. */ + AUDIOTESTOBJMETADATATYPE_INVALID = 0, + /** Meta data is an UTF-8 string. */ + AUDIOTESTOBJMETADATATYPE_STRING, + /** The usual 32-bit hack. */ + AUDIOTESTOBJMETADATATYPE_32BIT_HACK = 0x7fffffff +} AUDIOTESTOBJMETADATATYPE; + +/** + * Structure for keeping a meta data block. + */ +typedef struct AUDIOTESTOBJMETA +{ + /** List node. */ + RTLISTNODE Node; + /** Meta data type. */ + AUDIOTESTOBJMETADATATYPE enmType; + /** Meta data block. */ + void *pvMeta; + /** Size (in bytes) of \a pvMeta. */ + size_t cbMeta; +} AUDIOTESTOBJMETA; +/** Pointer to an audio test object file. */ +typedef AUDIOTESTOBJMETA *PAUDIOTESTOBJMETA; + +/** + * Structure for keeping a single audio test object. + * + * A test object is data which is needed in order to perform and verify one or + * more audio test case(s). + */ +typedef struct AUDIOTESTOBJINT +{ + /** List node. */ + RTLISTNODE Node; + /** Pointer to test set this handle is bound to. */ + PAUDIOTESTSET pSet; + /** As we only support .INI-style files for now, this only has the object's section name in it. */ + /** @todo Make this more generic (union, ++). */ + char szSec[AUDIOTEST_MAX_SEC_LEN]; + /** The UUID of the object. + * Used to identify an object within a test set. */ + RTUUID Uuid; + /** Number of references to this test object. */ + uint32_t cRefs; + /** Name of the test object. + * Must not contain a path and has to be able to serialize to disk. */ + char szName[256]; + /** The test type. */ + AUDIOTESTTYPE enmTestType; + /** The object type. */ + AUDIOTESTOBJTYPE enmType; + /** Meta data list. */ + RTLISTANCHOR lstMeta; + /** Union for holding the object type-specific data. */ + union + { + AUDIOTESTOBJFILE File; + }; +} AUDIOTESTOBJINT; +/** Pointer to an audio test object. */ +typedef AUDIOTESTOBJINT *PAUDIOTESTOBJINT; + +/** + * Structure for keeping an audio test verification job. + */ +typedef struct AUDIOTESTVERIFYJOB +{ + /** Pointer to set A. */ + PAUDIOTESTSET pSetA; + /** Pointer to set B. */ + PAUDIOTESTSET pSetB; + /** Pointer to the error description to use. */ + PAUDIOTESTERRORDESC pErr; + /** Zero-based index of current test being verified. */ + uint32_t idxTest; + /** The verification options to use. */ + AUDIOTESTVERIFYOPTS Opts; + /** PCM properties to use for verification. */ + PDMAUDIOPCMPROPS PCMProps; +} AUDIOTESTVERIFYJOB; +/** Pointer to an audio test verification job. */ +typedef AUDIOTESTVERIFYJOB *PAUDIOTESTVERIFYJOB; + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** Well-known frequency selection test tones. */ +static const double s_aAudioTestToneFreqsHz[] = +{ + 349.2282 /*F4*/, + 440.0000 /*A4*/, + 523.2511 /*C5*/, + 698.4565 /*F5*/, + 880.0000 /*A5*/, + 1046.502 /*C6*/, + 1174.659 /*D6*/, + 1396.913 /*F6*/, + 1760.0000 /*A6*/ +}; + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ +static int audioTestObjClose(PAUDIOTESTOBJINT pObj); +static void audioTestObjFinalize(PAUDIOTESTOBJINT pObj); +static void audioTestObjInit(PAUDIOTESTOBJINT pObj); +static bool audioTestObjIsOpen(PAUDIOTESTOBJINT pObj); + + +/** + * Initializes a test tone with a specific frequency (in Hz). + * + * @returns Used tone frequency (in Hz). + * @param pTone Pointer to test tone to initialize. + * @param pProps PCM properties to use for the test tone. + * @param dbFreq Frequency (in Hz) to initialize tone with. + * When set to 0.0, a random frequency will be chosen. + */ +double AudioTestToneInit(PAUDIOTESTTONE pTone, PPDMAUDIOPCMPROPS pProps, double dbFreq) +{ + if (dbFreq == 0.0) + dbFreq = AudioTestToneGetRandomFreq(); + + pTone->rdFreqHz = dbFreq; + pTone->rdFixed = 2.0 * M_PI * pTone->rdFreqHz / PDMAudioPropsHz(pProps); + pTone->uSample = 0; + + memcpy(&pTone->Props, pProps, sizeof(PDMAUDIOPCMPROPS)); + + pTone->enmType = AUDIOTESTTONETYPE_SINE; /* Only type implemented so far. */ + + return dbFreq; +} + +/** + * Initializes a test tone by picking a random but well-known frequency (in Hz). + * + * @returns Randomly picked tone frequency (in Hz). + * @param pTone Pointer to test tone to initialize. + * @param pProps PCM properties to use for the test tone. + */ +double AudioTestToneInitRandom(PAUDIOTESTTONE pTone, PPDMAUDIOPCMPROPS pProps) +{ + return AudioTestToneInit(pTone, pProps, + /* Pick a frequency from our selection, so that every time a recording starts + * we'll hopfully generate a different note. */ + 0.0); +} + +/** + * Writes (and iterates) a given test tone to an output buffer. + * + * @returns VBox status code. + * @param pTone Pointer to test tone to write. + * @param pvBuf Pointer to output buffer to write test tone to. + * @param cbBuf Size (in bytes) of output buffer. + * @param pcbWritten How many bytes were written on success. + */ +int AudioTestToneGenerate(PAUDIOTESTTONE pTone, void *pvBuf, uint32_t cbBuf, uint32_t *pcbWritten) +{ + /* + * Clear the buffer first so we don't need to think about additional channels. + */ + uint32_t cFrames = PDMAudioPropsBytesToFrames(&pTone->Props, cbBuf); + + /* Input cbBuf not necessarily is aligned to the frames, so re-calculate it. */ + const uint32_t cbToWrite = PDMAudioPropsFramesToBytes(&pTone->Props, cFrames); + + PDMAudioPropsClearBuffer(&pTone->Props, pvBuf, cbBuf, cFrames); + + /* + * Generate the select sin wave in the first channel: + */ + uint32_t const cbFrame = PDMAudioPropsFrameSize(&pTone->Props); + double const rdFixed = pTone->rdFixed; + uint64_t iSrcFrame = pTone->uSample; + switch (PDMAudioPropsSampleSize(&pTone->Props)) + { + case 1: + /* untested */ + if (PDMAudioPropsIsSigned(&pTone->Props)) + { + int8_t *piSample = (int8_t *)pvBuf; + while (cFrames-- > 0) + { + *piSample = (int8_t)(126 /*Amplitude*/ * sin(rdFixed * (double)iSrcFrame)); + iSrcFrame++; + piSample += cbFrame; + } + } + else + { + /* untested */ + uint8_t *pbSample = (uint8_t *)pvBuf; + while (cFrames-- > 0) + { + *pbSample = (uint8_t)(126 /*Amplitude*/ * sin(rdFixed * (double)iSrcFrame) + 0x80); + iSrcFrame++; + pbSample += cbFrame; + } + } + break; + + case 2: + if (PDMAudioPropsIsSigned(&pTone->Props)) + { + int16_t *piSample = (int16_t *)pvBuf; + while (cFrames-- > 0) + { + *piSample = (int16_t)(32760 /*Amplitude*/ * sin(rdFixed * (double)iSrcFrame)); + iSrcFrame++; + piSample = (int16_t *)((uint8_t *)piSample + cbFrame); + } + } + else + { + /* untested */ + uint16_t *puSample = (uint16_t *)pvBuf; + while (cFrames-- > 0) + { + *puSample = (uint16_t)(32760 /*Amplitude*/ * sin(rdFixed * (double)iSrcFrame) + 0x8000); + iSrcFrame++; + puSample = (uint16_t *)((uint8_t *)puSample + cbFrame); + } + } + break; + + case 4: + /* untested */ + if (PDMAudioPropsIsSigned(&pTone->Props)) + { + int32_t *piSample = (int32_t *)pvBuf; + while (cFrames-- > 0) + { + *piSample = (int32_t)((32760 << 16) /*Amplitude*/ * sin(rdFixed * (double)iSrcFrame)); + iSrcFrame++; + piSample = (int32_t *)((uint8_t *)piSample + cbFrame); + } + } + else + { + uint32_t *puSample = (uint32_t *)pvBuf; + while (cFrames-- > 0) + { + *puSample = (uint32_t)((32760 << 16) /*Amplitude*/ * sin(rdFixed * (double)iSrcFrame) + UINT32_C(0x80000000)); + iSrcFrame++; + puSample = (uint32_t *)((uint8_t *)puSample + cbFrame); + } + } + break; + + default: + AssertFailedReturn(VERR_NOT_SUPPORTED); + } + + pTone->uSample = iSrcFrame; + + if (pcbWritten) + *pcbWritten = cbToWrite; + + return VINF_SUCCESS; +} + +/** + * Returns a random test tone frequency. + */ +double AudioTestToneGetRandomFreq(void) +{ + return s_aAudioTestToneFreqsHz[RTRandU32Ex(0, RT_ELEMENTS(s_aAudioTestToneFreqsHz) - 1)]; +} + +/** + * Finds the next audible *or* silent audio sample and returns its offset. + * + * @returns Offset (in bytes) of the next found sample, or \a cbMax if not found / invalid parameters. + * @param hFile File handle of file to search in. + * @param fFindSilence Whether to search for a silent sample or not (i.e. audible). + * What a silent sample is depends on \a pToneParms PCM parameters. + * @param uOff Absolute offset (in bytes) to start searching from. + * @param cbMax Maximum amount of bytes to process. + * @param pToneParms Tone parameters to use. + * @param cbWindow Search window size (in bytes). + */ +static uint64_t audioTestToneFileFind(RTFILE hFile, bool fFindSilence, uint64_t uOff, uint64_t cbMax, + PAUDIOTESTTONEPARMS pToneParms, size_t cbWindow) +{ + int rc = RTFileSeek(hFile, uOff, RTFILE_SEEK_BEGIN, NULL); + AssertRCReturn(rc, UINT64_MAX); + + uint64_t offFound = 0; + uint8_t abBuf[_64K]; + + size_t const cbFrame = PDMAudioPropsFrameSize(&pToneParms->Props); + AssertReturn(cbFrame, UINT64_MAX); + + AssertReturn(PDMAudioPropsIsSizeAligned(&pToneParms->Props, (uint32_t)cbWindow), UINT64_MAX); + + size_t cbRead; + for (;;) + { + rc = RTFileRead(hFile, &abBuf, RT_MIN(cbWindow, sizeof(abBuf)), &cbRead); + if ( RT_FAILURE(rc) + || !cbRead) + break; + + AssertReturn(PDMAudioPropsIsSizeAligned(&pToneParms->Props, (uint32_t)cbRead), UINT64_MAX); + AssertReturn(cbRead % cbFrame == 0, UINT64_MAX); + + /** @todo Do we need to have a sliding window here? */ + + for (size_t i = 0; i < cbRead; i += cbWindow) /** @todo Slow as heck, but works for now. */ + { + bool const fIsSilence = PDMAudioPropsIsBufferSilence(&pToneParms->Props, (const uint8_t *)abBuf + i, cbWindow); + if (fIsSilence != fFindSilence) + { + AssertReturn(PDMAudioPropsIsSizeAligned(&pToneParms->Props, offFound), 0); + return offFound; + } + offFound += cbWindow; + } + } + + return cbMax; +} + +/** + * Generates a tag. + * + * @returns VBox status code. + * @param pszTag The output buffer. + * @param cbTag The size of the output buffer. + * AUDIOTEST_TAG_MAX is a good size. + */ +int AudioTestGenTag(char *pszTag, size_t cbTag) +{ + RTUUID UUID; + int rc = RTUuidCreate(&UUID); + AssertRCReturn(rc, rc); + rc = RTUuidToStr(&UUID, pszTag, cbTag); + AssertRCReturn(rc, rc); + return rc; +} + +/** + * Return the tag to use in the given buffer, generating one if needed. + * + * @returns VBox status code. + * @param pszTag The output buffer. + * @param cbTag The size of the output buffer. + * AUDIOTEST_TAG_MAX is a good size. + * @param pszTagUser User specified tag, optional. + */ +static int audioTestCopyOrGenTag(char *pszTag, size_t cbTag, const char *pszTagUser) +{ + if (pszTagUser && *pszTagUser) + return RTStrCopy(pszTag, cbTag, pszTagUser); + return AudioTestGenTag(pszTag, cbTag); +} + + +/** + * Creates a new path (directory) for a specific audio test set tag. + * + * @returns VBox status code. + * @param pszPath On input, specifies the absolute base path where to create the test set path. + * On output this specifies the absolute path created. + * @param cbPath Size (in bytes) of \a pszPath. + * @param pszTag Tag to use for path creation. + * + * @note Can be used multiple times with the same tag; a sub directory with an ISO time string will be used + * on each call. + */ +int AudioTestPathCreate(char *pszPath, size_t cbPath, const char *pszTag) +{ + char szTag[AUDIOTEST_TAG_MAX]; + int rc = audioTestCopyOrGenTag(szTag, sizeof(szTag), pszTag); + AssertRCReturn(rc, rc); + + char szName[RT_ELEMENTS(AUDIOTEST_PATH_PREFIX_STR) + AUDIOTEST_TAG_MAX + 4]; + if (RTStrPrintf2(szName, sizeof(szName), "%s-%s", AUDIOTEST_PATH_PREFIX_STR, szTag) < 0) + AssertFailedReturn(VERR_BUFFER_OVERFLOW); + + rc = RTPathAppend(pszPath, cbPath, szName); + AssertRCReturn(rc, rc); + +#ifndef DEBUG /* Makes debugging easier to have a deterministic directory. */ + char szTime[64]; + RTTIMESPEC time; + if (!RTTimeSpecToString(RTTimeNow(&time), szTime, sizeof(szTime))) + return VERR_BUFFER_UNDERFLOW; + + /* Colons aren't allowed in windows filenames, so change to dashes. */ + char *pszColon; + while ((pszColon = strchr(szTime, ':')) != NULL) + *pszColon = '-'; + + rc = RTPathAppend(pszPath, cbPath, szTime); + AssertRCReturn(rc, rc); +#endif + + return RTDirCreateFullPath(pszPath, RTFS_UNIX_IRWXU); +} + +DECLINLINE(int) audioTestManifestWriteData(PAUDIOTESTSET pSet, const void *pvData, size_t cbData) +{ + /** @todo Use RTIniFileWrite once its implemented. */ + return RTFileWrite(pSet->f.hFile, pvData, cbData, NULL); +} + +/** + * Writes string data to a test set manifest. + * + * @returns VBox status code. + * @param pSet Test set to write manifest for. + * @param pszFormat Format string to write. + * @param args Variable arguments for \a pszFormat. + */ +static int audioTestManifestWriteV(PAUDIOTESTSET pSet, const char *pszFormat, va_list args) +{ + /** @todo r=bird: Use RTStrmOpen + RTStrmPrintf instead of this slow + * do-it-all-yourself stuff. */ + char *psz = NULL; + if (RTStrAPrintfV(&psz, pszFormat, args) == -1) + return VERR_NO_MEMORY; + AssertPtrReturn(psz, VERR_NO_MEMORY); + + int rc = audioTestManifestWriteData(pSet, psz, strlen(psz)); + AssertRC(rc); + + RTStrFree(psz); + + return rc; +} + +/** + * Writes a string to a test set manifest. + * Convenience function. + * + * @returns VBox status code. + * @param pSet Test set to write manifest for. + * @param pszFormat Format string to write. + * @param ... Variable arguments for \a pszFormat. Optional. + */ +static int audioTestManifestWrite(PAUDIOTESTSET pSet, const char *pszFormat, ...) +{ + va_list va; + va_start(va, pszFormat); + + int rc = audioTestManifestWriteV(pSet, pszFormat, va); + AssertRC(rc); + + va_end(va); + + return rc; +} + +/** + * Returns the current read/write offset (in bytes) of the opened manifest file. + * + * @returns Current read/write offset (in bytes). + * @param pSet Set to return offset for. + * Must have an opened manifest file. + */ +DECLINLINE(uint64_t) audioTestManifestGetOffsetAbs(PAUDIOTESTSET pSet) +{ + AssertReturn(RTFileIsValid(pSet->f.hFile), 0); + return RTFileTell(pSet->f.hFile); +} + +/** + * Writes a section header to a test set manifest. + * + * @returns VBox status code. + * @param pSet Test set to write manifest for. + * @param pszSection Format string of section to write. + * @param ... Variable arguments for \a pszSection. Optional. + */ +static int audioTestManifestWriteSectionHdr(PAUDIOTESTSET pSet, const char *pszSection, ...) +{ + va_list va; + va_start(va, pszSection); + + /** @todo Keep it as simple as possible for now. Improve this later. */ + int rc = audioTestManifestWrite(pSet, "[%N]\n", pszSection, &va); + + va_end(va); + + return rc; +} + +/** + * Initializes an audio test set, internal function. + * + * @param pSet Test set to initialize. + */ +static void audioTestSetInitInternal(PAUDIOTESTSET pSet) +{ + pSet->f.hFile = NIL_RTFILE; + + RTListInit(&pSet->lstObj); + pSet->cObj = 0; + + RTListInit(&pSet->lstTest); + pSet->cTests = 0; + pSet->cTestsRunning = 0; + pSet->offTestCount = 0; + pSet->pTestCur = NULL; + pSet->cObj = 0; + pSet->offObjCount = 0; + pSet->cTotalFailures = 0; +} + +/** + * Returns whether a test set's manifest file is open (and thus ready) or not. + * + * @returns \c true if open (and ready), or \c false if not. + * @retval VERR_ + * @param pSet Test set to return open status for. + */ +static bool audioTestManifestIsOpen(PAUDIOTESTSET pSet) +{ + if ( pSet->enmMode == AUDIOTESTSETMODE_TEST + && pSet->f.hFile != NIL_RTFILE) + return true; + else if ( pSet->enmMode == AUDIOTESTSETMODE_VERIFY + && pSet->f.hIniFile != NIL_RTINIFILE) + return true; + + return false; +} + +/** + * Initializes an audio test error description. + * + * @param pErr Test error description to initialize. + */ +static void audioTestErrorDescInit(PAUDIOTESTERRORDESC pErr) +{ + RTListInit(&pErr->List); + pErr->cErrors = 0; +} + +/** + * Destroys an audio test error description. + * + * @param pErr Test error description to destroy. + */ +void AudioTestErrorDescDestroy(PAUDIOTESTERRORDESC pErr) +{ + if (!pErr) + return; + + PAUDIOTESTERRORENTRY pErrEntry, pErrEntryNext; + RTListForEachSafe(&pErr->List, pErrEntry, pErrEntryNext, AUDIOTESTERRORENTRY, Node) + { + RTListNodeRemove(&pErrEntry->Node); + + RTMemFree(pErrEntry); + } + + pErr->cErrors = 0; +} + +/** + * Returns the the number of errors of an audio test error description. + * + * @returns Error count. + * @param pErr Test error description to return error count for. + */ +uint32_t AudioTestErrorDescCount(PCAUDIOTESTERRORDESC pErr) +{ + return pErr->cErrors; +} + +/** + * Returns if an audio test error description contains any errors or not. + * + * @returns \c true if it contains errors, or \c false if not. + * @param pErr Test error description to return error status for. + */ +bool AudioTestErrorDescFailed(PCAUDIOTESTERRORDESC pErr) +{ + if (pErr->cErrors) + { + Assert(!RTListIsEmpty(&pErr->List)); + return true; + } + + return false; +} + +/** + * Adds a single error entry to an audio test error description, va_list version. + * + * @returns VBox status code. + * @param pErr Test error description to add entry for. + * @param idxTest Index of failing test (zero-based). + * @param rc Result code of entry to add. + * @param pszFormat Error description format string to add. + * @param va Optional format arguments of \a pszDesc to add. + */ +static int audioTestErrorDescAddV(PAUDIOTESTERRORDESC pErr, uint32_t idxTest, int rc, const char *pszFormat, va_list va) +{ + PAUDIOTESTERRORENTRY pEntry = (PAUDIOTESTERRORENTRY)RTMemAlloc(sizeof(AUDIOTESTERRORENTRY)); + AssertPtrReturn(pEntry, VERR_NO_MEMORY); + + char *pszDescTmp; + if (RTStrAPrintfV(&pszDescTmp, pszFormat, va) < 0) + AssertFailedReturn(VERR_NO_MEMORY); + + const ssize_t cch = RTStrPrintf2(pEntry->szDesc, sizeof(pEntry->szDesc), "Test #%RU32 %s: %s", + idxTest, RT_FAILURE(rc) ? "failed" : "info", pszDescTmp); + RTStrFree(pszDescTmp); + AssertReturn(cch > 0, VERR_BUFFER_OVERFLOW); + + pEntry->rc = rc; + + RTListAppend(&pErr->List, &pEntry->Node); + + if (RT_FAILURE(rc)) + pErr->cErrors++; + + return VINF_SUCCESS; +} + +/** + * Adds a single error entry to an audio test error description. + * + * @returns VBox status code. + * @param pErr Test error description to add entry for. + * @param idxTest Index of failing test (zero-based). + * @param pszFormat Error description format string to add. + * @param ... Optional format arguments of \a pszDesc to add. + */ +static int audioTestErrorDescAddError(PAUDIOTESTERRORDESC pErr, uint32_t idxTest, const char *pszFormat, ...) +{ + va_list va; + va_start(va, pszFormat); + + int rc = audioTestErrorDescAddV(pErr, idxTest, VERR_GENERAL_FAILURE /** @todo Fudge! */, pszFormat, va); + + va_end(va); + return rc; +} + +/** + * Adds a single info entry to an audio test error description, va_list version. + * + * @returns VBox status code. + * @param pErr Test error description to add entry for. + * @param idxTest Index of failing test (zero-based). + * @param pszFormat Error description format string to add. + * @param ... Optional format arguments of \a pszDesc to add. + */ +static int audioTestErrorDescAddInfo(PAUDIOTESTERRORDESC pErr, uint32_t idxTest, const char *pszFormat, ...) +{ + va_list va; + va_start(va, pszFormat); + + int rc = audioTestErrorDescAddV(pErr, idxTest, VINF_SUCCESS, pszFormat, va); + + va_end(va); + return rc; +} + +#if 0 +static int audioTestErrorDescAddRc(PAUDIOTESTERRORDESC pErr, int rc, const char *pszFormat, ...) +{ + va_list va; + va_start(va, pszFormat); + + int rc2 = audioTestErrorDescAddV(pErr, rc, pszFormat, va); + + va_end(va); + return rc2; +} +#endif + +/** + * Retrieves the temporary directory. + * + * @returns VBox status code. + * @param pszPath Where to return the absolute path of the created directory on success. + * @param cbPath Size (in bytes) of \a pszPath. + */ +int AudioTestPathGetTemp(char *pszPath, size_t cbPath) +{ + int rc = RTEnvGetEx(RTENV_DEFAULT, "TESTBOX_PATH_SCRATCH", pszPath, cbPath, NULL); + if (RT_FAILURE(rc)) + { + rc = RTPathTemp(pszPath, cbPath); + AssertRCReturn(rc, rc); + } + + return rc; +} + +/** + * Creates a new temporary directory with a specific (test) tag. + * + * @returns VBox status code. + * @param pszPath Where to return the absolute path of the created directory on success. + * @param cbPath Size (in bytes) of \a pszPath. + * @param pszTag Tag name to use for directory creation. + * + * @note Can be used multiple times with the same tag; a sub directory with an ISO time string will be used + * on each call. + */ +int AudioTestPathCreateTemp(char *pszPath, size_t cbPath, const char *pszTag) +{ + AssertReturn(pszTag && strlen(pszTag) <= AUDIOTEST_TAG_MAX, VERR_INVALID_PARAMETER); + + char szTemp[RTPATH_MAX]; + int rc = AudioTestPathGetTemp(szTemp, sizeof(szTemp)); + AssertRCReturn(rc, rc); + + rc = AudioTestPathCreate(szTemp, sizeof(szTemp), pszTag); + AssertRCReturn(rc, rc); + + return RTStrCopy(pszPath, cbPath, szTemp); +} + +/** + * Gets a value as string. + * + * @returns VBox status code. + * @param pObj Object handle to get value for. + * @param pszKey Key to get value from. + * @param pszVal Where to return the value on success. + * @param cbVal Size (in bytes) of \a pszVal. + */ +static int audioTestObjGetStr(PAUDIOTESTOBJINT pObj, const char *pszKey, char *pszVal, size_t cbVal) +{ + /** @todo For now we only support .INI-style files. */ + AssertPtrReturn(pObj->pSet, VERR_WRONG_ORDER); + return RTIniFileQueryValue(pObj->pSet->f.hIniFile, pObj->szSec, pszKey, pszVal, cbVal, NULL); +} + +/** + * Gets a value as boolean. + * + * @returns VBox status code. + * @param pObj Object handle to get value for. + * @param pszKey Key to get value from. + * @param pbVal Where to return the value on success. + */ +static int audioTestObjGetBool(PAUDIOTESTOBJINT pObj, const char *pszKey, bool *pbVal) +{ + char szVal[_1K]; + int rc = audioTestObjGetStr(pObj, pszKey, szVal, sizeof(szVal)); + if (RT_SUCCESS(rc)) + *pbVal = (RTStrICmp(szVal, "true") == 0) + || (RTStrICmp(szVal, "1") == 0) ? true : false; + + return rc; +} + +/** + * Gets a value as uint8_t. + * + * @returns VBox status code. + * @param pObj Object handle to get value for. + * @param pszKey Key to get value from. + * @param puVal Where to return the value on success. + */ +static int audioTestObjGetUInt8(PAUDIOTESTOBJINT pObj, const char *pszKey, uint8_t *puVal) +{ + char szVal[_1K]; + int rc = audioTestObjGetStr(pObj, pszKey, szVal, sizeof(szVal)); + if (RT_SUCCESS(rc)) + *puVal = RTStrToUInt8(szVal); + + return rc; +} + +/** + * Gets a value as uint32_t. + * + * @returns VBox status code. + * @param pObj Object handle to get value for. + * @param pszKey Key to get value from. + * @param puVal Where to return the value on success. + */ +static int audioTestObjGetUInt32(PAUDIOTESTOBJINT pObj, const char *pszKey, uint32_t *puVal) +{ + char szVal[_1K]; + int rc = audioTestObjGetStr(pObj, pszKey, szVal, sizeof(szVal)); + if (RT_SUCCESS(rc)) + *puVal = RTStrToUInt32(szVal); + + return rc; +} + +/** + * Returns the absolute path of a given audio test set object. + * + * @returns VBox status code. + * @param pSet Test set the object contains. + * @param pszPathAbs Where to return the absolute path on success. + * @param cbPathAbs Size (in bytes) of \a pszPathAbs. + * @param pszObjName Name of the object to create absolute path for. + */ +DECLINLINE(int) audioTestSetGetObjPath(PAUDIOTESTSET pSet, char *pszPathAbs, size_t cbPathAbs, const char *pszObjName) +{ + return RTPathJoin(pszPathAbs, cbPathAbs, pSet->szPathAbs, pszObjName); +} + +/** + * Returns the tag of a test set. + * + * @returns Test set tag. + * @param pSet Test set to return tag for. + */ +const char *AudioTestSetGetTag(PAUDIOTESTSET pSet) +{ + return pSet->szTag; +} + +/** + * Returns the total number of registered tests. + * + * @returns Total number of registered tests. + * @param pSet Test set to return value for. + */ +uint32_t AudioTestSetGetTestsTotal(PAUDIOTESTSET pSet) +{ + return pSet->cTests; +} + +/** + * Returns the total number of (still) running tests. + * + * @returns Total number of (still) running tests. + * @param pSet Test set to return value for. + */ +uint32_t AudioTestSetGetTestsRunning(PAUDIOTESTSET pSet) +{ + return pSet->cTestsRunning; +} + +/** + * Returns the total number of test failures occurred. + * + * @returns Total number of test failures occurred. + * @param pSet Test set to return value for. + */ +uint32_t AudioTestSetGetTotalFailures(PAUDIOTESTSET pSet) +{ + return pSet->cTotalFailures; +} + +/** + * Creates a new audio test set. + * + * @returns VBox status code. + * @param pSet Test set to create. + * @param pszPath Where to store the set set data. If NULL, the + * temporary directory will be used. + * @param pszTag Tag name to use for this test set. + */ +int AudioTestSetCreate(PAUDIOTESTSET pSet, const char *pszPath, const char *pszTag) +{ + audioTestSetInitInternal(pSet); + + int rc = audioTestCopyOrGenTag(pSet->szTag, sizeof(pSet->szTag), pszTag); + AssertRCReturn(rc, rc); + + /* + * Test set directory. + */ + if (pszPath) + { + rc = RTPathAbs(pszPath, pSet->szPathAbs, sizeof(pSet->szPathAbs)); + AssertRCReturn(rc, rc); + + rc = AudioTestPathCreate(pSet->szPathAbs, sizeof(pSet->szPathAbs), pSet->szTag); + } + else + rc = AudioTestPathCreateTemp(pSet->szPathAbs, sizeof(pSet->szPathAbs), pSet->szTag); + AssertRCReturn(rc, rc); + + /* + * Create the manifest file. + */ + char szTmp[RTPATH_MAX]; + rc = RTPathJoin(szTmp, sizeof(szTmp), pSet->szPathAbs, AUDIOTEST_MANIFEST_FILE_STR); + AssertRCReturn(rc, rc); + + rc = RTFileOpen(&pSet->f.hFile, szTmp, RTFILE_O_CREATE | RTFILE_O_WRITE | RTFILE_O_DENY_WRITE); + AssertRCReturn(rc, rc); + + rc = audioTestManifestWriteSectionHdr(pSet, "header"); + AssertRCReturn(rc, rc); + + rc = audioTestManifestWrite(pSet, "magic=vkat_ini\n"); /* VKAT Manifest, .INI-style. */ + AssertRCReturn(rc, rc); + rc = audioTestManifestWrite(pSet, "ver=%d\n", AUDIOTEST_MANIFEST_VER); + AssertRCReturn(rc, rc); + rc = audioTestManifestWrite(pSet, "tag=%s\n", pSet->szTag); + AssertRCReturn(rc, rc); + + AssertCompile(sizeof(szTmp) > RTTIME_STR_LEN); + RTTIMESPEC Now; + rc = audioTestManifestWrite(pSet, "date_created=%s\n", RTTimeSpecToString(RTTimeNow(&Now), szTmp, sizeof(szTmp))); + AssertRCReturn(rc, rc); + + RTSystemQueryOSInfo(RTSYSOSINFO_PRODUCT, szTmp, sizeof(szTmp)); /* do NOT return on failure. */ + rc = audioTestManifestWrite(pSet, "os_product=%s\n", szTmp); + AssertRCReturn(rc, rc); + + RTSystemQueryOSInfo(RTSYSOSINFO_RELEASE, szTmp, sizeof(szTmp)); /* do NOT return on failure. */ + rc = audioTestManifestWrite(pSet, "os_rel=%s\n", szTmp); + AssertRCReturn(rc, rc); + + RTSystemQueryOSInfo(RTSYSOSINFO_VERSION, szTmp, sizeof(szTmp)); /* do NOT return on failure. */ + rc = audioTestManifestWrite(pSet, "os_ver=%s\n", szTmp); + AssertRCReturn(rc, rc); + + rc = audioTestManifestWrite(pSet, "vbox_ver=%s r%u %s (%s %s)\n", + VBOX_VERSION_STRING, RTBldCfgRevision(), RTBldCfgTargetDotArch(), __DATE__, __TIME__); + AssertRCReturn(rc, rc); + + rc = audioTestManifestWrite(pSet, "test_count="); + AssertRCReturn(rc, rc); + pSet->offTestCount = audioTestManifestGetOffsetAbs(pSet); + rc = audioTestManifestWrite(pSet, "0000\n"); /* A bit messy, but does the trick for now. */ + AssertRCReturn(rc, rc); + + rc = audioTestManifestWrite(pSet, "obj_count="); + AssertRCReturn(rc, rc); + pSet->offObjCount = audioTestManifestGetOffsetAbs(pSet); + rc = audioTestManifestWrite(pSet, "0000\n"); /* A bit messy, but does the trick for now. */ + AssertRCReturn(rc, rc); + + pSet->enmMode = AUDIOTESTSETMODE_TEST; + + return rc; +} + +/** + * Destroys a test set. + * + * @returns VBox status code. + * @param pSet Test set to destroy. + */ +int AudioTestSetDestroy(PAUDIOTESTSET pSet) +{ + if (!pSet) + return VINF_SUCCESS; + + /* No more validation (no / still running tests) here -- just pack all stuff we got so far + * and let the verification routine deal with it later. */ + + int rc = AudioTestSetClose(pSet); + if (RT_FAILURE(rc)) + return rc; + + PAUDIOTESTOBJINT pObj, pObjNext; + RTListForEachSafe(&pSet->lstObj, pObj, pObjNext, AUDIOTESTOBJINT, Node) + { + rc = audioTestObjClose(pObj); + if (RT_SUCCESS(rc)) + { + PAUDIOTESTOBJMETA pMeta, pMetaNext; + RTListForEachSafe(&pObj->lstMeta, pMeta, pMetaNext, AUDIOTESTOBJMETA, Node) + { + switch (pMeta->enmType) + { + case AUDIOTESTOBJMETADATATYPE_STRING: + { + RTStrFree((char *)pMeta->pvMeta); + break; + } + + default: + AssertFailed(); + break; + } + + RTListNodeRemove(&pMeta->Node); + RTMemFree(pMeta); + } + + RTListNodeRemove(&pObj->Node); + RTMemFree(pObj); + + Assert(pSet->cObj); + pSet->cObj--; + } + else + break; + } + + if (RT_FAILURE(rc)) + return rc; + + Assert(pSet->cObj == 0); + + PAUDIOTESTENTRY pEntry, pEntryNext; + RTListForEachSafe(&pSet->lstTest, pEntry, pEntryNext, AUDIOTESTENTRY, Node) + { + RTListNodeRemove(&pEntry->Node); + RTMemFree(pEntry); + + Assert(pSet->cTests); + pSet->cTests--; + } + + if (RT_FAILURE(rc)) + return rc; + + Assert(pSet->cTests == 0); + + return rc; +} + +/** + * Opens an existing audio test set. + * + * @returns VBox status code. + * @param pSet Test set to open. + * @param pszPath Absolute path of the test set to open. + */ +int AudioTestSetOpen(PAUDIOTESTSET pSet, const char *pszPath) +{ + audioTestSetInitInternal(pSet); + + char szManifest[RTPATH_MAX]; + int rc = RTPathJoin(szManifest, sizeof(szManifest), pszPath, AUDIOTEST_MANIFEST_FILE_STR); + AssertRCReturn(rc, rc); + + RTVFSFILE hVfsFile; + rc = RTVfsFileOpenNormal(szManifest, RTFILE_O_OPEN | RTFILE_O_READ | RTFILE_O_DENY_WRITE, &hVfsFile); + if (RT_FAILURE(rc)) + return rc; + + rc = RTIniFileCreateFromVfsFile(&pSet->f.hIniFile, hVfsFile, RTINIFILE_F_READONLY); + RTVfsFileRelease(hVfsFile); + AssertRCReturn(rc, rc); + + rc = RTStrCopy(pSet->szPathAbs, sizeof(pSet->szPathAbs), pszPath); + AssertRCReturn(rc, rc); + + pSet->enmMode = AUDIOTESTSETMODE_VERIFY; + + return rc; +} + +/** + * Closes an opened audio test set. + * + * @returns VBox status code. + * @param pSet Test set to close. + */ +int AudioTestSetClose(PAUDIOTESTSET pSet) +{ + AssertPtrReturn(pSet, VERR_INVALID_POINTER); + + if (!audioTestManifestIsOpen(pSet)) + return VINF_SUCCESS; + + int rc; + + if (pSet->enmMode == AUDIOTESTSETMODE_TEST) + { + /* Update number of bound test objects. */ + PAUDIOTESTENTRY pTest; + uint32_t cTests = 0; + RTListForEach(&pSet->lstTest, pTest, AUDIOTESTENTRY, Node) + { + rc = RTFileSeek(pSet->f.hFile, pTest->offObjCount, RTFILE_SEEK_BEGIN, NULL); + AssertRCReturn(rc, rc); + rc = audioTestManifestWrite(pSet, "%04RU32", pTest->cObj); + AssertRCReturn(rc, rc); + cTests++; /* Sanity checking. */ + } + + AssertMsgReturn(pSet->cTests == cTests, ("Test count and list don't match"), VERR_INTERNAL_ERROR); + + /* + * Update number of total objects. + */ + rc = RTFileSeek(pSet->f.hFile, pSet->offObjCount, RTFILE_SEEK_BEGIN, NULL); + AssertRCReturn(rc, rc); + rc = audioTestManifestWrite(pSet, "%04RU32", pSet->cObj); + AssertRCReturn(rc, rc); + + /* + * Update number of total tests. + */ + rc = RTFileSeek(pSet->f.hFile, pSet->offTestCount, RTFILE_SEEK_BEGIN, NULL); + AssertRCReturn(rc, rc); + rc = audioTestManifestWrite(pSet, "%04RU32", pSet->cTests); + AssertRCReturn(rc, rc); + + /* + * Serialize all registered test objects. + */ + rc = RTFileSeek(pSet->f.hFile, 0, RTFILE_SEEK_END, NULL); + AssertRCReturn(rc, rc); + + PAUDIOTESTOBJINT pObj; + uint32_t cObj = 0; + RTListForEach(&pSet->lstObj, pObj, AUDIOTESTOBJINT, Node) + { + /* First, close the object. + * This also does some needed finalization. */ + rc = AudioTestObjClose(pObj); + AssertRCReturn(rc, rc); + rc = audioTestManifestWrite(pSet, "\n"); + AssertRCReturn(rc, rc); + char szUuid[AUDIOTEST_MAX_SEC_LEN]; + rc = RTUuidToStr(&pObj->Uuid, szUuid, sizeof(szUuid)); + AssertRCReturn(rc, rc); + rc = audioTestManifestWriteSectionHdr(pSet, "obj_%s", szUuid); + AssertRCReturn(rc, rc); + rc = audioTestManifestWrite(pSet, "obj_type=%RU32\n", pObj->enmType); + AssertRCReturn(rc, rc); + rc = audioTestManifestWrite(pSet, "obj_name=%s\n", pObj->szName); + AssertRCReturn(rc, rc); + + switch (pObj->enmType) + { + case AUDIOTESTOBJTYPE_FILE: + { + rc = audioTestManifestWrite(pSet, "obj_size=%RU64\n", pObj->File.cbSize); + AssertRCReturn(rc, rc); + break; + } + + default: + AssertFailed(); + break; + } + + /* + * Write all meta data. + */ + PAUDIOTESTOBJMETA pMeta; + RTListForEach(&pObj->lstMeta, pMeta, AUDIOTESTOBJMETA, Node) + { + switch (pMeta->enmType) + { + case AUDIOTESTOBJMETADATATYPE_STRING: + { + rc = audioTestManifestWrite(pSet, (const char *)pMeta->pvMeta); + AssertRCReturn(rc, rc); + break; + } + + default: + AssertFailed(); + break; + } + } + + cObj++; /* Sanity checking. */ + } + + AssertMsgReturn(pSet->cObj == cObj, ("Object count and list don't match"), VERR_INTERNAL_ERROR); + + int rc2 = RTFileClose(pSet->f.hFile); + if (RT_SUCCESS(rc2)) + pSet->f.hFile = NIL_RTFILE; + + if (RT_SUCCESS(rc)) + rc = rc2; + } + else if (pSet->enmMode == AUDIOTESTSETMODE_VERIFY) + { + RTIniFileRelease(pSet->f.hIniFile); + pSet->f.hIniFile = NIL_RTINIFILE; + + rc = VINF_SUCCESS; + } + else + AssertFailedStmt(rc = VERR_NOT_SUPPORTED); + + return rc; +} + +/** + * Physically wipes all related test set files off the disk. + * + * @returns VBox status code. + * @param pSet Test set to wipe. + */ +int AudioTestSetWipe(PAUDIOTESTSET pSet) +{ + AssertPtrReturn(pSet, VERR_INVALID_POINTER); + + int rc = VINF_SUCCESS; + char szFilePath[RTPATH_MAX]; + + PAUDIOTESTOBJINT pObj; + RTListForEach(&pSet->lstObj, pObj, AUDIOTESTOBJINT, Node) + { + int rc2 = audioTestObjClose(pObj); + if (RT_SUCCESS(rc2)) + { + rc2 = audioTestSetGetObjPath(pSet, szFilePath, sizeof(szFilePath), pObj->szName); + if (RT_SUCCESS(rc2)) + rc2 = RTFileDelete(szFilePath); + } + + if (RT_SUCCESS(rc)) + rc = rc2; + /* Keep going. */ + } + + if (RT_SUCCESS(rc)) + { + rc = RTPathJoin(szFilePath, sizeof(szFilePath), pSet->szPathAbs, AUDIOTEST_MANIFEST_FILE_STR); + if (RT_SUCCESS(rc)) + rc = RTFileDelete(szFilePath); + } + + /* Remove the (hopefully now empty) directory. Otherwise let this fail. */ + if (RT_SUCCESS(rc)) + rc = RTDirRemove(pSet->szPathAbs); + + return rc; +} + +/** + * Creates and registers a new audio test object to the current running test. + * + * @returns VBox status code. + * @param pSet Test set to create and register new object for. + * @param pszName Name of new object to create. + * @param pObj Where to return the pointer to the newly created object on success. + */ +int AudioTestSetObjCreateAndRegister(PAUDIOTESTSET pSet, const char *pszName, PAUDIOTESTOBJ pObj) +{ + AssertReturn(pSet->cTestsRunning == 1, VERR_WRONG_ORDER); /* No test nesting allowed. */ + + AssertPtrReturn(pszName, VERR_INVALID_POINTER); + + PAUDIOTESTOBJINT pThis = (PAUDIOTESTOBJINT)RTMemAlloc(sizeof(AUDIOTESTOBJINT)); + AssertPtrReturn(pThis, VERR_NO_MEMORY); + + audioTestObjInit(pThis); + + if (RTStrPrintf2(pThis->szName, sizeof(pThis->szName), "%04RU32-%s", pSet->cObj, pszName) <= 0) + AssertFailedReturn(VERR_BUFFER_OVERFLOW); + + /** @todo Generalize this function more once we have more object types. */ + + char szObjPathAbs[RTPATH_MAX]; + int rc = audioTestSetGetObjPath(pSet, szObjPathAbs, sizeof(szObjPathAbs), pThis->szName); + if (RT_SUCCESS(rc)) + { + rc = RTFileOpen(&pThis->File.hFile, szObjPathAbs, RTFILE_O_CREATE | RTFILE_O_WRITE | RTFILE_O_DENY_WRITE); + if (RT_SUCCESS(rc)) + { + pThis->enmType = AUDIOTESTOBJTYPE_FILE; + pThis->cRefs = 1; /* Currently only 1:1 mapping. */ + + RTListAppend(&pSet->lstObj, &pThis->Node); + pSet->cObj++; + + /* Generate + set an UUID for the object and assign it to the current test. */ + rc = RTUuidCreate(&pThis->Uuid); + AssertRCReturn(rc, rc); + char szUuid[AUDIOTEST_MAX_OBJ_LEN]; + rc = RTUuidToStr(&pThis->Uuid, szUuid, sizeof(szUuid)); + AssertRCReturn(rc, rc); + + rc = audioTestManifestWrite(pSet, "obj%RU32_uuid=%s\n", pSet->pTestCur->cObj, szUuid); + AssertRCReturn(rc, rc); + + AssertPtr(pSet->pTestCur); + pSet->pTestCur->cObj++; + + *pObj = pThis; + } + } + + if (RT_FAILURE(rc)) + RTMemFree(pThis); + + return rc; +} + +/** + * Writes to a created audio test object. + * + * @returns VBox status code. + * @param hObj Handle to the audio test object to write to. + * @param pvBuf Pointer to data to write. + * @param cbBuf Size (in bytes) of \a pvBuf to write. + */ +int AudioTestObjWrite(AUDIOTESTOBJ hObj, const void *pvBuf, size_t cbBuf) +{ + AUDIOTESTOBJINT *pThis = hObj; + + /** @todo Generalize this function more once we have more object types. */ + AssertReturn(pThis->enmType == AUDIOTESTOBJTYPE_FILE, VERR_INVALID_PARAMETER); + + return RTFileWrite(pThis->File.hFile, pvBuf, cbBuf, NULL); +} + +/** + * Adds meta data to a test object as a string, va_list version. + * + * @returns VBox status code. + * @param pObj Test object to add meta data for. + * @param pszFormat Format string to add. + * @param va Variable arguments list to use for the format string. + */ +static int audioTestObjAddMetadataStrV(PAUDIOTESTOBJINT pObj, const char *pszFormat, va_list va) +{ + PAUDIOTESTOBJMETA pMeta = (PAUDIOTESTOBJMETA)RTMemAlloc(sizeof(AUDIOTESTOBJMETA)); + AssertPtrReturn(pMeta, VERR_NO_MEMORY); + + pMeta->pvMeta = RTStrAPrintf2V(pszFormat, va); + AssertPtrReturn(pMeta->pvMeta, VERR_BUFFER_OVERFLOW); + pMeta->cbMeta = RTStrNLen((const char *)pMeta->pvMeta, RTSTR_MAX); + + pMeta->enmType = AUDIOTESTOBJMETADATATYPE_STRING; + + RTListAppend(&pObj->lstMeta, &pMeta->Node); + + return VINF_SUCCESS; +} + +/** + * Adds meta data to a test object as a string. + * + * @returns VBox status code. + * @param hObj Handle to the test object to add meta data for. + * @param pszFormat Format string to add. + * @param ... Variable arguments for the format string. + */ +int AudioTestObjAddMetadataStr(AUDIOTESTOBJ hObj, const char *pszFormat, ...) +{ + AUDIOTESTOBJINT *pThis = hObj; + + va_list va; + + va_start(va, pszFormat); + int rc = audioTestObjAddMetadataStrV(pThis, pszFormat, va); + va_end(va); + + return rc; +} + +/** + * Closes an opened audio test object. + * + * @returns VBox status code. + * @param hObj Handle to the audio test object to close. + */ +int AudioTestObjClose(AUDIOTESTOBJ hObj) +{ + AUDIOTESTOBJINT *pThis = hObj; + + if (!pThis) + return VINF_SUCCESS; + + audioTestObjFinalize(pThis); + + return audioTestObjClose(pThis); +} + +/** + * Begins a new test of a test set. + * + * @returns VBox status code. + * @param pSet Test set to begin new test for. + * @param pszDesc Test description. + * @param pParms Test parameters to use. + * @param ppEntry Where to return the new test + */ +int AudioTestSetTestBegin(PAUDIOTESTSET pSet, const char *pszDesc, PAUDIOTESTPARMS pParms, PAUDIOTESTENTRY *ppEntry) +{ + AssertReturn(pSet->cTestsRunning == 0, VERR_WRONG_ORDER); /* No test nesting allowed. */ + + PAUDIOTESTENTRY pEntry = (PAUDIOTESTENTRY)RTMemAllocZ(sizeof(AUDIOTESTENTRY)); + AssertPtrReturn(pEntry, VERR_NO_MEMORY); + + int rc = RTStrCopy(pEntry->szDesc, sizeof(pEntry->szDesc), pszDesc); + AssertRCReturn(rc, rc); + + memcpy(&pEntry->Parms, pParms, sizeof(AUDIOTESTPARMS)); + + pEntry->pParent = pSet; + pEntry->rc = VERR_IPE_UNINITIALIZED_STATUS; + + rc = audioTestManifestWrite(pSet, "\n"); + AssertRCReturn(rc, rc); + + rc = audioTestManifestWriteSectionHdr(pSet, "test_%04RU32", pSet->cTests); + AssertRCReturn(rc, rc); + rc = audioTestManifestWrite(pSet, "test_desc=%s\n", pszDesc); + AssertRCReturn(rc, rc); + rc = audioTestManifestWrite(pSet, "test_type=%RU32\n", pParms->enmType); + AssertRCReturn(rc, rc); + rc = audioTestManifestWrite(pSet, "test_delay_ms=%RU32\n", pParms->msDelay); + AssertRCReturn(rc, rc); + rc = audioTestManifestWrite(pSet, "audio_direction=%s\n", PDMAudioDirGetName(pParms->enmDir)); + AssertRCReturn(rc, rc); + + rc = audioTestManifestWrite(pSet, "obj_count="); + AssertRCReturn(rc, rc); + pEntry->offObjCount = audioTestManifestGetOffsetAbs(pSet); + rc = audioTestManifestWrite(pSet, "0000\n"); /* A bit messy, but does the trick for now. */ + AssertRCReturn(rc, rc); + + switch (pParms->enmType) + { + case AUDIOTESTTYPE_TESTTONE_PLAY: + RT_FALL_THROUGH(); + case AUDIOTESTTYPE_TESTTONE_RECORD: + { + rc = audioTestManifestWrite(pSet, "tone_freq_hz=%RU16\n", (uint16_t)pParms->TestTone.dbFreqHz); + AssertRCReturn(rc, rc); + rc = audioTestManifestWrite(pSet, "tone_prequel_ms=%RU32\n", pParms->TestTone.msPrequel); + AssertRCReturn(rc, rc); + rc = audioTestManifestWrite(pSet, "tone_duration_ms=%RU32\n", pParms->TestTone.msDuration); + AssertRCReturn(rc, rc); + rc = audioTestManifestWrite(pSet, "tone_sequel_ms=%RU32\n", pParms->TestTone.msSequel); + AssertRCReturn(rc, rc); + rc = audioTestManifestWrite(pSet, "tone_volume_percent=%RU32\n", pParms->TestTone.uVolumePercent); + AssertRCReturn(rc, rc); + rc = audioTestManifestWrite(pSet, "tone_pcm_hz=%RU32\n", PDMAudioPropsHz(&pParms->TestTone.Props)); + AssertRCReturn(rc, rc); + rc = audioTestManifestWrite(pSet, "tone_pcm_channels=%RU8\n", PDMAudioPropsChannels(&pParms->TestTone.Props)); + AssertRCReturn(rc, rc); + rc = audioTestManifestWrite(pSet, "tone_pcm_bits=%RU8\n", PDMAudioPropsSampleBits(&pParms->TestTone.Props)); + AssertRCReturn(rc, rc); + rc = audioTestManifestWrite(pSet, "tone_pcm_is_signed=%RTbool\n", PDMAudioPropsIsSigned(&pParms->TestTone.Props)); + AssertRCReturn(rc, rc); + break; + } + + default: + AssertFailed(); + break; + } + + RTListAppend(&pSet->lstTest, &pEntry->Node); + + pSet->cTests++; + pSet->cTestsRunning++; + pSet->pTestCur = pEntry; + + *ppEntry = pEntry; + + return rc; +} + +/** + * Marks a running test as failed. + * + * @returns VBox status code. + * @param pEntry Test to mark. + * @param rc Error code. + * @param pszErr Error description. + */ +int AudioTestSetTestFailed(PAUDIOTESTENTRY pEntry, int rc, const char *pszErr) +{ + AssertReturn(pEntry->pParent->cTestsRunning == 1, VERR_WRONG_ORDER); /* No test nesting allowed. */ + AssertReturn(pEntry->rc == VERR_IPE_UNINITIALIZED_STATUS, VERR_WRONG_ORDER); + + pEntry->rc = rc; + + int rc2 = audioTestManifestWrite(pEntry->pParent, "error_rc=%RI32\n", rc); + AssertRCReturn(rc2, rc2); + rc2 = audioTestManifestWrite(pEntry->pParent, "error_desc=%s\n", pszErr); + AssertRCReturn(rc2, rc2); + + pEntry->pParent->cTestsRunning--; + pEntry->pParent->pTestCur = NULL; + + return rc2; +} + +/** + * Marks a running test as successfully done. + * + * @returns VBox status code. + * @param pEntry Test to mark. + */ +int AudioTestSetTestDone(PAUDIOTESTENTRY pEntry) +{ + AssertReturn(pEntry->pParent->cTestsRunning == 1, VERR_WRONG_ORDER); /* No test nesting allowed. */ + AssertReturn(pEntry->rc == VERR_IPE_UNINITIALIZED_STATUS, VERR_WRONG_ORDER); + + pEntry->rc = VINF_SUCCESS; + + int rc2 = audioTestManifestWrite(pEntry->pParent, "error_rc=%RI32\n", VINF_SUCCESS); + AssertRCReturn(rc2, rc2); + + pEntry->pParent->cTestsRunning--; + pEntry->pParent->pTestCur = NULL; + + return rc2; +} + +/** + * Returns whether a test is still running or not. + * + * @returns \c true if test is still running, or \c false if not. + * @param pEntry Test to get running status for. + */ +bool AudioTestSetTestIsRunning(PAUDIOTESTENTRY pEntry) +{ + return (pEntry->rc == VERR_IPE_UNINITIALIZED_STATUS); +} + +/** + * Packs a closed audio test so that it's ready for transmission. + * + * @returns VBox status code. + * @param pSet Test set to pack. + * @param pszOutDir Directory where to store the packed test set. + * @param pszFileName Where to return the final name of the packed test set. Optional and can be NULL. + * @param cbFileName Size (in bytes) of \a pszFileName. + */ +int AudioTestSetPack(PAUDIOTESTSET pSet, const char *pszOutDir, char *pszFileName, size_t cbFileName) +{ + AssertPtrReturn(pSet, VERR_INVALID_POINTER); + AssertReturn(!pszFileName || cbFileName, VERR_INVALID_PARAMETER); + AssertReturn(!audioTestManifestIsOpen(pSet), VERR_WRONG_ORDER); + + /* No more validation (no / still running tests) here -- just pack all stuff we got so far + * and let the verification routine deal with it later. */ + + /** @todo Check and deny if \a pszOutDir is part of the set's path. */ + + int rc = RTDirCreateFullPath(pszOutDir, 0755); + if (RT_FAILURE(rc)) + return rc; + + char szOutName[RT_ELEMENTS(AUDIOTEST_PATH_PREFIX_STR) + AUDIOTEST_TAG_MAX + 16]; + if (RTStrPrintf2(szOutName, sizeof(szOutName), "%s-%s%s", + AUDIOTEST_PATH_PREFIX_STR, pSet->szTag, AUDIOTEST_ARCHIVE_SUFF_STR) <= 0) + AssertFailedReturn(VERR_BUFFER_OVERFLOW); + + char szOutPath[RTPATH_MAX]; + rc = RTPathJoin(szOutPath, sizeof(szOutPath), pszOutDir, szOutName); + AssertRCReturn(rc, rc); + + const char *apszArgs[10]; + unsigned cArgs = 0; + + apszArgs[cArgs++] = "vkat"; + apszArgs[cArgs++] = "--create"; + apszArgs[cArgs++] = "--gzip"; + apszArgs[cArgs++] = "--directory"; + apszArgs[cArgs++] = pSet->szPathAbs; + apszArgs[cArgs++] = "--file"; + apszArgs[cArgs++] = szOutPath; + apszArgs[cArgs++] = "."; + + RTEXITCODE rcExit = RTZipTarCmd(cArgs, (char **)apszArgs); + if (rcExit != RTEXITCODE_SUCCESS) + rc = VERR_GENERAL_FAILURE; /** @todo Fudge! */ + + if (RT_SUCCESS(rc)) + { + if (pszFileName) + rc = RTStrCopy(pszFileName, cbFileName, szOutPath); + } + + return rc; +} + +/** + * Returns whether a test set archive is packed (as .tar.gz by default) or + * a plain directory. + * + * @returns \c true if packed (as .tar.gz), or \c false if not (directory). + * @param pszPath Path to return packed staus for. + */ +bool AudioTestSetIsPacked(const char *pszPath) +{ + /** @todo Improve this, good enough for now. */ + return (RTStrIStr(pszPath, AUDIOTEST_ARCHIVE_SUFF_STR) != NULL); +} + +/** + * Returns whether a test set has running (active) tests or not. + * + * @returns \c true if it has running tests, or \c false if not. + * @param pSet Test set to return status for. + */ +bool AudioTestSetIsRunning(PAUDIOTESTSET pSet) +{ + return (pSet->cTestsRunning > 0); +} + +/** + * Unpacks a formerly packed audio test set. + * + * @returns VBox status code. + * @param pszFile Test set file to unpack. Must contain the absolute path. + * @param pszOutDir Directory where to unpack the test set into. + * If the directory does not exist it will be created. + */ +int AudioTestSetUnpack(const char *pszFile, const char *pszOutDir) +{ + AssertReturn(pszFile && pszOutDir, VERR_INVALID_PARAMETER); + + int rc = VINF_SUCCESS; + + if (!RTDirExists(pszOutDir)) + { + rc = RTDirCreateFullPath(pszOutDir, 0755); + if (RT_FAILURE(rc)) + return rc; + } + + const char *apszArgs[8]; + unsigned cArgs = 0; + + apszArgs[cArgs++] = "vkat"; + apszArgs[cArgs++] = "--extract"; + apszArgs[cArgs++] = "--gunzip"; + apszArgs[cArgs++] = "--directory"; + apszArgs[cArgs++] = pszOutDir; + apszArgs[cArgs++] = "--file"; + apszArgs[cArgs++] = pszFile; + + RTEXITCODE rcExit = RTZipTarCmd(cArgs, (char **)apszArgs); + if (rcExit != RTEXITCODE_SUCCESS) + rc = VERR_GENERAL_FAILURE; /** @todo Fudge! */ + + return rc; +} + +/** + * Retrieves an object handle of a specific test set section. + * + * @returns VBox status code. + * @param pSet Test set the section contains. + * @param pszSec Name of section to retrieve object handle for. + * @param phSec Where to store the object handle on success. + */ +static int audioTestSetGetSection(PAUDIOTESTSET pSet, const char *pszSec, PAUDIOTESTOBJINT phSec) +{ + int rc = RTStrCopy(phSec->szSec, sizeof(phSec->szSec), pszSec); + if (RT_FAILURE(rc)) + return rc; + + phSec->pSet = pSet; + + /** @todo Check for section existence. */ + RT_NOREF(pSet); + + return VINF_SUCCESS; +} + +/** + * Retrieves an object handle of a specific test. + * + * @returns VBox status code. + * @param pSet Test set the test contains. + * @param idxTst Index of test to retrieve the object handle for. + * @param phTst Where to store the object handle on success. + */ +static int audioTestSetGetTest(PAUDIOTESTSET pSet, uint32_t idxTst, PAUDIOTESTOBJINT phTst) +{ + char szSec[AUDIOTEST_MAX_SEC_LEN]; + if (RTStrPrintf2(szSec, sizeof(szSec), "test_%04RU32", idxTst) <= 0) + return VERR_BUFFER_OVERFLOW; + + return audioTestSetGetSection(pSet, szSec, phTst); +} + +/** + * Initializes a test object. + * + * @param pObj Object to initialize. + */ +static void audioTestObjInit(PAUDIOTESTOBJINT pObj) +{ + RT_BZERO(pObj, sizeof(AUDIOTESTOBJINT)); + + pObj->cRefs = 1; + + RTListInit(&pObj->lstMeta); +} + +/** + * Retrieves a child object of a specific parent object. + * + * @returns VBox status code. + * @param pParent Parent object the child object contains. + * @param idxObj Index of object to retrieve the object handle for. + * @param pObj Where to store the object handle on success. + */ +static int audioTestObjGetChild(PAUDIOTESTOBJINT pParent, uint32_t idxObj, PAUDIOTESTOBJINT pObj) +{ + char szObj[AUDIOTEST_MAX_SEC_LEN]; + if (RTStrPrintf2(szObj, sizeof(szObj), "obj%RU32_uuid", idxObj) <= 0) + AssertFailedReturn(VERR_BUFFER_OVERFLOW); + + char szUuid[AUDIOTEST_MAX_SEC_LEN]; + int rc = audioTestObjGetStr(pParent, szObj, szUuid, sizeof(szUuid)); + if (RT_SUCCESS(rc)) + { + audioTestObjInit(pObj); + + AssertReturn(RTStrPrintf2(pObj->szSec, sizeof(pObj->szSec), "obj_%s", szUuid) > 0, VERR_BUFFER_OVERFLOW); + + /** @todo Check test section existence. */ + + pObj->pSet = pParent->pSet; + } + + return rc; +} + +/** + * Verifies a value of a test verification job. + * + * @returns VBox status code. + * @returns Error if the verification failed and test verification job has fKeepGoing not set. + * @param pVerJob Verification job to verify value for. + * @param pObjA Object handle A to verify value for. + * @param pObjB Object handle B to verify value for. + * @param pszKey Key to verify. + * @param pszVal Value to verify. + * @param pszErrFmt Error format string in case the verification failed. + * @param ... Variable aruments for error format string. + */ +static int audioTestVerifyValue(PAUDIOTESTVERIFYJOB pVerJob, + PAUDIOTESTOBJINT pObjA, PAUDIOTESTOBJINT pObjB, const char *pszKey, const char *pszVal, const char *pszErrFmt, ...) +{ + va_list va; + va_start(va, pszErrFmt); + + char szValA[_1K]; + int rc = audioTestObjGetStr(pObjA, pszKey, szValA, sizeof(szValA)); + if (RT_SUCCESS(rc)) + { + char szValB[_1K]; + rc = audioTestObjGetStr(pObjB, pszKey, szValB, sizeof(szValB)); + if (RT_SUCCESS(rc)) + { + if (RTStrCmp(szValA, szValB)) + { + int rc2 = audioTestErrorDescAddError(pVerJob->pErr, pVerJob->idxTest, + "Values are not equal ('%s' vs. '%s')", szValA, szValB); + AssertRC(rc2); + rc = VERR_WRONG_TYPE; /** @todo Fudge! */ + } + + if (pszVal) + { + if (RTStrCmp(szValA, pszVal)) + { + int rc2 = audioTestErrorDescAddError(pVerJob->pErr, pVerJob->idxTest, + "Values don't match expected value (got '%s', expected '%s')", szValA, pszVal); + AssertRC(rc2); + rc = VERR_WRONG_TYPE; /** @todo Fudge! */ + } + } + } + } + + if (RT_FAILURE(rc)) + { + int rc2 = audioTestErrorDescAddV(pVerJob->pErr, pVerJob->idxTest, rc, pszErrFmt, va); + AssertRC(rc2); + } + + va_end(va); + + return pVerJob->Opts.fKeepGoing ? VINF_SUCCESS : rc; +} + +/** + * Opens a test object which is a regular file. + * + * @returns VBox status code. + * @param pObj Test object to open. + * @param pszFile Absolute file path of file to open. + */ +static int audioTestObjOpenFile(PAUDIOTESTOBJINT pObj, const char *pszFile) +{ + int rc = RTFileOpen(&pObj->File.hFile, pszFile, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_NONE); + if (RT_SUCCESS(rc)) + { + int rc2 = RTStrCopy(pObj->szName, sizeof(pObj->szName), pszFile); + AssertRC(rc2); + + pObj->enmType = AUDIOTESTOBJTYPE_FILE; + } + + return rc; +} + +/** + * Opens an existing audio test object. + * + * @returns VBox status code. + * @param pObj Object to open. + */ +static int audioTestObjOpen(PAUDIOTESTOBJINT pObj) +{ + AssertReturn(pObj->enmType == AUDIOTESTOBJTYPE_UNKNOWN, VERR_WRONG_ORDER); + + char szFileName[AUDIOTEST_MAX_SEC_LEN]; + int rc = audioTestObjGetStr(pObj, "obj_name", szFileName, sizeof(szFileName)); + if (RT_SUCCESS(rc)) + { + char szFilePath[RTPATH_MAX]; + rc = RTPathJoin(szFilePath, sizeof(szFilePath), pObj->pSet->szPathAbs, szFileName); + if (RT_SUCCESS(rc)) + { + /** @todo Check "obj_type". */ + rc = audioTestObjOpenFile(pObj, szFilePath); + } + } + return rc; +} + +/** + * Closes an audio test set object. + * + * @returns VBox status code. + * @param pObj Object to close. + */ +static int audioTestObjClose(PAUDIOTESTOBJINT pObj) +{ + if (!audioTestObjIsOpen(pObj)) + return VINF_SUCCESS; + + int rc; + + /** @todo Generalize this function more once we have more object types. */ + + if (RTFileIsValid(pObj->File.hFile)) + { + rc = RTFileClose(pObj->File.hFile); + if (RT_SUCCESS(rc)) + pObj->File.hFile = NIL_RTFILE; + } + else + rc = VINF_SUCCESS; + + return rc; +} + +/** + * Returns whether a test set object is in opened state or not. + * + * @returns \c true if open, or \c false if not. + * @param pObj Object to return status for. + */ +static bool audioTestObjIsOpen(PAUDIOTESTOBJINT pObj) +{ + return pObj->enmType != AUDIOTESTOBJTYPE_UNKNOWN; +} + +/** + * Finalizes an audio test set object. + * + * @param pObj Test object to finalize. + */ +static void audioTestObjFinalize(PAUDIOTESTOBJINT pObj) +{ + /** @todo Generalize this function more once we have more object types. */ + AssertReturnVoid(pObj->enmType == AUDIOTESTOBJTYPE_FILE); + + if (RTFileIsValid(pObj->File.hFile)) + pObj->File.cbSize = RTFileTell(pObj->File.hFile); +} + +/** + * Retrieves tone PCM properties of an object. + * + * @returns VBox status code. + * @param pObj Object to retrieve PCM properties for. + * @param pProps Where to store the PCM properties on success. + */ +static int audioTestObjGetTonePcmProps(PAUDIOTESTOBJINT pObj, PPDMAUDIOPCMPROPS pProps) +{ + int rc; + uint32_t uHz; + rc = audioTestObjGetUInt32(pObj, "tone_pcm_hz", &uHz); + AssertRCReturn(rc, rc); + uint8_t cBits; + rc = audioTestObjGetUInt8(pObj, "tone_pcm_bits", &cBits); + AssertRCReturn(rc, rc); + uint8_t cChan; + rc = audioTestObjGetUInt8(pObj, "tone_pcm_channels", &cChan); + AssertRCReturn(rc, rc); + bool fSigned; + rc = audioTestObjGetBool(pObj, "tone_pcm_is_signed", &fSigned); + AssertRCReturn(rc, rc); + + PDMAudioPropsInit(pProps, (cBits / 8), fSigned, cChan, uHz); + + return VINF_SUCCESS; +} + +/** + * Normalizes PCM audio data. + * Only supports 16 bit stereo PCM data for now. + * + * @returns VBox status code. + * @param hFileSrc Source file handle of audio data to normalize. + * @param pProps PCM properties to use for normalization. + * @param cbSize Size (in bytes) of audio data to normalize. + * @param dbNormalizePercent Normalization (percent) to achieve. + * @param hFileDst Destiation file handle (must be open) where to write the normalized audio data to. + * @param pdbRatio Where to store the normalization ratio used on success. Optional and can be NULL. + * A ration of exactly 1 means no normalization. + * + * @note The source file handle must point at the beginning of the PCM audio data to normalize. + */ +static int audioTestFileNormalizePCM(RTFILE hFileSrc, PCPDMAUDIOPCMPROPS pProps, uint64_t cbSize, + double dbNormalizePercent, RTFILE hFileDst, double *pdbRatio) +{ + if ( !pProps->fSigned + || pProps->cbSampleX != 2) /* Fend-off non-supported stuff first. */ + return VERR_NOT_SUPPORTED; + + int rc = VINF_SUCCESS; /* Shut up MSVC. */ + + if (!cbSize) + { + rc = RTFileQuerySize(hFileSrc, &cbSize); + AssertRCReturn(rc, rc); + } + else + AssertReturn(PDMAudioPropsIsSizeAligned(pProps, (uint32_t)cbSize), VERR_INVALID_PARAMETER); + + uint64_t offStart = RTFileTell(hFileSrc); + size_t cbToRead = cbSize; + + /* Find minimum and maximum peaks. */ + int16_t iMin = 0; + int16_t iMax = 0; + double dbRatio = 0.0; + + uint8_t auBuf[_64K]; + while (cbToRead) + { + size_t const cbChunk = RT_MIN(cbToRead, sizeof(auBuf)); + size_t cbRead = 0; + rc = RTFileRead(hFileSrc, auBuf, cbChunk, &cbRead); + if (rc == VERR_EOF) + break; + AssertRCBreak(rc); + + AssertBreak(PDMAudioPropsIsSizeAligned(pProps, (uint32_t)cbRead)); + + switch (pProps->cbSampleX) + { + case 2: /* 16 bit signed */ + { + int16_t *pi16Src = (int16_t *)auBuf; + for (size_t i = 0; i < cbRead / pProps->cbSampleX; i += pProps->cbSampleX) + { + if (*pi16Src < iMin) + iMin = *pi16Src; + if (*pi16Src > iMax) + iMax = *pi16Src; + pi16Src++; + } + break; + } + + default: + AssertMsgFailedBreakStmt(("Invalid bytes per sample: %RU8\n", pProps->cbSampleX), rc = VERR_NOT_SUPPORTED); + } + + Assert(cbToRead >= cbRead); + cbToRead -= cbRead; + } + + if (RT_FAILURE(rc)) + return rc; + + /* Now rewind and do the actual gain / attenuation. */ + rc = RTFileSeek(hFileSrc, offStart, RTFILE_SEEK_BEGIN, NULL /* poffActual */); + AssertRCReturn(rc, rc); + cbToRead = cbSize; + + switch (pProps->cbSampleX) + { + case 2: /* 16 bit signed */ + { + if (iMin == INT16_MIN) + iMin = INT16_MIN + 1; + if ((-iMin) > iMax) + iMax = -iMin; + + dbRatio = iMax == 0 ? 1.0 : ((double)INT16_MAX * dbNormalizePercent) / ((double)iMax * 100.0); + + while (cbToRead) + { + size_t const cbChunk = RT_MIN(cbToRead, sizeof(auBuf)); + size_t cbRead; + rc = RTFileRead(hFileSrc, auBuf, cbChunk, &cbRead); + if (rc == VERR_EOF) + break; + AssertRCBreak(rc); + + int16_t *pi16Src = (int16_t *)auBuf; + for (size_t i = 0; i < cbRead / pProps->cbSampleX; i += pProps->cbSampleX) + { + /** @todo Optimize this -- use a lookup table for sample indices? */ + if ((*pi16Src * dbRatio) > INT16_MAX) + *pi16Src = INT16_MAX; + else if ((*pi16Src * dbRatio) < INT16_MIN) + *pi16Src = INT16_MIN; + else + *pi16Src = (int16_t)(*pi16Src * dbRatio); + pi16Src++; + } + + size_t cbWritten; + rc = RTFileWrite(hFileDst, auBuf, cbChunk, &cbWritten); + AssertRCBreak(rc); + Assert(cbWritten == cbChunk); + + Assert(cbToRead >= cbRead); + cbToRead -= cbRead; + } + break; + } + + default: + AssertMsgFailedBreakStmt(("Invalid bytes per sample: %RU8\n", pProps->cbSampleX), rc = VERR_NOT_SUPPORTED); + } + + if (RT_SUCCESS(rc)) + { + if (pdbRatio) + *pdbRatio = dbRatio; + } + + return rc; +} + +/** + * Normalizes a test set audio object's audio data, extended version. + * + * @returns VBox status code. On success the test set object will point to the (temporary) normalized file data. + * @param pVerJob Verification job that contains \a pObj. + * @param pObj Test set object to normalize. + * @param pProps PCM properties to use for normalization. + * @param cbSize Size (in bytes) of audio data to normalize. + * @param dbNormalizePercent Normalization to achieve (in percent). + * + * @note The test set's file pointer must point to beginning of PCM data to normalize. + */ +static int audioTestObjFileNormalizeEx(PAUDIOTESTVERIFYJOB pVerJob, + PAUDIOTESTOBJINT pObj, PPDMAUDIOPCMPROPS pProps, uint64_t cbSize, double dbNormalizePercent) +{ + /* Store normalized file into a temporary file. */ + char szFileDst[RTPATH_MAX]; + int rc = RTPathTemp(szFileDst, sizeof(szFileDst)); + AssertRCReturn(rc, rc); + + rc = RTPathAppend(szFileDst, sizeof(szFileDst), "VBoxAudioTest-normalized-XXX.pcm"); + AssertRCReturn(rc, rc); + + rc = RTFileCreateTemp(szFileDst, 0600); + AssertRCReturn(rc, rc); + + RTFILE hFileDst; + rc = RTFileOpen(&hFileDst, szFileDst, RTFILE_O_OPEN | RTFILE_O_WRITE | RTFILE_O_DENY_WRITE); + AssertRCReturn(rc, rc); + + double dbRatio = 0.0; + rc = audioTestFileNormalizePCM(pObj->File.hFile, pProps, cbSize, dbNormalizePercent, hFileDst, &dbRatio); + if (RT_SUCCESS(rc)) + { + int rc2 = audioTestErrorDescAddInfo(pVerJob->pErr, pVerJob->idxTest, "Normalized '%s' -> '%s' (ratio is %u.%02u%%)\n", + pObj->szName, szFileDst, (unsigned)dbRatio, (unsigned)(dbRatio * 100) % 100); + AssertRC(rc2); + } + + int rc2 = RTFileClose(hFileDst); + if (RT_SUCCESS(rc)) + rc = rc2; + + if (RT_SUCCESS(rc)) + { + /* Close the original test set object and use the (temporary) normalized file instead now. */ + rc = audioTestObjClose(pObj); + if (RT_SUCCESS(rc)) + rc = audioTestObjOpenFile(pObj, szFileDst); + } + + return rc; +} + +/** + * Normalizes a test set audio object's audio data. + * + * @returns VBox status code. + * @param pVerJob Verification job that contains \a pObj. + * @param pObj Test set object to normalize. + * @param pProps PCM properties to use for normalization. + * + * @note The test set's file pointer must point to beginning of PCM data to normalize. + */ +static int audioTestObjFileNormalize(PAUDIOTESTVERIFYJOB pVerJob, PAUDIOTESTOBJINT pObj, PPDMAUDIOPCMPROPS pProps) +{ + return audioTestObjFileNormalizeEx(pVerJob, + pObj, pProps, 0 /* cbSize, 0 means all */, 100.0 /* dbNormalizePercent */); +} + +/** + * Structure for keeping file comparison parameters for one file. + */ +typedef struct AUDIOTESTFILECMPPARMS +{ + /** File name for logging purposes. */ + const char *pszName; + /** File handle to file to compare. */ + RTFILE hFile; + /** Absolute offset (in bytes) to start comparing. + * Ignored when set to 0. */ + uint64_t offStart; + /** Size (in bytes) of area to compare. + * Starts at \a offStart. */ + uint64_t cbSize; +} AUDIOTESTFILECMPPARMS; +/** Pointer to file comparison parameters for one file. */ +typedef AUDIOTESTFILECMPPARMS *PAUDIOTESTFILECMPPARMS; + +/** + * Determines if a given file chunk contains all silence (i.e. non-audible audio data) or not. + * + * What "silence" means depends on the given PCM parameters. + * + * @returns VBox status code. + * @param phFile File handle of file to determine silence for. + * @param pProps PCM properties to use. + * @param offStart Start offset (absolute, in bytes) to start at. + * @param cbSize Size (in bytes) to process. + * @param pfIsSilence Where to return the result. + * + * @note Does *not* modify the file's current position. + */ +static int audioTestFileChunkIsSilence(PRTFILE phFile, PPDMAUDIOPCMPROPS pProps, uint64_t offStart, size_t cbSize, + bool *pfIsSilence) +{ + bool fIsSilence = true; + + int rc = RTFileSeek(*phFile, offStart, RTFILE_SEEK_BEGIN, NULL); + AssertRCReturn(rc, rc); + + uint8_t auBuf[_64K]; + while (cbSize) + { + size_t cbRead; + rc = RTFileRead(*phFile, auBuf, RT_MIN(cbSize, sizeof(auBuf)), &cbRead); + AssertRC(rc); + + if (!PDMAudioPropsIsBufferSilence(pProps, auBuf, cbRead)) + { + fIsSilence = false; + break; + } + + AssertBreak(cbSize >= cbRead); + cbSize -= cbRead; + } + + if (RT_SUCCESS(rc)) + *pfIsSilence = fIsSilence; + + return RTFileSeek(*phFile, offStart, RTFILE_SEEK_BEGIN, NULL); +} + +/** + * Finds differences in two audio test files by binary comparing chunks. + * + * @returns Number of differences. 0 means they are equal (but not necessarily identical). + * @param pVerJob Verification job to verify PCM data for. + * @param pCmpA File comparison parameters to file A to compare file B with. + * @param pCmpB File comparison parameters to file B to compare file A with. + * @param pToneParms Tone parameters to use for comparison. + */ +static uint32_t audioTestFilesFindDiffsBinary(PAUDIOTESTVERIFYJOB pVerJob, + PAUDIOTESTFILECMPPARMS pCmpA, PAUDIOTESTFILECMPPARMS pCmpB, + PAUDIOTESTTONEPARMS pToneParms) +{ + uint8_t auBufA[_4K]; + uint8_t auBufB[_4K]; + + int rc = RTFileSeek(pCmpA->hFile, pCmpA->offStart, RTFILE_SEEK_BEGIN, NULL); + AssertRC(rc); + + rc = RTFileSeek(pCmpB->hFile, pCmpB->offStart, RTFILE_SEEK_BEGIN, NULL); + AssertRC(rc); + + uint32_t cDiffs = 0; + uint64_t cbDiffs = 0; + + uint32_t const cbChunkSize = PDMAudioPropsFrameSize(&pToneParms->Props); /* Use the audio frame size as chunk size. */ + + uint64_t offCur = 0; + uint64_t offDiffStart = 0; + bool fInDiff = false; + uint64_t cbSize = RT_MIN(pCmpA->cbSize, pCmpB->cbSize); + uint64_t cbToCompare = cbSize; + + while (cbToCompare) + { + size_t cbReadA; + rc = RTFileRead(pCmpA->hFile, auBufA, RT_MIN(cbToCompare, cbChunkSize), &cbReadA); + AssertRCBreak(rc); + size_t cbReadB; + rc = RTFileRead(pCmpB->hFile, auBufB, RT_MIN(cbToCompare, cbChunkSize), &cbReadB); + AssertRCBreak(rc); + AssertBreakStmt(cbReadA == cbReadB, rc = VERR_INVALID_PARAMETER); /** @todo Find a better rc. */ + + const size_t cbToCmp = RT_MIN(cbReadA, cbReadB); + if (memcmp(auBufA, auBufB, cbToCmp) != 0) + { + if (!fInDiff) /* No consequitive different chunk? Count as new then. */ + { + cDiffs++; + offDiffStart = offCur; + fInDiff = true; + } + } + else /* Reset and count next difference as new then. */ + { + if (fInDiff) + { + bool fIsAllSilenceA; + rc = audioTestFileChunkIsSilence(&pCmpA->hFile, &pToneParms->Props, + pCmpA->offStart + offDiffStart, offCur - offDiffStart, &fIsAllSilenceA); + AssertRCBreak(rc); + + bool fIsAllSilenceB; + rc = audioTestFileChunkIsSilence(&pCmpB->hFile, &pToneParms->Props, + pCmpB->offStart + offDiffStart, offCur - offDiffStart, &fIsAllSilenceB); + AssertRCBreak(rc); + + uint32_t const cbDiff = offCur - offDiffStart; + int rc2 = audioTestErrorDescAddInfo(pVerJob->pErr, pVerJob->idxTest, "Chunks differ: '%s' @ %#x [%08RU64-%08RU64] vs. '%s' @ %#x [%08RU64-%08RU64] (%RU64 bytes, %RU64ms)", + pCmpA->pszName, pCmpA->offStart + offDiffStart, pCmpA->offStart + offDiffStart, pCmpA->offStart + offCur, + pCmpB->pszName, pCmpB->offStart + offDiffStart, pCmpB->offStart + offDiffStart, pCmpB->offStart + offCur, + cbDiff, PDMAudioPropsBytesToMilli(&pToneParms->Props, cbDiff)); + AssertRC(rc2); + if ( fIsAllSilenceA + || fIsAllSilenceB) + { + rc2 = audioTestErrorDescAddInfo(pVerJob->pErr, pVerJob->idxTest, "Chunk %s @ %#x (%RU64 bytes, %RU64ms) is all silence", + fIsAllSilenceA ? pCmpA->pszName : pCmpB->pszName, + offDiffStart, cbDiff, PDMAudioPropsBytesToMilli(&pToneParms->Props, cbDiff)); + AssertRC(rc2); + } + + cbDiffs += cbDiff; + } + fInDiff = false; + } + + AssertBreakStmt(cbToCompare >= cbReadA, VERR_INTERNAL_ERROR); + cbToCompare -= cbReadA; + offCur += cbReadA; + } + + /* If we didn't mention the last diff yet, do so now. */ + if (fInDiff) + { + uint32_t const cbDiff = offCur - offDiffStart; + int rc2 = audioTestErrorDescAddInfo(pVerJob->pErr, pVerJob->idxTest, "Chunks differ: '%s' @ %#x [%08RU64-%08RU64] vs. '%s' @ %#x [%08RU64-%08RU64] (%RU64 bytes, %RU64ms)", + pCmpA->pszName, pCmpA->offStart + offDiffStart, pCmpA->offStart + offDiffStart, pCmpA->offStart + offCur, + pCmpB->pszName, pCmpB->offStart + offDiffStart, pCmpB->offStart + offDiffStart, pCmpB->offStart + offCur, + cbDiff, PDMAudioPropsBytesToMilli(&pToneParms->Props, cbDiff)); + AssertRC(rc2); + + cbDiffs += cbDiff; + } + + if ( cbSize + && cbDiffs) + { + uint8_t const uDiffPercent = cbDiffs / (cbSize * 100); + if (uDiffPercent > pVerJob->Opts.uMaxDiffPercent) + { + int rc2 = audioTestErrorDescAddInfo(pVerJob->pErr, pVerJob->idxTest, "Files binary-differ too much (expected maximum %RU8%%, got %RU8%%)", + pVerJob->Opts.uMaxDiffPercent, uDiffPercent); + AssertRC(rc2); + } + } + + return cDiffs; +} + +/** + * Initializes a audio test audio beacon. + * + * @returns VBox status code. + * @param pBeacon Audio test beacon to (re-)initialize. + * @param uTest Test number to set beacon to. + * @param enmType Beacon type to set. + * @param pProps PCM properties to use for producing audio beacon data. + */ +void AudioTestBeaconInit(PAUDIOTESTTONEBEACON pBeacon, uint8_t uTest, AUDIOTESTTONEBEACONTYPE enmType, PPDMAUDIOPCMPROPS pProps) +{ + AssertReturnVoid(PDMAudioPropsFrameSize(pProps) == 4); /** @todo Make this more dynamic. */ + + RT_BZERO(pBeacon, sizeof(AUDIOTESTTONEBEACON)); + + pBeacon->uTest = uTest; + pBeacon->enmType = enmType; + memcpy(&pBeacon->Props, pProps, sizeof(PDMAUDIOPCMPROPS)); + + pBeacon->cbSize = PDMAudioPropsFramesToBytes(&pBeacon->Props, AUDIOTEST_BEACON_SIZE_FRAMES); +} + +/** + * Returns the beacon byte of a beacon type. + * + * @returns Beacon byte if found, 0 otherwise. + * @param uTest Test number to get beacon byte for. + * @param enmType Beacon type to get beacon byte for. + */ +DECLINLINE(uint8_t) AudioTestBeaconByteFromType(uint8_t uTest, AUDIOTESTTONEBEACONTYPE enmType) +{ + switch (enmType) + { + case AUDIOTESTTONEBEACONTYPE_PLAY_PRE: return AUDIOTEST_BEACON_MAKE_PRE(uTest); + case AUDIOTESTTONEBEACONTYPE_PLAY_POST: return AUDIOTEST_BEACON_MAKE_POST(uTest); + case AUDIOTESTTONEBEACONTYPE_REC_PRE: return AUDIOTEST_BEACON_MAKE_PRE(uTest); + case AUDIOTESTTONEBEACONTYPE_REC_POST: return AUDIOTEST_BEACON_MAKE_POST(uTest); + default: break; + } + + AssertFailed(); + return 0; +} + +/** + * Returns the total expected (total) size of an audio beacon (in bytes). + * + * @returns Beacon size in bytes. + * @param pBeacon Beacon to get beacon size for. + */ +uint32_t AudioTestBeaconGetSize(PCAUDIOTESTTONEBEACON pBeacon) +{ + return pBeacon->cbSize; +} + +/** + * Returns the beacon type of an audio beacon. + * + * @returns Beacon type. + * @param pBeacon Beacon to get beacon size for. + */ +AUDIOTESTTONEBEACONTYPE AudioTestBeaconGetType(PCAUDIOTESTTONEBEACON pBeacon) +{ + return pBeacon->enmType; +} + +/** + * Returns the remaining bytes (to be complete) of an audio beacon. + * + * @returns Remaining bytes. + * @param pBeacon Beacon to get remaining size for. + */ +uint32_t AudioTestBeaconGetRemaining(PCAUDIOTESTTONEBEACON pBeacon) +{ + return pBeacon->cbSize - pBeacon->cbUsed; +} + +/** + * Returns the already used (received) bytes (to be complete) of an audio beacon. + * + * @returns Used bytes. + * @param pBeacon Beacon to get remaining size for. + */ +uint32_t AudioTestBeaconGetUsed(PCAUDIOTESTTONEBEACON pBeacon) +{ + return pBeacon->cbUsed; +} + +/** + * Writes audio beacon data to a given buffer. + * + * @returns VBox status code. + * @param pBeacon Beacon to write to buffer. + * @param pvBuf Buffer to write to. + * @param cbBuf Size (in bytes) of buffer to write to. + */ +int AudioTestBeaconWrite(PAUDIOTESTTONEBEACON pBeacon, void *pvBuf, uint32_t cbBuf) +{ + AssertReturn(pBeacon->cbUsed + cbBuf <= pBeacon->cbSize, VERR_BUFFER_OVERFLOW); + + memset(pvBuf, AudioTestBeaconByteFromType(pBeacon->uTest, pBeacon->enmType), cbBuf); + + pBeacon->cbUsed += cbBuf; + + return VINF_SUCCESS; +} + +/** + * Converts an audio beacon type to a string. + * + * @returns Pointer to read-only audio beacon type string on success, + * "illegal" if invalid command value. + * @param enmType The type to convert. + */ +const char *AudioTestBeaconTypeGetName(AUDIOTESTTONEBEACONTYPE enmType) +{ + switch (enmType) + { + case AUDIOTESTTONEBEACONTYPE_PLAY_PRE: return "pre-playback"; + case AUDIOTESTTONEBEACONTYPE_PLAY_POST: return "post-playback"; + case AUDIOTESTTONEBEACONTYPE_REC_PRE: return "pre-recording"; + case AUDIOTESTTONEBEACONTYPE_REC_POST: return "post-recording"; + default: break; + } + AssertMsgFailedReturn(("Invalid beacon type: #%x\n", enmType), "illegal"); +} + +/** + * Adds audio data to a given beacon. + * + * @returns VBox status code, VERR_NOT_FOUND if not beacon data was not found. + * @param pBeacon Beacon to add data for. + * @param pauBuf Buffer of audio data to add. + * @param cbBuf Size (in bytes) of \a pauBuf. + * @param pOff Where to return the offset within \a pauBuf where beacon ended on success. + * Optional and can be NULL. + * + * @note The audio data must be a) match the beacon type and b) consecutive, that is, without any gaps, + * to be added as valid to the beacon. + */ +int AudioTestBeaconAddConsecutive(PAUDIOTESTTONEBEACON pBeacon, const uint8_t *pauBuf, size_t cbBuf, size_t *pOff) +{ + AssertPtrReturn(pBeacon, VERR_INVALID_POINTER); + AssertPtrReturn(pauBuf, VERR_INVALID_POINTER); + /* pOff is optional. */ + + uint64_t offBeacon = UINT64_MAX; + uint32_t const cbFrameSize = PDMAudioPropsFrameSize(&pBeacon->Props); /* Use the audio frame size as chunk size. */ + + uint8_t const byBeacon = AudioTestBeaconByteFromType(pBeacon->uTest, pBeacon->enmType); + unsigned const cbStep = cbFrameSize; + + /* Make sure that we do frame-aligned reads. */ + cbBuf = PDMAudioPropsFloorBytesToFrame(&pBeacon->Props, (uint32_t)cbBuf); + + for (size_t i = 0; i < cbBuf; i += cbStep) + { + if ( pauBuf[i] == byBeacon + && pauBuf[i + 1] == byBeacon + && pauBuf[i + 2] == byBeacon + && pauBuf[i + 3] == byBeacon) + { + /* Make sure to handle overflows and let beacon start from scratch. */ + pBeacon->cbUsed = (pBeacon->cbUsed + cbStep) % pBeacon->cbSize; + if (pBeacon->cbUsed == 0) /* Beacon complete (see module line above)? */ + { + pBeacon->cbUsed = pBeacon->cbSize; + offBeacon = i + cbStep; /* Point to data right *after* the beacon. */ + } + } + else + { + /* If beacon is not complete yet, we detected a gap here. Start all over then. */ + if (RT_LIKELY(pBeacon->cbUsed != pBeacon->cbSize)) + pBeacon->cbUsed = 0; + } + } + + if (offBeacon != UINT64_MAX) + { + if (pOff) + *pOff = offBeacon; + } + + return offBeacon == UINT64_MAX ? VERR_NOT_FOUND : VINF_SUCCESS; +} + +/** + * Returns whether a beacon is considered to be complete or not. + * + * A complete beacon means that all data for it has been retrieved. + * + * @returns \c true if complete, or \c false if not. + * @param pBeacon Beacon to get completion status for. + */ +bool AudioTestBeaconIsComplete(PCAUDIOTESTTONEBEACON pBeacon) +{ + AssertReturn(pBeacon->cbUsed <= pBeacon->cbSize, true); + return (pBeacon->cbUsed == pBeacon->cbSize); +} + +/** + * Verifies a pre/post beacon of a test tone. + * + * @returns VBox status code, VERR_NOT_FOUND if beacon was not found. + * @param pVerJob Verification job to verify PCM data for. + * @param fIn Set to \c true for recording, \c false for playback. + * @param fPre Set to \c true to verify a pre beacon, or \c false to verify a post beacon. + * @param pCmp File comparison parameters to file to verify beacon for. + * @param pToneParms Tone parameters to use for verification. + * @param puOff Where to return the absolute file offset (in bytes) right after the found beacon on success. + * Optional and can be NULL. + */ +static int audioTestToneVerifyBeacon(PAUDIOTESTVERIFYJOB pVerJob, + bool fIn, bool fPre, PAUDIOTESTFILECMPPARMS pCmp, PAUDIOTESTTONEPARMS pToneParms, + uint64_t *puOff) +{ + int rc = RTFileSeek(pCmp->hFile, pCmp->offStart, RTFILE_SEEK_BEGIN, NULL); + AssertRCReturn(rc, rc); + + AUDIOTESTTONEBEACON Beacon; + RT_ZERO(Beacon); + AudioTestBeaconInit(&Beacon, pVerJob->idxTest, + fIn + ? (fPre ? AUDIOTESTTONEBEACONTYPE_PLAY_PRE : AUDIOTESTTONEBEACONTYPE_PLAY_POST) + : (fPre ? AUDIOTESTTONEBEACONTYPE_REC_PRE : AUDIOTESTTONEBEACONTYPE_REC_POST), &pToneParms->Props); + + uint8_t auBuf[_64K]; + uint64_t cbToCompare = pCmp->cbSize; + uint32_t const cbFrameSize = PDMAudioPropsFrameSize(&Beacon.Props); + uint64_t offBeaconLast = UINT64_MAX; + + Assert(sizeof(auBuf) % cbFrameSize == 0); + + while (cbToCompare) + { + size_t cbRead; + rc = RTFileRead(pCmp->hFile, auBuf, RT_MIN(cbToCompare, sizeof(auBuf)), &cbRead); + AssertRCBreak(rc); + + if (cbRead < cbFrameSize) + break; + + size_t uOff; + int rc2 = AudioTestBeaconAddConsecutive(&Beacon, auBuf, cbRead, &uOff); + if (RT_SUCCESS(rc2)) + { + /* Save the last found (absolute bytes, in file) position of a (partially) found beacon. */ + offBeaconLast = RTFileTell(pCmp->hFile) - (cbRead - uOff); + } + + Assert(cbToCompare >= cbRead); + cbToCompare -= cbRead; + } + + uint32_t const cbBeacon = AudioTestBeaconGetUsed(&Beacon); + + if (!AudioTestBeaconIsComplete(&Beacon)) + { + int rc2 = audioTestErrorDescAddError(pVerJob->pErr, pVerJob->idxTest, "File '%s': %s beacon %s (got %RU32 bytes, expected %RU32)", + pCmp->pszName, + AudioTestBeaconTypeGetName(Beacon.enmType), + cbBeacon ? "found" : "not found", cbBeacon, + AudioTestBeaconGetSize(&Beacon)); + AssertRC(rc2); + return VERR_NOT_FOUND; + } + else + { + AssertReturn(AudioTestBeaconGetRemaining(&Beacon) == 0, VERR_INTERNAL_ERROR); + AssertReturn(offBeaconLast != UINT32_MAX, VERR_INTERNAL_ERROR); + AssertReturn(offBeaconLast >= AudioTestBeaconGetSize(&Beacon), VERR_INTERNAL_ERROR); + + int rc2 = audioTestErrorDescAddInfo(pVerJob->pErr, pVerJob->idxTest, "File '%s': %s beacon found at offset %RU64 and valid", + pCmp->pszName, AudioTestBeaconTypeGetName(Beacon.enmType), + offBeaconLast - AudioTestBeaconGetSize(&Beacon)); + AssertRC(rc2); + + if (puOff) + *puOff = offBeaconLast; + } + + return rc; +} + +#define CHECK_RC_MAYBE_RET(a_rc, a_pVerJob) \ + if (RT_FAILURE(a_rc)) \ + { \ + if (!a_pVerJob->Opts.fKeepGoing) \ + return VINF_SUCCESS; \ + } + +#define CHECK_RC_MSG_MAYBE_RET(a_rc, a_pVerJob, a_Msg) \ + if (RT_FAILURE(a_rc)) \ + { \ + int rc3 = audioTestErrorDescAddError(a_pVerJob->pErr, a_pVerJob->idxTest, a_Msg); \ + AssertRC(rc3); \ + if (!a_pVerJob->Opts.fKeepGoing) \ + return VINF_SUCCESS; \ + } + +#define CHECK_RC_MSG_VA_MAYBE_RET(a_rc, a_pVerJob, a_Msg, ...) \ + if (RT_FAILURE(a_rc)) \ + { \ + int rc3 = audioTestErrorDescAddError(a_pVerJob->pErr, a_pVerJob->idxTest, a_Msg, __VA_ARGS__); \ + AssertRC(rc3); \ + if (!a_pVerJob->Opts.fKeepGoing) \ + return VINF_SUCCESS; \ + +/** + * Does the actual PCM data verification of a test tone. + * + * @returns VBox status code. + * @param pVerJob Verification job to verify PCM data for. + * @param phTestA Test handle A of test to verify PCM data for. + * @param phTestB Test handle B of test to verify PCM data for. + */ +static int audioTestVerifyTestToneData(PAUDIOTESTVERIFYJOB pVerJob, PAUDIOTESTOBJINT phTestA, PAUDIOTESTOBJINT phTestB) +{ + int rc; + + /** @todo For now ASSUME that we only have one object per test. */ + + AUDIOTESTOBJINT ObjA; + rc = audioTestObjGetChild(phTestA, 0 /* idxObj */, &ObjA); + CHECK_RC_MSG_MAYBE_RET(rc, pVerJob, "Unable to get object A"); + + rc = audioTestObjOpen(&ObjA); + CHECK_RC_MSG_MAYBE_RET(rc, pVerJob, "Unable to open object A"); + + AUDIOTESTOBJINT ObjB; + rc = audioTestObjGetChild(phTestB, 0 /* idxObj */, &ObjB); + CHECK_RC_MSG_MAYBE_RET(rc, pVerJob, "Unable to get object B"); + + rc = audioTestObjOpen(&ObjB); + CHECK_RC_MSG_MAYBE_RET(rc, pVerJob, "Unable to open object B"); + + /* + * Start with most obvious methods first. + */ + uint64_t cbFileSizeA, cbFileSizeB; + rc = RTFileQuerySize(ObjA.File.hFile, &cbFileSizeA); + AssertRCReturn(rc, rc); + rc = RTFileQuerySize(ObjB.File.hFile, &cbFileSizeB); + AssertRCReturn(rc, rc); + + if (!cbFileSizeA) + { + int rc2 = audioTestErrorDescAddError(pVerJob->pErr, pVerJob->idxTest, "File '%s' is empty", ObjA.szName); + AssertRC(rc2); + } + + if (!cbFileSizeB) + { + int rc2 = audioTestErrorDescAddError(pVerJob->pErr, pVerJob->idxTest, "File '%s' is empty", ObjB.szName); + AssertRC(rc2); + } + + if (cbFileSizeA != cbFileSizeB) + { + size_t const cbDiffAbs = cbFileSizeA > cbFileSizeB ? cbFileSizeA - cbFileSizeB : cbFileSizeB - cbFileSizeA; + + int rc2 = audioTestErrorDescAddInfo(pVerJob->pErr, pVerJob->idxTest, "File '%s': %zu bytes (%RU64ms)", + ObjA.szName, cbFileSizeA, PDMAudioPropsBytesToMilli(&pVerJob->PCMProps, cbFileSizeA)); + AssertRC(rc2); + rc2 = audioTestErrorDescAddInfo(pVerJob->pErr, pVerJob->idxTest, "File '%s': %zu bytes (%RU64ms)", + ObjB.szName, cbFileSizeB, PDMAudioPropsBytesToMilli(&pVerJob->PCMProps, cbFileSizeB)); + AssertRC(rc2); + + uint8_t const uSizeDiffPercentAbs + = cbFileSizeA > cbFileSizeB ? 100 - ((cbFileSizeB * 100) / cbFileSizeA) : 100 - ((cbFileSizeA * 100) / cbFileSizeB); + + if (uSizeDiffPercentAbs > pVerJob->Opts.uMaxSizePercent) + { + rc2 = audioTestErrorDescAddError(pVerJob->pErr, pVerJob->idxTest, + "File '%s' is %RU8%% (%zu bytes, %RU64ms) %s than '%s' (threshold is %RU8%%)", + ObjA.szName, + uSizeDiffPercentAbs, + cbDiffAbs, PDMAudioPropsBytesToMilli(&pVerJob->PCMProps, (uint32_t)cbDiffAbs), + cbFileSizeA > cbFileSizeB ? "bigger" : "smaller", + ObjB.szName, pVerJob->Opts.uMaxSizePercent); + AssertRC(rc2); + } + } + + /* Do normalization first if enabled. */ + if (pVerJob->Opts.fNormalize) + { + rc = audioTestObjFileNormalize(pVerJob, &ObjA, &pVerJob->PCMProps); + if (RT_SUCCESS(rc)) + rc = audioTestObjFileNormalize(pVerJob, &ObjB, &pVerJob->PCMProps); + } + + /** @todo For now we only support comparison of data which do have identical PCM properties! */ + + AUDIOTESTTONEPARMS ToneParmsA; + RT_ZERO(ToneParmsA); + ToneParmsA.Props = pVerJob->PCMProps; + + size_t cbSearchWindow = PDMAudioPropsMilliToBytes(&ToneParmsA.Props, pVerJob->Opts.msSearchWindow); + + AUDIOTESTFILECMPPARMS FileA; + RT_ZERO(FileA); + FileA.pszName = ObjA.szName; + FileA.hFile = ObjA.File.hFile; + FileA.offStart = audioTestToneFileFind(ObjA.File.hFile, true /* fFindSilence */, + 0 /* uOff */, cbFileSizeA /* cbMax */, &ToneParmsA, cbSearchWindow); + FileA.cbSize = audioTestToneFileFind(ObjA.File.hFile, false /* fFindSilence */, + FileA.offStart /* uOff */, cbFileSizeA - FileA.offStart /* cbMax */, &ToneParmsA, cbSearchWindow); + AssertReturn(FileA.offStart + FileA.cbSize <= cbFileSizeA, VERR_INTERNAL_ERROR); + + AUDIOTESTTONEPARMS ToneParmsB; + RT_ZERO(ToneParmsB); + ToneParmsB.Props = pVerJob->PCMProps; + + AUDIOTESTFILECMPPARMS FileB; + RT_ZERO(FileB); + FileB.pszName = ObjB.szName; + FileB.hFile = ObjB.File.hFile; + FileB.offStart = audioTestToneFileFind(ObjB.File.hFile, true /* fFindSilence */, + 0 /* uOff */, cbFileSizeB /* cbMax */, &ToneParmsB, cbSearchWindow); + FileB.cbSize = audioTestToneFileFind(ObjB.File.hFile, false /* fFindSilence */, + FileB.offStart /* uOff */, cbFileSizeB - FileB.offStart /* cbMax */, &ToneParmsB, cbSearchWindow); + AssertReturn(FileB.offStart + FileB.cbSize <= cbFileSizeB, VERR_INTERNAL_ERROR); + + int rc2; + + uint64_t offBeaconAbs; + rc = audioTestToneVerifyBeacon(pVerJob, phTestA->enmTestType == AUDIOTESTTYPE_TESTTONE_PLAY /* fIn */, + true /* fPre */, &FileA, &ToneParmsA, &offBeaconAbs); + if (RT_SUCCESS(rc)) + { + FileA.offStart = offBeaconAbs; + FileA.cbSize = cbFileSizeA - FileA.offStart; + rc = audioTestToneVerifyBeacon(pVerJob, phTestA->enmTestType == AUDIOTESTTYPE_TESTTONE_PLAY /* fIn */, + false /* fPre */, &FileA, &ToneParmsA, &offBeaconAbs); + if (RT_SUCCESS(rc)) + { + /* Adjust the size of the area to compare so that it's within the pre + post beacons. */ + Assert(offBeaconAbs >= FileA.offStart); + FileA.cbSize = offBeaconAbs - FileA.offStart; + } + } + + rc = audioTestToneVerifyBeacon(pVerJob, phTestB->enmTestType == AUDIOTESTTYPE_TESTTONE_RECORD /* fIn */, + true /* fPre */, &FileB, &ToneParmsB, &offBeaconAbs); + if (RT_SUCCESS(rc)) + { + FileB.offStart = offBeaconAbs; + FileB.cbSize = cbFileSizeB - FileB.offStart; + rc = audioTestToneVerifyBeacon(pVerJob, phTestB->enmTestType == AUDIOTESTTYPE_TESTTONE_RECORD /* fIn */, + false /* fPre */, &FileB, &ToneParmsB, &offBeaconAbs); + if (RT_SUCCESS(rc)) + { + /* Adjust the size of the area to compare so that it's within the pre + post beacons. */ + Assert(offBeaconAbs >= FileB.offStart); + FileB.cbSize = offBeaconAbs - FileB.offStart; + } + } + + if (RT_SUCCESS(rc)) + { + uint32_t const cDiffs = audioTestFilesFindDiffsBinary(pVerJob, &FileA, &FileB, &ToneParmsA); + + if (cDiffs > pVerJob->Opts.cMaxDiff) + { + rc2 = audioTestErrorDescAddError(pVerJob->pErr, pVerJob->idxTest, + "Files '%s' and '%s' have too many different chunks (got %RU32, expected %RU32)", + ObjA.szName, ObjB.szName, cDiffs, pVerJob->Opts.cMaxDiff); + AssertRC(rc2); + } + } + + if (AudioTestErrorDescFailed(pVerJob->pErr)) + { + rc2 = audioTestErrorDescAddInfo(pVerJob->pErr, pVerJob->idxTest, "Files '%s' and '%s' do not match", + ObjA.szName, ObjB.szName); + AssertRC(rc2); + } + + rc = audioTestObjClose(&ObjA); + AssertRCReturn(rc, rc); + rc = audioTestObjClose(&ObjB); + AssertRCReturn(rc, rc); + + return rc; +} + +/** + * Verifies a test tone test. + * + * @returns VBox status code. + * @returns Error if the verification failed and test verification job has fKeepGoing not set. + * @retval VERR_ + * @param pVerJob Verification job to verify test tone for. + * @param phTestA Test handle of test tone A to verify tone B with. + * @param phTestB Test handle of test tone B to verify tone A with.* + */ +static int audioTestVerifyTestTone(PAUDIOTESTVERIFYJOB pVerJob, PAUDIOTESTOBJINT phTestA, PAUDIOTESTOBJINT phTestB) +{ + int rc; + + /* + * Verify test parameters. + * More important items have precedence. + */ + rc = audioTestVerifyValue(pVerJob, phTestA, phTestB, "error_rc", "0", "Test was reported as failed"); + CHECK_RC_MAYBE_RET(rc, pVerJob); + rc = audioTestVerifyValue(pVerJob, phTestA, phTestB, "obj_count", NULL, "Object counts don't match"); + CHECK_RC_MAYBE_RET(rc, pVerJob); + rc = audioTestVerifyValue(pVerJob, phTestA, phTestB, "tone_freq_hz", NULL, "Tone frequency doesn't match"); + CHECK_RC_MAYBE_RET(rc, pVerJob); + rc = audioTestVerifyValue(pVerJob, phTestA, phTestB, "tone_prequel_ms", NULL, "Tone prequel (ms) doesn't match"); + CHECK_RC_MAYBE_RET(rc, pVerJob); + rc = audioTestVerifyValue(pVerJob, phTestA, phTestB, "tone_duration_ms", NULL, "Tone duration (ms) doesn't match"); + CHECK_RC_MAYBE_RET(rc, pVerJob); + rc = audioTestVerifyValue(pVerJob, phTestA, phTestB, "tone_sequel_ms", NULL, "Tone sequel (ms) doesn't match"); + CHECK_RC_MAYBE_RET(rc, pVerJob); + rc = audioTestVerifyValue(pVerJob, phTestA, phTestB, "tone_volume_percent", NULL, "Tone volume (percent) doesn't match"); + CHECK_RC_MAYBE_RET(rc, pVerJob); + rc = audioTestVerifyValue(pVerJob, phTestA, phTestB, "tone_pcm_hz", NULL, "Tone PCM Hz doesn't match"); + CHECK_RC_MAYBE_RET(rc, pVerJob); + rc = audioTestVerifyValue(pVerJob, phTestA, phTestB, "tone_pcm_channels", NULL, "Tone PCM channels don't match"); + CHECK_RC_MAYBE_RET(rc, pVerJob); + rc = audioTestVerifyValue(pVerJob, phTestA, phTestB, "tone_pcm_bits", NULL, "Tone PCM bits don't match"); + CHECK_RC_MAYBE_RET(rc, pVerJob); + rc = audioTestVerifyValue(pVerJob, phTestA, phTestB, "tone_pcm_is_signed", NULL, "Tone PCM signed bit doesn't match"); + CHECK_RC_MAYBE_RET(rc, pVerJob); + + rc = audioTestObjGetTonePcmProps(phTestA, &pVerJob->PCMProps); + CHECK_RC_MAYBE_RET(rc, pVerJob); + + /* + * Now the fun stuff, PCM data analysis. + */ + rc = audioTestVerifyTestToneData(pVerJob, phTestA, phTestB); + if (RT_FAILURE(rc)) + { + int rc2 = audioTestErrorDescAddError(pVerJob->pErr, pVerJob->idxTest, "Verififcation of test tone data failed\n"); + AssertRC(rc2); + } + + return VINF_SUCCESS; +} + +/** + * Verifies an opened audio test set, extended version. + * + * @returns VBox status code. + * @param pSetA Test set A to verify. + * @param pSetB Test set to verify test set A with. + * @param pOpts Verification options to use. + * @param pErrDesc Where to return the test verification errors. + * + * @note Test verification errors have to be checked for errors, regardless of the + * actual return code. + * @note Uses the standard verification options. Use AudioTestSetVerifyEx() to specify + * own options. + */ +int AudioTestSetVerifyEx(PAUDIOTESTSET pSetA, PAUDIOTESTSET pSetB, PAUDIOTESTVERIFYOPTS pOpts, PAUDIOTESTERRORDESC pErrDesc) +{ + AssertPtrReturn(pSetA, VERR_INVALID_POINTER); + AssertPtrReturn(pSetB, VERR_INVALID_POINTER); + AssertReturn(audioTestManifestIsOpen(pSetA), VERR_WRONG_ORDER); + AssertReturn(audioTestManifestIsOpen(pSetB), VERR_WRONG_ORDER); + AssertPtrReturn(pOpts, VERR_INVALID_POINTER); + + /* We ASSUME the caller has not init'd pErrDesc. */ + audioTestErrorDescInit(pErrDesc); + + AUDIOTESTVERIFYJOB VerJob; + RT_ZERO(VerJob); + VerJob.pErr = pErrDesc; + VerJob.pSetA = pSetA; + VerJob.pSetB = pSetB; + + memcpy(&VerJob.Opts, pOpts, sizeof(AUDIOTESTVERIFYOPTS)); + + PAUDIOTESTVERIFYJOB pVerJob = &VerJob; + + int rc; + + /* + * Compare obvious values first. + */ + AUDIOTESTOBJINT hHdrA; + rc = audioTestSetGetSection(pVerJob->pSetA, AUDIOTEST_SEC_HDR_STR, &hHdrA); + CHECK_RC_MAYBE_RET(rc, pVerJob); + + AUDIOTESTOBJINT hHdrB; + rc = audioTestSetGetSection(pVerJob->pSetB, AUDIOTEST_SEC_HDR_STR, &hHdrB); + CHECK_RC_MAYBE_RET(rc, pVerJob); + + rc = audioTestVerifyValue(&VerJob, &hHdrA, &hHdrB, "magic", "vkat_ini", "Manifest magic wrong"); + CHECK_RC_MAYBE_RET(rc, pVerJob); + rc = audioTestVerifyValue(&VerJob, &hHdrA, &hHdrB, "ver", "1" , "Manifest version wrong"); + CHECK_RC_MAYBE_RET(rc, pVerJob); + rc = audioTestVerifyValue(&VerJob, &hHdrA, &hHdrB, "tag", NULL, "Manifest tags don't match"); + CHECK_RC_MAYBE_RET(rc, pVerJob); + rc = audioTestVerifyValue(&VerJob, &hHdrA, &hHdrB, "test_count", NULL, "Test counts don't match"); + CHECK_RC_MAYBE_RET(rc, pVerJob); + rc = audioTestVerifyValue(&VerJob, &hHdrA, &hHdrB, "obj_count", NULL, "Object counts don't match"); + CHECK_RC_MAYBE_RET(rc, pVerJob); + + /* + * Compare ran tests. + */ + uint32_t cTests; + rc = audioTestObjGetUInt32(&hHdrA, "test_count", &cTests); + AssertRCReturn(rc, rc); + + for (uint32_t i = 0; i < cTests; i++) + { + VerJob.idxTest = i; + + AUDIOTESTOBJINT hTestA; + rc = audioTestSetGetTest(VerJob.pSetA, i, &hTestA); + CHECK_RC_MSG_MAYBE_RET(rc, pVerJob, "Test A not found"); + + AUDIOTESTOBJINT hTestB; + rc = audioTestSetGetTest(VerJob.pSetB, i, &hTestB); + CHECK_RC_MSG_MAYBE_RET(rc, pVerJob, "Test B not found"); + + rc = audioTestObjGetUInt32(&hTestA, "test_type", (uint32_t *)&hTestA.enmTestType); + CHECK_RC_MSG_MAYBE_RET(rc, pVerJob, "Test type A not found"); + + rc = audioTestObjGetUInt32(&hTestB, "test_type", (uint32_t *)&hTestB.enmTestType); + CHECK_RC_MSG_MAYBE_RET(rc, pVerJob, "Test type B not found"); + + switch (hTestA.enmTestType) + { + case AUDIOTESTTYPE_TESTTONE_PLAY: + { + if (hTestB.enmTestType == AUDIOTESTTYPE_TESTTONE_RECORD) + rc = audioTestVerifyTestTone(&VerJob, &hTestA, &hTestB); + else + rc = audioTestErrorDescAddError(pErrDesc, i, "Playback test types don't match (set A=%#x, set B=%#x)", + hTestA.enmTestType, hTestB.enmTestType); + break; + } + + case AUDIOTESTTYPE_TESTTONE_RECORD: + { + if (hTestB.enmTestType == AUDIOTESTTYPE_TESTTONE_PLAY) + rc = audioTestVerifyTestTone(&VerJob, &hTestB, &hTestA); + else + rc = audioTestErrorDescAddError(pErrDesc, i, "Recording test types don't match (set A=%#x, set B=%#x)", + hTestA.enmTestType, hTestB.enmTestType); + break; + } + + case AUDIOTESTTYPE_INVALID: + rc = VERR_INVALID_PARAMETER; + break; + + default: + rc = VERR_NOT_IMPLEMENTED; + break; + } + + AssertRC(rc); + } + + /* Only return critical stuff not related to actual testing here. */ + return VINF_SUCCESS; +} + +/** + * Initializes audio test verification options in a strict manner. + * + * @param pOpts Verification options to initialize. + */ +void AudioTestSetVerifyOptsInitStrict(PAUDIOTESTVERIFYOPTS pOpts) +{ + RT_BZERO(pOpts, sizeof(AUDIOTESTVERIFYOPTS)); + + pOpts->fKeepGoing = true; + pOpts->fNormalize = false; /* Skip normalization by default now, as we now use the OS' master volume to play/record tones. */ + pOpts->cMaxDiff = 0; /* By default we're very strict and consider any diff as being erroneous. */ + pOpts->uMaxSizePercent = 10; /* 10% is okay for us for now; might be due to any buffering / setup phase. + Anything above this is suspicious and should be reported for further investigation. */ + pOpts->msSearchWindow = 10; /* We use a search window of 10ms by default for finding (non-)silent parts. */ +} + +/** + * Initializes audio test verification options with default values (strict!). + * + * @param pOpts Verification options to initialize. + */ +void AudioTestSetVerifyOptsInit(PAUDIOTESTVERIFYOPTS pOpts) +{ + AudioTestSetVerifyOptsInitStrict(pOpts); +} + +/** + * Returns whether two audio test verification options are equal. + * + * @returns \c true if equal, or \c false if not. + * @param pOptsA Options A to compare. + * @param pOptsB Options B to compare Options A with. + */ +bool AudioTestSetVerifyOptsAreEqual(PAUDIOTESTVERIFYOPTS pOptsA, PAUDIOTESTVERIFYOPTS pOptsB) +{ + if (pOptsA == pOptsB) + return true; + + return ( pOptsA->cMaxDiff == pOptsB->cMaxDiff + && pOptsA->fKeepGoing == pOptsB->fKeepGoing + && pOptsA->fNormalize == pOptsB->fNormalize + && pOptsA->uMaxDiffPercent == pOptsB->uMaxDiffPercent + && pOptsA->uMaxSizePercent == pOptsB->uMaxSizePercent + && pOptsA->msSearchWindow == pOptsB->msSearchWindow); +} + +/** + * Verifies an opened audio test set. + * + * @returns VBox status code. + * @param pSetA Test set A to verify. + * @param pSetB Test set to verify test set A with. + * @param pErrDesc Where to return the test verification errors. + * + * @note Test verification errors have to be checked for errors, regardless of the + * actual return code. + * @note Uses the standard verification options (strict!). + * Use AudioTestSetVerifyEx() to specify own options. + */ +int AudioTestSetVerify(PAUDIOTESTSET pSetA, PAUDIOTESTSET pSetB, PAUDIOTESTERRORDESC pErrDesc) +{ + AUDIOTESTVERIFYOPTS Opts; + AudioTestSetVerifyOptsInitStrict(&Opts); + + return AudioTestSetVerifyEx(pSetA,pSetB, &Opts, pErrDesc); +} + +#undef CHECK_RC_MAYBE_RET +#undef CHECK_RC_MSG_MAYBE_RET + +/** + * Converts an audio test state enum value to a string. + * + * @returns Pointer to read-only internal test state string on success, + * "illegal" if invalid command value. + * @param enmState The state to convert. + */ +const char *AudioTestStateToStr(AUDIOTESTSTATE enmState) +{ + switch (enmState) + { + case AUDIOTESTSTATE_INIT: return "init"; + case AUDIOTESTSTATE_PRE: return "pre"; + case AUDIOTESTSTATE_RUN: return "run"; + case AUDIOTESTSTATE_POST: return "post"; + case AUDIOTESTSTATE_DONE: return "done"; + case AUDIOTESTSTATE_32BIT_HACK: + break; + } + AssertMsgFailedReturn(("Invalid test state: #%x\n", enmState), "illegal"); +} + + +/********************************************************************************************************************************* +* WAVE File Reader. * +*********************************************************************************************************************************/ + +/** + * Counts the number of set bits in @a fMask. + */ +static unsigned audioTestWaveCountBits(uint32_t fMask) +{ + unsigned cBits = 0; + while (fMask) + { + if (fMask & 1) + cBits++; + fMask >>= 1; + } + return cBits; +} + +/** + * Opens a wave (.WAV) file for reading. + * + * @returns VBox status code. + * @param pszFile The file to open. + * @param pWaveFile The open wave file structure to fill in on success. + * @param pErrInfo Where to return addition error details on failure. + */ +int AudioTestWaveFileOpen(const char *pszFile, PAUDIOTESTWAVEFILE pWaveFile, PRTERRINFO pErrInfo) +{ + pWaveFile->u32Magic = AUDIOTESTWAVEFILE_MAGIC_DEAD; + RT_ZERO(pWaveFile->Props); + pWaveFile->hFile = NIL_RTFILE; + int rc = RTFileOpen(&pWaveFile->hFile, pszFile, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_NONE); + if (RT_FAILURE(rc)) + return RTErrInfoSet(pErrInfo, rc, "RTFileOpen failed"); + uint64_t cbFile = 0; + rc = RTFileQuerySize(pWaveFile->hFile, &cbFile); + if (RT_SUCCESS(rc)) + { + union + { + uint8_t ab[512]; + struct + { + RTRIFFHDR Hdr; + union + { + RTRIFFWAVEFMTCHUNK Fmt; + RTRIFFWAVEFMTEXTCHUNK FmtExt; + } u; + } Wave; + RTRIFFLIST List; + RTRIFFCHUNK Chunk; + RTRIFFWAVEDATACHUNK Data; + } uBuf; + + rc = RTFileRead(pWaveFile->hFile, &uBuf.Wave, sizeof(uBuf.Wave), NULL); + if (RT_SUCCESS(rc)) + { + rc = VERR_VFS_UNKNOWN_FORMAT; + if ( uBuf.Wave.Hdr.uMagic == RTRIFFHDR_MAGIC + && uBuf.Wave.Hdr.uFileType == RTRIFF_FILE_TYPE_WAVE + && uBuf.Wave.u.Fmt.Chunk.uMagic == RTRIFFWAVEFMT_MAGIC + && uBuf.Wave.u.Fmt.Chunk.cbChunk >= sizeof(uBuf.Wave.u.Fmt.Data)) + { + if (uBuf.Wave.Hdr.cbFile != cbFile - sizeof(RTRIFFCHUNK)) + RTErrInfoSetF(pErrInfo, rc, "File size mismatch: %#x, actual %#RX64 (ignored)", + uBuf.Wave.Hdr.cbFile, cbFile - sizeof(RTRIFFCHUNK)); + rc = VERR_VFS_BOGUS_FORMAT; + if ( uBuf.Wave.u.Fmt.Data.uFormatTag != RTRIFFWAVEFMT_TAG_PCM + && uBuf.Wave.u.Fmt.Data.uFormatTag != RTRIFFWAVEFMT_TAG_EXTENSIBLE) + RTErrInfoSetF(pErrInfo, rc, "Unsupported uFormatTag value: %#x (expected %#x or %#x)", + uBuf.Wave.u.Fmt.Data.uFormatTag, RTRIFFWAVEFMT_TAG_PCM, RTRIFFWAVEFMT_TAG_EXTENSIBLE); + else if ( uBuf.Wave.u.Fmt.Data.cBitsPerSample != 8 + && uBuf.Wave.u.Fmt.Data.cBitsPerSample != 16 + /* && uBuf.Wave.u.Fmt.Data.cBitsPerSample != 24 - not supported by our stack */ + && uBuf.Wave.u.Fmt.Data.cBitsPerSample != 32) + RTErrInfoSetF(pErrInfo, rc, "Unsupported cBitsPerSample value: %u", uBuf.Wave.u.Fmt.Data.cBitsPerSample); + else if ( uBuf.Wave.u.Fmt.Data.cChannels < 1 + || uBuf.Wave.u.Fmt.Data.cChannels >= 16) + RTErrInfoSetF(pErrInfo, rc, "Unsupported cChannels value: %u (expected 1..15)", uBuf.Wave.u.Fmt.Data.cChannels); + else if ( uBuf.Wave.u.Fmt.Data.uHz < 4096 + || uBuf.Wave.u.Fmt.Data.uHz > 768000) + RTErrInfoSetF(pErrInfo, rc, "Unsupported uHz value: %u (expected 4096..768000)", uBuf.Wave.u.Fmt.Data.uHz); + else if (uBuf.Wave.u.Fmt.Data.cbFrame != uBuf.Wave.u.Fmt.Data.cChannels * uBuf.Wave.u.Fmt.Data.cBitsPerSample / 8) + RTErrInfoSetF(pErrInfo, rc, "Invalid cbFrame value: %u (expected %u)", uBuf.Wave.u.Fmt.Data.cbFrame, + uBuf.Wave.u.Fmt.Data.cChannels * uBuf.Wave.u.Fmt.Data.cBitsPerSample / 8); + else if (uBuf.Wave.u.Fmt.Data.cbRate != uBuf.Wave.u.Fmt.Data.cbFrame * uBuf.Wave.u.Fmt.Data.uHz) + RTErrInfoSetF(pErrInfo, rc, "Invalid cbRate value: %u (expected %u)", uBuf.Wave.u.Fmt.Data.cbRate, + uBuf.Wave.u.Fmt.Data.cbFrame * uBuf.Wave.u.Fmt.Data.uHz); + else if ( uBuf.Wave.u.Fmt.Data.uFormatTag == RTRIFFWAVEFMT_TAG_EXTENSIBLE + && uBuf.Wave.u.FmtExt.Data.cbExtra < RTRIFFWAVEFMTEXT_EXTRA_SIZE) + RTErrInfoSetF(pErrInfo, rc, "Invalid cbExtra value: %#x (expected at least %#x)", + uBuf.Wave.u.FmtExt.Data.cbExtra, RTRIFFWAVEFMTEXT_EXTRA_SIZE); + else if ( uBuf.Wave.u.Fmt.Data.uFormatTag == RTRIFFWAVEFMT_TAG_EXTENSIBLE + && audioTestWaveCountBits(uBuf.Wave.u.FmtExt.Data.fChannelMask) != uBuf.Wave.u.Fmt.Data.cChannels) + RTErrInfoSetF(pErrInfo, rc, "fChannelMask does not match cChannels: %#x (%u bits set) vs %u channels", + uBuf.Wave.u.FmtExt.Data.fChannelMask, + audioTestWaveCountBits(uBuf.Wave.u.FmtExt.Data.fChannelMask), uBuf.Wave.u.Fmt.Data.cChannels); + else if ( uBuf.Wave.u.Fmt.Data.uFormatTag == RTRIFFWAVEFMT_TAG_EXTENSIBLE + && RTUuidCompareStr(&uBuf.Wave.u.FmtExt.Data.SubFormat, RTRIFFWAVEFMTEXT_SUBTYPE_PCM) != 0) + RTErrInfoSetF(pErrInfo, rc, "SubFormat is not PCM: %RTuuid (expected %s)", + &uBuf.Wave.u.FmtExt.Data.SubFormat, RTRIFFWAVEFMTEXT_SUBTYPE_PCM); + else + { + /* + * Copy out the data we need from the file format structure. + */ + PDMAudioPropsInit(&pWaveFile->Props, uBuf.Wave.u.Fmt.Data.cBitsPerSample / 8, true /*fSigned*/, + uBuf.Wave.u.Fmt.Data.cChannels, uBuf.Wave.u.Fmt.Data.uHz); + pWaveFile->offSamples = sizeof(RTRIFFHDR) + sizeof(RTRIFFCHUNK) + uBuf.Wave.u.Fmt.Chunk.cbChunk; + + /* + * Pick up channel assignments if present. + */ + if (uBuf.Wave.u.Fmt.Data.uFormatTag == RTRIFFWAVEFMT_TAG_EXTENSIBLE) + { + static unsigned const s_cStdIds = (unsigned)PDMAUDIOCHANNELID_END_STANDARD + - (unsigned)PDMAUDIOCHANNELID_FIRST_STANDARD; + unsigned iCh = 0; + for (unsigned idCh = 0; idCh < 32 && iCh < uBuf.Wave.u.Fmt.Data.cChannels; idCh++) + if (uBuf.Wave.u.FmtExt.Data.fChannelMask & RT_BIT_32(idCh)) + { + pWaveFile->Props.aidChannels[iCh] = idCh < s_cStdIds + ? idCh + (unsigned)PDMAUDIOCHANNELID_FIRST_STANDARD + : (unsigned)PDMAUDIOCHANNELID_UNKNOWN; + iCh++; + } + } + + /* + * Find the 'data' chunk with the audio samples. + * + * There can be INFO lists both preceeding this and succeeding + * it, containing IART and other things we can ignored. Thus + * we read a list header here rather than just a chunk header, + * since it doesn't matter if we read 4 bytes extra as + * AudioTestWaveFileRead uses RTFileReadAt anyway. + */ + rc = RTFileReadAt(pWaveFile->hFile, pWaveFile->offSamples, &uBuf, sizeof(uBuf.List), NULL); + for (uint32_t i = 0; + i < 128 + && RT_SUCCESS(rc) + && uBuf.Chunk.uMagic != RTRIFFWAVEDATACHUNK_MAGIC + && (uint64_t)uBuf.Chunk.cbChunk + sizeof(RTRIFFCHUNK) * 2 <= cbFile - pWaveFile->offSamples; + i++) + { + if ( uBuf.List.uMagic == RTRIFFLIST_MAGIC + && uBuf.List.uListType == RTRIFFLIST_TYPE_INFO) + { /*skip*/ } + else if (uBuf.Chunk.uMagic == RTRIFFPADCHUNK_MAGIC) + { /*skip*/ } + else + break; + pWaveFile->offSamples += sizeof(RTRIFFCHUNK) + uBuf.Chunk.cbChunk; + rc = RTFileReadAt(pWaveFile->hFile, pWaveFile->offSamples, &uBuf, sizeof(uBuf.List), NULL); + } + if (RT_SUCCESS(rc)) + { + pWaveFile->offSamples += sizeof(uBuf.Data.Chunk); + pWaveFile->cbSamples = (uint32_t)cbFile - pWaveFile->offSamples; + + rc = VERR_VFS_BOGUS_FORMAT; + if ( uBuf.Data.Chunk.uMagic == RTRIFFWAVEDATACHUNK_MAGIC + && uBuf.Data.Chunk.cbChunk <= pWaveFile->cbSamples + && PDMAudioPropsIsSizeAligned(&pWaveFile->Props, uBuf.Data.Chunk.cbChunk)) + { + pWaveFile->cbSamples = uBuf.Data.Chunk.cbChunk; + + /* + * We're good! + */ + pWaveFile->offCur = 0; + pWaveFile->fReadMode = true; + pWaveFile->u32Magic = AUDIOTESTWAVEFILE_MAGIC; + return VINF_SUCCESS; + } + + RTErrInfoSetF(pErrInfo, rc, "Bad data header: uMagic=%#x (expected %#x), cbChunk=%#x (max %#RX64, align %u)", + uBuf.Data.Chunk.uMagic, RTRIFFWAVEDATACHUNK_MAGIC, + uBuf.Data.Chunk.cbChunk, pWaveFile->cbSamples, PDMAudioPropsFrameSize(&pWaveFile->Props)); + } + else + RTErrInfoSet(pErrInfo, rc, "Failed to read data header"); + } + } + else + RTErrInfoSetF(pErrInfo, rc, "Bad file header: uMagic=%#x (vs. %#x), uFileType=%#x (vs %#x), uFmtMagic=%#x (vs %#x) cbFmtChunk=%#x (min %#x)", + uBuf.Wave.Hdr.uMagic, RTRIFFHDR_MAGIC, uBuf.Wave.Hdr.uFileType, RTRIFF_FILE_TYPE_WAVE, + uBuf.Wave.u.Fmt.Chunk.uMagic, RTRIFFWAVEFMT_MAGIC, + uBuf.Wave.u.Fmt.Chunk.cbChunk, sizeof(uBuf.Wave.u.Fmt.Data)); + } + else + rc = RTErrInfoSet(pErrInfo, rc, "Failed to read file header"); + } + else + rc = RTErrInfoSet(pErrInfo, rc, "Failed to query file size"); + + RTFileClose(pWaveFile->hFile); + pWaveFile->hFile = NIL_RTFILE; + return rc; +} + + +/** + * Creates a new wave file. + * + * @returns VBox status code. + * @param pszFile The filename. + * @param pProps The audio format properties. + * @param pWaveFile The wave file structure to fill in on success. + * @param pErrInfo Where to return addition error details on failure. + */ +int AudioTestWaveFileCreate(const char *pszFile, PCPDMAUDIOPCMPROPS pProps, PAUDIOTESTWAVEFILE pWaveFile, PRTERRINFO pErrInfo) +{ + /* + * Construct the file header first (we'll do some input validation + * here, so better do it before creating the file). + */ + struct + { + RTRIFFHDR Hdr; + RTRIFFWAVEFMTEXTCHUNK FmtExt; + RTRIFFCHUNK Data; + } FileHdr; + + FileHdr.Hdr.uMagic = RTRIFFHDR_MAGIC; + FileHdr.Hdr.cbFile = 0; /* need to update this later */ + FileHdr.Hdr.uFileType = RTRIFF_FILE_TYPE_WAVE; + FileHdr.FmtExt.Chunk.uMagic = RTRIFFWAVEFMT_MAGIC; + FileHdr.FmtExt.Chunk.cbChunk = sizeof(RTRIFFWAVEFMTEXTCHUNK) - sizeof(RTRIFFCHUNK); + FileHdr.FmtExt.Data.Core.uFormatTag = RTRIFFWAVEFMT_TAG_EXTENSIBLE; + FileHdr.FmtExt.Data.Core.cChannels = PDMAudioPropsChannels(pProps); + FileHdr.FmtExt.Data.Core.uHz = PDMAudioPropsHz(pProps); + FileHdr.FmtExt.Data.Core.cbRate = PDMAudioPropsFramesToBytes(pProps, PDMAudioPropsHz(pProps)); + FileHdr.FmtExt.Data.Core.cbFrame = PDMAudioPropsFrameSize(pProps); + FileHdr.FmtExt.Data.Core.cBitsPerSample = PDMAudioPropsSampleBits(pProps); + FileHdr.FmtExt.Data.cbExtra = sizeof(FileHdr.FmtExt.Data) - sizeof(FileHdr.FmtExt.Data.Core); + FileHdr.FmtExt.Data.cValidBitsPerSample = PDMAudioPropsSampleBits(pProps); + FileHdr.FmtExt.Data.fChannelMask = 0; + for (uintptr_t idxCh = 0; idxCh < FileHdr.FmtExt.Data.Core.cChannels; idxCh++) + { + PDMAUDIOCHANNELID const idCh = (PDMAUDIOCHANNELID)pProps->aidChannels[idxCh]; + if ( idCh >= PDMAUDIOCHANNELID_FIRST_STANDARD + && idCh < PDMAUDIOCHANNELID_END_STANDARD) + { + if (!(FileHdr.FmtExt.Data.fChannelMask & RT_BIT_32(idCh - PDMAUDIOCHANNELID_FIRST_STANDARD))) + FileHdr.FmtExt.Data.fChannelMask |= RT_BIT_32(idCh - PDMAUDIOCHANNELID_FIRST_STANDARD); + else + return RTErrInfoSetF(pErrInfo, VERR_INVALID_PARAMETER, "Channel #%u repeats channel ID %d", idxCh, idCh); + } + else + return RTErrInfoSetF(pErrInfo, VERR_INVALID_PARAMETER, "Invalid channel ID %d for channel #%u", idCh, idxCh); + } + + RTUUID UuidTmp; + int rc = RTUuidFromStr(&UuidTmp, RTRIFFWAVEFMTEXT_SUBTYPE_PCM); + AssertRCReturn(rc, rc); + FileHdr.FmtExt.Data.SubFormat = UuidTmp; /* (64-bit field maybe unaligned) */ + + FileHdr.Data.uMagic = RTRIFFWAVEDATACHUNK_MAGIC; + FileHdr.Data.cbChunk = 0; /* need to update this later */ + + /* + * Create the file and write the header. + */ + pWaveFile->hFile = NIL_RTFILE; + rc = RTFileOpen(&pWaveFile->hFile, pszFile, RTFILE_O_CREATE | RTFILE_O_WRITE | RTFILE_O_DENY_WRITE); + if (RT_FAILURE(rc)) + return RTErrInfoSet(pErrInfo, rc, "RTFileOpen failed"); + + rc = RTFileWrite(pWaveFile->hFile, &FileHdr, sizeof(FileHdr), NULL); + if (RT_SUCCESS(rc)) + { + /* + * Initialize the wave file structure. + */ + pWaveFile->fReadMode = false; + pWaveFile->offCur = 0; + pWaveFile->offSamples = 0; + pWaveFile->cbSamples = 0; + pWaveFile->Props = *pProps; + pWaveFile->offSamples = RTFileTell(pWaveFile->hFile); + if (pWaveFile->offSamples != UINT32_MAX) + { + pWaveFile->u32Magic = AUDIOTESTWAVEFILE_MAGIC; + return VINF_SUCCESS; + } + rc = RTErrInfoSet(pErrInfo, VERR_SEEK, "RTFileTell failed"); + } + else + RTErrInfoSet(pErrInfo, rc, "RTFileWrite failed writing header"); + + RTFileClose(pWaveFile->hFile); + pWaveFile->hFile = NIL_RTFILE; + pWaveFile->u32Magic = AUDIOTESTWAVEFILE_MAGIC_DEAD; + + RTFileDelete(pszFile); + return rc; +} + + +/** + * Closes a wave file. + */ +int AudioTestWaveFileClose(PAUDIOTESTWAVEFILE pWaveFile) +{ + AssertReturn(pWaveFile->u32Magic == AUDIOTESTWAVEFILE_MAGIC, VERR_INVALID_MAGIC); + int rcRet = VINF_SUCCESS; + int rc; + + /* + * Update the size fields if writing. + */ + if (!pWaveFile->fReadMode) + { + uint64_t cbFile = RTFileTell(pWaveFile->hFile); + if (cbFile != UINT64_MAX) + { + uint32_t cbFile32 = cbFile - sizeof(RTRIFFCHUNK); + rc = RTFileWriteAt(pWaveFile->hFile, RT_OFFSETOF(RTRIFFHDR, cbFile), &cbFile32, sizeof(cbFile32), NULL); + AssertRCStmt(rc, rcRet = rc); + + uint32_t cbSamples = cbFile - pWaveFile->offSamples; + rc = RTFileWriteAt(pWaveFile->hFile, pWaveFile->offSamples - sizeof(uint32_t), &cbSamples, sizeof(cbSamples), NULL); + AssertRCStmt(rc, rcRet = rc); + } + else + rcRet = VERR_SEEK; + } + + /* + * Close it. + */ + rc = RTFileClose(pWaveFile->hFile); + AssertRCStmt(rc, rcRet = rc); + + pWaveFile->hFile = NIL_RTFILE; + pWaveFile->u32Magic = AUDIOTESTWAVEFILE_MAGIC_DEAD; + return rcRet; +} + +/** + * Reads samples from a wave file. + * + * @returns VBox status code. See RTVfsFileRead for EOF status handling. + * @param pWaveFile The file to read from. + * @param pvBuf Where to put the samples. + * @param cbBuf How much to read at most. + * @param pcbRead Where to return the actual number of bytes read, + * optional. + */ +int AudioTestWaveFileRead(PAUDIOTESTWAVEFILE pWaveFile, void *pvBuf, size_t cbBuf, size_t *pcbRead) +{ + AssertReturn(pWaveFile->u32Magic == AUDIOTESTWAVEFILE_MAGIC, VERR_INVALID_MAGIC); + AssertReturn(pWaveFile->fReadMode, VERR_ACCESS_DENIED); + + bool fEofAdjusted; + if (pWaveFile->offCur + cbBuf <= pWaveFile->cbSamples) + fEofAdjusted = false; + else if (pcbRead) + { + fEofAdjusted = true; + cbBuf = pWaveFile->cbSamples - pWaveFile->offCur; + } + else + return VERR_EOF; + + int rc = RTFileReadAt(pWaveFile->hFile, pWaveFile->offSamples + pWaveFile->offCur, pvBuf, cbBuf, pcbRead); + if (RT_SUCCESS(rc)) + { + if (pcbRead) + { + pWaveFile->offCur += (uint32_t)*pcbRead; + if (fEofAdjusted || cbBuf > *pcbRead) + rc = VINF_EOF; + else if (!cbBuf && pWaveFile->offCur == pWaveFile->cbSamples) + rc = VINF_EOF; + } + else + pWaveFile->offCur += (uint32_t)cbBuf; + } + return rc; +} + + +/** + * Writes samples to a wave file. + * + * @returns VBox status code. + * @param pWaveFile The file to write to. + * @param pvBuf The samples to write. + * @param cbBuf How many bytes of samples to write. + */ +int AudioTestWaveFileWrite(PAUDIOTESTWAVEFILE pWaveFile, const void *pvBuf, size_t cbBuf) +{ + AssertReturn(pWaveFile->u32Magic == AUDIOTESTWAVEFILE_MAGIC, VERR_INVALID_MAGIC); + AssertReturn(!pWaveFile->fReadMode, VERR_ACCESS_DENIED); + + pWaveFile->cbSamples += (uint32_t)cbBuf; + return RTFileWrite(pWaveFile->hFile, pvBuf, cbBuf, NULL); +} + diff --git a/src/VBox/Devices/Audio/AudioTest.h b/src/VBox/Devices/Audio/AudioTest.h new file mode 100644 index 00000000..aa9addb8 --- /dev/null +++ b/src/VBox/Devices/Audio/AudioTest.h @@ -0,0 +1,491 @@ +/* $Id: AudioTest.h $ */ +/** @file + * Audio testing routines. + * Common code which is being used by the ValidationKit audio test (VKAT) + * and the debug / ValdikationKit audio driver(s). + */ + +/* + * Copyright (C) 2021-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 + */ + +#ifndef VBOX_INCLUDED_SRC_Audio_AudioTest_h +#define VBOX_INCLUDED_SRC_Audio_AudioTest_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +/** @todo Some stuff here can be private-only to the implementation. */ + +/** Maximum length in characters an audio test tag can have. */ +#define AUDIOTEST_TAG_MAX 64 +/** Maximum length in characters a single audio test error description can have. */ +#define AUDIOTEST_ERROR_DESC_MAX 256 +/** Prefix for audio test (set) directories. */ +#define AUDIOTEST_PATH_PREFIX_STR "vkat" +/** Maximum tests a beacon can have. + * Maximum number of tests is 240 (so it can fit into 8-bit mono channels). */ +#define AUDIOTEST_BEACON_TESTS_MAX 240 +/** Returns a pre-beacon for a given test number. + * Maximum number of tests is 240 (so it can fit into 8-bit mono channels). + * + * That way it's easy to visually inspect beacon data in a hex editor -- + * e.g. for test #5 a pre-beacon would be 0x5A + post-beacon 0x5B. */ +#define AUDIOTEST_BEACON_MAKE_PRE(a_TstNum) \ + ( (uint8_t)((a_TstNum) & 0xf) << 4 \ + | (uint8_t)(0xA)) +/** Returns a post-beacon for a given test number. + * Maximum number of tests is 250 (so it can fit into 8-bit mono channels). + * + * That way it's easy to visually inspect beacon data in a hex editor -- + * e.g. for test #2 a pre-beacon would be 0x2A + post-beacon 0x2B. */ +#define AUDIOTEST_BEACON_MAKE_POST(a_TstNum) \ + ( (uint8_t)((a_TstNum) & 0xf) << 4 \ + | (uint8_t)(0xB)) +/** Pre / post audio beacon size (in audio frames). */ +#define AUDIOTEST_BEACON_SIZE_FRAMES 1024 + +/** + * Enumeration for an audio test tone (wave) type. + */ +typedef enum AUDIOTESTTONETYPE +{ + /** Invalid type. */ + AUDIOTESTTONETYPE_INVALID = 0, + /** Sine wave. */ + AUDIOTESTTONETYPE_SINE, + /** Square wave. Not implemented yet. */ + AUDIOTESTTONETYPE_SQUARE, + /** Triangluar wave. Not implemented yet. */ + AUDIOTESTTONETYPE_TRIANGLE, + /** Sawtooth wave. Not implemented yet. */ + AUDIOTESTTONETYPE_SAWTOOTH, + /** The usual 32-bit hack. */ + AUDIOTESTTONETYPE_32BIT_HACK = 0x7fffffff +} AUDIOTESTTONETYPE; + +/** + * Structure for handling an audio (sine wave) test tone. + */ +typedef struct AUDIOTESTTONE +{ + /** The tone's wave type. */ + AUDIOTESTTONETYPE enmType; + /** The PCM properties. */ + PDMAUDIOPCMPROPS Props; + /** Current sample index for generate the sine wave. */ + uint64_t uSample; + /** The fixed portion of the sin() input. */ + double rdFixed; + /** Frequency (in Hz) of the sine wave to generate. */ + double rdFreqHz; +} AUDIOTESTTONE; +/** Pointer to an audio test tone. */ +typedef AUDIOTESTTONE *PAUDIOTESTTONE; + +/** + * Structure for a common test parameters header. + */ +typedef struct AUDIOTESTPARMSHDR +{ + /** Test index these test parameters belong to. + * Set to UINT32_MAX if not being used. */ + uint32_t idxTest; + /** Time of the caller when this test was being created. */ + RTTIME tsCreated; +} AUDIOTESTPARMSHDR; +/** Pointer to an audio test tone. */ +typedef AUDIOTESTPARMSHDR *PAUDIOTESTPARMSHDR; + +/** + * Structure for handling audio test tone parameters. + */ +typedef struct AUDIOTESTTONEPARMS +{ + /** Common test header. */ + AUDIOTESTPARMSHDR Hdr; + /** The PCM properties. */ + PDMAUDIOPCMPROPS Props; + /** Tone frequency (in Hz) to use. + * Will be later converted to a double value. */ + double dbFreqHz; + /** Prequel (in ms) to play silence. Optional and can be set to 0. */ + RTMSINTERVAL msPrequel; + /** Duration (in ms) to play the test tone. */ + RTMSINTERVAL msDuration; + /** Sequel (in ms) to play silence. Optional and can be set to 0. */ + RTMSINTERVAL msSequel; + /** Volume (in percent, 0-100) to use. + * If set to 0, the tone is muted (i.e. silent). */ + uint8_t uVolumePercent; +} AUDIOTESTTONEPARMS; +/** Pointer to audio test tone parameters. */ +typedef AUDIOTESTTONEPARMS *PAUDIOTESTTONEPARMS; + +/** + * Enumeration defining an audio test beacon type. + */ +typedef enum AUDIOTESTTONEBEACONTYPE +{ + /** Invalid type. */ + AUDIOTESTTONEBEACONTYPE_INVALID = 0, + /** Playback beacon (pre). */ + AUDIOTESTTONEBEACONTYPE_PLAY_PRE = 1, + /** Playback beacon (post). */ + AUDIOTESTTONEBEACONTYPE_PLAY_POST = 2, + /** Recording beacon (pre). */ + AUDIOTESTTONEBEACONTYPE_REC_PRE = 3, + /** Recording beacon (post). */ + AUDIOTESTTONEBEACONTYPE_REC_POST = 4, + /** The usual 32-bit hack. */ + OTESTTONEBEACONTYPE_32BIT_HACK = 0x7fffffff +} AUDIOTESTTONEBEACONTYPE; + +/** + * Structure defining an audio test tone beacon. + * + * This is being used for (optionally) marking beginning/ending of audio test data. + */ +typedef struct AUDIOTESTTONEBEACON +{ + /** Test number this beacon is for. */ + uint8_t uTest; + /** The beacon type. */ + AUDIOTESTTONEBEACONTYPE enmType; + /** PCM properties to use for this beacon. */ + PDMAUDIOPCMPROPS Props; + /** Beacon bytes to process. + * When doing test tone playback: Beacon bytes to write. + * When doing test tone recording: Beacon bytes to read. */ + uint32_t cbSize; + /** Beacon bytes already processed. + * When doing test tone playback: Beacon bytes written. + * When doing test tone recording: Beacon bytes read. */ + uint32_t cbUsed; +} AUDIOTESTTONEBEACON; +/** Pointer to audio test tone beacon. */ +typedef AUDIOTESTTONEBEACON *PAUDIOTESTTONEBEACON; +/** Pointer (const) to audio test tone beacon. */ +typedef AUDIOTESTTONEBEACON const *PCAUDIOTESTTONEBEACON; + +/** + * Enumeration for the test set mode. + */ +typedef enum AUDIOTESTSETMODE +{ + /** Invalid test set mode. */ + AUDIOTESTSETMODE_INVALID = 0, + /** Test set is being created (testing in progress). */ + AUDIOTESTSETMODE_TEST, + /** Existing test set is being verified. */ + AUDIOTESTSETMODE_VERIFY, + /** The usual 32-bit hack. */ + AUDIOTESTSETMODE_32BIT_HACK = 0x7fffffff +} AUDIOTESTSETMODE; + +/** + * Enumeration to specify an audio test type. + */ +typedef enum AUDIOTESTTYPE +{ + /** Invalid test type, do not use. */ + AUDIOTESTTYPE_INVALID = 0, + /** Play a test tone. */ + AUDIOTESTTYPE_TESTTONE_PLAY, + /** Record a test tone. */ + AUDIOTESTTYPE_TESTTONE_RECORD, + /** The usual 32-bit hack. */ + AUDIOTESTTYPE_32BIT_HACK = 0x7fffffff +} AUDIOTESTTYPE; + +/** + * Audio test request data. + */ +typedef struct AUDIOTESTPARMS +{ + /** Audio device to use. */ + PDMAUDIOHOSTDEV Dev; + /** How much to delay (wait, in ms) the test being executed. */ + RTMSINTERVAL msDelay; + /** The test direction. */ + PDMAUDIODIR enmDir; + /** The test type. */ + AUDIOTESTTYPE enmType; + /** Union for test type-specific data. */ + union + { + AUDIOTESTTONEPARMS TestTone; + }; +} AUDIOTESTPARMS; +/** Pointer to a test parameter structure. */ +typedef AUDIOTESTPARMS *PAUDIOTESTPARMS; + +/** Test object handle. */ +typedef R3R0PTRTYPE(struct AUDIOTESTOBJINT RT_FAR *) AUDIOTESTOBJ; +/** Pointer to test object handle. */ +typedef AUDIOTESTOBJ RT_FAR *PAUDIOTESTOBJ; +/** Nil test object handle. */ +#define NIL_AUDIOTESTOBJ ((AUDIOTESTOBJ)~(RTHCINTPTR)0) + +struct AUDIOTESTSET; + +/** + * Structure specifying a single audio test entry of a test set. + * + * A test set can contain zero or more test entry (tests). + */ +typedef struct AUDIOTESTENTRY +{ + /** List node. */ + RTLISTNODE Node; + /** Pointer to test set parent. */ + AUDIOTESTSET *pParent; + /** Friendly description of the test. */ + char szDesc[64]; + /** Audio test parameters this test needs to perform the actual test. */ + AUDIOTESTPARMS Parms; + /** Number of test objects bound to this test. */ + uint32_t cObj; + /** Absolute offset (in bytes) where to write the "obj_count" value later. */ + uint64_t offObjCount; + /** Overall test result. */ + int rc; +} AUDIOTESTENTRY; +/** Pointer to an audio test entry. */ +typedef AUDIOTESTENTRY *PAUDIOTESTENTRY; + +/** + * Structure specifying an audio test set. + */ +typedef struct AUDIOTESTSET +{ + /** The set's tag. */ + char szTag[AUDIOTEST_TAG_MAX]; + /** Absolute path where to store the test audio data. */ + char szPathAbs[RTPATH_MAX]; + /** Current mode the test set is in. */ + AUDIOTESTSETMODE enmMode; + union + { + /** @todo r=bird: RTSTREAM not RTFILE. That means you don't have to check + * every write status code and it's buffered and thus faster. Also, + * you don't have to re-invent fprintf-style RTFileWrite wrappers. */ + RTFILE hFile; + RTINIFILE hIniFile; + } f; + /** Number of test objects in lstObj. */ + uint32_t cObj; + /** Absolute offset (in bytes) where to write the "obj_count" value later. */ + uint64_t offObjCount; + /** List containing PAUDIOTESTOBJ test object entries. */ + RTLISTANCHOR lstObj; + /** Number of performed tests. + * Not necessarily bound to the test object entries above. */ + uint32_t cTests; + /** Absolute offset (in bytes) where to write the "test_count" value later. */ + uint64_t offTestCount; + /** List containing PAUDIOTESTENTRY test entries. */ + RTLISTANCHOR lstTest; + /** Current test running. Can be NULL if no test is running. */ + PAUDIOTESTENTRY pTestCur; + /** Number of tests currently running. + * Currently we only allow one concurrent test running at a given time. */ + uint32_t cTestsRunning; + /** Number of total (test) failures. */ + uint32_t cTotalFailures; +} AUDIOTESTSET; +/** Pointer to an audio test set. */ +typedef AUDIOTESTSET *PAUDIOTESTSET; + +/** + * Audio test verification options. + */ +typedef struct AUDIOTESTVERIFYOPTS +{ + /** Flag indicating whether to keep going after an error has occurred. */ + bool fKeepGoing; + /** Whether to perform audio normalization or not. */ + bool fNormalize; + /** Threshold of file differences (number of chunks) at when we consider audio files + * as not matching. 0 means an exact match. */ + uint32_t cMaxDiff; + /** Threshold of file differences (difference in percent) at when we consider audio files + * as not matching. 0 means an exact match. */ + uint8_t uMaxDiffPercent; + /** Threshold of file size (+/-, in percent) at when we consider audio files + * as not matching. 0 means an exact match.*/ + uint8_t uMaxSizePercent; + /** Search window (in ms) to use for treating / classifying audio data. */ + uint32_t msSearchWindow; +} AUDIOTESTVERIFYOPTS; +/** Pointer to audio test verification options. */ +typedef AUDIOTESTVERIFYOPTS *PAUDIOTESTVERIFYOPTS; + +/** + * Structure for holding a single audio test error entry. + */ +typedef struct AUDIOTESTERRORENTRY +{ + /** The entrie's list node. */ + RTLISTNODE Node; + /** Additional rc. */ + int rc; + /** Actual error description. */ + char szDesc[AUDIOTEST_ERROR_DESC_MAX]; +} AUDIOTESTERRORENTRY; +/** Pointer to an audio test error description. */ +typedef AUDIOTESTERRORENTRY *PAUDIOTESTERRORENTRY; + +/** + * Structure for holding an audio test error description. + * This can contain multiple errors (FIFO list). + */ +typedef struct AUDIOTESTERRORDESC +{ + /** List entries containing the (FIFO-style) errors of type AUDIOTESTERRORENTRY. */ + RTLISTANCHOR List; + /** Number of errors in the list. */ + uint32_t cErrors; +} AUDIOTESTERRORDESC; +/** Pointer to an audio test error description. */ +typedef AUDIOTESTERRORDESC *PAUDIOTESTERRORDESC; +/** Const pointer to an audio test error description. */ +typedef AUDIOTESTERRORDESC const *PCAUDIOTESTERRORDESC; + +/** + * Enumeration specifying an internal test state. + */ +typedef enum AUDIOTESTSTATE +{ + /** Test is initializing. */ + AUDIOTESTSTATE_INIT = 0, + /** Test is in pre-run phase. */ + AUDIOTESTSTATE_PRE, + /** Test is running */ + AUDIOTESTSTATE_RUN, + /** Test is in post-run phase. */ + AUDIOTESTSTATE_POST, + /** Test has been run. */ + AUDIOTESTSTATE_DONE, + /** The usual 32-bit hack. */ + AUDIOTESTSTATE_32BIT_HACK = 0x7fffffff +} AUDIOTESTSTATE; + +double AudioTestToneInit(PAUDIOTESTTONE pTone, PPDMAUDIOPCMPROPS pProps, double dbFreq); +double AudioTestToneInitRandom(PAUDIOTESTTONE pTone, PPDMAUDIOPCMPROPS pProps); +double AudioTestToneGetRandomFreq(void); +int AudioTestToneGenerate(PAUDIOTESTTONE pTone, void *pvBuf, uint32_t cbBuf, uint32_t *pcbWritten); + +void AudioTestBeaconInit(PAUDIOTESTTONEBEACON pBeacon, uint8_t uTest, AUDIOTESTTONEBEACONTYPE enmType, PPDMAUDIOPCMPROPS pProps); +int AudioTestBeaconAddConsecutive(PAUDIOTESTTONEBEACON pBeacon, const uint8_t *auBuf, size_t cbBuf, size_t *pOff); +int AudioTestBeaconWrite(PAUDIOTESTTONEBEACON pBeacon, void *pvBuf, uint32_t cbBuf); +uint32_t AudioTestBeaconGetSize(PCAUDIOTESTTONEBEACON pBeacon); +const char *AudioTestBeaconTypeGetName(AUDIOTESTTONEBEACONTYPE enmType); +AUDIOTESTTONEBEACONTYPE AudioTestBeaconGetType(PCAUDIOTESTTONEBEACON pBeacon); +uint32_t AudioTestBeaconGetRemaining(PCAUDIOTESTTONEBEACON pBeacon); +uint32_t AudioTestBeaconGetUsed(PCAUDIOTESTTONEBEACON pBeacon); +bool AudioTestBeaconIsComplete(PCAUDIOTESTTONEBEACON pBeacon); + +int AudioTestGenTag(char *pszTag, size_t cbTag); + +int AudioTestPathGetTemp(char *pszPath, size_t cbPath); +int AudioTestPathCreateTemp(char *pszPath, size_t cbPath, const char *pszUUID); +int AudioTestPathCreate(char *pszPath, size_t cbPath, const char *pszUUID); + +int AudioTestSetObjCreateAndRegister(PAUDIOTESTSET pSet, const char *pszName, PAUDIOTESTOBJ pObj); + +int AudioTestObjWrite(AUDIOTESTOBJ Obj, const void *pvBuf, size_t cbBuf); +int AudioTestObjAddMetadataStr(AUDIOTESTOBJ Obj, const char *pszFormat, ...); +int AudioTestObjClose(AUDIOTESTOBJ Obj); + +int AudioTestSetTestBegin(PAUDIOTESTSET pSet, const char *pszDesc, PAUDIOTESTPARMS pParms, PAUDIOTESTENTRY *ppEntry); +int AudioTestSetTestFailed(PAUDIOTESTENTRY pEntry, int rc, const char *pszErr); +int AudioTestSetTestDone(PAUDIOTESTENTRY pEntry); +bool AudioTestSetTestIsRunning(PAUDIOTESTENTRY pEntry); + +int AudioTestSetCreate(PAUDIOTESTSET pSet, const char *pszPath, const char *pszTag); +int AudioTestSetDestroy(PAUDIOTESTSET pSet); +int AudioTestSetOpen(PAUDIOTESTSET pSet, const char *pszPath); +int AudioTestSetClose(PAUDIOTESTSET pSet); +int AudioTestSetWipe(PAUDIOTESTSET pSet); +const char *AudioTestSetGetTag(PAUDIOTESTSET pSet); +uint32_t AudioTestSetGetTestsTotal(PAUDIOTESTSET pSet); +uint32_t AudioTestSetGetTestsRunning(PAUDIOTESTSET pSet); +uint32_t AudioTestSetGetTotalFailures(PAUDIOTESTSET pSet); +bool AudioTestSetIsPacked(const char *pszPath); +bool AudioTestSetIsRunning(PAUDIOTESTSET pSet); +int AudioTestSetPack(PAUDIOTESTSET pSet, const char *pszOutDir, char *pszFileName, size_t cbFileName); +int AudioTestSetUnpack(const char *pszFile, const char *pszOutDir); + +void AudioTestSetVerifyOptsInitStrict(PAUDIOTESTVERIFYOPTS pOpts); +void AudioTestSetVerifyOptsInit(PAUDIOTESTVERIFYOPTS pOpts); +bool AudioTestSetVerifyOptsAreEqual(PAUDIOTESTVERIFYOPTS pOptsA, PAUDIOTESTVERIFYOPTS pOptsB); + +int AudioTestSetVerify(PAUDIOTESTSET pSetA, PAUDIOTESTSET pSetB, PAUDIOTESTERRORDESC pErrDesc); +int AudioTestSetVerifyEx(PAUDIOTESTSET pSetA, PAUDIOTESTSET pSetB, PAUDIOTESTVERIFYOPTS pOpts, PAUDIOTESTERRORDESC pErrDesc); + +uint32_t AudioTestErrorDescCount(PCAUDIOTESTERRORDESC pErr); +bool AudioTestErrorDescFailed(PCAUDIOTESTERRORDESC pErr); + +void AudioTestErrorDescDestroy(PAUDIOTESTERRORDESC pErr); + +const char *AudioTestStateToStr(AUDIOTESTSTATE enmState); + +/** @name Wave File Accessors + * @{ */ +/** + * An open wave (.WAV) file. + */ +typedef struct AUDIOTESTWAVEFILE +{ + /** Magic value (AUDIOTESTWAVEFILE_MAGIC). */ + uint32_t u32Magic; + /** Set if we're in read-mode, clear if in write mode. */ + bool fReadMode; + /** The file handle. */ + RTFILE hFile; + /** The absolute file offset of the first sample */ + uint32_t offSamples; + /** Number of bytes of samples. */ + uint32_t cbSamples; + /** The current read position relative to @a offSamples. */ + uint32_t offCur; + /** The PCM properties for the file format. */ + PDMAUDIOPCMPROPS Props; +} AUDIOTESTWAVEFILE; +/** Pointer to an open wave file. */ +typedef AUDIOTESTWAVEFILE *PAUDIOTESTWAVEFILE; + +/** Magic value for AUDIOTESTWAVEFILE::u32Magic (Miles Dewey Davis III). */ +#define AUDIOTESTWAVEFILE_MAGIC UINT32_C(0x19260526) +/** Magic value for AUDIOTESTWAVEFILE::u32Magic after closing. */ +#define AUDIOTESTWAVEFILE_MAGIC_DEAD UINT32_C(0x19910928) + +int AudioTestWaveFileOpen(const char *pszFile, PAUDIOTESTWAVEFILE pWaveFile, PRTERRINFO pErrInfo); +int AudioTestWaveFileCreate(const char *pszFile, PCPDMAUDIOPCMPROPS pProps, PAUDIOTESTWAVEFILE pWaveFile, PRTERRINFO pErrInfo); +int AudioTestWaveFileRead(PAUDIOTESTWAVEFILE pWaveFile, void *pvBuf, size_t cbBuf, size_t *pcbRead); +int AudioTestWaveFileWrite(PAUDIOTESTWAVEFILE pWaveFile, const void *pvBuf, size_t cbBuf); +int AudioTestWaveFileClose(PAUDIOTESTWAVEFILE pWaveFile); + +/** @} */ + +#endif /* !VBOX_INCLUDED_SRC_Audio_AudioTest_h */ + diff --git a/src/VBox/Devices/Audio/AudioTestService.cpp b/src/VBox/Devices/Audio/AudioTestService.cpp new file mode 100644 index 00000000..2b602cc5 --- /dev/null +++ b/src/VBox/Devices/Audio/AudioTestService.cpp @@ -0,0 +1,1323 @@ +/* $Id: AudioTestService.cpp $ */ +/** @file + * AudioTestService - Audio test execution server. + */ + +/* + * Copyright (C) 2021-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 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_AUDIO_TEST +#include <iprt/log.h> + +#include <iprt/alloca.h> +#include <iprt/asm.h> +#include <iprt/assert.h> +#include <iprt/critsect.h> +#include <iprt/crc.h> +#include <iprt/ctype.h> +#include <iprt/dir.h> +#include <iprt/env.h> +#include <iprt/err.h> +#include <iprt/file.h> +#include <iprt/getopt.h> +#include <iprt/handle.h> +#include <iprt/initterm.h> +#include <iprt/json.h> +#include <iprt/list.h> +#include <iprt/mem.h> +#include <iprt/message.h> +#include <iprt/param.h> +#include <iprt/path.h> +#include <iprt/pipe.h> +#include <iprt/poll.h> +#include <iprt/process.h> +#include <iprt/stream.h> +#include <iprt/string.h> +#include <iprt/thread.h> + +#include <VBox/log.h> + +#include "AudioTestService.h" +#include "AudioTestServiceInternal.h" + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +/** + * A generic ATS reply, used by the client + * to process the incoming packets. + */ +typedef struct ATSSRVREPLY +{ + char szOp[ATSPKT_OPCODE_MAX_LEN]; + void *pvPayload; + size_t cbPayload; +} ATSSRVREPLY; +/** Pointer to a generic ATS reply. */ +typedef struct ATSSRVREPLY *PATSSRVREPLY; + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** + * Transport layers. + */ +const PCATSTRANSPORT g_apTransports[] = +{ + &g_TcpTransport +}; +/** Number of transport layers in \a g_apTransports. */ +const size_t g_cTransports = RT_ELEMENTS(g_apTransports); + +/** + * ATS client state. + */ +typedef enum ATSCLIENTSTATE +{ + /** Invalid client state. */ + ATSCLIENTSTATE_INVALID = 0, + /** Client is initialising, only the HOWDY and BYE packets are allowed. */ + ATSCLIENTSTATE_INITIALISING, + /** Client is in fully cuntional state and ready to process all requests. */ + ATSCLIENTSTATE_READY, + /** Client is destroying. */ + ATSCLIENTSTATE_DESTROYING, + /** 32bit hack. */ + ATSCLIENTSTATE_32BIT_HACK = 0x7fffffff +} ATSCLIENTSTATE; + +/** + * ATS client instance. + */ +typedef struct ATSCLIENTINST +{ + /** List node for new clients. */ + RTLISTNODE NdLst; + /** The current client state. */ + ATSCLIENTSTATE enmState; + /** Transport backend specific data. */ + PATSTRANSPORTCLIENT pTransportClient; + /** Client hostname. */ + char *pszHostname; +} ATSCLIENTINST; +/** Pointer to a ATS client instance. */ +typedef ATSCLIENTINST *PATSCLIENTINST; + + +/********************************************************************************************************************************* +* Prototypes * +*********************************************************************************************************************************/ +static int atsClientDisconnect(PATSSERVER pThis, PATSCLIENTINST pInst); + + + +/** + * Returns the string represenation of the given state. + */ +static const char *atsClientStateStringify(ATSCLIENTSTATE enmState) +{ + switch (enmState) + { + case ATSCLIENTSTATE_INVALID: + return "INVALID"; + case ATSCLIENTSTATE_INITIALISING: + return "INITIALISING"; + case ATSCLIENTSTATE_READY: + return "READY"; + case ATSCLIENTSTATE_DESTROYING: + return "DESTROYING"; + case ATSCLIENTSTATE_32BIT_HACK: + default: + break; + } + + AssertMsgFailed(("Unknown state %#x\n", enmState)); + return "UNKNOWN"; +} + +/** + * Calculates the checksum value, zero any padding space and send the packet. + * + * @returns IPRT status code. + * @param pThis The ATS instance. + * @param pInst The ATS client structure. + * @param pPkt The packet to send. Must point to a correctly + * aligned buffer. + */ +static int atsSendPkt(PATSSERVER pThis, PATSCLIENTINST pInst, PATSPKTHDR pPkt) +{ + Assert(pPkt->cb >= sizeof(*pPkt)); + pPkt->uCrc32 = RTCrc32(pPkt->achOpcode, pPkt->cb - RT_UOFFSETOF(ATSPKTHDR, achOpcode)); + if (pPkt->cb != RT_ALIGN_32(pPkt->cb, ATSPKT_ALIGNMENT)) + memset((uint8_t *)pPkt + pPkt->cb, '\0', RT_ALIGN_32(pPkt->cb, ATSPKT_ALIGNMENT) - pPkt->cb); + + LogFlowFunc(("cb=%RU32 (%#x), payload=%RU32 (%#x), opcode=%.8s\n", + pPkt->cb, pPkt->cb, pPkt->cb - sizeof(ATSPKTHDR), pPkt->cb - sizeof(ATSPKTHDR), pPkt->achOpcode)); + int rc = pThis->pTransport->pfnSendPkt(pThis->pTransportInst, pInst->pTransportClient, pPkt); + while (RT_UNLIKELY(rc == VERR_INTERRUPTED) && !pThis->fTerminate) + rc = pThis->pTransport->pfnSendPkt(pThis->pTransportInst, pInst->pTransportClient, pPkt); + + return rc; +} + +/** + * Sends a babble reply and disconnects the client (if applicable). + * + * @param pThis The ATS instance. + * @param pInst The ATS server instance. + * @param pszOpcode The BABBLE opcode. + */ +static void atsReplyBabble(PATSSERVER pThis, PATSCLIENTINST pInst, const char *pszOpcode) +{ + ATSPKTHDR Reply; + Reply.cb = sizeof(Reply); + Reply.uCrc32 = 0; + memcpy(Reply.achOpcode, pszOpcode, sizeof(Reply.achOpcode)); + + pThis->pTransport->pfnBabble(pThis->pTransportInst, pInst->pTransportClient, &Reply, 20*1000); +} + +/** + * Receive and validate a packet. + * + * Will send bable responses to malformed packets that results in a error status + * code. + * + * @returns IPRT status code. + * @param pThis The ATS instance. + * @param pInst The opaque ATS instance structure. + * @param ppPktHdr Where to return the packet on success. Free + * with RTMemFree. + * @param fAutoRetryOnFailure Whether to retry on error. + */ +static int atsRecvPkt(PATSSERVER pThis, PATSCLIENTINST pInst, PPATSPKTHDR ppPktHdr, bool fAutoRetryOnFailure) +{ + for (;;) + { + PATSPKTHDR pPktHdr; + int rc = pThis->pTransport->pfnRecvPkt(pThis->pTransportInst, pInst->pTransportClient, &pPktHdr); + if (RT_SUCCESS(rc)) + { + /* validate the packet. */ + if ( pPktHdr->cb >= sizeof(ATSPKTHDR) + && pPktHdr->cb < ATSPKT_MAX_SIZE) + { + Log2Func(("pPktHdr=%p cb=%#x crc32=%#x opcode=%.8s\n", + pPktHdr, pPktHdr->cb, pPktHdr->uCrc32, pPktHdr->achOpcode)); + uint32_t uCrc32Calc = pPktHdr->uCrc32 != 0 + ? RTCrc32(&pPktHdr->achOpcode[0], pPktHdr->cb - RT_UOFFSETOF(ATSPKTHDR, achOpcode)) + : 0; + if (pPktHdr->uCrc32 == uCrc32Calc) + { + AssertCompileMemberSize(ATSPKTHDR, achOpcode, 8); + if ( RT_C_IS_UPPER(pPktHdr->achOpcode[0]) + && RT_C_IS_UPPER(pPktHdr->achOpcode[1]) + && (RT_C_IS_UPPER(pPktHdr->achOpcode[2]) || pPktHdr->achOpcode[2] == ' ') + && (RT_C_IS_PRINT(pPktHdr->achOpcode[3]) || pPktHdr->achOpcode[3] == ' ') + && (RT_C_IS_PRINT(pPktHdr->achOpcode[4]) || pPktHdr->achOpcode[4] == ' ') + && (RT_C_IS_PRINT(pPktHdr->achOpcode[5]) || pPktHdr->achOpcode[5] == ' ') + && (RT_C_IS_PRINT(pPktHdr->achOpcode[6]) || pPktHdr->achOpcode[6] == ' ') + && (RT_C_IS_PRINT(pPktHdr->achOpcode[7]) || pPktHdr->achOpcode[7] == ' ') + ) + { + Log(("cb=%#x opcode=%.8s\n", pPktHdr->cb, pPktHdr->achOpcode)); + *ppPktHdr = pPktHdr; + return rc; + } + + rc = VERR_IO_BAD_COMMAND; + } + else + { + Log(("cb=%#x opcode=%.8s crc32=%#x actual=%#x\n", + pPktHdr->cb, pPktHdr->achOpcode, pPktHdr->uCrc32, uCrc32Calc)); + rc = VERR_IO_CRC; + } + } + else + rc = VERR_IO_BAD_LENGTH; + + /* Send babble reply and disconnect the client if the transport is + connection oriented. */ + if (rc == VERR_IO_BAD_LENGTH) + atsReplyBabble(pThis, pInst, "BABBLE L"); + else if (rc == VERR_IO_CRC) + atsReplyBabble(pThis, pInst, "BABBLE C"); + else if (rc == VERR_IO_BAD_COMMAND) + atsReplyBabble(pThis, pInst, "BABBLE O"); + else + atsReplyBabble(pThis, pInst, "BABBLE "); + RTMemFree(pPktHdr); + } + + /* Try again or return failure? */ + if ( pThis->fTerminate + || rc != VERR_INTERRUPTED + || !fAutoRetryOnFailure + ) + { + Log(("rc=%Rrc\n", rc)); + return rc; + } + } +} + +/** + * Make a simple reply, only status opcode. + * + * @returns IPRT status code of the send. + * @param pThis The ATS instance. + * @param pInst The opaque ATS instance structure. + * @param pReply The reply packet. + * @param pszOpcode The status opcode. Exactly 8 chars long, padd + * with space. + * @param cbExtra Bytes in addition to the header. + */ +static int atsReplyInternal(PATSSERVER pThis, PATSCLIENTINST pInst, PATSPKTHDR pReply, const char *pszOpcode, size_t cbExtra) +{ + /* copy the opcode, don't be too strict in case of a padding screw up. */ + size_t cchOpcode = strlen(pszOpcode); + if (RT_LIKELY(cchOpcode == sizeof(pReply->achOpcode))) + memcpy(pReply->achOpcode, pszOpcode, sizeof(pReply->achOpcode)); + else + { + Assert(cchOpcode == sizeof(pReply->achOpcode)); + while (cchOpcode > 0 && pszOpcode[cchOpcode - 1] == ' ') + cchOpcode--; + AssertMsgReturn(cchOpcode < sizeof(pReply->achOpcode), ("%d/'%.8s'\n", cchOpcode, pszOpcode), VERR_INTERNAL_ERROR_4); + memcpy(pReply->achOpcode, pszOpcode, cchOpcode); + memset(&pReply->achOpcode[cchOpcode], ' ', sizeof(pReply->achOpcode) - cchOpcode); + } + + pReply->cb = (uint32_t)sizeof(ATSPKTHDR) + (uint32_t)cbExtra; + pReply->uCrc32 = 0; + + return atsSendPkt(pThis, pInst, pReply); +} + +/** + * Make a simple reply, only status opcode. + * + * @returns IPRT status code of the send. + * @param pThis The ATS instance. + * @param pInst The opaque ATS instance structure. + * @param pPktHdr The original packet (for future use). + * @param pszOpcode The status opcode. Exactly 8 chars long, padd + * with space. + */ +static int atsReplySimple(PATSSERVER pThis, PATSCLIENTINST pInst, PATSPKTHDR pPktHdr, const char *pszOpcode) +{ + return atsReplyInternal(pThis, pInst, pPktHdr, pszOpcode, 0); +} + +/** + * Acknowledges a packet with success. + * + * @returns IPRT status code of the send. + * @param pThis The ATS instance. + * @param pInst The opaque ATS instance structure. + * @param pPktHdr The original packet (for future use). + */ +static int atsReplyAck(PATSSERVER pThis, PATSCLIENTINST pInst, PATSPKTHDR pPktHdr) +{ + return atsReplySimple(pThis, pInst, pPktHdr, "ACK "); +} + +/** + * Replies with a failure. + * + * @returns IPRT status code of the send. + * @param pThis The ATS instance. + * @param pInst The opaque ATS instance structure. + * @param pPktHdr The original packet (for future use). + * @param pszOpcode The status opcode. Exactly 8 chars long, padd + * with space. + * @param rcReq The status code of the request. + * @param pszDetailFmt Longer description of the problem (format string). + * @param va Format arguments. + */ +static int atsReplyFailureV(PATSSERVER pThis, PATSCLIENTINST pInst, PATSPKTHDR pPktHdr, + const char *pszOpcode, int rcReq, const char *pszDetailFmt, va_list va) +{ + RT_NOREF(pPktHdr); + + ATSPKTREPFAIL Rep; + RT_ZERO(Rep); + + size_t cchDetail = RTStrPrintfV(Rep.ach, sizeof(Rep.ach), pszDetailFmt, va); + + Rep.rc = rcReq; + + return atsReplyInternal(pThis, pInst, &Rep.Hdr, pszOpcode, sizeof(Rep.rc) + cchDetail + 1); +} + +/** + * Replies with a failure. + * + * @returns IPRT status code of the send. + * @param pThis The ATS instance. + * @param pInst The opaque ATS instance structure. + * @param pPktHdr The original packet (for future use). + * @param pszOpcode The status opcode. Exactly 8 chars long, padd + * with space. + * @param rcReq Status code. + * @param pszDetailFmt Longer description of the problem (format string). + * @param ... Format arguments. + */ +static int atsReplyFailure(PATSSERVER pThis, PATSCLIENTINST pInst, PATSPKTHDR pPktHdr, + const char *pszOpcode, int rcReq, const char *pszDetailFmt, ...) +{ + va_list va; + va_start(va, pszDetailFmt); + int rc = atsReplyFailureV(pThis, pInst, pPktHdr, pszOpcode, rcReq, pszDetailFmt, va); + va_end(va); + return rc; +} + +/** + * Replies according to the return code. + * + * @returns IPRT status code of the send. + * @param pThis The ATS instance. + * @param pInst The opaque ATS instance structure. + * @param pPktHdr The packet to reply to. + * @param rcOperation The status code to report. + * @param pszOperationFmt The operation that failed. Typically giving the + * function call with important arguments. + * @param ... Arguments to the format string. + */ +static int atsReplyRC(PATSSERVER pThis, + PATSCLIENTINST pInst, PATSPKTHDR pPktHdr, int rcOperation, const char *pszOperationFmt, ...) +{ + if (RT_SUCCESS(rcOperation)) + return atsReplyAck(pThis, pInst, pPktHdr); + + char szOperation[128]; + va_list va; + va_start(va, pszOperationFmt); + RTStrPrintfV(szOperation, sizeof(szOperation), pszOperationFmt, va); + va_end(va); + + return atsReplyFailure(pThis, pInst, pPktHdr, "FAILED ", rcOperation, "%s failed with rc=%Rrc (opcode '%.8s')", + szOperation, rcOperation, pPktHdr->achOpcode); +} + +/** + * Signal a bad packet exact size. + * + * @returns IPRT status code of the send. + * @param pThis The ATS instance. + * @param pInst The opaque ATS instance structure. + * @param pPktHdr The packet to reply to. + * @param cb The wanted size. + */ +static int atsReplyBadSize(PATSSERVER pThis, PATSCLIENTINST pInst, PATSPKTHDR pPktHdr, size_t cb) +{ + return atsReplyFailure(pThis, pInst, pPktHdr, "BAD SIZE", VERR_INVALID_PARAMETER, "Expected at %zu bytes, got %u (opcode '%.8s')", + cb, pPktHdr->cb, pPktHdr->achOpcode); +} + +/** + * Deals with a unknown command. + * + * @returns IPRT status code of the send. + * @param pThis The ATS instance. + * @param pInst The opaque ATS instance structure. + * @param pPktHdr The packet to reply to. + */ +static int atsReplyUnknown(PATSSERVER pThis, PATSCLIENTINST pInst, PATSPKTHDR pPktHdr) +{ + return atsReplyFailure(pThis, pInst, pPktHdr, "UNKNOWN ", VERR_NOT_FOUND, "Opcode '%.8s' is not known", pPktHdr->achOpcode); +} + +/** + * Deals with a command sent in an invalid client state. + * + * @returns IPRT status code of the send. + * @param pThis The ATS instance. + * @param pInst The opaque ATS instance structure. + * @param pPktHdr The packet containing the unterminated string. + */ +static int atsReplyInvalidState(PATSSERVER pThis, PATSCLIENTINST pInst, PATSPKTHDR pPktHdr) +{ + return atsReplyFailure(pThis, pInst, pPktHdr, "INVSTATE", VERR_INVALID_STATE, "Opcode '%.8s' is not supported at client state '%s", + pPktHdr->achOpcode, atsClientStateStringify(pInst->enmState)); +} + +/** + * Verifies and acknowledges a "BYE" request. + * + * @returns IPRT status code. + * @param pThis The ATS instance. + * @param pInst The opaque ATS instance structure. + * @param pPktHdr The bye packet. + */ +static int atsDoBye(PATSSERVER pThis, PATSCLIENTINST pInst, PATSPKTHDR pPktHdr) +{ + int rc; + if (pPktHdr->cb == sizeof(ATSPKTHDR)) + { + if (pThis->Callbacks.pfnBye) + { + rc = pThis->Callbacks.pfnBye(pThis->Callbacks.pvUser); + } + else + rc = VINF_SUCCESS; + + if (RT_SUCCESS(rc)) + { + rc = atsReplyAck(pThis, pInst, pPktHdr); + } + else + rc = atsReplyRC(pThis, pInst, pPktHdr, rc, "Disconnecting client failed"); + } + else + rc = atsReplyBadSize(pThis, pInst, pPktHdr, sizeof(ATSPKTHDR)); + return rc; +} + +/** + * Verifies and acknowledges a "HOWDY" request. + * + * @returns IPRT status code. + * @param pThis The ATS instance. + * @param pInst The opaque ATS instance structure. + * @param pPktHdr The howdy packet. + */ +static int atsDoHowdy(PATSSERVER pThis, PATSCLIENTINST pInst, PATSPKTHDR pPktHdr) +{ + int rc = VINF_SUCCESS; + + if (pPktHdr->cb != sizeof(ATSPKTREQHOWDY)) + return atsReplyBadSize(pThis, pInst, pPktHdr, sizeof(ATSPKTREQHOWDY)); + + if (pInst->enmState != ATSCLIENTSTATE_INITIALISING) + return atsReplyInvalidState(pThis, pInst, pPktHdr); + + PATSPKTREQHOWDY pReq = (PATSPKTREQHOWDY)pPktHdr; + + if (pReq->uVersion != ATS_PROTOCOL_VS) + return atsReplyRC(pThis, pInst, pPktHdr, VERR_VERSION_MISMATCH, "The given version %#x is not supported", pReq->uVersion); + + ATSPKTREPHOWDY Rep; + RT_ZERO(Rep); + + Rep.uVersion = ATS_PROTOCOL_VS; + + rc = atsReplyInternal(pThis, pInst, &Rep.Hdr, "ACK ", sizeof(Rep) - sizeof(ATSPKTHDR)); + if (RT_SUCCESS(rc)) + { + pThis->pTransport->pfnNotifyHowdy(pThis->pTransportInst, pInst->pTransportClient); + + if (pThis->Callbacks.pfnHowdy) + rc = pThis->Callbacks.pfnHowdy(pThis->Callbacks.pvUser); + + if (RT_SUCCESS(rc)) + pInst->enmState = ATSCLIENTSTATE_READY; + } + + return rc; +} + +/** + * Verifies and acknowledges a "TSET BEG" request. + * + * @returns IPRT status code. + * @param pThis The ATS instance. + * @param pInst The opaque ATS instance structure. + * @param pPktHdr The test set begin packet. + */ +static int atsDoTestSetBegin(PATSSERVER pThis, PATSCLIENTINST pInst, PATSPKTHDR pPktHdr) +{ + if (pPktHdr->cb != sizeof(ATSPKTREQTSETBEG)) + return atsReplyBadSize(pThis, pInst, pPktHdr, sizeof(ATSPKTREQTSETBEG)); + + PATSPKTREQTSETBEG pReq = (PATSPKTREQTSETBEG)pPktHdr; + + int rc = VINF_SUCCESS; + + if (pThis->Callbacks.pfnTestSetBegin) + rc = pThis->Callbacks.pfnTestSetBegin(pThis->Callbacks.pvUser, pReq->szTag); + + if (RT_SUCCESS(rc)) + rc = atsReplyAck(pThis, pInst, pPktHdr); + else + rc = atsReplyRC(pThis, pInst, pPktHdr, rc, "Beginning test set failed"); + return rc; +} + +/** + * Verifies and acknowledges a "TSET END" request. + * + * @returns IPRT status code. + * @param pThis The ATS instance. + * @param pInst The opaque ATS instance structure. + * @param pPktHdr The test set end packet. + */ +static int atsDoTestSetEnd(PATSSERVER pThis, PATSCLIENTINST pInst, PATSPKTHDR pPktHdr) +{ + if (pPktHdr->cb != sizeof(ATSPKTREQTSETEND)) + return atsReplyBadSize(pThis, pInst, pPktHdr, sizeof(ATSPKTREQTSETEND)); + + PATSPKTREQTSETEND pReq = (PATSPKTREQTSETEND)pPktHdr; + + int rc = VINF_SUCCESS; + + if (pThis->Callbacks.pfnTestSetEnd) + rc = pThis->Callbacks.pfnTestSetEnd(pThis->Callbacks.pvUser, pReq->szTag); + + if (RT_SUCCESS(rc)) + rc = atsReplyAck(pThis, pInst, pPktHdr); + else + rc = atsReplyRC(pThis, pInst, pPktHdr, rc, "Ending test set failed"); + return rc; +} + +/** + * Used by atsDoTestSetSend to wait for a reply ACK from the client. + * + * @returns VINF_SUCCESS on ACK, VERR_GENERAL_FAILURE on NACK, + * VERR_NET_NOT_CONNECTED on unknown response (sending a bable reply), + * or whatever atsRecvPkt returns. + * @param pThis The ATS instance. + * @param pInst The opaque ATS instance structure. + * @param pPktHdr The original packet (for future use). + */ +static int atsWaitForAck(PATSSERVER pThis, PATSCLIENTINST pInst, PATSPKTHDR pPktHdr) +{ + RT_NOREF(pPktHdr); + /** @todo timeout? */ + PATSPKTHDR pReply; + int rc = atsRecvPkt(pThis, pInst, &pReply, false /*fAutoRetryOnFailure*/); + if (RT_SUCCESS(rc)) + { + if (atsIsSameOpcode(pReply, "ACK")) + rc = VINF_SUCCESS; + else if (atsIsSameOpcode(pReply, "NACK")) + rc = VERR_GENERAL_FAILURE; + else + { + atsReplyBabble(pThis, pInst, "BABBLE "); + rc = VERR_NET_NOT_CONNECTED; + } + RTMemFree(pReply); + } + return rc; +} + +/** + * Verifies and acknowledges a "TSET SND" request. + * + * @returns IPRT status code. + * @param pThis The ATS instance. + * @param pInst The opaque ATS instance structure. + * @param pPktHdr The test set end packet. + */ +static int atsDoTestSetSend(PATSSERVER pThis, PATSCLIENTINST pInst, PATSPKTHDR pPktHdr) +{ + if (pPktHdr->cb != sizeof(ATSPKTREQTSETSND)) + return atsReplyBadSize(pThis, pInst, pPktHdr, sizeof(ATSPKTREQTSETSND)); + + PATSPKTREQTSETSND pReq = (PATSPKTREQTSETSND)pPktHdr; + + int rc = VINF_SUCCESS; + + if (!pThis->Callbacks.pfnTestSetSendRead) + return atsReplyRC(pThis, pInst, pPktHdr, VERR_NOT_SUPPORTED, "Sending test set not implemented"); + + if (pThis->Callbacks.pfnTestSetSendBegin) + { + rc = pThis->Callbacks.pfnTestSetSendBegin(pThis->Callbacks.pvUser, pReq->szTag); + if (RT_FAILURE(rc)) + return atsReplyRC(pThis, pInst, pPktHdr, rc, "Beginning sending test set '%s' failed", pReq->szTag); + } + + for (;;) + { + uint32_t uMyCrc32 = RTCrc32Start(); + struct + { + ATSPKTHDR Hdr; + uint32_t uCrc32; + char ab[_64K]; + char abPadding[ATSPKT_ALIGNMENT]; + } Pkt; +#ifdef DEBUG + RT_ZERO(Pkt); +#endif + size_t cbRead = 0; + rc = pThis->Callbacks.pfnTestSetSendRead(pThis->Callbacks.pvUser, pReq->szTag, &Pkt.ab, sizeof(Pkt.ab), &cbRead); + if ( RT_FAILURE(rc) + || cbRead == 0) + { + if ( rc == VERR_EOF + || (RT_SUCCESS(rc) && cbRead == 0)) + { + Pkt.uCrc32 = RTCrc32Finish(uMyCrc32); + rc = atsReplyInternal(pThis, pInst, &Pkt.Hdr, "DATA EOF", sizeof(uint32_t) /* uCrc32 */); + if (RT_SUCCESS(rc)) + rc = atsWaitForAck(pThis, pInst, &Pkt.Hdr); + } + else + rc = atsReplyRC(pThis, pInst, pPktHdr, rc, "Sending data for test set '%s' failed", pReq->szTag); + break; + } + + uMyCrc32 = RTCrc32Process(uMyCrc32, &Pkt.ab[0], cbRead); + Pkt.uCrc32 = RTCrc32Finish(uMyCrc32); + + Log2Func(("cbRead=%zu -> uCrc32=%#x\n", cbRead, Pkt.uCrc32)); + + Assert(cbRead <= sizeof(Pkt.ab)); + + rc = atsReplyInternal(pThis, pInst, &Pkt.Hdr, "DATA ", sizeof(uint32_t) /* uCrc32 */ + cbRead); + if (RT_FAILURE(rc)) + break; + + rc = atsWaitForAck(pThis, pInst, &Pkt.Hdr); + if (RT_FAILURE(rc)) + break; + } + + if (pThis->Callbacks.pfnTestSetSendEnd) + { + int rc2 = pThis->Callbacks.pfnTestSetSendEnd(pThis->Callbacks.pvUser, pReq->szTag); + if (RT_FAILURE(rc2)) + return atsReplyRC(pThis, pInst, pPktHdr, rc2, "Ending sending test set '%s' failed", pReq->szTag); + } + + return rc; +} + +/** + * Verifies and processes a "TN PLY" request. + * + * @returns IPRT status code. + * @param pThis The ATS instance. + * @param pInst The opaque ATS instance structure. + * @param pPktHdr The packet header. + */ +static int atsDoTonePlay(PATSSERVER pThis, PATSCLIENTINST pInst, PATSPKTHDR pPktHdr) +{ + if (pPktHdr->cb < sizeof(ATSPKTREQTONEPLAY)) + return atsReplyBadSize(pThis, pInst, pPktHdr, sizeof(ATSPKTREQTONEPLAY)); + + if (pInst->enmState != ATSCLIENTSTATE_READY) + return atsReplyInvalidState(pThis, pInst, pPktHdr); + + int rc = VINF_SUCCESS; + + PATSPKTREQTONEPLAY pReq = (PATSPKTREQTONEPLAY)pPktHdr; + + if (pThis->Callbacks.pfnTonePlay) + rc = pThis->Callbacks.pfnTonePlay(pThis->Callbacks.pvUser, &pReq->ToneParms); + + if (RT_SUCCESS(rc)) + rc = atsReplyAck(pThis, pInst, pPktHdr); + else + rc = atsReplyRC(pThis, pInst, pPktHdr, rc, "Playing test tone failed"); + return rc; +} + +/** + * Verifies and processes a "TN REC" request. + * + * @returns IPRT status code. + * @param pThis The ATS instance. + * @param pInst The opaque ATS instance structure. + * @param pPktHdr The packet header. + */ +static int atsDoToneRecord(PATSSERVER pThis, PATSCLIENTINST pInst, PATSPKTHDR pPktHdr) +{ + if (pPktHdr->cb < sizeof(ATSPKTREQTONEREC)) + return atsReplyBadSize(pThis, pInst, pPktHdr, sizeof(ATSPKTREQTONEREC)); + + if (pInst->enmState != ATSCLIENTSTATE_READY) + return atsReplyInvalidState(pThis, pInst, pPktHdr); + + int rc = VINF_SUCCESS; + + PATSPKTREQTONEREC pReq = (PATSPKTREQTONEREC)pPktHdr; + + if (pThis->Callbacks.pfnToneRecord) + rc = pThis->Callbacks.pfnToneRecord(pThis->Callbacks.pvUser, &pReq->ToneParms); + + if (RT_SUCCESS(rc)) + rc = atsReplyAck(pThis, pInst, pPktHdr); + else + rc = atsReplyRC(pThis, pInst, pPktHdr, rc, "Recording test tone failed"); + return rc; +} + +/** + * Main request processing routine for each client. + * + * @returns IPRT status code. + * @param pThis The ATS instance. + * @param pInst The ATS client structure sending the request. + * @param pfDisconnect Where to return whether to disconnect the client on success or not. + */ +static int atsClientReqProcess(PATSSERVER pThis, PATSCLIENTINST pInst, bool *pfDisconnect) +{ + LogRelFlowFuncEnter(); + + /* + * Read client command packet and process it. + */ + PATSPKTHDR pPktHdr = NULL; + int rc = atsRecvPkt(pThis, pInst, &pPktHdr, true /*fAutoRetryOnFailure*/); + if (RT_FAILURE(rc)) + return rc; + + /* + * Do a string switch on the opcode bit. + */ + /* Connection: */ + if ( atsIsSameOpcode(pPktHdr, ATSPKT_OPCODE_HOWDY)) + rc = atsDoHowdy(pThis, pInst, pPktHdr); + else if (atsIsSameOpcode(pPktHdr, ATSPKT_OPCODE_BYE)) + { + rc = atsDoBye(pThis, pInst, pPktHdr); + if (RT_SUCCESS(rc)) + *pfDisconnect = true; + } + /* Test set handling: */ + else if (atsIsSameOpcode(pPktHdr, ATSPKT_OPCODE_TESTSET_BEGIN)) + rc = atsDoTestSetBegin(pThis, pInst, pPktHdr); + else if (atsIsSameOpcode(pPktHdr, ATSPKT_OPCODE_TESTSET_END)) + rc = atsDoTestSetEnd(pThis, pInst, pPktHdr); + else if (atsIsSameOpcode(pPktHdr, ATSPKT_OPCODE_TESTSET_SEND)) + rc = atsDoTestSetSend(pThis, pInst, pPktHdr); + /* Audio testing: */ + else if (atsIsSameOpcode(pPktHdr, ATSPKT_OPCODE_TONE_PLAY)) + rc = atsDoTonePlay(pThis, pInst, pPktHdr); + else if (atsIsSameOpcode(pPktHdr, ATSPKT_OPCODE_TONE_RECORD)) + rc = atsDoToneRecord(pThis, pInst, pPktHdr); + /* Misc: */ + else + rc = atsReplyUnknown(pThis, pInst, pPktHdr); + + RTMemFree(pPktHdr); + + LogRelFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Disconnects a client. + * + * @returns VBox status code. + * @param pThis The ATS instance. + * @param pInst The ATS client to disconnect. + */ +static int atsClientDisconnect(PATSSERVER pThis, PATSCLIENTINST pInst) +{ + AssertReturn(pInst->enmState != ATSCLIENTSTATE_DESTROYING, VERR_WRONG_ORDER); + + pInst->enmState = ATSCLIENTSTATE_DESTROYING; + + if ( pThis->pTransportInst + && pInst->pTransportClient) + { + if (pThis->pTransport->pfnNotifyBye) + pThis->pTransport->pfnNotifyBye(pThis->pTransportInst, pInst->pTransportClient); + + pThis->pTransport->pfnDisconnect(pThis->pTransportInst, pInst->pTransportClient); + /* Pointer is now invalid due to the call above. */ + pInst->pTransportClient = NULL; + } + + return VINF_SUCCESS; +} + +/** + * Free's (destroys) a client instance. + * + * @returns nothing. + * @param pInst The opaque ATS instance structure. + */ +static void atsClientFree(PATSCLIENTINST pInst) +{ + if (!pInst) + return; + + /* Make sure that there is no transport client associated with it anymore. */ + AssertReturnVoid(pInst->enmState == ATSCLIENTSTATE_DESTROYING); + AssertReturnVoid(pInst->pTransportClient == NULL); + + if (pInst->pszHostname) + { + RTStrFree(pInst->pszHostname); + pInst->pszHostname = NULL; + } + + RTMemFree(pInst); + pInst = NULL; +} + +/** + * The main thread worker serving the clients. + */ +static DECLCALLBACK(int) atsClientWorker(RTTHREAD hThread, void *pvUser) +{ + RT_NOREF(hThread); + + PATSSERVER pThis = (PATSSERVER)pvUser; + AssertPtrReturn(pThis, VERR_INVALID_POINTER); + + unsigned cClientsMax = 0; + unsigned cClientsCur = 0; + PATSCLIENTINST *papInsts = NULL; + + /* Add the pipe to the poll set. */ + int rc = RTPollSetAddPipe(pThis->hPollSet, pThis->hPipeR, RTPOLL_EVT_READ | RTPOLL_EVT_ERROR, 0); + if (RT_SUCCESS(rc)) + { + while (!pThis->fTerminate) + { + uint32_t fEvts; + uint32_t uId; + rc = RTPoll(pThis->hPollSet, RT_INDEFINITE_WAIT, &fEvts, &uId); + LogRelFlowFunc(("RTPoll(...) returned fEvts=#%x, uId=%RU32 -> %Rrc\n", fEvts, uId, rc)); + if (RT_SUCCESS(rc)) + { + if (uId == 0) + { + if (fEvts & RTPOLL_EVT_ERROR) + break; + + /* We got woken up because of a new client. */ + Assert(fEvts & RTPOLL_EVT_READ); + + uint8_t bRead; + size_t cbRead = 0; + rc = RTPipeRead(pThis->hPipeR, &bRead, 1, &cbRead); + AssertRC(rc); + + RTCritSectEnter(&pThis->CritSectClients); + /* Walk the list and add all new clients. */ + PATSCLIENTINST pIt, pItNext; + RTListForEachSafe(&pThis->LstClientsNew, pIt, pItNext, ATSCLIENTINST, NdLst) + { + RTListNodeRemove(&pIt->NdLst); + Assert(cClientsCur <= cClientsMax); + if (cClientsCur == cClientsMax) + { + /* Realloc to accommodate for the new clients. */ + PATSCLIENTINST *papInstsNew = (PATSCLIENTINST *)RTMemReallocZ(papInsts, cClientsMax * sizeof(PATSCLIENTINST), (cClientsMax + 10) * sizeof(PATSCLIENTINST)); + if (RT_LIKELY(papInstsNew)) + { + cClientsMax += 10; + papInsts = papInstsNew; + } + } + if (cClientsCur < cClientsMax) + { + /* Find a free slot in the client array. */ + unsigned idxSlt = 0; + while ( idxSlt < cClientsMax + && papInsts[idxSlt] != NULL) + idxSlt++; + + rc = pThis->pTransport->pfnPollSetAdd(pThis->pTransportInst, pThis->hPollSet, pIt->pTransportClient, idxSlt + 1); + if (RT_SUCCESS(rc)) + { + cClientsCur++; + papInsts[idxSlt] = pIt; + } + else + { + atsClientDisconnect(pThis, pIt); + atsClientFree(pIt); + pIt = NULL; + } + } + else + { + atsClientDisconnect(pThis, pIt); + atsClientFree(pIt); + pIt = NULL; + } + } + RTCritSectLeave(&pThis->CritSectClients); + } + else + { + bool fDisconnect = false; + + /* Client sends a request, pick the right client and process it. */ + PATSCLIENTINST pInst = papInsts[uId - 1]; + AssertPtr(pInst); + if (fEvts & RTPOLL_EVT_READ) + rc = atsClientReqProcess(pThis, pInst, &fDisconnect); + + if ( (fEvts & RTPOLL_EVT_ERROR) + || RT_FAILURE(rc) + || fDisconnect) + { + /* Close connection and remove client from array. */ + int rc2 = pThis->pTransport->pfnPollSetRemove(pThis->pTransportInst, pThis->hPollSet, pInst->pTransportClient, uId); + AssertRC(rc2); + + atsClientDisconnect(pThis, pInst); + atsClientFree(pInst); + pInst = NULL; + + papInsts[uId - 1] = NULL; + Assert(cClientsCur); + cClientsCur--; + } + } + } + } + } + + if (papInsts) + { + for (size_t i = 0; i < cClientsMax; i++) + RTMemFree(papInsts[i]); + RTMemFree(papInsts); + } + + return rc; +} + +/** + * The main thread waiting for new client connections. + * + * @returns VBox status code. + */ +static DECLCALLBACK(int) atsMainThread(RTTHREAD hThread, void *pvUser) +{ + RT_NOREF(hThread); + + LogRelFlowFuncEnter(); + + PATSSERVER pThis = (PATSSERVER)pvUser; + AssertPtrReturn(pThis, VERR_INVALID_POINTER); + + int rc = RTThreadUserSignal(hThread); + AssertRCReturn(rc, rc); + + while (!pThis->fTerminate) + { + /* + * Wait for new connection and spin off a new thread + * for every new client. + */ + bool fFromServer; + PATSTRANSPORTCLIENT pTransportClient; + rc = pThis->pTransport->pfnWaitForConnect(pThis->pTransportInst, 1000 /* msTimeout */, &fFromServer, &pTransportClient); + if (RT_FAILURE(rc)) + continue; + + /* + * New connection, create new client structure and spin off + * the request handling thread. + */ + PATSCLIENTINST pInst = (PATSCLIENTINST)RTMemAllocZ(sizeof(ATSCLIENTINST)); + if (RT_LIKELY(pInst)) + { + pInst->enmState = ATSCLIENTSTATE_INITIALISING; + pInst->pTransportClient = pTransportClient; + pInst->pszHostname = NULL; + + /* Add client to the new list and inform the worker thread. */ + RTCritSectEnter(&pThis->CritSectClients); + RTListAppend(&pThis->LstClientsNew, &pInst->NdLst); + RTCritSectLeave(&pThis->CritSectClients); + + size_t cbWritten = 0; + rc = RTPipeWrite(pThis->hPipeW, "", 1, &cbWritten); + if (RT_FAILURE(rc)) + LogRelFunc(("Failed to inform worker thread of a new client, rc=%Rrc\n", rc)); + } + else + { + LogRelFunc(("Creating new client structure failed with out of memory error\n")); + pThis->pTransport->pfnNotifyBye(pThis->pTransportInst, pTransportClient); + rc = VERR_NO_MEMORY; + break; /* This is fatal, break out of the loop. */ + } + + if (RT_SUCCESS(rc)) + { + LogRelFunc(("New connection established (%s)\n", fFromServer ? "from server" : "as client")); + + /** + * If the new client is not from our server but from a remote server (also called a reverse connection), + * exit this loop and stop trying to connect to the remote server. + * + * Otherwise we would connect lots and lots of clients without any real use. + * + ** @todo Improve this handling -- there might be a better / more elegant solution. + */ + if (!fFromServer) + break; + } + } + + LogRelFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Initializes an ATS instance. + * + * @note This does *not* start the server! + * + * @returns VBox status code. + * @param pThis The ATS instance. + * @param pCallbacks The callbacks table to use. + */ +int AudioTestSvcInit(PATSSERVER pThis, PCATSCALLBACKS pCallbacks) +{ + LogRelFlowFuncEnter(); + + RT_BZERO(pThis, sizeof(ATSSERVER)); + + pThis->hPipeR = NIL_RTPIPE; + pThis->hPipeW = NIL_RTPIPE; + + RTListInit(&pThis->LstClientsNew); + + /* Copy callback table. */ + memcpy(&pThis->Callbacks, pCallbacks, sizeof(ATSCALLBACKS)); + + int rc = RTCritSectInit(&pThis->CritSectClients); + if (RT_SUCCESS(rc)) + { + rc = RTPollSetCreate(&pThis->hPollSet); + if (RT_SUCCESS(rc)) + { + rc = RTPipeCreate(&pThis->hPipeR, &pThis->hPipeW, 0); + if (RT_SUCCESS(rc)) + { + /* + * The default transporter is the first one. + */ + pThis->pTransport = g_apTransports[0]; /** @todo Make this dynamic. */ + + rc = pThis->pTransport->pfnCreate(&pThis->pTransportInst); + if (RT_SUCCESS(rc)) + return VINF_SUCCESS; + + RTPipeClose(pThis->hPipeR); + RTPipeClose(pThis->hPipeW); + } + else + LogRel(("Creating communications pipe failed with %Rrc\n", rc)); + + RTPollSetDestroy(pThis->hPollSet); + } + else + LogRel(("Creating pollset failed with %Rrc\n", rc)); + + RTCritSectDelete(&pThis->CritSectClients); + } + else + LogRel(("Creating critical section failed with %Rrc\n", rc)); + + if (RT_FAILURE(rc)) + LogRel(("Creating server failed with %Rrc\n", rc)); + + LogRelFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Handles a command line option. + * + * @returns VBox status code. + * @param pThis The ATS instance to handle option for. + * @param ch Option (short) to handle. + * @param pVal Option union to store the result in on success. + */ +int AudioTestSvcHandleOption(PATSSERVER pThis, int ch, PCRTGETOPTUNION pVal) +{ + AssertPtrReturn(pThis->pTransport, VERR_WRONG_ORDER); /* Must be creatd first. */ + if (!pThis->pTransport->pfnOption) + return VERR_GETOPT_UNKNOWN_OPTION; + return pThis->pTransport->pfnOption(pThis->pTransportInst, ch, pVal); +} + +/** + * Starts a formerly initialized ATS instance. + * + * @returns VBox status code. + * @param pThis The ATS instance to start. + */ +int AudioTestSvcStart(PATSSERVER pThis) +{ + LogRelFlowFuncEnter(); + + /* Spin off the thread serving connections. */ + int rc = RTThreadCreate(&pThis->hThreadServing, atsClientWorker, pThis, 0, RTTHREADTYPE_IO, RTTHREADFLAGS_WAITABLE, + "ATSCLWORK"); + if (RT_FAILURE(rc)) + { + LogRel(("Creating the client worker thread failed with %Rrc\n", rc)); + return rc; + } + + rc = pThis->pTransport->pfnStart(pThis->pTransportInst); + if (RT_SUCCESS(rc)) + { + /* Spin off the connection thread. */ + rc = RTThreadCreate(&pThis->hThreadMain, atsMainThread, pThis, 0, RTTHREADTYPE_DEFAULT, RTTHREADFLAGS_WAITABLE, + "ATSMAIN"); + if (RT_SUCCESS(rc)) + { + rc = RTThreadUserWait(pThis->hThreadMain, RT_MS_30SEC); + if (RT_SUCCESS(rc)) + pThis->fStarted = true; + } + } + + LogRelFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Stops (shuts down) a formerly started ATS instance. + * + * @returns VBox status code. + * @param pThis The ATS instance. + */ +int AudioTestSvcStop(PATSSERVER pThis) +{ + if (!pThis->fStarted) + return VINF_SUCCESS; + + LogRelFlowFuncEnter(); + + ASMAtomicXchgBool(&pThis->fTerminate, true); + + if (pThis->pTransport) + pThis->pTransport->pfnStop(pThis->pTransportInst); + + size_t cbWritten; + int rc = RTPipeWrite(pThis->hPipeW, "", 1, &cbWritten); + AssertRCReturn(rc, rc); + + /* First close serving thread. */ + int rcThread; + rc = RTThreadWait(pThis->hThreadServing, RT_MS_30SEC, &rcThread); + if (RT_SUCCESS(rc)) + { + rc = rcThread; + if (RT_SUCCESS(rc)) + { + /* Close the main thread last. */ + rc = RTThreadWait(pThis->hThreadMain, RT_MS_30SEC, &rcThread); + if (RT_SUCCESS(rc)) + rc = rcThread; + + if (rc == VERR_TCP_SERVER_DESTROYED) + rc = VINF_SUCCESS; + } + } + + if (RT_SUCCESS(rc)) + pThis->fStarted = false; + + LogRelFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Destroys an ATS instance, internal version. + * + * @returns VBox status code. + * @param pThis ATS instance to destroy. + */ +static int audioTestSvcDestroyInternal(PATSSERVER pThis) +{ + int rc = VINF_SUCCESS; + + if (pThis->hPipeR != NIL_RTPIPE) + { + rc = RTPipeClose(pThis->hPipeR); + AssertRCReturn(rc, rc); + pThis->hPipeR = NIL_RTPIPE; + } + + if (pThis->hPipeW != NIL_RTPIPE) + { + rc = RTPipeClose(pThis->hPipeW); + AssertRCReturn(rc, rc); + pThis->hPipeW = NIL_RTPIPE; + } + + RTPollSetDestroy(pThis->hPollSet); + pThis->hPollSet = NIL_RTPOLLSET; + + PATSCLIENTINST pIt, pItNext; + RTListForEachSafe(&pThis->LstClientsNew, pIt, pItNext, ATSCLIENTINST, NdLst) + { + RTListNodeRemove(&pIt->NdLst); + atsClientDisconnect(pThis, pIt); + atsClientFree(pIt); + } + + if (RTCritSectIsInitialized(&pThis->CritSectClients)) + { + rc = RTCritSectDelete(&pThis->CritSectClients); + AssertRCReturn(rc, rc); + } + + return rc; +} + +/** + * Destroys an ATS instance. + * + * @returns VBox status code. + * @param pThis ATS instance to destroy. + */ +int AudioTestSvcDestroy(PATSSERVER pThis) +{ + LogRelFlowFuncEnter(); + + int rc = audioTestSvcDestroyInternal(pThis); + if (RT_SUCCESS(rc)) + { + if (pThis->pTransport) + { + if ( pThis->pTransport->pfnDestroy + && pThis->pTransportInst) + { + pThis->pTransport->pfnDestroy(pThis->pTransportInst); + pThis->pTransportInst = NULL; + } + } + } + + LogRelFlowFuncLeaveRC(rc); + return rc; +} diff --git a/src/VBox/Devices/Audio/AudioTestService.h b/src/VBox/Devices/Audio/AudioTestService.h new file mode 100644 index 00000000..3fa13a8b --- /dev/null +++ b/src/VBox/Devices/Audio/AudioTestService.h @@ -0,0 +1,217 @@ +/* $Id: AudioTestService.h $ */ +/** @file + * AudioTestService - Audio test execution server, Public Header. + */ + +/* + * Copyright (C) 2021-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 + */ + +#ifndef VBOX_INCLUDED_SRC_Audio_AudioTestService_h +#define VBOX_INCLUDED_SRC_Audio_AudioTestService_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include <iprt/tcp.h> + +#include "AudioTestServiceInternal.h" + +extern const PCATSTRANSPORT g_apTransports[]; +extern const size_t g_cTransports; + +/** Default TCP/IP bind port the guest ATS (Audio Test Service) is listening on. */ +#define ATS_TCP_DEF_BIND_PORT_GUEST 6042 +/** Default TCP/IP bind port the host ATS is listening on. */ +#define ATS_TCP_DEF_BIND_PORT_HOST 6052 +/** Default TCP/IP ATS bind port the ValidationKit Audio Driver ATS is listening on. */ +#define ATS_TCP_DEF_BIND_PORT_VALKIT 6062 +/** Default TCP/IP port the guest ATS is connecting to. */ +#define ATS_TCP_DEF_CONNECT_PORT_GUEST ATS_TCP_DEF_BIND_PORT_HOST +/** Default TCP/IP port the host ATS is connecting to the guest (needs NAT port forwarding). */ +#define ATS_TCP_DEF_CONNECT_PORT_HOST_PORT_FWD 6072 +/** Default TCP/IP port the host ATS is connecting to. */ +#define ATS_TCP_DEF_CONNECT_PORT_VALKIT ATS_TCP_DEF_BIND_PORT_VALKIT +/** Default TCP/IP address the host is connecting to. */ +#define ATS_TCP_DEF_CONNECT_HOST_ADDR_STR "127.0.0.1" +/** Default TCP/IP address the guest ATS connects to when + * running in client mode (reversed mode, needed for NATed VMs). */ +#define ATS_TCP_DEF_CONNECT_GUEST_STR "10.0.2.2" + +/** + * Structure for keeping an Audio Test Service (ATS) callback table. + */ +typedef struct ATSCALLBACKS +{ + /** + * Tells the implementation that a new client connected. Optional. + * + * @param pvUser User-supplied pointer to context data. Optional. + */ + DECLR3CALLBACKMEMBER(int, pfnHowdy, (void const *pvUser)); + + /** + * Tells the implementation that a client disconnected. Optional. + * + * @param pvUser User-supplied pointer to context data. Optional. + */ + DECLR3CALLBACKMEMBER(int, pfnBye, (void const *pvUser)); + + /** + * Begins a test set. Optional. + * + * @returns VBox status code. + * @param pvUser User-supplied pointer to context data. Optional. + * @param pszTag Tag of test set to begin. + */ + DECLR3CALLBACKMEMBER(int, pfnTestSetBegin, (void const *pvUser, const char *pszTag)); + + /** + * Ends the current test set. Optional. + * + * @returns VBox status code. + * @param pvUser User-supplied pointer to context data. Optional. + * @param pszTag Tag of test set to end. + */ + DECLR3CALLBACKMEMBER(int, pfnTestSetEnd, (void const *pvUser, const char *pszTag)); + + /** + * Marks the begin of sending a test set. Optional. + * + * @returns VBox status code. + * @param pvUser User-supplied pointer to context data. Optional. + * @param pszTag Tag of test set to begin sending. + */ + DECLR3CALLBACKMEMBER(int, pfnTestSetSendBegin, (void const *pvUser, const char *pszTag)); + + /** + * Reads data from a test set for sending it. + * + * @returns VBox status code. + * @param pvUser User-supplied pointer to context data. Optional. + * @param pszTag Tag of test set to begin sending. + * @param pvBuf Where to store the read test set data. + * @param cbBuf Size of \a pvBuf (in bytes). + * @param pcbRead Where to return the amount of read data in bytes. Optional and can be NULL. + */ + DECLR3CALLBACKMEMBER(int, pfnTestSetSendRead, (void const *pvUser, const char *pszTag, void *pvBuf, size_t cbBuf, size_t *pcbRead)); + + /** + * Marks the end of sending a test set. Optional. + * + * @returns VBox status code. + * @param pvUser User-supplied pointer to context data. Optional. + * @param pszTag Tag of test set to end sending. + */ + DECLR3CALLBACKMEMBER(int, pfnTestSetSendEnd, (void const *pvUser, const char *pszTag)); + + /** + * Plays a test tone. + * + * @returns VBox status code. + * @param pvUser User-supplied pointer to context data. Optional. + * @param pToneParms Tone parameters to use for playback. + */ + DECLR3CALLBACKMEMBER(int, pfnTonePlay, (void const *pvUser, PAUDIOTESTTONEPARMS pToneParms)); + + /** + * Records a test tone. + * + * @returns VBox status code. + * @param pvUser User-supplied pointer to context data. Optional. + * @param pToneParms Tone parameters to use for recording. + */ + DECLR3CALLBACKMEMBER(int, pfnToneRecord, (void const *pvUser, PAUDIOTESTTONEPARMS pToneParms)); + + /** Pointer to opaque user-provided context data. */ + void const *pvUser; +} ATSCALLBACKS; +/** Pointer to a const ATS callbacks table. */ +typedef const struct ATSCALLBACKS *PCATSCALLBACKS; + +/** + * Structure for keeping an Audio Test Service (ATS) instance. + */ +typedef struct ATSSERVER +{ + /** Pointer to the selected transport layer. */ + PCATSTRANSPORT pTransport; + /** Pointer to the transport instance. */ + PATSTRANSPORTINST pTransportInst; + /** The callbacks table. */ + ATSCALLBACKS Callbacks; + /** Whether server is in started state or not. */ + bool volatile fStarted; + /** Whether to terminate or not. */ + bool volatile fTerminate; + /** The main thread's poll set to handle new clients. */ + RTPOLLSET hPollSet; + /** Pipe for communicating with the serving thread about new clients. - read end */ + RTPIPE hPipeR; + /** Pipe for communicating with the serving thread about new clients. - write end */ + RTPIPE hPipeW; + /** Main thread waiting for connections. */ + RTTHREAD hThreadMain; + /** Thread serving connected clients. */ + RTTHREAD hThreadServing; + /** Critical section protecting the list of new clients. */ + RTCRITSECT CritSectClients; + /** List of new clients waiting to be picked up by the client worker thread. */ + RTLISTANCHOR LstClientsNew; +} ATSSERVER; +/** Pointer to an Audio Test Service (ATS) instance. */ +typedef ATSSERVER *PATSSERVER; + +int AudioTestSvcInit(PATSSERVER pThis, PCATSCALLBACKS pCallbacks); +int AudioTestSvcDestroy(PATSSERVER pThis); +int AudioTestSvcHandleOption(PATSSERVER pThis, int ch, PCRTGETOPTUNION pVal); +int AudioTestSvcStart(PATSSERVER pThis); +int AudioTestSvcStop(PATSSERVER pThis); + +/** + * Enumeration for the server connection mode. + * Only applies to certain transport implementation like TCP/IP. + */ +typedef enum ATSCONNMODE +{ + /** Both: Uses parallel client and server connection methods (via threads). */ + ATSCONNMODE_BOTH = 0, + /** Client only: Connects to a server. */ + ATSCONNMODE_CLIENT, + /** Server only: Listens for new incoming client connections. */ + ATSCONNMODE_SERVER, + /** 32bit hack. */ + ATSCONNMODE_32BIT_HACK = 0x7fffffff +} ATSCONNMODE; + +/** TCP/IP options for the ATS server. + * @todo Make this more abstract later. */ +enum ATSTCPOPT +{ + ATSTCPOPT_CONN_MODE = 5000, + ATSTCPOPT_BIND_ADDRESS, + ATSTCPOPT_BIND_PORT, + ATSTCPOPT_CONNECT_ADDRESS, + ATSTCPOPT_CONNECT_PORT +}; + +#endif /* !VBOX_INCLUDED_SRC_Audio_AudioTestService_h */ + diff --git a/src/VBox/Devices/Audio/AudioTestServiceClient.cpp b/src/VBox/Devices/Audio/AudioTestServiceClient.cpp new file mode 100644 index 00000000..b759a3ff --- /dev/null +++ b/src/VBox/Devices/Audio/AudioTestServiceClient.cpp @@ -0,0 +1,610 @@ +/* $Id: AudioTestServiceClient.cpp $ */ +/** @file + * AudioTestServiceClient - Audio Test Service (ATS), Client helpers. + * + * Note: Only does TCP/IP as transport layer for now. + */ + +/* + * Copyright (C) 2021-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 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_AUDIO_TEST + +#include <iprt/crc.h> +#include <iprt/err.h> +#include <iprt/file.h> +#include <iprt/mem.h> +#include <iprt/string.h> +#include <iprt/tcp.h> + +#include <VBox/log.h> + +#include "AudioTestService.h" +#include "AudioTestServiceInternal.h" +#include "AudioTestServiceClient.h" + +/** @todo Use common defines between server protocol and this client. */ + +/** + * A generic ATS reply, used by the client + * to process the incoming packets. + */ +typedef struct ATSSRVREPLY +{ + char szOp[ATSPKT_OPCODE_MAX_LEN]; + /** Pointer to payload data. + * This does *not* include the header! */ + void *pvPayload; + /** Size (in bytes) of the payload data. + * This does *not* include the header! */ + size_t cbPayload; +} ATSSRVREPLY; +/** Pointer to a generic ATS reply. */ +typedef struct ATSSRVREPLY *PATSSRVREPLY; + + +/********************************************************************************************************************************* +* Prototypes * +*********************************************************************************************************************************/ +static int audioTestSvcClientDisconnectInternal(PATSCLIENT pClient); + +/** + * Initializes an ATS client, internal version. + * + * @param pClient Client to initialize. + */ +static void audioTestSvcClientInit(PATSCLIENT pClient) +{ + RT_BZERO(pClient, sizeof(ATSCLIENT)); +} + +/** + * Destroys an ATS server reply. + * + * @param pReply Reply to destroy. + */ +static void audioTestSvcClientReplyDestroy(PATSSRVREPLY pReply) +{ + if (!pReply) + return; + + if (pReply->pvPayload) + { + Assert(pReply->cbPayload); + RTMemFree(pReply->pvPayload); + pReply->pvPayload = NULL; + } + + pReply->cbPayload = 0; +} + +/** + * Receives a reply from an ATS server. + * + * @returns VBox status code. + * @param pClient Client to receive reply for. + * @param pReply Where to store the reply. + * The reply must be destroyed with audioTestSvcClientReplyDestroy() then. + * @param fNoDataOk If it's okay that the reply is not expected to have any payload. + */ +static int audioTestSvcClientRecvReply(PATSCLIENT pClient, PATSSRVREPLY pReply, bool fNoDataOk) +{ + LogFlowFuncEnter(); + + PATSPKTHDR pPktHdr; + int rc = pClient->pTransport->pfnRecvPkt(pClient->pTransportInst, pClient->pTransportClient, &pPktHdr); + if (RT_SUCCESS(rc)) + { + AssertReleaseMsgReturn(pPktHdr->cb >= sizeof(ATSPKTHDR), + ("audioTestSvcClientRecvReply: Received invalid packet size (%RU32)\n", pPktHdr->cb), + VERR_NET_PROTOCOL_ERROR); + pReply->cbPayload = pPktHdr->cb - sizeof(ATSPKTHDR); + Log3Func(("szOp=%.8s, cb=%RU32\n", pPktHdr->achOpcode, pPktHdr->cb)); + if (pReply->cbPayload) + { + pReply->pvPayload = RTMemDup((uint8_t *)pPktHdr + sizeof(ATSPKTHDR), pReply->cbPayload); + } + else + pReply->pvPayload = NULL; + + if ( !pReply->cbPayload + && !fNoDataOk) + { + LogRelFunc(("Payload is empty (%zu), but caller expected data\n", pReply->cbPayload)); + rc = VERR_NET_PROTOCOL_ERROR; + } + else + { + memcpy(&pReply->szOp, &pPktHdr->achOpcode, sizeof(pReply->szOp)); + } + + RTMemFree(pPktHdr); + pPktHdr = NULL; + } + + if (RT_FAILURE(rc)) + LogRelFunc(("Receiving reply from server failed with %Rrc\n", rc)); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Receives a reply for an ATS server and checks if it is an acknowledge (success) one. + * + * @returns VBox status code. + * @retval VERR_NET_PROTOCOL_ERROR if the reply indicates a failure. + * @param pClient Client to receive reply for. + */ +static int audioTestSvcClientRecvAck(PATSCLIENT pClient) +{ + ATSSRVREPLY Reply; + RT_ZERO(Reply); + + int rc = audioTestSvcClientRecvReply(pClient, &Reply, true /* fNoDataOk */); + if (RT_SUCCESS(rc)) + { + /* Most likely cases first. */ + if ( RTStrNCmp(Reply.szOp, "ACK ", ATSPKT_OPCODE_MAX_LEN) == 0) + { + /* Nothing to do here. */ + } + else if (RTStrNCmp(Reply.szOp, "FAILED ", ATSPKT_OPCODE_MAX_LEN) == 0) + { + LogRelFunc(("Received error from server (cbPayload=%zu)\n", Reply.cbPayload)); + + if (Reply.cbPayload) + { + if ( Reply.cbPayload >= sizeof(int) /* At least the rc must be present. */ + && Reply.cbPayload <= sizeof(ATSPKTREPFAIL) - sizeof(ATSPKTHDR)) + { + rc = *(int *)Reply.pvPayload; /* Reach error code back to caller. */ + + const char *pcszMsg = (char *)Reply.pvPayload + sizeof(int); + /** @todo Check NULL termination of pcszMsg? */ + + LogRelFunc(("Error message: %s (%Rrc)\n", pcszMsg, rc)); + } + else + { + LogRelFunc(("Received invalid failure payload (cb=%zu)\n", Reply.cbPayload)); + rc = VERR_NET_PROTOCOL_ERROR; + } + } + } + else + { + LogRelFunc(("Received invalid opcode ('%.8s')\n", Reply.szOp)); + rc = VERR_NET_PROTOCOL_ERROR; + } + + audioTestSvcClientReplyDestroy(&Reply); + } + + LogRelFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Sends a message plus optional payload to an ATS server. + * + * @returns VBox status code. + * @param pClient Client to send message for. + * @param pvHdr Pointer to header data to send. + * @param cbHdr Size (in bytes) of \a pvHdr to send. + */ +static int audioTestSvcClientSendMsg(PATSCLIENT pClient, void *pvHdr, size_t cbHdr) +{ + RT_NOREF(cbHdr); + AssertPtrReturn(pClient->pTransport, VERR_INVALID_POINTER); + AssertPtrReturn(pClient->pTransportInst, VERR_INVALID_POINTER); + AssertPtrReturn(pClient->pTransportClient, VERR_INVALID_POINTER); + return pClient->pTransport->pfnSendPkt(pClient->pTransportInst, pClient->pTransportClient, (PCATSPKTHDR)pvHdr); +} + +/** + * Initializes a client request header. + * + * @returns VBox status code. + * @param pReqHdr Request header to initialize. + * @param cbReq Size (in bytes) the request will have (does *not* include payload). + * @param pszOp Operation to perform with the request. + * @param cbPayload Size (in bytes) of payload that will follow the header. Optional and can be 0. + */ +DECLINLINE (void) audioTestSvcClientReqHdrInit(PATSPKTHDR pReqHdr, size_t cbReq, const char *pszOp, size_t cbPayload) +{ + AssertReturnVoid(strlen(pszOp) >= 2); + AssertReturnVoid(strlen(pszOp) <= ATSPKT_OPCODE_MAX_LEN); + + /** @todo Validate opcode. */ + + RT_BZERO(pReqHdr, sizeof(ATSPKTHDR)); + + memcpy(pReqHdr->achOpcode, pszOp, strlen(pszOp)); + pReqHdr->uCrc32 = 0; /** @todo Do CRC-32 calculation. */ + pReqHdr->cb = (uint32_t)cbReq + (uint32_t)cbPayload; + + Assert(pReqHdr->cb <= ATSPKT_MAX_SIZE); +} + +/** + * Sends an acknowledege response back to the server. + * + * @returns VBox status code. + * @param pClient Client to send command for. + */ +static int audioTestSvcClientSendAck(PATSCLIENT pClient) +{ + ATSPKTHDR Req; + audioTestSvcClientReqHdrInit(&Req, sizeof(Req), "ACK ", 0); + + return audioTestSvcClientSendMsg(pClient, &Req, sizeof(Req)); +} + +/** + * Sends a greeting command (handshake) to an ATS server. + * + * @returns VBox status code. + * @param pClient Client to send command for. + */ +static int audioTestSvcClientDoGreet(PATSCLIENT pClient) +{ + ATSPKTREQHOWDY Req; + Req.uVersion = ATS_PROTOCOL_VS; + audioTestSvcClientReqHdrInit(&Req.Hdr, sizeof(Req), ATSPKT_OPCODE_HOWDY, 0); + int rc = audioTestSvcClientSendMsg(pClient, &Req, sizeof(Req)); + if (RT_SUCCESS(rc)) + rc = audioTestSvcClientRecvAck(pClient); + return rc; +} + +/** + * Tells the ATS server that we want to disconnect. + * + * @returns VBox status code. + * @param pClient Client to disconnect. + */ +static int audioTestSvcClientDoBye(PATSCLIENT pClient) +{ + ATSPKTHDR Req; + audioTestSvcClientReqHdrInit(&Req, sizeof(Req), ATSPKT_OPCODE_BYE, 0); + int rc = audioTestSvcClientSendMsg(pClient, &Req, sizeof(Req)); + if (RT_SUCCESS(rc)) + rc = audioTestSvcClientRecvAck(pClient); + return rc; +} + +/** + * Creates an ATS client. + * + * @returns VBox status code. + * @param pClient Client to create. + */ +int AudioTestSvcClientCreate(PATSCLIENT pClient) +{ + audioTestSvcClientInit(pClient); + + /* + * The default transporter is the first one. + */ + pClient->pTransport = g_apTransports[0]; /** @todo Make this dynamic. */ + + return pClient->pTransport->pfnCreate(&pClient->pTransportInst); +} + +/** + * Destroys an ATS client. + * + * @returns VBox status code. + * @param pClient Client to destroy. + */ +void AudioTestSvcClientDestroy(PATSCLIENT pClient) +{ + if (!pClient) + return; + + /* ignore rc */ audioTestSvcClientDisconnectInternal(pClient); + + if (pClient->pTransport) + { + pClient->pTransport->pfnDestroy(pClient->pTransportInst); + pClient->pTransportInst = NULL; /* Invalidate pointer. */ + } +} + +/** + * Handles a command line option. + * + * @returns VBox status code. + * @param pClient Client to handle option for. + * @param ch Option (short) to handle. + * @param pVal Option union to store the result in on success. + */ +int AudioTestSvcClientHandleOption(PATSCLIENT pClient, int ch, PCRTGETOPTUNION pVal) +{ + AssertPtrReturn(pClient->pTransport, VERR_WRONG_ORDER); /* Must be created first via AudioTestSvcClientCreate(). */ + if (!pClient->pTransport->pfnOption) + return VERR_GETOPT_UNKNOWN_OPTION; + return pClient->pTransport->pfnOption(pClient->pTransportInst, ch, pVal); +} + +/** + * Connects to an ATS peer, extended version. + * + * @returns VBox status code. + * @param pClient Client to connect. + * @param msTimeout Timeout (in ms) waiting for a connection to be established. + * Use RT_INDEFINITE_WAIT to wait indefinitely. + */ +int AudioTestSvcClientConnectEx(PATSCLIENT pClient, RTMSINTERVAL msTimeout) +{ + if (pClient->pTransportClient) + return VERR_NET_ALREADY_CONNECTED; + + int rc = pClient->pTransport->pfnStart(pClient->pTransportInst); + if (RT_SUCCESS(rc)) + { + rc = pClient->pTransport->pfnWaitForConnect(pClient->pTransportInst, + msTimeout, NULL /* pfFromServer */, &pClient->pTransportClient); + if (RT_SUCCESS(rc)) + { + rc = audioTestSvcClientDoGreet(pClient); + } + } + + if (RT_FAILURE(rc)) + LogRelFunc(("Connecting to server (%RU32ms timeout) failed with %Rrc\n", msTimeout, rc)); + + return rc; +} + +/** + * Connects to an ATS peer. + * + * @returns VBox status code. + * @param pClient Client to connect. + */ +int AudioTestSvcClientConnect(PATSCLIENT pClient) +{ + return AudioTestSvcClientConnectEx(pClient, 30 * 1000 /* msTimeout */); +} + +/** + * Tells the server to begin a new test set. + * + * @returns VBox status code. + * @param pClient Client to issue command for. + * @param pszTag Tag to use for the test set to begin. + */ +int AudioTestSvcClientTestSetBegin(PATSCLIENT pClient, const char *pszTag) +{ + ATSPKTREQTSETBEG Req; + + int rc = RTStrCopy(Req.szTag, sizeof(Req.szTag), pszTag); + AssertRCReturn(rc, rc); + + audioTestSvcClientReqHdrInit(&Req.Hdr, sizeof(Req), ATSPKT_OPCODE_TESTSET_BEGIN, 0); + + rc = audioTestSvcClientSendMsg(pClient, &Req, sizeof(Req)); + if (RT_SUCCESS(rc)) + rc = audioTestSvcClientRecvAck(pClient); + + return rc; +} + +/** + * Tells the server to end a runing test set. + * + * @returns VBox status code. + * @param pClient Client to issue command for. + * @param pszTag Tag of test set to end. + */ +int AudioTestSvcClientTestSetEnd(PATSCLIENT pClient, const char *pszTag) +{ + ATSPKTREQTSETEND Req; + + int rc = RTStrCopy(Req.szTag, sizeof(Req.szTag), pszTag); + AssertRCReturn(rc, rc); + + audioTestSvcClientReqHdrInit(&Req.Hdr, sizeof(Req), ATSPKT_OPCODE_TESTSET_END, 0); + + rc = audioTestSvcClientSendMsg(pClient, &Req, sizeof(Req)); + if (RT_SUCCESS(rc)) + rc = audioTestSvcClientRecvAck(pClient); + + return rc; +} + +/** + * Tells the server to play a (test) tone. + * + * @returns VBox status code. + * @param pClient Client to issue command for. + * @param pToneParms Tone parameters to use. + * @note How (and if) the server plays a tone depends on the actual implementation side. + */ +int AudioTestSvcClientTonePlay(PATSCLIENT pClient, PAUDIOTESTTONEPARMS pToneParms) +{ + ATSPKTREQTONEPLAY Req; + + memcpy(&Req.ToneParms, pToneParms, sizeof(AUDIOTESTTONEPARMS)); + + audioTestSvcClientReqHdrInit(&Req.Hdr, sizeof(Req), ATSPKT_OPCODE_TONE_PLAY, 0); + + int rc = audioTestSvcClientSendMsg(pClient, &Req, sizeof(Req)); + if (RT_SUCCESS(rc)) + rc = audioTestSvcClientRecvAck(pClient); + + return rc; +} + +/** + * Tells the server to record a (test) tone. + * + * @returns VBox status code. + * @param pClient Client to issue command for. + * @param pToneParms Tone parameters to use. + * @note How (and if) the server plays a tone depends on the actual implementation side. + */ +int AudioTestSvcClientToneRecord(PATSCLIENT pClient, PAUDIOTESTTONEPARMS pToneParms) +{ + ATSPKTREQTONEREC Req; + + memcpy(&Req.ToneParms, pToneParms, sizeof(AUDIOTESTTONEPARMS)); + + audioTestSvcClientReqHdrInit(&Req.Hdr, sizeof(Req), ATSPKT_OPCODE_TONE_RECORD, 0); + + int rc = audioTestSvcClientSendMsg(pClient, &Req, sizeof(Req)); + if (RT_SUCCESS(rc)) + rc = audioTestSvcClientRecvAck(pClient); + + return rc; +} + +/** + * Tells the server to send (download) a (packed up) test set archive. + * The test set must not be running / open anymore. + * + * @returns VBox status code. + * @param pClient Client to issue command for. + * @param pszTag Tag of test set to send. + * @param pszPathOutAbs Absolute path where to store the downloaded test set archive. + */ +int AudioTestSvcClientTestSetDownload(PATSCLIENT pClient, const char *pszTag, const char *pszPathOutAbs) +{ + ATSPKTREQTSETSND Req; + + int rc = RTStrCopy(Req.szTag, sizeof(Req.szTag), pszTag); + AssertRCReturn(rc, rc); + + audioTestSvcClientReqHdrInit(&Req.Hdr, sizeof(Req), ATSPKT_OPCODE_TESTSET_SEND, 0); + + RTFILE hFile; + rc = RTFileOpen(&hFile, pszPathOutAbs, RTFILE_O_WRITE | RTFILE_O_CREATE | RTFILE_O_DENY_WRITE); + AssertRCReturn(rc, rc); + + rc = audioTestSvcClientSendMsg(pClient, &Req, sizeof(Req)); + while (RT_SUCCESS(rc)) + { + ATSSRVREPLY Reply; + RT_ZERO(Reply); + + rc = audioTestSvcClientRecvReply(pClient, &Reply, false /* fNoDataOk */); + if (RT_SUCCESS(rc)) + { + /* Extract received CRC32 checksum. */ + const size_t cbCrc32 = sizeof(uint32_t); /* Skip CRC32 in payload for actual CRC verification. */ + + uint32_t uSrcCrc32; + memcpy(&uSrcCrc32, Reply.pvPayload, cbCrc32); + + if (uSrcCrc32) + { + const uint32_t uDstCrc32 = RTCrc32((uint8_t *)Reply.pvPayload + cbCrc32, Reply.cbPayload - cbCrc32); + + Log2Func(("uSrcCrc32=%#x, cbRead=%zu -> uDstCrc32=%#x\n" + "%.*Rhxd\n", + uSrcCrc32, Reply.cbPayload - cbCrc32, uDstCrc32, + RT_MIN(64, Reply.cbPayload - cbCrc32), (uint8_t *)Reply.pvPayload + cbCrc32)); + + if (uSrcCrc32 != uDstCrc32) + rc = VERR_TAR_CHKSUM_MISMATCH; /** @todo Fudge! */ + } + + if (RT_SUCCESS(rc)) + { + if ( RTStrNCmp(Reply.szOp, "DATA ", ATSPKT_OPCODE_MAX_LEN) == 0 + && Reply.pvPayload + && Reply.cbPayload) + { + rc = RTFileWrite(hFile, (uint8_t *)Reply.pvPayload + cbCrc32, Reply.cbPayload - cbCrc32, NULL); + } + else if (RTStrNCmp(Reply.szOp, "DATA EOF", ATSPKT_OPCODE_MAX_LEN) == 0) + { + rc = VINF_EOF; + } + else + { + AssertMsgFailed(("Got unexpected reply '%s'", Reply.szOp)); + rc = VERR_NOT_SUPPORTED; + } + } + } + + audioTestSvcClientReplyDestroy(&Reply); + + int rc2 = audioTestSvcClientSendAck(pClient); + if (rc == VINF_SUCCESS) /* Might be VINF_EOF already. */ + rc = rc2; + + if (rc == VINF_EOF) + break; + } + + int rc2 = RTFileClose(hFile); + if (RT_SUCCESS(rc)) + rc = rc2; + + return rc; +} + +/** + * Disconnects from an ATS server, internal version. + * + * @returns VBox status code. + * @param pClient Client to disconnect. + */ +static int audioTestSvcClientDisconnectInternal(PATSCLIENT pClient) +{ + if (!pClient->pTransportClient) /* Not connected (yet)? Bail out early. */ + return VINF_SUCCESS; + + int rc = audioTestSvcClientDoBye(pClient); + if (RT_SUCCESS(rc)) + { + if (pClient->pTransport->pfnNotifyBye) + pClient->pTransport->pfnNotifyBye(pClient->pTransportInst, pClient->pTransportClient); + + pClient->pTransport->pfnDisconnect(pClient->pTransportInst, pClient->pTransportClient); + pClient->pTransportClient = NULL; + + pClient->pTransport->pfnStop(pClient->pTransportInst); + } + + return rc; +} + +/** + * Disconnects from an ATS server. + * + * @returns VBox status code. + * @param pClient Client to disconnect. + */ +int AudioTestSvcClientDisconnect(PATSCLIENT pClient) +{ + return audioTestSvcClientDisconnectInternal(pClient); +} + diff --git a/src/VBox/Devices/Audio/AudioTestServiceClient.h b/src/VBox/Devices/Audio/AudioTestServiceClient.h new file mode 100644 index 00000000..2865f363 --- /dev/null +++ b/src/VBox/Devices/Audio/AudioTestServiceClient.h @@ -0,0 +1,83 @@ +/* $Id: AudioTestServiceClient.h $ */ +/** @file + * AudioTestServiceClient - Audio test execution server, Public Header. + */ + +/* + * Copyright (C) 2021-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 + */ + +#ifndef VBOX_INCLUDED_SRC_Audio_AudioTestServiceClient_h +#define VBOX_INCLUDED_SRC_Audio_AudioTestServiceClient_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include "AudioTestServiceInternal.h" + +/** + * Structure for maintaining an ATS client. + */ +typedef struct ATSCLIENT +{ + /** Pointer to the selected transport layer. */ + PCATSTRANSPORT pTransport; + /** Pointer to the selected transport instance to use. */ + PATSTRANSPORTINST pTransportInst; + /** The opaque client instance. */ + PATSTRANSPORTCLIENT pTransportClient; +} ATSCLIENT; +/** Pointer to an ATS client. */ +typedef ATSCLIENT *PATSCLIENT; + +/** @name Creation / destruction. + * @{ */ +int AudioTestSvcClientCreate(PATSCLIENT pClient); +void AudioTestSvcClientDestroy(PATSCLIENT pClient); +/** @} */ + +/** @name Connection handling. + * @{ */ +int AudioTestSvcClientConnectEx(PATSCLIENT pClient, RTMSINTERVAL msTimeout); +int AudioTestSvcClientConnect(PATSCLIENT pClient); +int AudioTestSvcClientDisconnect(PATSCLIENT pClient); +/** @} */ + +/** @name Option handling. + * @{ */ +int AudioTestSvcClientHandleOption(PATSCLIENT pClient, int ch, PCRTGETOPTUNION pVal); +/** @} */ + +/** @name Test set handling. + * @{ */ +int AudioTestSvcClientTestSetBegin(PATSCLIENT pClient, const char *pszTag); +int AudioTestSvcClientTestSetEnd(PATSCLIENT pClient, const char *pszTag); +int AudioTestSvcClientTestSetDownload(PATSCLIENT pClient, const char *pszTag, const char *pszPathOutAbs); +/** @} */ + +/** @name Tone handling. + * @{ */ +int AudioTestSvcClientTonePlay(PATSCLIENT pClient, PAUDIOTESTTONEPARMS pToneParms); +int AudioTestSvcClientToneRecord(PATSCLIENT pClient, PAUDIOTESTTONEPARMS pToneParms); +/** @} */ + +#endif /* !VBOX_INCLUDED_SRC_Audio_AudioTestServiceClient_h */ + diff --git a/src/VBox/Devices/Audio/AudioTestServiceInternal.h b/src/VBox/Devices/Audio/AudioTestServiceInternal.h new file mode 100644 index 00000000..c8b3cd2d --- /dev/null +++ b/src/VBox/Devices/Audio/AudioTestServiceInternal.h @@ -0,0 +1,276 @@ +/* $Id: AudioTestServiceInternal.h $ */ +/** @file + * AudioTestService - Audio test execution server, Internal Header. + */ + +/* + * Copyright (C) 2021-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 + */ + +#ifndef VBOX_INCLUDED_SRC_Audio_AudioTestServiceInternal_h +#define VBOX_INCLUDED_SRC_Audio_AudioTestServiceInternal_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include <iprt/getopt.h> +#include <iprt/stream.h> + +#include "AudioTestServiceProtocol.h" + +RT_C_DECLS_BEGIN + +/** Opaque ATS transport layer specific client data. */ +typedef struct ATSTRANSPORTCLIENT *PATSTRANSPORTCLIENT; +typedef PATSTRANSPORTCLIENT *PPATSTRANSPORTCLIENT; + +/** Opaque ATS transport specific instance data. */ +typedef struct ATSTRANSPORTINST *PATSTRANSPORTINST; +typedef PATSTRANSPORTINST *PPATSTRANSPORTINST; + +/** + * Transport layer descriptor. + */ +typedef struct ATSTRANSPORT +{ + /** The name. */ + char szName[16]; + /** The description. */ + const char *pszDesc; + /** Pointer to an array of options. */ + PCRTGETOPTDEF paOpts; + /** The number of options in the array. */ + size_t cOpts; + + /** + * Print the usage information for this transport layer. + * + * @param pStream The stream to print the usage info to. + * + * @remarks This is only required if TXSTRANSPORT::cOpts is greater than 0. + */ + DECLR3CALLBACKMEMBER(void, pfnUsage,(PRTSTREAM pStream)); + + /** + * Creates a transport instance. + * + * @returns IPRT status code. On errors, the transport layer shall call + * RTMsgError to display the error details to the user. + * @param ppThis Where to return the created transport instance on success. + */ + DECLR3CALLBACKMEMBER(int, pfnCreate, (PPATSTRANSPORTINST ppThis)); + + /** + * Destroys a transport instance. + * + * On errors, the transport layer shall call RTMsgError to display the error + * details to the user. + * + * @returns IPRT status code. On errors, the transport layer shall call + * RTMsgError to display the error details to the user. + * @param pThis The transport instance. + * The pointer will be invalid on return. + */ + DECLR3CALLBACKMEMBER(int, pfnDestroy, (PATSTRANSPORTINST pThis)); + + /** + * Handle an option. + * + * When encountering an options that is not part of the base options, we'll call + * this method for each transport layer until one handles it. + * + * @retval VINF_SUCCESS if handled. + * @retval VERR_TRY_AGAIN if not handled. + * @retval VERR_INVALID_PARAMETER if we should exit with a non-zero status. + * + * @param pThis Transport instance to set options for. + * @param ch The short option value. + * @param pVal Pointer to the value union. + * + * @remarks This is only required if TXSTRANSPORT::cOpts is greater than 0. + */ + DECLR3CALLBACKMEMBER(int, pfnOption,(PATSTRANSPORTINST pThis, int ch, PCRTGETOPTUNION pVal)); + + /** + * Starts a transport instance. + * + * @returns IPRT status code. On errors, the transport layer shall call + * RTMsgError to display the error details to the user. + * @param pThis Transport instance to initialize. + */ + DECLR3CALLBACKMEMBER(int, pfnStart, (PATSTRANSPORTINST pThis)); + + /** + * Stops a transport instance, closing and freeing resources. + * + * On errors, the transport layer shall call RTMsgError to display the error + * details to the user. + * + * @param pThis The transport instance. + */ + DECLR3CALLBACKMEMBER(void, pfnStop, (PATSTRANSPORTINST pThis)); + + /** + * Waits for a new client to connect and returns the client specific data on + * success. + * + * @returns VBox status code. + * @param pThis The transport instance. + * @param msTimeout Timeout (in ms) waiting for a connection to be established. + * Use RT_INDEFINITE_WAIT to wait indefinitely. + * This might or might not be supported by the specific transport implementation. + * @param pfFromServer Returns \c true if the returned client is from a remote server (called a reverse connection), + * or \c false if not (regular client). Optional and can be NULL. + * @param ppClientNew Where to return the allocated client on success. + * Must be destroyed with pfnDisconnect() when done. + */ + DECLR3CALLBACKMEMBER(int, pfnWaitForConnect, (PATSTRANSPORTINST pThis, RTMSINTERVAL msTimeout, bool *pfFromServer, PPATSTRANSPORTCLIENT ppClientNew)); + + /** + * Disconnects a client and frees up its resources. + * + * @param pThis The transport instance. + * @param pClient Client to disconnect. + * The pointer will be invalid after calling. + */ + DECLR3CALLBACKMEMBER(void, pfnDisconnect, (PATSTRANSPORTINST pThis, PATSTRANSPORTCLIENT pClient)); + + /** + * Polls for incoming packets. + * + * @returns true if there are pending packets, false if there isn't. + * @param pThis The transport instance. + * @param pClient The client to poll for data. + */ + DECLR3CALLBACKMEMBER(bool, pfnPollIn, (PATSTRANSPORTINST pThis, PATSTRANSPORTCLIENT pClient)); + + /** + * Adds any pollable handles to the poll set. + * + * @returns IPRT status code. + * @param pThis The transport instance. + * @param hPollSet The poll set to add them to. + * @param pClient The transport client structure. + * @param idStart The handle ID to start at. + */ + DECLR3CALLBACKMEMBER(int, pfnPollSetAdd, (PATSTRANSPORTINST pThis, RTPOLLSET hPollSet, PATSTRANSPORTCLIENT pClient, uint32_t idStart)); + + /** + * Removes the given client frmo the given pollset. + * + * @returns IPRT status code. + * @param pThis The transport instance. + * @param hPollSet The poll set to remove from. + * @param pClient The transport client structure. + * @param idStart The handle ID to remove. + */ + DECLR3CALLBACKMEMBER(int, pfnPollSetRemove, (PATSTRANSPORTINST pThis, RTPOLLSET hPollSet, PATSTRANSPORTCLIENT pClient, uint32_t idStart)); + + /** + * Receives an incoming packet. + * + * This will block until the data becomes available or we're interrupted by a + * signal or something. + * + * @returns IPRT status code. On error conditions other than VERR_INTERRUPTED, + * the current operation will be aborted when applicable. When + * interrupted, the transport layer will store the data until the next + * receive call. + * + * @param pThis The transport instance. + * @param pClient The transport client structure. + * @param ppPktHdr Where to return the pointer to the packet we've + * read. This is allocated from the heap using + * RTMemAlloc (w/ ATSPKT_ALIGNMENT) and must be + * free by calling RTMemFree. + */ + DECLR3CALLBACKMEMBER(int, pfnRecvPkt, (PATSTRANSPORTINST pThis, PATSTRANSPORTCLIENT pClient, PPATSPKTHDR ppPktHdr)); + + /** + * Sends an outgoing packet. + * + * This will block until the data has been written. + * + * @returns IPRT status code. + * @retval VERR_INTERRUPTED if interrupted before anything was sent. + * + * @param pThis The transport instance. + * @param pClient The transport client structure. + * @param pPktHdr The packet to send. The size is given by + * aligning the size in the header by + * ATSPKT_ALIGNMENT. + */ + DECLR3CALLBACKMEMBER(int, pfnSendPkt, (PATSTRANSPORTINST pThis, PATSTRANSPORTCLIENT pClient, PCATSPKTHDR pPktHdr)); + + /** + * Sends a babble packet and disconnects the client (if applicable). + * + * @param pThis The transport instance. + * @param pClient The transport client structure. + * @param pPktHdr The packet to send. The size is given by + * aligning the size in the header by + * ATSPKT_ALIGNMENT. + * @param cMsSendTimeout The send timeout measured in milliseconds. + */ + DECLR3CALLBACKMEMBER(void, pfnBabble, (PATSTRANSPORTINST pThis, PATSTRANSPORTCLIENT pClient, PCATSPKTHDR pPktHdr, RTMSINTERVAL cMsSendTimeout)); + + /** + * Notification about a client HOWDY. + * + * @param pThis The transport instance. + * @param pClient The transport client structure. + */ + DECLR3CALLBACKMEMBER(void, pfnNotifyHowdy, (PATSTRANSPORTINST pThis, PATSTRANSPORTCLIENT pClient)); + + /** + * Notification about a client BYE. + * + * For connection oriented transport layers, it would be good to disconnect the + * client at this point. + * + * @param pThis The transport instance. + * @param pClient The transport client structure. + */ + DECLR3CALLBACKMEMBER(void, pfnNotifyBye, (PATSTRANSPORTINST pThis, PATSTRANSPORTCLIENT pClient)); + + /** + * Notification about a REBOOT or SHUTDOWN. + * + * For connection oriented transport layers, stop listening for and + * accepting at this point. + * + * @param pThis The transport instance. + */ + DECLR3CALLBACKMEMBER(void, pfnNotifyReboot, (PATSTRANSPORTINST pThis)); + + /** Non-zero end marker. */ + uint32_t u32EndMarker; +} ATSTRANSPORT; +/** Pointer to a const transport layer descriptor. */ +typedef const struct ATSTRANSPORT *PCATSTRANSPORT; + + +extern ATSTRANSPORT const g_TcpTransport; + +RT_C_DECLS_END + +#endif /* !VBOX_INCLUDED_SRC_Audio_AudioTestServiceInternal_h */ + diff --git a/src/VBox/Devices/Audio/AudioTestServiceProtocol.cpp b/src/VBox/Devices/Audio/AudioTestServiceProtocol.cpp new file mode 100644 index 00000000..97341f17 --- /dev/null +++ b/src/VBox/Devices/Audio/AudioTestServiceProtocol.cpp @@ -0,0 +1,37 @@ +/* $Id: AudioTestServiceProtocol.cpp $ */ +/** @file + * AudioTestService - Audio test execution server, Protocol helpers. + */ + +/* + * Copyright (C) 2021-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 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP RTLOGGROUP_DEFAULT +#include <iprt/asm.h> +#include <iprt/cdefs.h> + +#include "AudioTestServiceProtocol.h" + diff --git a/src/VBox/Devices/Audio/AudioTestServiceProtocol.h b/src/VBox/Devices/Audio/AudioTestServiceProtocol.h new file mode 100644 index 00000000..41fedcd8 --- /dev/null +++ b/src/VBox/Devices/Audio/AudioTestServiceProtocol.h @@ -0,0 +1,268 @@ +/* $Id: AudioTestServiceProtocol.h $ */ +/** @file + * AudioTestServiceProtocol - Audio test execution server, Protocol Header. + */ + +/* + * Copyright (C) 2021-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 + */ + +#ifndef VBOX_INCLUDED_SRC_Audio_AudioTestServiceProtocol_h +#define VBOX_INCLUDED_SRC_Audio_AudioTestServiceProtocol_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include <iprt/cdefs.h> +#include <iprt/list.h> + +#include <VBox/vmm/pdmaudioifs.h> + +#include "AudioTest.h" + +RT_C_DECLS_BEGIN + +/** Maximum length (in bytes) an opcode can have. */ +#define ATSPKT_OPCODE_MAX_LEN 8 +/** Packet alignment. */ +#define ATSPKT_ALIGNMENT 16 +/** Max packet size. */ +#define ATSPKT_MAX_SIZE _256K + +/** + * Common Packet header (for requests and replies). + */ +typedef struct ATSPKTHDR +{ + /** The unpadded packet length. This include this header. */ + uint32_t cb; + /** The CRC-32 for the packet starting from the opcode field. 0 if the packet + * hasn't been CRCed. */ + uint32_t uCrc32; + /** Packet opcode, an unterminated ASCII string. */ + uint8_t achOpcode[ATSPKT_OPCODE_MAX_LEN]; +} ATSPKTHDR; +AssertCompileSize(ATSPKTHDR, 16); +/** Pointer to a packet header. */ +typedef ATSPKTHDR *PATSPKTHDR; +/** Pointer to a packet header. */ +typedef ATSPKTHDR const *PCATSPKTHDR; +/** Pointer to a packet header pointer. */ +typedef PATSPKTHDR *PPATSPKTHDR; + +#define ATSPKT_OPCODE_HOWDY "HOWDY " + +/** 32bit protocol version consisting of a 16bit major and 16bit minor part. */ +#define ATS_PROTOCOL_VS (ATS_PROTOCOL_VS_MAJOR | ATS_PROTOCOL_VS_MINOR) +/** The major version part of the protocol version. */ +#define ATS_PROTOCOL_VS_MAJOR (1 << 16) +/** The minor version part of the protocol version. */ +#define ATS_PROTOCOL_VS_MINOR (0) + +/** + * The HOWDY request structure. + */ +typedef struct ATSPKTREQHOWDY +{ + /** Embedded packet header. */ + ATSPKTHDR Hdr; + /** Version of the protocol the client wants to use. */ + uint32_t uVersion; + /** Alignment. */ + uint8_t au8Padding[12]; +} ATSPKTREQHOWDY; +AssertCompileSizeAlignment(ATSPKTREQHOWDY, ATSPKT_ALIGNMENT); +/** Pointer to a HOWDY request structure. */ +typedef ATSPKTREQHOWDY *PATSPKTREQHOWDY; + +/** + * The HOWDY reply structure. + */ +typedef struct ATSPKTREPHOWDY +{ + /** Packet header. */ + ATSPKTHDR Hdr; + /** Version to use for the established connection. */ + uint32_t uVersion; + /** Padding - reserved. */ + uint8_t au8Padding[12]; +} ATSPKTREPHOWDY; +AssertCompileSizeAlignment(ATSPKTREPHOWDY, ATSPKT_ALIGNMENT); +/** Pointer to a HOWDY reply structure. */ +typedef ATSPKTREPHOWDY *PATSPKTREPHOWDY; + +#define ATSPKT_OPCODE_BYE "BYE " + +/* No additional structures for BYE. */ + +#define ATSPKT_OPCODE_TESTSET_BEGIN "TSET BEG" + +/** + * The TSET BEG (test set begin) request structure. + */ +typedef struct ATSPKTREQTSETBEG +{ + /** Embedded packet header. */ + ATSPKTHDR Hdr; + /** Audio test set tag to use. */ + char szTag[AUDIOTEST_TAG_MAX]; +} ATSPKTREQTSETBEG; +AssertCompileSizeAlignment(ATSPKTREQTSETBEG, ATSPKT_ALIGNMENT); +/** Pointer to a TSET BEG reply structure. */ +typedef ATSPKTREQTSETBEG *PATSPKTREQTSETBEG; + +#define ATSPKT_OPCODE_TESTSET_END "TSET END" + +/** + * The TSET END (test set end) request structure. + */ +typedef struct ATSPKTREQTSETEND +{ + /** Embedded packet header. */ + ATSPKTHDR Hdr; + /** Audio test set tag to use. */ + char szTag[AUDIOTEST_TAG_MAX]; +} ATSPKTREQTSETEND; +AssertCompileSizeAlignment(ATSPKTREQTSETEND, ATSPKT_ALIGNMENT); +/** Pointer to a TSET STA reply structure. */ +typedef ATSPKTREQTSETEND *PATSPKTREQTSETEND; + +#define ATSPKT_OPCODE_TESTSET_SEND "TSET SND" + +/** + * The TSET SND (test set send) request structure. + */ +typedef struct ATSPKTREQTSETSND +{ + /** Embedded packet header. */ + ATSPKTHDR Hdr; + /** Audio test set tag to use. */ + char szTag[AUDIOTEST_TAG_MAX]; +} ATSPKTREQTSETSND; +AssertCompileSizeAlignment(ATSPKTREQTSETSND, ATSPKT_ALIGNMENT); +/** Pointer to a ATSPKTREQTSETSND structure. */ +typedef ATSPKTREQTSETSND *PATSPKTREQTSETSND; + +#define ATSPKT_OPCODE_TONE_PLAY "TN PLY " + +/** + * The TN PLY (tone play) request structure. + */ +typedef struct ATSPKTREQTONEPLAY +{ + /** Embedded packet header. */ + ATSPKTHDR Hdr; + /** Test tone parameters for playback. */ + AUDIOTESTTONEPARMS ToneParms; +#if ARCH_BITS == 64 + uint8_t aPadding[8]; +#else +# ifdef RT_OS_WINDOWS + uint8_t aPadding[4]; +# else + uint8_t aPadding[12]; +# endif +#endif +} ATSPKTREQTONEPLAY; +AssertCompileSizeAlignment(ATSPKTREQTONEPLAY, ATSPKT_ALIGNMENT); +/** Pointer to a ATSPKTREQTONEPLAY structure. */ +typedef ATSPKTREQTONEPLAY *PATSPKTREQTONEPLAY; + +#define ATSPKT_OPCODE_TONE_RECORD "TN REC " + +/** + * The TN REC (tone record) request structure. + */ +typedef struct ATSPKTREQTONEREC +{ + /** Embedded packet header. */ + ATSPKTHDR Hdr; + /** Test tone parameters for playback. */ + AUDIOTESTTONEPARMS ToneParms; +#if ARCH_BITS == 64 + uint8_t aPadding[8]; +#else +# ifdef RT_OS_WINDOWS + uint8_t aPadding[4]; +# else + uint8_t aPadding[12]; +# endif +#endif +} ATSPKTREQTONEREC; +AssertCompileSizeAlignment(ATSPKTREQTONEREC, ATSPKT_ALIGNMENT); +/** Pointer to a ATSPKTREQTONEREC structure. */ +typedef ATSPKTREQTONEREC *PATSPKTREQTONEREC; + +/* No additional structure for the reply (just standard STATUS packet). */ + +/** + * The failure reply structure. + */ +typedef struct ATSPKTREPFAIL +{ + /** Embedded packet header. */ + ATSPKTHDR Hdr; + /** Error code (IPRT-style). */ + int rc; + /** Error description. */ + char ach[256]; +} ATSPKTREPFAIL; +/** Pointer to a ATSPKTREPFAIL structure. */ +typedef ATSPKTREPFAIL *PATSPKTREPFAIL; + +/** + * Checks if the two opcodes match. + * + * @returns true on match, false on mismatch. + * @param pPktHdr The packet header. + * @param pszOpcode2 The opcode we're comparing with. Does not have + * to be the whole 8 chars long. + */ +DECLINLINE(bool) atsIsSameOpcode(PCATSPKTHDR pPktHdr, const char *pszOpcode2) +{ + if (pPktHdr->achOpcode[0] != pszOpcode2[0]) + return false; + if (pPktHdr->achOpcode[1] != pszOpcode2[1]) + return false; + + unsigned i = 2; + while ( i < RT_SIZEOFMEMB(ATSPKTHDR, achOpcode) + && pszOpcode2[i] != '\0') + { + if (pPktHdr->achOpcode[i] != pszOpcode2[i]) + break; + i++; + } + + if ( i < RT_SIZEOFMEMB(ATSPKTHDR, achOpcode) + && pszOpcode2[i] == '\0') + { + while ( i < RT_SIZEOFMEMB(ATSPKTHDR, achOpcode) + && pPktHdr->achOpcode[i] == ' ') + i++; + } + + return i == RT_SIZEOFMEMB(ATSPKTHDR, achOpcode); +} + +RT_C_DECLS_END + +#endif /* !VBOX_INCLUDED_SRC_Audio_AudioTestServiceProtocol_h */ diff --git a/src/VBox/Devices/Audio/AudioTestServiceTcp.cpp b/src/VBox/Devices/Audio/AudioTestServiceTcp.cpp new file mode 100644 index 00000000..71303e2e --- /dev/null +++ b/src/VBox/Devices/Audio/AudioTestServiceTcp.cpp @@ -0,0 +1,967 @@ +/* $Id: AudioTestServiceTcp.cpp $ */ +/** @file + * AudioTestServiceTcp - Audio test execution server, TCP/IP Transport Layer. + */ + +/* + * Copyright (C) 2021-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 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_AUDIO_TEST +#include <iprt/log.h> + +#include <iprt/asm.h> +#include <iprt/assert.h> +#include <iprt/critsect.h> +#include <iprt/err.h> +#include <iprt/mem.h> +#include <iprt/message.h> +#include <iprt/poll.h> +#include <iprt/string.h> +#include <iprt/tcp.h> +#include <iprt/thread.h> +#include <iprt/time.h> + +#include <VBox/log.h> + +#include "AudioTestService.h" +#include "AudioTestServiceInternal.h" + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +/** + * TCP specific client data. + */ +typedef struct ATSTRANSPORTCLIENT +{ + /** Socket of the current client. */ + RTSOCKET hTcpClient; + /** Indicates whether \a hTcpClient comes from the server or from a client + * connect (relevant when closing it). */ + bool fFromServer; + /** The size of the stashed data. */ + size_t cbTcpStashed; + /** The size of the stashed data allocation. */ + size_t cbTcpStashedAlloced; + /** The stashed data. */ + uint8_t *pbTcpStashed; +} ATSTRANSPORTCLIENT; + +/** + * Structure for keeping Audio Test Service (ATS) transport instance-specific data. + */ +typedef struct ATSTRANSPORTINST +{ + /** Critical section for serializing access. */ + RTCRITSECT CritSect; + /** Connection mode to use. */ + ATSCONNMODE enmConnMode; + /** The addresses to bind to. Empty string means any. */ + char szBindAddr[256]; + /** The TCP port to listen to. */ + uint32_t uBindPort; + /** The addresses to connect to if running in reversed (VM NATed) mode. */ + char szConnectAddr[256]; + /** The TCP port to connect to if running in reversed (VM NATed) mode. */ + uint32_t uConnectPort; + /** Pointer to the TCP server instance. */ + PRTTCPSERVER pTcpServer; + /** Thread calling RTTcpServerListen2. */ + RTTHREAD hThreadServer; + /** Thread calling RTTcpClientConnect. */ + RTTHREAD hThreadConnect; + /** The main thread handle (for signalling). */ + RTTHREAD hThreadMain; + /** Stop connecting attempts when set. */ + bool fStopConnecting; + /** Connect cancel cookie. */ + PRTTCPCLIENTCONNECTCANCEL volatile pConnectCancelCookie; +} ATSTRANSPORTINST; +/** Pointer to an Audio Test Service (ATS) TCP/IP transport instance. */ +typedef ATSTRANSPORTINST *PATSTRANSPORTINST; + +/** + * Structure holding an ATS connection context, which is + * required when connecting a client via server (listening) or client (connecting). + */ +typedef struct ATSCONNCTX +{ + /** Pointer to transport instance to use. */ + PATSTRANSPORTINST pInst; + /** Pointer to transport client to connect. */ + PATSTRANSPORTCLIENT pClient; + /** Connection timeout (in ms). + * Use RT_INDEFINITE_WAIT to wait indefinitely. */ + uint32_t msTimeout; +} ATSCONNCTX; +/** Pointer to an Audio Test Service (ATS) TCP/IP connection context. */ +typedef ATSCONNCTX *PATSCONNCTX; + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ + +/** + * Disconnects the current client and frees all stashed data. + * + * @param pThis Transport instance. + * @param pClient Client to disconnect. + */ +static void atsTcpDisconnectClient(PATSTRANSPORTINST pThis, PATSTRANSPORTCLIENT pClient) +{ + RT_NOREF(pThis); + + LogRelFlowFunc(("pClient=%RTsock\n", pClient->hTcpClient)); + + if (pClient->hTcpClient != NIL_RTSOCKET) + { + LogRelFlowFunc(("%RTsock\n", pClient->hTcpClient)); + + int rc; + if (pClient->fFromServer) + rc = RTTcpServerDisconnectClient2(pClient->hTcpClient); + else + rc = RTTcpClientClose(pClient->hTcpClient); + pClient->hTcpClient = NIL_RTSOCKET; + AssertRCSuccess(rc); + } + + if (pClient->pbTcpStashed) + { + RTMemFree(pClient->pbTcpStashed); + pClient->pbTcpStashed = NULL; + } +} + +/** + * Free's a client. + * + * @param pThis Transport instance. + * @param pClient Client to free. + * The pointer will be invalid after calling. + */ +static void atsTcpFreeClient(PATSTRANSPORTINST pThis, PATSTRANSPORTCLIENT pClient) +{ + if (!pClient) + return; + + /* Make sure to disconnect first. */ + atsTcpDisconnectClient(pThis, pClient); + + RTMemFree(pClient); + pClient = NULL; +} + +/** + * Sets the current client socket in a safe manner. + * + * @returns NIL_RTSOCKET if consumed, otherwise hTcpClient. + * @param pThis Transport instance. + * @param pClient Client to set the socket for. + * @param fFromServer Whether the socket is from a server (listening) or client (connecting) call. + * Important when closing / disconnecting. + * @param hTcpClient The client socket. + */ +static RTSOCKET atsTcpSetClient(PATSTRANSPORTINST pThis, PATSTRANSPORTCLIENT pClient, bool fFromServer, RTSOCKET hTcpClient) +{ + RTCritSectEnter(&pThis->CritSect); + if ( pClient->hTcpClient == NIL_RTSOCKET + && !pThis->fStopConnecting) + { + LogRelFlowFunc(("New client %RTsock connected (fFromServer=%RTbool)\n", hTcpClient, fFromServer)); + + pClient->fFromServer = fFromServer; + pClient->hTcpClient = hTcpClient; + hTcpClient = NIL_RTSOCKET; /* Invalidate, as pClient has now ownership. */ + } + RTCritSectLeave(&pThis->CritSect); + return hTcpClient; +} + +/** + * Checks if it's a fatal RTTcpClientConnect return code. + * + * @returns true / false. + * @param rc The IPRT status code. + */ +static bool atsTcpIsFatalClientConnectStatus(int rc) +{ + return rc != VERR_NET_UNREACHABLE + && rc != VERR_NET_HOST_DOWN + && rc != VERR_NET_HOST_UNREACHABLE + && rc != VERR_NET_CONNECTION_REFUSED + && rc != VERR_TIMEOUT + && rc != VERR_NET_CONNECTION_TIMED_OUT; +} + +/** + * Server mode connection thread. + * + * @returns iprt status code. + * @param hSelf Thread handle. Ignored. + * @param pvUser Pointer to ATSTRANSPORTINST the thread is bound to. + */ +static DECLCALLBACK(int) atsTcpServerConnectThread(RTTHREAD hSelf, void *pvUser) +{ + RT_NOREF(hSelf); + + PATSCONNCTX pConnCtx = (PATSCONNCTX)pvUser; + PATSTRANSPORTINST pThis = pConnCtx->pInst; + PATSTRANSPORTCLIENT pClient = pConnCtx->pClient; + + /** @todo Implement cancellation support for using pConnCtx->msTimeout. */ + + LogRelFlowFuncEnter(); + + RTSOCKET hTcpClient; + int rc = RTTcpServerListen2(pThis->pTcpServer, &hTcpClient); + if (RT_SUCCESS(rc)) + { + hTcpClient = atsTcpSetClient(pThis, pClient, true /* fFromServer */, hTcpClient); + RTTcpServerDisconnectClient2(hTcpClient); + } + + LogRelFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Client mode connection thread. + * + * @returns iprt status code. + * @param hSelf Thread handle. Use to sleep on. The main thread will + * signal it to speed up thread shutdown. + * @param pvUser Pointer to a connection context (PATSCONNCTX) the thread is bound to. + */ +static DECLCALLBACK(int) atsTcpClientConnectThread(RTTHREAD hSelf, void *pvUser) +{ + PATSCONNCTX pConnCtx = (PATSCONNCTX)pvUser; + PATSTRANSPORTINST pThis = pConnCtx->pInst; + PATSTRANSPORTCLIENT pClient = pConnCtx->pClient; + + uint64_t msStartTs = RTTimeMilliTS(); + + LogRelFlowFuncEnter(); + + for (;;) + { + /* Stop? */ + RTCritSectEnter(&pThis->CritSect); + bool fStop = pThis->fStopConnecting; + RTCritSectLeave(&pThis->CritSect); + if (fStop) + return VINF_SUCCESS; + + /* Try connect. */ /** @todo make cancelable! */ + RTSOCKET hTcpClient; + int rc = RTTcpClientConnectEx(pThis->szConnectAddr, pThis->uConnectPort, &hTcpClient, + RT_SOCKETCONNECT_DEFAULT_WAIT, &pThis->pConnectCancelCookie); + if (RT_SUCCESS(rc)) + { + hTcpClient = atsTcpSetClient(pThis, pClient, false /* fFromServer */, hTcpClient); + RTTcpClientCloseEx(hTcpClient, true /* fGracefulShutdown*/); + break; + } + + if (atsTcpIsFatalClientConnectStatus(rc)) + return rc; + + if ( pConnCtx->msTimeout != RT_INDEFINITE_WAIT + && RTTimeMilliTS() - msStartTs >= pConnCtx->msTimeout) + { + LogRelFlowFunc(("Timed out (%RU32ms)\n", pConnCtx->msTimeout)); + return VERR_TIMEOUT; + } + + /* Delay a wee bit before retrying. */ + RTThreadUserWait(hSelf, 1536); + } + + LogRelFlowFuncLeave(); + return VINF_SUCCESS; +} + +/** + * Wait on the threads to complete. + * + * @returns Thread status (if collected), otherwise VINF_SUCCESS. + * @param pThis Transport instance. + * @param cMillies The period to wait on each thread. + */ +static int atsTcpConnectWaitOnThreads(PATSTRANSPORTINST pThis, RTMSINTERVAL cMillies) +{ + int rcRet = VINF_SUCCESS; + + LogRelFlowFuncEnter(); + + if (pThis->hThreadConnect != NIL_RTTHREAD) + { + int rcThread; + int rc2 = RTThreadWait(pThis->hThreadConnect, cMillies, &rcThread); + if (RT_SUCCESS(rc2)) + { + pThis->hThreadConnect = NIL_RTTHREAD; + rcRet = rcThread; + } + } + + if (pThis->hThreadServer != NIL_RTTHREAD) + { + int rcThread; + int rc2 = RTThreadWait(pThis->hThreadServer, cMillies, &rcThread); + if (RT_SUCCESS(rc2)) + { + pThis->hThreadServer = NIL_RTTHREAD; + if (RT_SUCCESS(rc2)) + rcRet = rcThread; + } + } + + LogRelFlowFuncLeaveRC(rcRet); + return rcRet; +} + +/** + * @interface_method_impl{ATSTRANSPORT,pfnWaitForConnect} + */ +static DECLCALLBACK(int) atsTcpWaitForConnect(PATSTRANSPORTINST pThis, RTMSINTERVAL msTimeout, + bool *pfFromServer, PPATSTRANSPORTCLIENT ppClientNew) +{ + PATSTRANSPORTCLIENT pClient = (PATSTRANSPORTCLIENT)RTMemAllocZ(sizeof(ATSTRANSPORTCLIENT)); + AssertPtrReturn(pClient, VERR_NO_MEMORY); + + int rc; + + LogRelFlowFunc(("msTimeout=%RU32, enmConnMode=%#x\n", msTimeout, pThis->enmConnMode)); + + uint64_t msStartTs = RTTimeMilliTS(); + + if (pThis->enmConnMode == ATSCONNMODE_SERVER) + { + /** @todo Implement cancellation support for using \a msTimeout. */ + + pClient->fFromServer = true; + rc = RTTcpServerListen2(pThis->pTcpServer, &pClient->hTcpClient); + LogRelFlowFunc(("RTTcpServerListen2(%RTsock) -> %Rrc\n", pClient->hTcpClient, rc)); + } + else if (pThis->enmConnMode == ATSCONNMODE_CLIENT) + { + pClient->fFromServer = false; + for (;;) + { + LogRelFlowFunc(("Calling RTTcpClientConnect(%s, %u,)...\n", pThis->szConnectAddr, pThis->uConnectPort)); + rc = RTTcpClientConnect(pThis->szConnectAddr, pThis->uConnectPort, &pClient->hTcpClient); + LogRelFlowFunc(("RTTcpClientConnect(%RTsock) -> %Rrc\n", pClient->hTcpClient, rc)); + if (RT_SUCCESS(rc) || atsTcpIsFatalClientConnectStatus(rc)) + break; + + if ( msTimeout != RT_INDEFINITE_WAIT + && RTTimeMilliTS() - msStartTs >= msTimeout) + { + rc = VERR_TIMEOUT; + break; + } + + if (pThis->fStopConnecting) + { + rc = VINF_SUCCESS; + break; + } + + /* Delay a wee bit before retrying. */ + RTThreadSleep(1536); + } + } + else + { + Assert(pThis->enmConnMode == ATSCONNMODE_BOTH); + + /* + * Create client threads. + */ + RTCritSectEnter(&pThis->CritSect); + + pThis->fStopConnecting = false; + RTCritSectLeave(&pThis->CritSect); + + atsTcpConnectWaitOnThreads(pThis, 32 /* cMillies */); + + ATSCONNCTX ConnCtx; + RT_ZERO(ConnCtx); + ConnCtx.pInst = pThis; + ConnCtx.pClient = pClient; + ConnCtx.msTimeout = msTimeout; + + rc = VINF_SUCCESS; + if (pThis->hThreadConnect == NIL_RTTHREAD) + { + pThis->pConnectCancelCookie = NULL; + rc = RTThreadCreate(&pThis->hThreadConnect, atsTcpClientConnectThread, &ConnCtx, 0, RTTHREADTYPE_DEFAULT, + RTTHREADFLAGS_WAITABLE, "tcpconn"); + } + if (pThis->hThreadServer == NIL_RTTHREAD && RT_SUCCESS(rc)) + rc = RTThreadCreate(&pThis->hThreadServer, atsTcpServerConnectThread, &ConnCtx, 0, RTTHREADTYPE_DEFAULT, + RTTHREADFLAGS_WAITABLE, "tcpserv"); + + RTCritSectEnter(&pThis->CritSect); + + /* + * Wait for connection to be established. + */ + while ( RT_SUCCESS(rc) + && pClient->hTcpClient == NIL_RTSOCKET) + { + RTCritSectLeave(&pThis->CritSect); + rc = atsTcpConnectWaitOnThreads(pThis, 10 /* cMillies */); + RTCritSectEnter(&pThis->CritSect); + } + + /* + * Cancel the threads. + */ + pThis->fStopConnecting = true; + + RTCritSectLeave(&pThis->CritSect); + RTTcpClientCancelConnect(&pThis->pConnectCancelCookie); + } + + if (RT_SUCCESS(rc)) + { + if (pfFromServer) + *pfFromServer = pClient->fFromServer; + *ppClientNew = pClient; + } + else + { + if (pClient) + { + atsTcpFreeClient(pThis, pClient); + pClient = NULL; + } + } + + if (RT_FAILURE(rc)) + LogRelFunc(("Failed with %Rrc\n", rc)); + + return rc; +} + +/** + * @interface_method_impl{ATSTRANSPORT,pfnNotifyReboot} + */ +static DECLCALLBACK(void) atsTcpNotifyReboot(PATSTRANSPORTINST pThis) +{ + LogRelFlowFuncEnter(); + if (pThis->pTcpServer) + { + int rc = RTTcpServerDestroy(pThis->pTcpServer); + if (RT_FAILURE(rc)) + LogRelFunc(("RTTcpServerDestroy failed, rc=%Rrc", rc)); + pThis->pTcpServer = NULL; + } + LogRelFlowFuncLeave(); +} + +/** + * @interface_method_impl{ATSTRANSPORT,pfnNotifyBye} + */ +static DECLCALLBACK(void) atsTcpNotifyBye(PATSTRANSPORTINST pThis, PATSTRANSPORTCLIENT pClient) +{ + LogRelFlowFunc(("pClient=%RTsock\n", pClient->hTcpClient)); + atsTcpDisconnectClient(pThis, pClient); +} + +/** + * @interface_method_impl{ATSTRANSPORT,pfnNotifyHowdy} + */ +static DECLCALLBACK(void) atsTcpNotifyHowdy(PATSTRANSPORTINST pThis, PATSTRANSPORTCLIENT pClient) +{ + LogRelFlowFunc(("pClient=%RTsock\n", pClient->hTcpClient)); + + /* nothing to do here */ + RT_NOREF(pThis); +} + +/** + * @interface_method_impl{ATSTRANSPORT,pfnBabble} + */ +static DECLCALLBACK(void) atsTcpBabble(PATSTRANSPORTINST pThis, PATSTRANSPORTCLIENT pClient, PCATSPKTHDR pPktHdr, RTMSINTERVAL cMsSendTimeout) +{ + /* + * Try send the babble reply. + */ + RT_NOREF(cMsSendTimeout); /** @todo implement the timeout here; non-blocking write + select-on-write. */ + int rc; + size_t cbToSend = RT_ALIGN_Z(pPktHdr->cb, ATSPKT_ALIGNMENT); + do rc = RTTcpWrite(pClient->hTcpClient, pPktHdr, cbToSend); + while (rc == VERR_INTERRUPTED); + + LogRelFlowFunc(("pClient=%RTsock, rc=%Rrc\n", pClient->hTcpClient, rc)); + + /* + * Disconnect the client. + */ + atsTcpDisconnectClient(pThis, pClient); +} + +/** + * @interface_method_impl{ATSTRANSPORT,pfnSendPkt} + */ +static DECLCALLBACK(int) atsTcpSendPkt(PATSTRANSPORTINST pThis, PATSTRANSPORTCLIENT pClient, PCATSPKTHDR pPktHdr) +{ + AssertReturn(pPktHdr->cb >= sizeof(ATSPKTHDR), VERR_INVALID_PARAMETER); + + /* + * Write it. + */ + size_t cbToSend = RT_ALIGN_Z(pPktHdr->cb, ATSPKT_ALIGNMENT); + + Log3Func(("%RU32 -> %zu\n", pPktHdr->cb, cbToSend)); + + LogRel4(("pClient=%RTsock\n", pClient->hTcpClient)); + LogRel4(("Header:\n" + "%.*Rhxd\n", RT_MIN(sizeof(ATSPKTHDR), cbToSend), pPktHdr)); + + if (cbToSend > sizeof(ATSPKTHDR)) + LogRel4(("Payload:\n" + "%.*Rhxd\n", + RT_MIN(64, cbToSend - sizeof(ATSPKTHDR)), (uint8_t *)pPktHdr + sizeof(ATSPKTHDR))); + + int rc = RTTcpWrite(pClient->hTcpClient, pPktHdr, cbToSend); + if ( RT_FAILURE(rc) + && rc != VERR_INTERRUPTED) + { + /* assume fatal connection error. */ + LogRelFunc(("RTTcpWrite -> %Rrc -> atsTcpDisconnectClient(%RTsock)\n", rc, pClient->hTcpClient)); + atsTcpDisconnectClient(pThis, pClient); + } + + LogRel3(("atsTcpSendPkt: pClient=%RTsock, achOpcode=%.8s, cbSent=%zu -> %Rrc\n", pClient->hTcpClient, (const char *)pPktHdr->achOpcode, cbToSend, rc)); + return rc; +} + +/** + * @interface_method_impl{ATSTRANSPORT,pfnRecvPkt} + */ +static DECLCALLBACK(int) atsTcpRecvPkt(PATSTRANSPORTINST pThis, PATSTRANSPORTCLIENT pClient, PPATSPKTHDR ppPktHdr) +{ + int rc = VINF_SUCCESS; + *ppPktHdr = NULL; + + LogRel4(("pClient=%RTsock (cbTcpStashed=%zu, cbTcpStashedAlloced=%zu)\n", + pClient->hTcpClient, pClient->cbTcpStashed, pClient->cbTcpStashedAlloced)); + + /* + * Read state. + */ + size_t offData = 0; + size_t cbData = 0; + size_t cbDataAlloced; + uint8_t *pbData = NULL; + + /* + * Any stashed data? + */ + if (pClient->cbTcpStashedAlloced) + { + offData = pClient->cbTcpStashed; + cbDataAlloced = pClient->cbTcpStashedAlloced; + pbData = pClient->pbTcpStashed; + + pClient->cbTcpStashed = 0; + pClient->cbTcpStashedAlloced = 0; + pClient->pbTcpStashed = NULL; + } + else + { + cbDataAlloced = RT_ALIGN_Z(64, ATSPKT_ALIGNMENT); + pbData = (uint8_t *)RTMemAlloc(cbDataAlloced); + AssertPtrReturn(pbData, VERR_NO_MEMORY); + } + + /* + * Read and validate the length. + */ + while (offData < sizeof(uint32_t)) + { + size_t cbRead; + rc = RTTcpRead(pClient->hTcpClient, pbData + offData, sizeof(uint32_t) - offData, &cbRead); + if (RT_FAILURE(rc)) + break; + if (cbRead == 0) + { + LogRelFunc(("RTTcpRead -> %Rrc / cbRead=0 -> VERR_NET_NOT_CONNECTED (#1)\n", rc)); + rc = VERR_NET_NOT_CONNECTED; + break; + } + offData += cbRead; + } + if (RT_SUCCESS(rc)) + { + ASMCompilerBarrier(); /* paranoia^3 */ + cbData = *(uint32_t volatile *)pbData; + if (cbData >= sizeof(ATSPKTHDR) && cbData <= ATSPKT_MAX_SIZE) + { + /* + * Align the length and reallocate the return packet it necessary. + */ + cbData = RT_ALIGN_Z(cbData, ATSPKT_ALIGNMENT); + if (cbData > cbDataAlloced) + { + void *pvNew = RTMemRealloc(pbData, cbData); + if (pvNew) + { + pbData = (uint8_t *)pvNew; + cbDataAlloced = cbData; + } + else + rc = VERR_NO_MEMORY; + } + if (RT_SUCCESS(rc)) + { + /* + * Read the remainder of the data. + */ + while (offData < cbData) + { + size_t cbRead; + rc = RTTcpRead(pClient->hTcpClient, pbData + offData, cbData - offData, &cbRead); + if (RT_FAILURE(rc)) + break; + if (cbRead == 0) + { + LogRelFunc(("RTTcpRead -> %Rrc / cbRead=0 -> VERR_NET_NOT_CONNECTED (#2)\n", rc)); + rc = VERR_NET_NOT_CONNECTED; + break; + } + + offData += cbRead; + } + + LogRel4(("Header:\n" + "%.*Rhxd\n", sizeof(ATSPKTHDR), pbData)); + + if ( RT_SUCCESS(rc) + && cbData > sizeof(ATSPKTHDR)) + LogRel4(("Payload:\n" + "%.*Rhxd\n", RT_MIN(64, cbData - sizeof(ATSPKTHDR)), (uint8_t *)pbData + sizeof(ATSPKTHDR))); + } + } + else + { + LogRelFunc(("Received invalid packet size (%zu)\n", cbData)); + rc = VERR_NET_PROTOCOL_ERROR; + } + } + if (RT_SUCCESS(rc)) + *ppPktHdr = (PATSPKTHDR)pbData; + else + { + /* + * Deal with errors. + */ + if (rc == VERR_INTERRUPTED) + { + /* stash it away for the next call. */ + pClient->cbTcpStashed = cbData; + pClient->cbTcpStashedAlloced = cbDataAlloced; + pClient->pbTcpStashed = pbData; + } + else + { + RTMemFree(pbData); + + /* assume fatal connection error. */ + LogRelFunc(("RTTcpRead -> %Rrc -> atsTcpDisconnectClient(%RTsock)\n", rc, pClient->hTcpClient)); + atsTcpDisconnectClient(pThis, pClient); + } + } + + PATSPKTHDR pPktHdr = (PATSPKTHDR)pbData; + LogRel3(("atsTcpRecvPkt: pClient=%RTsock, achOpcode=%.8s, cbRead=%zu -> %Rrc\n", + pClient->hTcpClient, pPktHdr ? (const char *)pPktHdr->achOpcode : "NONE ", cbData, rc)); + return rc; +} + +/** + * @interface_method_impl{ATSTRANSPORT,pfnPollSetAdd} + */ +static DECLCALLBACK(int) atsTcpPollSetAdd(PATSTRANSPORTINST pThis, RTPOLLSET hPollSet, PATSTRANSPORTCLIENT pClient, uint32_t idStart) +{ + RT_NOREF(pThis); + return RTPollSetAddSocket(hPollSet, pClient->hTcpClient, RTPOLL_EVT_READ | RTPOLL_EVT_ERROR, idStart); +} + +/** + * @interface_method_impl{ATSTRANSPORT,pfnPollSetRemove} + */ +static DECLCALLBACK(int) atsTcpPollSetRemove(PATSTRANSPORTINST pThis, RTPOLLSET hPollSet, PATSTRANSPORTCLIENT pClient, uint32_t idStart) +{ + RT_NOREF(pThis, pClient); + return RTPollSetRemove(hPollSet, idStart); +} + +/** + * @interface_method_impl{ATSTRANSPORT,pfnDisconnect} + */ +static DECLCALLBACK(void) atsTcpDisconnect(PATSTRANSPORTINST pThis, PATSTRANSPORTCLIENT pClient) +{ + atsTcpFreeClient(pThis, pClient); +} + +/** + * @interface_method_impl{ATSTRANSPORT,pfnPollIn} + */ +static DECLCALLBACK(bool) atsTcpPollIn(PATSTRANSPORTINST pThis, PATSTRANSPORTCLIENT pClient) +{ + RT_NOREF(pThis); + int rc = RTTcpSelectOne(pClient->hTcpClient, 0/*cMillies*/); + return RT_SUCCESS(rc); +} + +/** + * @interface_method_impl{ATSTRANSPORT,pfnStop} + */ +static DECLCALLBACK(void) atsTcpStop(PATSTRANSPORTINST pThis) +{ + LogRelFlowFuncEnter(); + + /* Signal thread */ + if (RTCritSectIsInitialized(&pThis->CritSect)) + { + RTCritSectEnter(&pThis->CritSect); + pThis->fStopConnecting = true; + RTCritSectLeave(&pThis->CritSect); + } + + if (pThis->hThreadConnect != NIL_RTTHREAD) + { + RTThreadUserSignal(pThis->hThreadConnect); + RTTcpClientCancelConnect(&pThis->pConnectCancelCookie); + } + + /* Shut down the server (will wake up thread). */ + if (pThis->pTcpServer) + { + LogRelFlowFunc(("Destroying server...\n")); + int rc = RTTcpServerDestroy(pThis->pTcpServer); + if (RT_FAILURE(rc)) + LogRelFunc(("RTTcpServerDestroy failed with %Rrc", rc)); + pThis->pTcpServer = NULL; + } + + /* Wait for the thread (they should've had some time to quit by now). */ + atsTcpConnectWaitOnThreads(pThis, 15000); + + LogRelFlowFuncLeave(); +} + +/** + * @interface_method_impl{ATSTRANSPORT,pfnCreate} + */ +static DECLCALLBACK(int) atsTcpCreate(PATSTRANSPORTINST *ppThis) +{ + PATSTRANSPORTINST pThis = (PATSTRANSPORTINST)RTMemAllocZ(sizeof(ATSTRANSPORTINST)); + AssertPtrReturn(pThis, VERR_NO_MEMORY); + + int rc = RTCritSectInit(&pThis->CritSect); + if (RT_SUCCESS(rc)) + { + *ppThis = pThis; + } + + return rc; +} + +/** + * @interface_method_impl{ATSTRANSPORT,pfnDestroy} + */ +static DECLCALLBACK(int) atsTcpDestroy(PATSTRANSPORTINST pThis) +{ + /* Stop things first. */ + atsTcpStop(pThis); + + /* Finally, clean up the critical section. */ + if (RTCritSectIsInitialized(&pThis->CritSect)) + RTCritSectDelete(&pThis->CritSect); + + RTMemFree(pThis); + pThis = NULL; + + return VINF_SUCCESS; +} + +/** + * @interface_method_impl{ATSTRANSPORT,pfnStart} + */ +static DECLCALLBACK(int) atsTcpStart(PATSTRANSPORTINST pThis) +{ + int rc = VINF_SUCCESS; + + if (pThis->enmConnMode != ATSCONNMODE_CLIENT) + { + rc = RTTcpServerCreateEx(pThis->szBindAddr[0] ? pThis->szBindAddr : NULL, pThis->uBindPort, &pThis->pTcpServer); + if (RT_FAILURE(rc)) + { + if (rc == VERR_NET_DOWN) + { + LogRelFunc(("RTTcpServerCreateEx(%s, %u,) failed: %Rrc, retrying for 20 seconds...\n", + pThis->szBindAddr[0] ? pThis->szBindAddr : NULL, pThis->uBindPort, rc)); + uint64_t StartMs = RTTimeMilliTS(); + do + { + RTThreadSleep(1000); + rc = RTTcpServerCreateEx(pThis->szBindAddr[0] ? pThis->szBindAddr : NULL, pThis->uBindPort, &pThis->pTcpServer); + } while ( rc == VERR_NET_DOWN + && RTTimeMilliTS() - StartMs < 20000); + if (RT_SUCCESS(rc)) + LogRelFunc(("RTTcpServerCreateEx succceeded\n")); + } + + if (RT_FAILURE(rc)) + { + LogRelFunc(("RTTcpServerCreateEx(%s, %u,) failed: %Rrc\n", + pThis->szBindAddr[0] ? pThis->szBindAddr : NULL, pThis->uBindPort, rc)); + } + } + } + + return rc; +} + +/** + * @interface_method_impl{ATSTRANSPORT,pfnOption} + */ +static DECLCALLBACK(int) atsTcpOption(PATSTRANSPORTINST pThis, int ch, PCRTGETOPTUNION pVal) +{ + int rc; + + switch (ch) + { + case ATSTCPOPT_CONN_MODE: + pThis->enmConnMode = (ATSCONNMODE)pVal->u32; + return VINF_SUCCESS; + + case ATSTCPOPT_BIND_ADDRESS: + rc = RTStrCopy(pThis->szBindAddr, sizeof(pThis->szBindAddr), pVal->psz); + if (RT_FAILURE(rc)) + return RTMsgErrorRc(VERR_INVALID_PARAMETER, "TCP bind address is too long (%Rrc)", rc); + if (!pThis->szBindAddr[0]) + return RTMsgErrorRc(VERR_INVALID_PARAMETER, "No TCP bind address specified: %s", pThis->szBindAddr); + return VINF_SUCCESS; + + case ATSTCPOPT_BIND_PORT: + pThis->uBindPort = pVal->u16; + return VINF_SUCCESS; + + case ATSTCPOPT_CONNECT_ADDRESS: + rc = RTStrCopy(pThis->szConnectAddr, sizeof(pThis->szConnectAddr), pVal->psz); + if (RT_FAILURE(rc)) + return RTMsgErrorRc(VERR_INVALID_PARAMETER, "TCP connect address is too long (%Rrc)", rc); + if (!pThis->szConnectAddr[0]) + return RTMsgErrorRc(VERR_INVALID_PARAMETER, "No TCP connect address specified"); + return VINF_SUCCESS; + + case ATSTCPOPT_CONNECT_PORT: + pThis->uConnectPort = pVal->u16; + return VINF_SUCCESS; + + default: + break; + } + return VERR_TRY_AGAIN; +} + +/** + * @interface_method_impl{ATSTRANSPORT,pfnUsage} + */ +DECLCALLBACK(void) atsTcpUsage(PRTSTREAM pStream) +{ + RTStrmPrintf(pStream, + " --tcp-conn-mode <0=both|1=client|2=server>\n" + " Selects the connection mode\n" + " Default: 0 (both)\n" + " --tcp-bind-addr[ess] <address>\n" + " The address(es) to listen to TCP connection on. Empty string\n" + " means any address, this is the default\n" + " --tcp-bind-port <port>\n" + " The port to listen to TCP connections on\n" + " Default: %u\n" + " --tcp-connect-addr[ess] <address>\n" + " The address of the server to try connect to in client mode\n" + " Default: " ATS_TCP_DEF_CONNECT_GUEST_STR "\n" + " --tcp-connect-port <port>\n" + " The port on the server to connect to in client mode\n" + " Default: %u\n" + , ATS_TCP_DEF_BIND_PORT_GUEST, ATS_TCP_DEF_CONNECT_PORT_GUEST); +} + +/** Command line options for the TCP/IP transport layer. */ +static const RTGETOPTDEF g_TcpOpts[] = +{ + { "--tcp-conn-mode", ATSTCPOPT_CONN_MODE, RTGETOPT_REQ_STRING }, + { "--tcp-bind-addr", ATSTCPOPT_BIND_ADDRESS, RTGETOPT_REQ_STRING }, + { "--tcp-bind-address", ATSTCPOPT_BIND_ADDRESS, RTGETOPT_REQ_STRING }, + { "--tcp-bind-port", ATSTCPOPT_BIND_PORT, RTGETOPT_REQ_UINT16 }, + { "--tcp-connect-addr", ATSTCPOPT_CONNECT_ADDRESS, RTGETOPT_REQ_STRING }, + { "--tcp-connect-address", ATSTCPOPT_CONNECT_ADDRESS, RTGETOPT_REQ_STRING }, + { "--tcp-connect-port", ATSTCPOPT_CONNECT_PORT, RTGETOPT_REQ_UINT16 } +}; + +/** TCP/IP transport layer. */ +const ATSTRANSPORT g_TcpTransport = +{ + /* .szName = */ "tcp", + /* .pszDesc = */ "TCP/IP", + /* .cOpts = */ &g_TcpOpts[0], + /* .paOpts = */ RT_ELEMENTS(g_TcpOpts), + /* .pfnUsage = */ atsTcpUsage, + /* .pfnCreate = */ atsTcpCreate, + /* .pfnDestroy = */ atsTcpDestroy, + /* .pfnOption = */ atsTcpOption, + /* .pfnStart = */ atsTcpStart, + /* .pfnStop = */ atsTcpStop, + /* .pfnWaitForConnect = */ atsTcpWaitForConnect, + /* .pfnDisconnect = */ atsTcpDisconnect, + /* .pfnPollIn = */ atsTcpPollIn, + /* .pfnPollSetAdd = */ atsTcpPollSetAdd, + /* .pfnPollSetRemove = */ atsTcpPollSetRemove, + /* .pfnRecvPkt = */ atsTcpRecvPkt, + /* .pfnSendPkt = */ atsTcpSendPkt, + /* .pfnBabble = */ atsTcpBabble, + /* .pfnNotifyHowdy = */ atsTcpNotifyHowdy, + /* .pfnNotifyBye = */ atsTcpNotifyBye, + /* .pfnNotifyReboot = */ atsTcpNotifyReboot, + /* .u32EndMarker = */ UINT32_C(0x12345678) +}; + diff --git a/src/VBox/Devices/Audio/DevHda.cpp b/src/VBox/Devices/Audio/DevHda.cpp new file mode 100644 index 00000000..d1ee37fc --- /dev/null +++ b/src/VBox/Devices/Audio/DevHda.cpp @@ -0,0 +1,5429 @@ +/* $Id: DevHda.cpp $ */ +/** @file + * Intel HD Audio Controller Emulation. + * + * Implemented against the specifications found in "High Definition Audio + * Specification", Revision 1.0a June 17, 2010, and "Intel I/O Controller + * HUB 6 (ICH6) Family, Datasheet", document number 301473-002. + */ + +/* + * Copyright (C) 2006-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 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_DEV_HDA +#include <VBox/log.h> + +#include <VBox/vmm/pdmdev.h> +#include <VBox/vmm/pdmaudioifs.h> +#include <VBox/vmm/pdmaudioinline.h> +#ifdef HDA_DEBUG_GUEST_RIP +# include <VBox/vmm/cpum.h> +#endif +#include <VBox/version.h> +#include <VBox/AssertGuest.h> + +#include <iprt/assert.h> +#include <iprt/asm.h> +#include <iprt/asm-math.h> +#include <iprt/file.h> +#include <iprt/list.h> +# include <iprt/string.h> +#ifdef IN_RING3 +# include <iprt/mem.h> +# include <iprt/semaphore.h> +# include <iprt/uuid.h> +#endif + +#include "VBoxDD.h" + +#include "AudioMixBuffer.h" +#include "AudioMixer.h" + +#define VBOX_HDA_CAN_ACCESS_REG_MAP /* g_aHdaRegMap is accessible */ +#include "DevHda.h" + +#include "AudioHlp.h" + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +#if defined(VBOX_WITH_HP_HDA) +/* HP Pavilion dv4t-1300 */ +# define HDA_PCI_VENDOR_ID 0x103c +# define HDA_PCI_DEVICE_ID 0x30f7 +#elif defined(VBOX_WITH_INTEL_HDA) +/* Intel HDA controller */ +# define HDA_PCI_VENDOR_ID 0x8086 +# define HDA_PCI_DEVICE_ID 0x2668 +#elif defined(VBOX_WITH_NVIDIA_HDA) +/* nVidia HDA controller */ +# define HDA_PCI_VENDOR_ID 0x10de +# define HDA_PCI_DEVICE_ID 0x0ac0 +#else +# error "Please specify your HDA device vendor/device IDs" +#endif + +/** + * Acquires the HDA lock. + */ +#define DEVHDA_LOCK(a_pDevIns, a_pThis) \ + do { \ + int const rcLock = PDMDevHlpCritSectEnter((a_pDevIns), &(a_pThis)->CritSect, VERR_IGNORED); \ + PDM_CRITSECT_RELEASE_ASSERT_RC_DEV((a_pDevIns), &(a_pThis)->CritSect, rcLock); \ + } while (0) + +/** + * Acquires the HDA lock or returns. + */ +#define DEVHDA_LOCK_RETURN(a_pDevIns, a_pThis, a_rcBusy) \ + do { \ + int const rcLock = PDMDevHlpCritSectEnter((a_pDevIns), &(a_pThis)->CritSect, a_rcBusy); \ + if (rcLock == VINF_SUCCESS) \ + { /* likely */ } \ + else \ + { \ + AssertRC(rcLock); \ + return rcLock; \ + } \ + } while (0) + +/** + * Acquires the HDA lock or returns. + */ +# define DEVHDA_LOCK_RETURN_VOID(a_pDevIns, a_pThis) \ + do { \ + int const rcLock = PDMDevHlpCritSectEnter((a_pDevIns), &(a_pThis)->CritSect, VERR_IGNORED); \ + if (rcLock == VINF_SUCCESS) \ + { /* likely */ } \ + else \ + { \ + PDM_CRITSECT_RELEASE_ASSERT_RC_DEV((a_pDevIns), &(a_pThis)->CritSect, rcLock); \ + return; \ + } \ + } while (0) + +/** + * Releases the HDA lock. + */ +#define DEVHDA_UNLOCK(a_pDevIns, a_pThis) \ + do { PDMDevHlpCritSectLeave((a_pDevIns), &(a_pThis)->CritSect); } while (0) + +/** + * Acquires the TM lock and HDA lock, returns on failure. + */ +#define DEVHDA_LOCK_BOTH_RETURN(a_pDevIns, a_pThis, a_pStream, a_rcBusy) \ + do { \ + VBOXSTRICTRC rcLock = PDMDevHlpTimerLockClock2(pDevIns, (a_pStream)->hTimer, &(a_pThis)->CritSect, (a_rcBusy)); \ + if (RT_LIKELY(rcLock == VINF_SUCCESS)) \ + { /* likely */ } \ + else \ + return VBOXSTRICTRC_TODO(rcLock); \ + } while (0) + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ + +/** + * Structure defining a (host backend) driver stream. + * Each driver has its own instances of audio mixer streams, which then + * can go into the same (or even different) audio mixer sinks. + */ +typedef struct HDADRIVERSTREAM +{ + /** Associated mixer handle. */ + R3PTRTYPE(PAUDMIXSTREAM) pMixStrm; +} HDADRIVERSTREAM, *PHDADRIVERSTREAM; + +/** + * Struct for maintaining a host backend driver. + * This driver must be associated to one, and only one, + * HDA codec. The HDA controller does the actual multiplexing + * of HDA codec data to various host backend drivers then. + * + * This HDA device uses a timer in order to synchronize all + * read/write accesses across all attached LUNs / backends. + */ +typedef struct HDADRIVER +{ + /** Node for storing this driver in our device driver list of HDASTATE. */ + RTLISTNODER3 Node; + /** Pointer to shared HDA device state. */ + R3PTRTYPE(PHDASTATE) pHDAStateShared; + /** Pointer to the ring-3 HDA device state. */ + R3PTRTYPE(PHDASTATER3) pHDAStateR3; + /** LUN to which this driver has been assigned. */ + uint8_t uLUN; + /** Whether this driver is in an attached state or not. */ + bool fAttached; + uint8_t u32Padding0[6]; + /** Pointer to attached driver base interface. */ + R3PTRTYPE(PPDMIBASE) pDrvBase; + /** Audio connector interface to the underlying host backend. */ + R3PTRTYPE(PPDMIAUDIOCONNECTOR) pConnector; + /** Mixer stream for line input. */ + HDADRIVERSTREAM LineIn; +#ifdef VBOX_WITH_AUDIO_HDA_MIC_IN + /** Mixer stream for mic input. */ + HDADRIVERSTREAM MicIn; +#endif + /** Mixer stream for front output. */ + HDADRIVERSTREAM Front; +#ifdef VBOX_WITH_AUDIO_HDA_51_SURROUND + /** Mixer stream for center/LFE output. */ + HDADRIVERSTREAM CenterLFE; + /** Mixer stream for rear output. */ + HDADRIVERSTREAM Rear; +#endif + /** The LUN description. */ + char szDesc[48 - 2]; +} HDADRIVER; +/** The HDA host driver backend. */ +typedef struct HDADRIVER *PHDADRIVER; + + +/** Internal state of this BDLE. + * Not part of the actual BDLE registers. + * @note Only for saved state. */ +typedef struct HDABDLESTATELEGACY +{ + /** Own index within the BDL (Buffer Descriptor List). */ + uint32_t u32BDLIndex; + /** Number of bytes below the stream's FIFO watermark (SDFIFOW). + * Used to check if we need fill up the FIFO again. */ + uint32_t cbBelowFIFOW; + /** Current offset in DMA buffer (in bytes).*/ + uint32_t u32BufOff; + uint32_t Padding; +} HDABDLESTATELEGACY; + +/** + * BDLE and state. + * @note Only for saved state. + */ +typedef struct HDABDLELEGACY +{ + /** The actual BDL description. */ + HDABDLEDESC Desc; + HDABDLESTATELEGACY State; +} HDABDLELEGACY; +AssertCompileSize(HDABDLELEGACY, 32); + + +/** Read callback. */ +typedef VBOXSTRICTRC FNHDAREGREAD(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t *pu32Value); +/** Write callback. */ +typedef VBOXSTRICTRC FNHDAREGWRITE(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value); + +/** + * HDA register descriptor. + */ +typedef struct HDAREGDESC +{ + /** Register offset in the register space. */ + uint32_t off; + /** Size in bytes. Registers of size > 4 are in fact tables. */ + uint8_t cb; + /** Register descriptor (RD) flags of type HDA_RD_F_XXX. These are used to + * specify the read/write handling policy of the register. */ + uint8_t fFlags; + /** Index into the register storage array (HDASTATE::au32Regs). */ + uint8_t idxReg; + uint8_t bUnused; + /** Readable bits. */ + uint32_t fReadableMask; + /** Writable bits. */ + uint32_t fWritableMask; + /** Read callback. */ + FNHDAREGREAD *pfnRead; + /** Write callback. */ + FNHDAREGWRITE *pfnWrite; +#if defined(IN_RING3) || defined(LOG_ENABLED) /* Saves 0x2f23 - 0x1888 = 0x169B (5787) bytes in VBoxDDR0. */ + /** Abbreviated name. */ + const char *pszName; +# ifdef IN_RING3 + /** Description (for stats). */ + const char *pszDesc; +# endif +#endif +} HDAREGDESC; + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ +#ifndef VBOX_DEVICE_STRUCT_TESTCASE +#ifdef IN_RING3 +static void hdaR3GCTLReset(PPDMDEVINS pDevIns, PHDASTATE pThis, PHDASTATER3 pThisCC); +#endif + +/** @name Register read/write stubs. + * @{ + */ +static FNHDAREGREAD hdaRegReadUnimpl; +static FNHDAREGWRITE hdaRegWriteUnimpl; +/** @} */ + +/** @name Global register set read/write functions. + * @{ + */ +static FNHDAREGWRITE hdaRegWriteGCTL; +static FNHDAREGREAD hdaRegReadLPIB; +static FNHDAREGREAD hdaRegReadWALCLK; +static FNHDAREGWRITE hdaRegWriteSSYNC; +static FNHDAREGWRITE hdaRegWriteNewSSYNC; +static FNHDAREGWRITE hdaRegWriteCORBWP; +static FNHDAREGWRITE hdaRegWriteCORBRP; +static FNHDAREGWRITE hdaRegWriteCORBCTL; +static FNHDAREGWRITE hdaRegWriteCORBSIZE; +static FNHDAREGWRITE hdaRegWriteCORBSTS; +static FNHDAREGWRITE hdaRegWriteRINTCNT; +static FNHDAREGWRITE hdaRegWriteRIRBWP; +static FNHDAREGWRITE hdaRegWriteRIRBSTS; +static FNHDAREGWRITE hdaRegWriteSTATESTS; +static FNHDAREGWRITE hdaRegWriteIRS; +static FNHDAREGREAD hdaRegReadIRS; +static FNHDAREGWRITE hdaRegWriteBase; +/** @} */ + +/** @name {IOB}SDn read/write functions. + * @{ + */ +static FNHDAREGWRITE hdaRegWriteSDCBL; +static FNHDAREGWRITE hdaRegWriteSDCTL; +static FNHDAREGWRITE hdaRegWriteSDSTS; +static FNHDAREGWRITE hdaRegWriteSDLVI; +static FNHDAREGWRITE hdaRegWriteSDFIFOW; +static FNHDAREGWRITE hdaRegWriteSDFIFOS; +static FNHDAREGWRITE hdaRegWriteSDFMT; +static FNHDAREGWRITE hdaRegWriteSDBDPL; +static FNHDAREGWRITE hdaRegWriteSDBDPU; +static FNHDAREGREAD hdaRegReadSDnPIB; +static FNHDAREGREAD hdaRegReadSDnEFIFOS; +/** @} */ + +/** @name Generic register read/write functions. + * @{ + */ +static FNHDAREGREAD hdaRegReadU32; +static FNHDAREGWRITE hdaRegWriteU32; +static FNHDAREGREAD hdaRegReadU24; +#ifdef IN_RING3 +static FNHDAREGWRITE hdaRegWriteU24; +#endif +static FNHDAREGREAD hdaRegReadU16; +static FNHDAREGWRITE hdaRegWriteU16; +static FNHDAREGREAD hdaRegReadU8; +static FNHDAREGWRITE hdaRegWriteU8; +/** @} */ + +/** @name HDA device functions. + * @{ + */ +#ifdef IN_RING3 +static int hdaR3AddStream(PHDASTATER3 pThisCC, PPDMAUDIOSTREAMCFG pCfg); +static int hdaR3RemoveStream(PHDASTATER3 pThisCC, PPDMAUDIOSTREAMCFG pCfg); +#endif /* IN_RING3 */ +/** @} */ + +/** @name HDA mixer functions. + * @{ + */ +#ifdef IN_RING3 +static int hdaR3MixerAddDrvStream(PPDMDEVINS pDevIns, PAUDMIXSINK pMixSink, PCPDMAUDIOSTREAMCFG pCfg, PHDADRIVER pDrv); +#endif +/** @} */ + +#ifdef IN_RING3 +static FNSSMFIELDGETPUT hdaR3GetPutTrans_HDABDLEDESC_fFlags_6; +static FNSSMFIELDGETPUT hdaR3GetPutTrans_HDABDLE_Desc_fFlags_1thru4; +#endif + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** No register description (RD) flags defined. */ +#define HDA_RD_F_NONE 0 +/** Writes to SD are allowed while RUN bit is set. */ +#define HDA_RD_F_SD_WRITE_RUN RT_BIT(0) + +/** @def HDA_REG_ENTRY_EX + * Maps the entry values to the actual HDAREGDESC layout, which is differs + * depending on context and build type. */ +#if defined(IN_RING3) || defined(LOG_ENABLED) +# ifdef IN_RING3 +# define HDA_REG_ENTRY_EX(a_offBar, a_cbReg, a_fReadMask, a_fWriteMask, a_fFlags, a_pfnRead, a_pfnWrite, a_idxMap, a_szName, a_szDesc) \ + { a_offBar, a_cbReg, a_fFlags, a_idxMap, 0, a_fReadMask, a_fWriteMask, a_pfnRead, a_pfnWrite, a_szName, a_szDesc } +# else +# define HDA_REG_ENTRY_EX(a_offBar, a_cbReg, a_fReadMask, a_fWriteMask, a_fFlags, a_pfnRead, a_pfnWrite, a_idxMap, a_szName, a_szDesc) \ + { a_offBar, a_cbReg, a_fFlags, a_idxMap, 0, a_fReadMask, a_fWriteMask, a_pfnRead, a_pfnWrite, a_szName } +# endif +#else +# define HDA_REG_ENTRY_EX(a_offBar, a_cbReg, a_fReadMask, a_fWriteMask, a_fFlags, a_pfnRead, a_pfnWrite, a_idxMap, a_szName, a_szDesc) \ + { a_offBar, a_cbReg, a_fFlags, a_idxMap, 0, a_fReadMask, a_fWriteMask, a_pfnRead, a_pfnWrite } +#endif + +#define HDA_REG_ENTRY(a_offBar, a_cbReg, a_fReadMask, a_fWriteMask, a_fFlags, a_pfnRead, a_pfnWrite, a_ShortRegNm, a_szDesc) \ + HDA_REG_ENTRY_EX(a_offBar, a_cbReg, a_fReadMask, a_fWriteMask, a_fFlags, a_pfnRead, a_pfnWrite, HDA_MEM_IND_NAME(a_ShortRegNm), #a_ShortRegNm, a_szDesc) +#define HDA_REG_ENTRY_STR(a_offBar, a_cbReg, a_fReadMask, a_fWriteMask, a_fFlags, a_pfnRead, a_pfnWrite, a_StrPrefix, a_ShortRegNm, a_szDesc) \ + HDA_REG_ENTRY_EX(a_offBar, a_cbReg, a_fReadMask, a_fWriteMask, a_fFlags, a_pfnRead, a_pfnWrite, HDA_MEM_IND_NAME(a_StrPrefix ## a_ShortRegNm), #a_StrPrefix #a_ShortRegNm, #a_StrPrefix ": " a_szDesc) + +/** Emits a single audio stream register set (e.g. OSD0) at a specified offset. */ +#define HDA_REG_MAP_STRM(offset, name) \ + /* offset size read mask write mask flags read callback write callback index, abbrev, description */ \ + /* ------- ------- ---------- ---------- ---------------------- -------------- ----------------- ----------------------------- ----------- */ \ + /* Offset 0x80 (SD0) */ \ + HDA_REG_ENTRY_STR(offset, 0x00003, 0x00FF001F, 0x00F0001F, HDA_RD_F_SD_WRITE_RUN, hdaRegReadU24 , hdaRegWriteSDCTL , name, CTL , "Stream Descriptor Control"), \ + /* Offset 0x83 (SD0) */ \ + HDA_REG_ENTRY_STR(offset + 0x3, 0x00001, 0x0000003C, 0x0000001C, HDA_RD_F_SD_WRITE_RUN, hdaRegReadU8 , hdaRegWriteSDSTS , name, STS , "Status" ), \ + /* Offset 0x84 (SD0) */ \ + HDA_REG_ENTRY_STR(offset + 0x4, 0x00004, 0xFFFFFFFF, 0x00000000, HDA_RD_F_NONE, hdaRegReadLPIB, hdaRegWriteU32 , name, LPIB , "Link Position In Buffer" ), \ + /* Offset 0x88 (SD0) */ \ + HDA_REG_ENTRY_STR(offset + 0x8, 0x00004, 0xFFFFFFFF, 0xFFFFFFFF, HDA_RD_F_NONE, hdaRegReadU32 , hdaRegWriteSDCBL , name, CBL , "Cyclic Buffer Length" ), \ + /* Offset 0x8C (SD0) -- upper 8 bits are reserved */ \ + HDA_REG_ENTRY_STR(offset + 0xC, 0x00002, 0x0000FFFF, 0x000000FF, HDA_RD_F_NONE, hdaRegReadU16 , hdaRegWriteSDLVI , name, LVI , "Last Valid Index" ), \ + /* Reserved: FIFO Watermark. ** @todo Document this! */ \ + HDA_REG_ENTRY_STR(offset + 0xE, 0x00002, 0x00000007, 0x00000007, HDA_RD_F_NONE, hdaRegReadU16 , hdaRegWriteSDFIFOW, name, FIFOW, "FIFO Watermark" ), \ + /* Offset 0x90 (SD0) */ \ + HDA_REG_ENTRY_STR(offset + 0x10, 0x00002, 0x000000FF, 0x000000FF, HDA_RD_F_NONE, hdaRegReadU16 , hdaRegWriteSDFIFOS, name, FIFOS, "FIFO Size" ), \ + /* Offset 0x92 (SD0) */ \ + HDA_REG_ENTRY_STR(offset + 0x12, 0x00002, 0x00007F7F, 0x00007F7F, HDA_RD_F_NONE, hdaRegReadU16 , hdaRegWriteSDFMT , name, FMT , "Stream Format" ), \ + /* Reserved: 0x94 - 0x98. */ \ + /* Offset 0x98 (SD0) */ \ + HDA_REG_ENTRY_STR(offset + 0x18, 0x00004, 0xFFFFFF80, 0xFFFFFF80, HDA_RD_F_NONE, hdaRegReadU32 , hdaRegWriteSDBDPL , name, BDPL , "Buffer Descriptor List Pointer-Lower Base Address" ), \ + /* Offset 0x9C (SD0) */ \ + HDA_REG_ENTRY_STR(offset + 0x1C, 0x00004, 0xFFFFFFFF, 0xFFFFFFFF, HDA_RD_F_NONE, hdaRegReadU32 , hdaRegWriteSDBDPU , name, BDPU , "Buffer Descriptor List Pointer-Upper Base Address" ) + +/** Defines a single audio stream register set (e.g. OSD0). */ +#define HDA_REG_MAP_DEF_STREAM(index, name) \ + HDA_REG_MAP_STRM(HDA_REG_DESC_SD0_BASE + (index * 32 /* 0x20 */), name) + +/** Skylake stream registers. */ +#define HDA_REG_MAP_SKYLAKE_STRM(a_off, a_StrPrefix) \ + /* offset size read mask write mask flags read callback write callback index, abbrev, description */ \ + /* ------- ------- ---------- ---------- -------------- -------------- ----------------- ----------------------------- ----------- */ \ + /* 0x1084 */ \ + HDA_REG_ENTRY_STR(a_off + 0x04, 0x00004, 0xffffffff, 0x00000000, HDA_RD_F_NONE, hdaRegReadSDnPIB, hdaRegWriteUnimpl, a_StrPrefix, DPIB, "DMA Position In Buffer" ), \ + /* 0x1094 */ \ + HDA_REG_ENTRY_STR(a_off + 0x14, 0x00004, 0xffffffff, 0x00000000, HDA_RD_F_NONE, hdaRegReadSDnEFIFOS, hdaRegWriteUnimpl, a_StrPrefix, EFIFOS, "Extended FIFO Size" ) + + +/** See 302349 p 6.2. */ +static const HDAREGDESC g_aHdaRegMap[HDA_NUM_REGS] = +{ + /* offset size read mask write mask flags read callback write callback index + abbrev */ + /*------- ------- ---------- ---------- -------------- ---------------- ------------------- ------------------------ */ + HDA_REG_ENTRY(0x00000, 0x00002, 0x0000FFFB, 0x00000000, HDA_RD_F_NONE, hdaRegReadU16 , hdaRegWriteUnimpl , GCAP, "Global Capabilities" ), + HDA_REG_ENTRY(0x00002, 0x00001, 0x000000FF, 0x00000000, HDA_RD_F_NONE, hdaRegReadU8 , hdaRegWriteUnimpl , VMIN, "Minor Version" ), + HDA_REG_ENTRY(0x00003, 0x00001, 0x000000FF, 0x00000000, HDA_RD_F_NONE, hdaRegReadU8 , hdaRegWriteUnimpl , VMAJ, "Major Version" ), + HDA_REG_ENTRY(0x00004, 0x00002, 0x0000FFFF, 0x00000000, HDA_RD_F_NONE, hdaRegReadU16 , hdaRegWriteU16 , OUTPAY, "Output Payload Capabilities" ), + HDA_REG_ENTRY(0x00006, 0x00002, 0x0000FFFF, 0x00000000, HDA_RD_F_NONE, hdaRegReadU16 , hdaRegWriteUnimpl , INPAY, "Input Payload Capabilities" ), + HDA_REG_ENTRY(0x00008, 0x00004, 0x00000103, 0x00000103, HDA_RD_F_NONE, hdaRegReadU32 , hdaRegWriteGCTL , GCTL, "Global Control" ), + HDA_REG_ENTRY(0x0000c, 0x00002, 0x00007FFF, 0x00007FFF, HDA_RD_F_NONE, hdaRegReadU16 , hdaRegWriteU16 , WAKEEN, "Wake Enable" ), + HDA_REG_ENTRY(0x0000e, 0x00002, 0x00000007, 0x00000007, HDA_RD_F_NONE, hdaRegReadU8 , hdaRegWriteSTATESTS, STATESTS, "State Change Status" ), + HDA_REG_ENTRY(0x00010, 0x00002, 0xFFFFFFFF, 0x00000000, HDA_RD_F_NONE, hdaRegReadUnimpl, hdaRegWriteUnimpl , GSTS, "Global Status" ), + HDA_REG_ENTRY(0x00014, 0x00002, 0xFFFFFFFF, 0x00000000, HDA_RD_F_NONE, hdaRegReadU16 , hdaRegWriteUnimpl , LLCH, "Linked List Capabilities Header" ), + HDA_REG_ENTRY(0x00018, 0x00002, 0x0000FFFF, 0x00000000, HDA_RD_F_NONE, hdaRegReadU16 , hdaRegWriteU16 , OUTSTRMPAY, "Output Stream Payload Capability" ), + HDA_REG_ENTRY(0x0001A, 0x00002, 0x0000FFFF, 0x00000000, HDA_RD_F_NONE, hdaRegReadU16 , hdaRegWriteUnimpl , INSTRMPAY, "Input Stream Payload Capability" ), + HDA_REG_ENTRY(0x00020, 0x00004, 0xC00000FF, 0xC00000FF, HDA_RD_F_NONE, hdaRegReadU32 , hdaRegWriteU32 , INTCTL, "Interrupt Control" ), + HDA_REG_ENTRY(0x00024, 0x00004, 0xC00000FF, 0x00000000, HDA_RD_F_NONE, hdaRegReadU32 , hdaRegWriteUnimpl , INTSTS, "Interrupt Status" ), + HDA_REG_ENTRY_EX(0x00030, 0x00004, 0xFFFFFFFF, 0x00000000, HDA_RD_F_NONE, hdaRegReadWALCLK, hdaRegWriteUnimpl , 0, "WALCLK", "Wall Clock Counter" ), + HDA_REG_ENTRY(0x00034, 0x00004, 0x000000FF, 0x000000FF, HDA_RD_F_NONE, hdaRegReadU32 , hdaRegWriteSSYNC , SSYNC, "Stream Synchronization (old)" ), + HDA_REG_ENTRY(0x00038, 0x00004, 0x000000FF, 0x000000FF, HDA_RD_F_NONE, hdaRegReadU32 , hdaRegWriteNewSSYNC, SSYNC, "Stream Synchronization (new)" ), + HDA_REG_ENTRY(0x00040, 0x00004, 0xFFFFFF80, 0xFFFFFF80, HDA_RD_F_NONE, hdaRegReadU32 , hdaRegWriteBase , CORBLBASE, "CORB Lower Base Address" ), + HDA_REG_ENTRY(0x00044, 0x00004, 0xFFFFFFFF, 0xFFFFFFFF, HDA_RD_F_NONE, hdaRegReadU32 , hdaRegWriteBase , CORBUBASE, "CORB Upper Base Address" ), + HDA_REG_ENTRY(0x00048, 0x00002, 0x000000FF, 0x000000FF, HDA_RD_F_NONE, hdaRegReadU16 , hdaRegWriteCORBWP , CORBWP, "CORB Write Pointer" ), + HDA_REG_ENTRY(0x0004A, 0x00002, 0x000080FF, 0x00008000, HDA_RD_F_NONE, hdaRegReadU16 , hdaRegWriteCORBRP , CORBRP, "CORB Read Pointer" ), + HDA_REG_ENTRY(0x0004C, 0x00001, 0x00000003, 0x00000003, HDA_RD_F_NONE, hdaRegReadU8 , hdaRegWriteCORBCTL , CORBCTL, "CORB Control" ), + HDA_REG_ENTRY(0x0004D, 0x00001, 0x00000001, 0x00000001, HDA_RD_F_NONE, hdaRegReadU8 , hdaRegWriteCORBSTS , CORBSTS, "CORB Status" ), + HDA_REG_ENTRY(0x0004E, 0x00001, 0x000000F3, 0x00000003, HDA_RD_F_NONE, hdaRegReadU8 , hdaRegWriteCORBSIZE, CORBSIZE, "CORB Size" ), + HDA_REG_ENTRY(0x00050, 0x00004, 0xFFFFFF80, 0xFFFFFF80, HDA_RD_F_NONE, hdaRegReadU32 , hdaRegWriteBase , RIRBLBASE, "RIRB Lower Base Address" ), + HDA_REG_ENTRY(0x00054, 0x00004, 0xFFFFFFFF, 0xFFFFFFFF, HDA_RD_F_NONE, hdaRegReadU32 , hdaRegWriteBase , RIRBUBASE, "RIRB Upper Base Address" ), + HDA_REG_ENTRY(0x00058, 0x00002, 0x000000FF, 0x00008000, HDA_RD_F_NONE, hdaRegReadU8 , hdaRegWriteRIRBWP , RIRBWP, "RIRB Write Pointer" ), + HDA_REG_ENTRY(0x0005A, 0x00002, 0x000000FF, 0x000000FF, HDA_RD_F_NONE, hdaRegReadU16 , hdaRegWriteRINTCNT , RINTCNT, "Response Interrupt Count" ), + HDA_REG_ENTRY(0x0005C, 0x00001, 0x00000007, 0x00000007, HDA_RD_F_NONE, hdaRegReadU8 , hdaRegWriteU8 , RIRBCTL, "RIRB Control" ), + HDA_REG_ENTRY(0x0005D, 0x00001, 0x00000005, 0x00000005, HDA_RD_F_NONE, hdaRegReadU8 , hdaRegWriteRIRBSTS , RIRBSTS, "RIRB Status" ), + HDA_REG_ENTRY(0x0005E, 0x00001, 0x000000F3, 0x00000000, HDA_RD_F_NONE, hdaRegReadU8 , hdaRegWriteUnimpl , RIRBSIZE, "RIRB Size" ), + HDA_REG_ENTRY(0x00060, 0x00004, 0xFFFFFFFF, 0xFFFFFFFF, HDA_RD_F_NONE, hdaRegReadU32 , hdaRegWriteU32 , IC, "Immediate Command" ), + HDA_REG_ENTRY(0x00064, 0x00004, 0x00000000, 0xFFFFFFFF, HDA_RD_F_NONE, hdaRegReadU32 , hdaRegWriteUnimpl , IR, "Immediate Response" ), + HDA_REG_ENTRY(0x00068, 0x00002, 0x00000002, 0x00000002, HDA_RD_F_NONE, hdaRegReadIRS , hdaRegWriteIRS , IRS, "Immediate Command Status" ), + HDA_REG_ENTRY(0x00070, 0x00004, 0xFFFFFFFF, 0xFFFFFF81, HDA_RD_F_NONE, hdaRegReadU32 , hdaRegWriteBase , DPLBASE, "DMA Position Lower Base" ), + HDA_REG_ENTRY(0x00074, 0x00004, 0xFFFFFFFF, 0xFFFFFFFF, HDA_RD_F_NONE, hdaRegReadU32 , hdaRegWriteBase , DPUBASE, "DMA Position Upper Base" ), + /* 4 Serial Data In (SDI). */ + HDA_REG_MAP_DEF_STREAM(0, SD0), + HDA_REG_MAP_DEF_STREAM(1, SD1), + HDA_REG_MAP_DEF_STREAM(2, SD2), + HDA_REG_MAP_DEF_STREAM(3, SD3), + /* 4 Serial Data Out (SDO). */ + HDA_REG_MAP_DEF_STREAM(4, SD4), + HDA_REG_MAP_DEF_STREAM(5, SD5), + HDA_REG_MAP_DEF_STREAM(6, SD6), + HDA_REG_MAP_DEF_STREAM(7, SD7), + HDA_REG_ENTRY(0x00c00, 0x00004, 0xFFFFFFFF, 0x00000000, HDA_RD_F_NONE, hdaRegReadU32 , hdaRegWriteUnimpl , MLCH, "Multiple Links Capability Header" ), + HDA_REG_ENTRY(0x00c04, 0x00004, 0xFFFFFFFF, 0x00000000, HDA_RD_F_NONE, hdaRegReadU32 , hdaRegWriteUnimpl , MLCD, "Multiple Links Capability Declaration" ), + HDA_REG_MAP_SKYLAKE_STRM(0x01080, SD0), + HDA_REG_MAP_SKYLAKE_STRM(0x010a0, SD1), + HDA_REG_MAP_SKYLAKE_STRM(0x010c0, SD2), + HDA_REG_MAP_SKYLAKE_STRM(0x010e0, SD3), + HDA_REG_MAP_SKYLAKE_STRM(0x01100, SD4), + HDA_REG_MAP_SKYLAKE_STRM(0x01120, SD5), + HDA_REG_MAP_SKYLAKE_STRM(0x01140, SD6), + HDA_REG_MAP_SKYLAKE_STRM(0x01160, SD7), +}; + +#undef HDA_REG_ENTRY_EX +#undef HDA_REG_ENTRY +#undef HDA_REG_ENTRY_STR +#undef HDA_REG_MAP_STRM +#undef HDA_REG_MAP_DEF_STREAM + +/** + * HDA register aliases (HDA spec 3.3.45). + * @remarks Sorted by offReg. + * @remarks Lookup code ASSUMES this starts somewhere after g_aHdaRegMap ends. + */ +static struct HDAREGALIAS +{ + /** The alias register offset. */ + uint32_t offReg; + /** The register index. */ + int idxAlias; +} const g_aHdaRegAliases[] = +{ + { 0x2030, HDA_REG_WALCLK }, + { 0x2084, HDA_REG_SD0LPIB }, + { 0x20a4, HDA_REG_SD1LPIB }, + { 0x20c4, HDA_REG_SD2LPIB }, + { 0x20e4, HDA_REG_SD3LPIB }, + { 0x2104, HDA_REG_SD4LPIB }, + { 0x2124, HDA_REG_SD5LPIB }, + { 0x2144, HDA_REG_SD6LPIB }, + { 0x2164, HDA_REG_SD7LPIB } +}; + +#ifdef IN_RING3 + +/** HDABDLEDESC field descriptors for the v7+ saved state. */ +static SSMFIELD const g_aSSMBDLEDescFields7[] = +{ + SSMFIELD_ENTRY(HDABDLEDESC, u64BufAddr), + SSMFIELD_ENTRY(HDABDLEDESC, u32BufSize), + SSMFIELD_ENTRY(HDABDLEDESC, fFlags), + SSMFIELD_ENTRY_TERM() +}; + +/** HDABDLEDESC field descriptors for the v6 saved states. */ +static SSMFIELD const g_aSSMBDLEDescFields6[] = +{ + SSMFIELD_ENTRY(HDABDLEDESC, u64BufAddr), + SSMFIELD_ENTRY(HDABDLEDESC, u32BufSize), + SSMFIELD_ENTRY_CALLBACK(HDABDLEDESC, fFlags, hdaR3GetPutTrans_HDABDLEDESC_fFlags_6), + SSMFIELD_ENTRY_TERM() +}; + +/** HDABDLESTATE field descriptors for the v6 saved state. */ +static SSMFIELD const g_aSSMBDLEStateFields6[] = +{ + SSMFIELD_ENTRY(HDABDLESTATELEGACY, u32BDLIndex), + SSMFIELD_ENTRY(HDABDLESTATELEGACY, cbBelowFIFOW), + SSMFIELD_ENTRY_OLD(FIFO, 256), /* Deprecated; now is handled in the stream's circular buffer. */ + SSMFIELD_ENTRY(HDABDLESTATELEGACY, u32BufOff), + SSMFIELD_ENTRY_TERM() +}; + +/** HDABDLESTATE field descriptors for the v7+ saved state. */ +static SSMFIELD const g_aSSMBDLEStateFields7[] = +{ + SSMFIELD_ENTRY(HDABDLESTATELEGACY, u32BDLIndex), + SSMFIELD_ENTRY(HDABDLESTATELEGACY, cbBelowFIFOW), + SSMFIELD_ENTRY(HDABDLESTATELEGACY, u32BufOff), + SSMFIELD_ENTRY_TERM() +}; + +/** HDASTREAMSTATE field descriptors for the v6 saved state. */ +static SSMFIELD const g_aSSMStreamStateFields6[] = +{ + SSMFIELD_ENTRY_OLD(cBDLE, sizeof(uint16_t)), /* Deprecated. */ + SSMFIELD_ENTRY_OLD(uCurBDLE, sizeof(uint16_t)), /* We figure it out from LPID */ + SSMFIELD_ENTRY_OLD(fStop, 1), /* Deprecated; see SSMR3PutBool(). */ + SSMFIELD_ENTRY_OLD(fRunning, 1), /* Deprecated; using the HDA_SDCTL_RUN bit is sufficient. */ + SSMFIELD_ENTRY(HDASTREAMSTATE, fInReset), + SSMFIELD_ENTRY_TERM() +}; + +/** HDASTREAMSTATE field descriptors for the v7+ saved state. */ +static SSMFIELD const g_aSSMStreamStateFields7[] = +{ + SSMFIELD_ENTRY(HDASTREAMSTATE, idxCurBdle), /* For backward compatibility we save this. We use LPIB on restore. */ + SSMFIELD_ENTRY_OLD(uCurBDLEHi, sizeof(uint8_t)), /* uCurBDLE was 16-bit for some reason, so store/ignore the zero top byte. */ + SSMFIELD_ENTRY(HDASTREAMSTATE, fInReset), + SSMFIELD_ENTRY(HDASTREAMSTATE, tsTransferNext), + SSMFIELD_ENTRY_TERM() +}; + +/** HDABDLE field descriptors for the v1 thru v4 saved states. */ +static SSMFIELD const g_aSSMStreamBdleFields1234[] = +{ + SSMFIELD_ENTRY(HDABDLELEGACY, Desc.u64BufAddr), /* u64BdleCviAddr */ + SSMFIELD_ENTRY_OLD(u32BdleMaxCvi, sizeof(uint32_t)), /* u32BdleMaxCvi */ + SSMFIELD_ENTRY(HDABDLELEGACY, State.u32BDLIndex), /* u32BdleCvi */ + SSMFIELD_ENTRY(HDABDLELEGACY, Desc.u32BufSize), /* u32BdleCviLen */ + SSMFIELD_ENTRY(HDABDLELEGACY, State.u32BufOff), /* u32BdleCviPos */ + SSMFIELD_ENTRY_CALLBACK(HDABDLELEGACY, Desc.fFlags, hdaR3GetPutTrans_HDABDLE_Desc_fFlags_1thru4), /* fBdleCviIoc */ + SSMFIELD_ENTRY(HDABDLELEGACY, State.cbBelowFIFOW), /* cbUnderFifoW */ + SSMFIELD_ENTRY_OLD(au8FIFO, 256), /* au8FIFO */ + SSMFIELD_ENTRY_TERM() +}; + +#endif /* IN_RING3 */ + +/** + * 32-bit size indexed masks, i.e. g_afMasks[2 bytes] = 0xffff. + */ +static uint32_t const g_afMasks[5] = +{ + UINT32_C(0), UINT32_C(0x000000ff), UINT32_C(0x0000ffff), UINT32_C(0x00ffffff), UINT32_C(0xffffffff) +}; + + +#ifdef VBOX_STRICT + +/** + * Strict register accessor verifing defines and mapping table. + * @see HDA_REG + */ +DECLINLINE(uint32_t *) hdaStrictRegAccessor(PHDASTATE pThis, uint32_t idxMap, uint32_t idxReg) +{ + Assert(idxMap < RT_ELEMENTS(g_aHdaRegMap)); + AssertMsg(idxReg == g_aHdaRegMap[idxMap].idxReg, ("idxReg=%d\n", idxReg)); + return &pThis->au32Regs[idxReg]; +} + +/** + * Strict stream register accessor verifing defines and mapping table. + * @see HDA_STREAM_REG + */ +DECLINLINE(uint32_t *) hdaStrictStreamRegAccessor(PHDASTATE pThis, uint32_t idxMap0, uint32_t idxReg0, size_t idxStream) +{ + Assert(idxMap0 < RT_ELEMENTS(g_aHdaRegMap)); + AssertMsg(idxStream < RT_ELEMENTS(pThis->aStreams), ("%#zx\n", idxStream)); + AssertMsg(idxReg0 + idxStream * 10 == g_aHdaRegMap[idxMap0 + idxStream * 10].idxReg, + ("idxReg0=%d idxStream=%zx\n", idxReg0, idxStream)); + return &pThis->au32Regs[idxReg0 + idxStream * 10]; +} + +#endif /* VBOX_STRICT */ + + +/** + * Returns a new INTSTS value based on the current device state. + * + * @returns Determined INTSTS register value. + * @param pThis The shared HDA device state. + * + * @remarks This function does *not* set INTSTS! + */ +static uint32_t hdaGetINTSTS(PHDASTATE pThis) +{ + uint32_t intSts = 0; + + /* Check controller interrupts (RIRB, STATEST). */ + if (HDA_REG(pThis, RIRBSTS) & HDA_REG(pThis, RIRBCTL) & (HDA_RIRBCTL_ROIC | HDA_RIRBCTL_RINTCTL)) + { + intSts |= HDA_INTSTS_CIS; /* Set the Controller Interrupt Status (CIS). */ + } + + /* Check SDIN State Change Status Flags. */ + if (HDA_REG(pThis, STATESTS) & HDA_REG(pThis, WAKEEN)) + { + intSts |= HDA_INTSTS_CIS; /* Touch Controller Interrupt Status (CIS). */ + } + + /* For each stream, check if any interrupt status bit is set and enabled. */ + for (uint8_t iStrm = 0; iStrm < HDA_MAX_STREAMS; ++iStrm) + { + if (HDA_STREAM_REG(pThis, STS, iStrm) & HDA_STREAM_REG(pThis, CTL, iStrm) & (HDA_SDCTL_DEIE | HDA_SDCTL_FEIE | HDA_SDCTL_IOCE)) + { + Log3Func(("[SD%d] interrupt status set\n", iStrm)); + intSts |= RT_BIT(iStrm); + } + } + + if (intSts) + intSts |= HDA_INTSTS_GIS; /* Set the Global Interrupt Status (GIS). */ + + Log3Func(("-> 0x%x\n", intSts)); + + return intSts; +} + + +/** + * Processes (asserts/deasserts) the HDA interrupt according to the current state. + * + * @param pDevIns The device instance. + * @param pThis The shared HDA device state. + * @param pszSource Caller information. + */ +#if defined(LOG_ENABLED) || defined(DOXYGEN_RUNNING) +void hdaProcessInterrupt(PPDMDEVINS pDevIns, PHDASTATE pThis, const char *pszSource) +#else +void hdaProcessInterrupt(PPDMDEVINS pDevIns, PHDASTATE pThis) +#endif +{ + uint32_t uIntSts = hdaGetINTSTS(pThis); + + HDA_REG(pThis, INTSTS) = uIntSts; + + /* NB: It is possible to have GIS set even when CIE/SIEn are all zero; the GIS bit does + * not control the interrupt signal. See Figure 4 on page 54 of the HDA 1.0a spec. + */ + /* Global Interrupt Enable (GIE) set? */ + if ( (HDA_REG(pThis, INTCTL) & HDA_INTCTL_GIE) + && (HDA_REG(pThis, INTSTS) & HDA_REG(pThis, INTCTL) & (HDA_INTCTL_CIE | HDA_STRMINT_MASK))) + { + Log3Func(("Asserted (%s)\n", pszSource)); + + PDMDevHlpPCISetIrq(pDevIns, 0, 1 /* Assert */); + pThis->u8IRQL = 1; + +#ifdef DEBUG + pThis->Dbg.IRQ.tsAssertedNs = RTTimeNanoTS(); + pThis->Dbg.IRQ.tsProcessedLastNs = pThis->Dbg.IRQ.tsAssertedNs; +#endif + } + else + { + Log3Func(("Deasserted (%s)\n", pszSource)); + + PDMDevHlpPCISetIrq(pDevIns, 0, 0 /* Deassert */); + pThis->u8IRQL = 0; + } +} + + +/** + * Looks up a register at the exact offset given by @a offReg. + * + * @returns Register index on success, -1 if not found. + * @param offReg The register offset. + */ +static int hdaRegLookup(uint32_t offReg) +{ + /* + * Aliases. + */ + if (offReg >= g_aHdaRegAliases[0].offReg) + { + for (unsigned i = 0; i < RT_ELEMENTS(g_aHdaRegAliases); i++) + if (offReg == g_aHdaRegAliases[i].offReg) + return g_aHdaRegAliases[i].idxAlias; + Assert(g_aHdaRegMap[RT_ELEMENTS(g_aHdaRegMap) - 1].off < offReg); + return -1; + } + + /* + * Binary search the + */ + int idxEnd = RT_ELEMENTS(g_aHdaRegMap); + int idxLow = 0; + for (;;) + { + int idxMiddle = idxLow + (idxEnd - idxLow) / 2; + if (offReg < g_aHdaRegMap[idxMiddle].off) + { + if (idxLow != idxMiddle) + idxEnd = idxMiddle; + else + break; + } + else if (offReg > g_aHdaRegMap[idxMiddle].off) + { + idxLow = idxMiddle + 1; + if (idxLow < idxEnd) + { /* likely */ } + else + break; + } + else + return idxMiddle; + } + +#ifdef RT_STRICT + for (unsigned i = 0; i < RT_ELEMENTS(g_aHdaRegMap); i++) + Assert(g_aHdaRegMap[i].off != offReg); +#endif + return -1; +} + +#ifdef IN_RING3 + +/** + * Looks up a register covering the offset given by @a offReg. + * + * @returns Register index on success, -1 if not found. + * @param offReg The register offset. + */ +static int hdaR3RegLookupWithin(uint32_t offReg) +{ + /* + * Aliases. + */ + if (offReg >= g_aHdaRegAliases[0].offReg) + { + for (unsigned i = 0; i < RT_ELEMENTS(g_aHdaRegAliases); i++) + { + uint32_t off = offReg - g_aHdaRegAliases[i].offReg; + if (off < 4 && off < g_aHdaRegMap[g_aHdaRegAliases[i].idxAlias].cb) + return g_aHdaRegAliases[i].idxAlias; + } + Assert(g_aHdaRegMap[RT_ELEMENTS(g_aHdaRegMap) - 1].off < offReg); + return -1; + } + + /* + * Binary search the register map. + */ + int idxEnd = RT_ELEMENTS(g_aHdaRegMap); + int idxLow = 0; + for (;;) + { + int idxMiddle = idxLow + (idxEnd - idxLow) / 2; + if (offReg < g_aHdaRegMap[idxMiddle].off) + { + if (idxLow == idxMiddle) + break; + idxEnd = idxMiddle; + } + else if (offReg >= g_aHdaRegMap[idxMiddle].off + g_aHdaRegMap[idxMiddle].cb) + { + idxLow = idxMiddle + 1; + if (idxLow >= idxEnd) + break; + } + else + return idxMiddle; + } + +# ifdef RT_STRICT + for (unsigned i = 0; i < RT_ELEMENTS(g_aHdaRegMap); i++) + Assert(offReg - g_aHdaRegMap[i].off >= g_aHdaRegMap[i].cb); +# endif + return -1; +} + +#endif /* IN_RING3 */ + +#ifdef IN_RING3 /* Codec is not yet kosher enough for ring-0. @bugref{9890c64} */ + +/** + * Synchronizes the CORB / RIRB buffers between internal <-> device state. + * + * @returns VBox status code. + * + * @param pDevIns The device instance. + * @param pThis The shared HDA device state. + * @param fLocal Specify true to synchronize HDA state's CORB buffer with the device state, + * or false to synchronize the device state's RIRB buffer with the HDA state. + * + * @todo r=andy Break this up into two functions? + */ +static int hdaR3CmdSync(PPDMDEVINS pDevIns, PHDASTATE pThis, bool fLocal) +{ + int rc = VINF_SUCCESS; + if (fLocal) + { + if (pThis->u64CORBBase) + { + Assert(pThis->cbCorbBuf); + rc = PDMDevHlpPCIPhysRead(pDevIns, pThis->u64CORBBase, pThis->au32CorbBuf, + RT_MIN(pThis->cbCorbBuf, sizeof(pThis->au32CorbBuf))); + Log3Func(("CORB: read %RGp LB %#x (%Rrc)\n", pThis->u64CORBBase, pThis->cbCorbBuf, rc)); + AssertRCReturn(rc, rc); + } + } + else + { + if (pThis->u64RIRBBase) + { + Assert(pThis->cbRirbBuf); + + rc = PDMDevHlpPCIPhysWrite(pDevIns, pThis->u64RIRBBase, pThis->au64RirbBuf, + RT_MIN(pThis->cbRirbBuf, sizeof(pThis->au64RirbBuf))); + Log3Func(("RIRB: phys read %RGp LB %#x (%Rrc)\n", pThis->u64RIRBBase, pThis->cbRirbBuf, rc)); + AssertRCReturn(rc, rc); + } + } + +# ifdef DEBUG_CMD_BUFFER + LogFunc(("fLocal=%RTbool\n", fLocal)); + + uint8_t i = 0; + do + { + LogFunc(("CORB%02x: ", i)); + uint8_t j = 0; + do + { + const char *pszPrefix; + if ((i + j) == HDA_REG(pThis, CORBRP)) + pszPrefix = "[R]"; + else if ((i + j) == HDA_REG(pThis, CORBWP)) + pszPrefix = "[W]"; + else + pszPrefix = " "; /* three spaces */ + Log((" %s%08x", pszPrefix, pThis->pu32CorbBuf[i + j])); + j++; + } while (j < 8); + Log(("\n")); + i += 8; + } while (i != 0); + + do + { + LogFunc(("RIRB%02x: ", i)); + uint8_t j = 0; + do + { + const char *prefix; + if ((i + j) == HDA_REG(pThis, RIRBWP)) + prefix = "[W]"; + else + prefix = " "; + Log((" %s%016lx", prefix, pThis->pu64RirbBuf[i + j])); + } while (++j < 8); + Log(("\n")); + i += 8; + } while (i != 0); +# endif + return rc; +} + + +/** + * Processes the next CORB buffer command in the queue. + * + * This will invoke the HDA codec ring-3 verb dispatcher. + * + * @returns VBox status code. + * @param pDevIns The device instance. + * @param pThis The shared HDA device state. + * @param pThisCC The ring-0 HDA device state. + */ +static int hdaR3CORBCmdProcess(PPDMDEVINS pDevIns, PHDASTATE pThis, PHDASTATECC pThisCC) +{ + Log3Func(("ENTER CORB(RP:%x, WP:%x) RIRBWP:%x\n", HDA_REG(pThis, CORBRP), HDA_REG(pThis, CORBWP), HDA_REG(pThis, RIRBWP))); + + if (!(HDA_REG(pThis, CORBCTL) & HDA_CORBCTL_DMA)) + { + LogFunc(("CORB DMA not active, skipping\n")); + return VINF_SUCCESS; + } + + Assert(pThis->cbCorbBuf); + + int rc = hdaR3CmdSync(pDevIns, pThis, true /* Sync from guest */); + AssertRCReturn(rc, rc); + + /* + * Prepare local copies of relevant registers. + */ + uint16_t cIntCnt = HDA_REG(pThis, RINTCNT) & 0xff; + if (!cIntCnt) /* 0 means 256 interrupts. */ + cIntCnt = HDA_MAX_RINTCNT; + + uint32_t const cCorbEntries = RT_MIN(RT_MAX(pThis->cbCorbBuf, 1), sizeof(pThis->au32CorbBuf)) / HDA_CORB_ELEMENT_SIZE; + uint8_t const corbWp = HDA_REG(pThis, CORBWP) % cCorbEntries; + uint8_t corbRp = HDA_REG(pThis, CORBRP); + uint8_t rirbWp = HDA_REG(pThis, RIRBWP); + + /* + * The loop. + */ + Log3Func(("START CORB(RP:%x, WP:%x) RIRBWP:%x, RINTCNT:%RU8/%RU8\n", corbRp, corbWp, rirbWp, pThis->u16RespIntCnt, cIntCnt)); + while (corbRp != corbWp) + { + /* Fetch the command from the CORB. */ + corbRp = (corbRp + 1) /* Advance +1 as the first command(s) are at CORBWP + 1. */ % cCorbEntries; + uint32_t const uCmd = pThis->au32CorbBuf[corbRp]; + + /* + * Execute the command. + */ + uint64_t uResp = 0; + rc = hdaR3CodecLookup(&pThisCC->Codec, HDA_CODEC_CMD(uCmd, 0 /* Codec index */), &uResp); + if (RT_SUCCESS(rc)) + AssertRCSuccess(rc); /* no informational statuses */ + else + Log3Func(("Lookup for codec verb %08x failed: %Rrc\n", uCmd, rc)); + Log3Func(("Codec verb %08x -> response %016RX64\n", uCmd, uResp)); + + if ( (uResp & CODEC_RESPONSE_UNSOLICITED) + && !(HDA_REG(pThis, GCTL) & HDA_GCTL_UNSOL)) + { + LogFunc(("Unexpected unsolicited response.\n")); + HDA_REG(pThis, CORBRP) = corbRp; + /** @todo r=andy No RIRB syncing to guest required in that case? */ + /** @todo r=bird: Why isn't RIRBWP updated here. The response might come + * after already processing several commands, can't it? (When you think + * about it, it is bascially the same question as Andy is asking.) */ + return VINF_SUCCESS; + } + + /* + * Store the response in the RIRB. + */ + AssertCompile(HDA_RIRB_SIZE == RT_ELEMENTS(pThis->au64RirbBuf)); + rirbWp = (rirbWp + 1) % HDA_RIRB_SIZE; + pThis->au64RirbBuf[rirbWp] = uResp; + + /* + * Send interrupt if needed. + */ + bool fSendInterrupt = false; + pThis->u16RespIntCnt++; + if (pThis->u16RespIntCnt >= cIntCnt) /* Response interrupt count reached? */ + { + pThis->u16RespIntCnt = 0; /* Reset internal interrupt response counter. */ + + Log3Func(("Response interrupt count reached (%RU16)\n", pThis->u16RespIntCnt)); + fSendInterrupt = true; + } + else if (corbRp == corbWp) /* Did we reach the end of the current command buffer? */ + { + Log3Func(("Command buffer empty\n")); + fSendInterrupt = true; + } + if (fSendInterrupt) + { + if (HDA_REG(pThis, RIRBCTL) & HDA_RIRBCTL_RINTCTL) /* Response Interrupt Control (RINTCTL) enabled? */ + { + HDA_REG(pThis, RIRBSTS) |= HDA_RIRBSTS_RINTFL; + HDA_PROCESS_INTERRUPT(pDevIns, pThis); + } + } + } + + /* + * Put register locals back. + */ + Log3Func(("END CORB(RP:%x, WP:%x) RIRBWP:%x, RINTCNT:%RU8/%RU8\n", corbRp, corbWp, rirbWp, pThis->u16RespIntCnt, cIntCnt)); + HDA_REG(pThis, CORBRP) = corbRp; + HDA_REG(pThis, RIRBWP) = rirbWp; + + /* + * Write out the response. + */ + rc = hdaR3CmdSync(pDevIns, pThis, false /* Sync to guest */); + AssertRC(rc); + + return rc; +} + +#endif /* IN_RING3 - @bugref{9890c64} */ + +#ifdef IN_RING3 +/** + * @callback_method_impl{FNPDMTASKDEV, Continue CORB DMA in ring-3} + */ +static DECLCALLBACK(void) hdaR3CorbDmaTaskWorker(PPDMDEVINS pDevIns, void *pvUser) +{ + PHDASTATE pThis = PDMDEVINS_2_DATA(pDevIns, PHDASTATE); + PHDASTATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATER3); + RT_NOREF(pvUser); + LogFlowFunc(("\n")); + + DEVHDA_LOCK(pDevIns, pThis); + hdaR3CORBCmdProcess(pDevIns, pThis, pThisCC); + DEVHDA_UNLOCK(pDevIns, pThis); + +} +#endif /* IN_RING3 */ + +/* Register access handlers. */ + +static VBOXSTRICTRC hdaRegReadUnimpl(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t *pu32Value) +{ + RT_NOREF(pDevIns, pThis, iReg); + *pu32Value = 0; + return VINF_SUCCESS; +} + +static VBOXSTRICTRC hdaRegWriteUnimpl(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + RT_NOREF(pDevIns, pThis, iReg, u32Value); + return VINF_SUCCESS; +} + +/* U8 */ +static VBOXSTRICTRC hdaRegReadU8(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t *pu32Value) +{ + Assert(((pThis->au32Regs[g_aHdaRegMap[iReg].idxReg] & g_aHdaRegMap[iReg].fReadableMask) & UINT32_C(0xffffff00)) == 0); + return hdaRegReadU32(pDevIns, pThis, iReg, pu32Value); +} + +static VBOXSTRICTRC hdaRegWriteU8(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + Assert((u32Value & 0xffffff00) == 0); + return hdaRegWriteU32(pDevIns, pThis, iReg, u32Value); +} + +/* U16 */ +static VBOXSTRICTRC hdaRegReadU16(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t *pu32Value) +{ + Assert(((pThis->au32Regs[g_aHdaRegMap[iReg].idxReg] & g_aHdaRegMap[iReg].fReadableMask) & UINT32_C(0xffff0000)) == 0); + return hdaRegReadU32(pDevIns, pThis, iReg, pu32Value); +} + +static VBOXSTRICTRC hdaRegWriteU16(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + Assert((u32Value & 0xffff0000) == 0); + return hdaRegWriteU32(pDevIns, pThis, iReg, u32Value); +} + +/* U24 */ +static VBOXSTRICTRC hdaRegReadU24(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t *pu32Value) +{ + Assert(((pThis->au32Regs[g_aHdaRegMap[iReg].idxReg] & g_aHdaRegMap[iReg].fReadableMask) & UINT32_C(0xff000000)) == 0); + return hdaRegReadU32(pDevIns, pThis, iReg, pu32Value); +} + +#ifdef IN_RING3 +static VBOXSTRICTRC hdaRegWriteU24(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + Assert((u32Value & 0xff000000) == 0); + return hdaRegWriteU32(pDevIns, pThis, iReg, u32Value); +} +#endif + +/* U32 */ +static VBOXSTRICTRC hdaRegReadU32(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t *pu32Value) +{ + RT_NOREF(pDevIns); + + uint32_t const iRegMem = g_aHdaRegMap[iReg].idxReg; + *pu32Value = pThis->au32Regs[iRegMem] & g_aHdaRegMap[iReg].fReadableMask; + return VINF_SUCCESS; +} + +static VBOXSTRICTRC hdaRegWriteU32(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + RT_NOREF(pDevIns); + + uint32_t const iRegMem = g_aHdaRegMap[iReg].idxReg; + pThis->au32Regs[iRegMem] = (u32Value & g_aHdaRegMap[iReg].fWritableMask) + | (pThis->au32Regs[iRegMem] & ~g_aHdaRegMap[iReg].fWritableMask); + return VINF_SUCCESS; +} + +static VBOXSTRICTRC hdaRegWriteGCTL(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + RT_NOREF(pDevIns, iReg); + + if (u32Value & HDA_GCTL_CRST) + { + /* Set the CRST bit to indicate that we're leaving reset mode. */ + HDA_REG(pThis, GCTL) |= HDA_GCTL_CRST; + LogFunc(("Guest leaving HDA reset\n")); + } + else + { +#ifdef IN_RING3 + /* Enter reset state. */ + LogFunc(("Guest entering HDA reset with DMA(RIRB:%s, CORB:%s)\n", + HDA_REG(pThis, CORBCTL) & HDA_CORBCTL_DMA ? "on" : "off", + HDA_REG(pThis, RIRBCTL) & HDA_RIRBCTL_RDMAEN ? "on" : "off")); + + /* Clear the CRST bit to indicate that we're in reset state. */ + HDA_REG(pThis, GCTL) &= ~HDA_GCTL_CRST; + + hdaR3GCTLReset(pDevIns, pThis, PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATER3)); +#else + return VINF_IOM_R3_MMIO_WRITE; +#endif + } + + if (u32Value & HDA_GCTL_FCNTRL) + { + /* Flush: GSTS:1 set, see 6.2.6. */ + HDA_REG(pThis, GSTS) |= HDA_GSTS_FSTS; /* Set the flush status. */ + /* DPLBASE and DPUBASE should be initialized with initial value (see 6.2.6). */ + } + + return VINF_SUCCESS; +} + +static VBOXSTRICTRC hdaRegWriteSTATESTS(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + RT_NOREF(pDevIns); + + uint32_t v = HDA_REG_IND(pThis, iReg); + uint32_t nv = u32Value & HDA_STATESTS_SCSF_MASK; + + HDA_REG(pThis, STATESTS) &= ~(v & nv); /* Write of 1 clears corresponding bit. */ + + return VINF_SUCCESS; +} + +static VBOXSTRICTRC hdaRegReadLPIB(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t *pu32Value) +{ + RT_NOREF(pDevIns); + uint8_t const uSD = HDA_SD_NUM_FROM_REG(pThis, LPIB, iReg); + uint32_t const uLPIB = HDA_STREAM_REG(pThis, LPIB, uSD); + +#ifdef VBOX_HDA_WITH_ON_REG_ACCESS_DMA + /* + * Should we consider doing DMA work while we're here? That would require + * the stream to have the DMA engine enabled and be an output stream. + */ + if ( (HDA_STREAM_REG(pThis, CTL, uSD) & HDA_SDCTL_RUN) + && hdaGetDirFromSD(uSD) == PDMAUDIODIR_OUT + && uSD < RT_ELEMENTS(pThis->aStreams) /* paranoia */) + { + PHDASTREAM const pStreamShared = &pThis->aStreams[uSD]; + Assert(pStreamShared->u8SD == uSD); + if (pStreamShared->State.fRunning /* should be same as HDA_SDCTL_RUN, but doesn't hurt to check twice */) + { + /* + * Calculate where the DMA engine should be according to the clock, if we can. + */ + uint32_t const cbFrame = PDMAudioPropsFrameSize(&pStreamShared->State.Cfg.Props); + uint32_t const cbPeriod = pStreamShared->State.cbCurDmaPeriod; + if (cbPeriod > cbFrame) + { + AssertMsg(pStreamShared->State.cbDmaTotal < cbPeriod, ("%#x vs %#x\n", pStreamShared->State.cbDmaTotal, cbPeriod)); + uint64_t const tsTransferNext = pStreamShared->State.tsTransferNext; + uint64_t const tsNow = PDMDevHlpTimerGet(pDevIns, pThis->aStreams[0].hTimer); /* only #0 works in r0 */ + uint32_t cbFuture; + if (tsNow < tsTransferNext) + { + /** @todo ASSUMES nanosecond clock ticks, need to make this + * resolution independent. */ + cbFuture = PDMAudioPropsNanoToBytes(&pStreamShared->State.Cfg.Props, tsTransferNext - tsNow); + cbFuture = RT_MIN(cbFuture, cbPeriod - cbFrame); + } + else + { + /* We've hit/overshot the timer deadline. Return to ring-3 if we're + not already there to increase the chance that we'll help expidite + the timer. If we're already in ring-3, do all but the last frame. */ +# ifndef IN_RING3 + LogFunc(("[SD%RU8] DMA period expired: tsNow=%RU64 >= tsTransferNext=%RU64 -> VINF_IOM_R3_MMIO_READ\n", + tsNow, tsTransferNext)); + return VINF_IOM_R3_MMIO_READ; +# else + cbFuture = cbPeriod - cbFrame; + LogFunc(("[SD%RU8] DMA period expired: tsNow=%RU64 >= tsTransferNext=%RU64 -> cbFuture=%#x (cbPeriod=%#x - cbFrame=%#x)\n", + tsNow, tsTransferNext, cbFuture, cbPeriod, cbFrame)); +# endif + } + uint32_t const offNow = PDMAudioPropsFloorBytesToFrame(&pStreamShared->State.Cfg.Props, cbPeriod - cbFuture); + + /* + * Should we transfer a little? Minimum is 64 bytes (semi-random, + * suspect real hardware might be doing some cache aligned stuff, + * which might soon get complicated if you take unaligned buffers + * into consideration and which cache line size (128 bytes is just + * as likely as 64 or 32 bytes)). + */ + uint32_t cbDmaTotal = pStreamShared->State.cbDmaTotal; + if (cbDmaTotal + 64 <= offNow) + { + VBOXSTRICTRC rcStrict = hdaStreamDoOnAccessDmaOutput(pDevIns, pThis, pStreamShared, + tsNow, offNow - cbDmaTotal); + + /* LPIB is updated by hdaStreamDoOnAccessDmaOutput, so get the new value. */ + uint32_t const uNewLpib = HDA_STREAM_REG(pThis, LPIB, uSD); + *pu32Value = uNewLpib; + + LogFlowFunc(("[SD%RU8] LPIB=%#RX32 (CBL=%#RX32 PrevLPIB=%#x offNow=%#x) rcStrict=%Rrc\n", uSD, + uNewLpib, HDA_STREAM_REG(pThis, CBL, uSD), uLPIB, offNow, VBOXSTRICTRC_VAL(rcStrict) )); + return rcStrict; + } + + /* + * Do nothing, just return LPIB as it is. + */ + LogFlowFunc(("[SD%RU8] Skipping DMA transfer: cbDmaTotal=%#x offNow=%#x\n", uSD, cbDmaTotal, offNow)); + } + else + LogFunc(("[SD%RU8] cbPeriod=%#x <= cbFrame=%#x!!\n", uSD, cbPeriod, cbFrame)); + } + else + LogFunc(("[SD%RU8] fRunning=0 SDnCTL=%#x!!\n", uSD, HDA_STREAM_REG(pThis, CTL, uSD) )); + } +#endif /* VBOX_HDA_WITH_ON_REG_ACCESS_DMA */ + + LogFlowFunc(("[SD%RU8] LPIB=%#RX32 (CBL=%#RX32 CTL=%#RX32)\n", + uSD, uLPIB, HDA_STREAM_REG(pThis, CBL, uSD), HDA_STREAM_REG(pThis, CTL, uSD) )); + *pu32Value = uLPIB; + return VINF_SUCCESS; +} + +/** + * Gets the wall clock. + * + * Used by hdaRegReadWALCLK() and 'info hda'. + * + * @returns Strict VBox status code if @a fDoDma is @c true, otherwise + * VINF_SUCCESS. + * @param pDevIns The device instance. + * @param pThis The shared HDA device state. + * @param fDoDma Whether to consider doing DMA work or not. + * @param puWallNow Where to return the current wall clock time. + */ +static VBOXSTRICTRC hdaQueryWallClock(PPDMDEVINS pDevIns, PHDASTATE pThis, bool fDoDma, uint64_t *puWallNow) +{ + /* + * The wall clock is calculated from the virtual sync clock. Since + * the clock is supposed to reset to zero on controller reset, a + * start offset is subtracted. + * + * In addition, we hold the clock back when there are active DMA engines + * so that the guest won't conclude we've gotten further in the buffer + * processing than what we really have. (We generally read a whole buffer + * at once when the IOC is due, so we're a lot later than what real + * hardware would be in reading/writing the buffers.) + * + * Here are some old notes from the DMA engine that might be useful even + * if a little dated: + * + * Note 1) Only certain guests (like Linux' snd_hda_intel) rely on the WALCLK register + * in order to determine the correct timing of the sound device. Other guests + * like Windows 7 + 10 (or even more exotic ones like Haiku) will completely + * ignore this. + * + * Note 2) When updating the WALCLK register too often / early (or even in a non-monotonic + * fashion) this *will* upset guest device drivers and will completely fuck up the + * sound output. Running VLC on the guest will tell! + */ + uint64_t const uFreq = PDMDevHlpTimerGetFreq(pDevIns, pThis->aStreams[0].hTimer); + Assert(uFreq <= UINT32_MAX); + uint64_t const tsStart = 0; /** @todo pThis->tsWallClkStart (as it is reset on controller reset) */ + uint64_t const tsNow = PDMDevHlpTimerGet(pDevIns, pThis->aStreams[0].hTimer); + + /* Find the oldest DMA transfer timestamp from the active streams. */ + int iDmaNow = -1; + uint64_t tsDmaNow = tsNow; + for (size_t i = 0; i < RT_ELEMENTS(pThis->aStreams); i++) + if (pThis->aStreams[i].State.fRunning) + { +#ifdef VBOX_HDA_WITH_ON_REG_ACCESS_DMA + /* Linux is reading WALCLK before one of the DMA position reads and + we've already got the current time from TM, so check if we should + do a little bit of DMA'ing here to help WALCLK ahead. */ + if (fDoDma) + { + if (hdaGetDirFromSD((uint8_t)i) == PDMAUDIODIR_OUT) + { + VBOXSTRICTRC rcStrict = hdaStreamMaybeDoOnAccessDmaOutput(pDevIns, pThis, &pThis->aStreams[i], tsNow); + if (rcStrict == VINF_SUCCESS) + { /* likely */ } + else + return rcStrict; + } + } +#endif + + if ( pThis->aStreams[i].State.tsTransferLast < tsDmaNow + && pThis->aStreams[i].State.tsTransferLast > tsStart) + { + tsDmaNow = pThis->aStreams[i].State.tsTransferLast; + iDmaNow = (int)i; + } + } + + /* Convert it to wall clock ticks. */ + uint64_t const uWallClkNow = ASMMultU64ByU32DivByU32(tsDmaNow - tsStart, + 24000000 /*Wall clock frequency */, + uFreq); + Log3Func(("Returning %#RX64 - tsNow=%#RX64 tsDmaNow=%#RX64 (%d) -> %#RX64\n", + uWallClkNow, tsNow, tsDmaNow, iDmaNow, tsNow - tsDmaNow)); + RT_NOREF(iDmaNow, fDoDma); + *puWallNow = uWallClkNow; + return VINF_SUCCESS; +} + +static VBOXSTRICTRC hdaRegReadWALCLK(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t *pu32Value) +{ + uint64_t uWallNow = 0; + VBOXSTRICTRC rcStrict = hdaQueryWallClock(pDevIns, pThis, true /*fDoDma*/, &uWallNow); + if (rcStrict == VINF_SUCCESS) + { + *pu32Value = (uint32_t)uWallNow; + return VINF_SUCCESS; + } + RT_NOREF(iReg); + return rcStrict; +} + +static VBOXSTRICTRC hdaRegWriteSSYNCWorker(PHDASTATE pThis, uint32_t iReg, uint32_t u32Value, const char *pszCaller) +{ + RT_NOREF(pszCaller); + + /* + * The SSYNC register is a DMA pause mask where each bit represents a stream. + * There should be no DMA transfers going down the driver chains when the a + * stream has its bit set here. There are two scenarios described in the + * specification, starting and stopping, though it can probably be used for + * other purposes if the guest gets creative... + * + * Anyway, if we ever want to implement this, we'd be manipulating the DMA + * timers of the affected streams here, I think. At least in the start + * scenario, we would run the first DMA transfers from here. + */ + uint32_t const fOld = HDA_REG(pThis, SSYNC); + uint32_t const fNew = (u32Value & g_aHdaRegMap[iReg].fWritableMask) + | (fOld & ~g_aHdaRegMap[iReg].fWritableMask); + uint32_t const fChanged = (fNew ^ fOld) & (RT_BIT_32(HDA_MAX_STREAMS) - 1); + if (fChanged) + { +#if 0 /** @todo implement SSYNC: ndef IN_RING3 */ + Log3(("%s: Going to ring-3 to handle SSYNC change: %#x\n", pszCaller, fChanged)); + return VINF_IOM_R3_MMIO_WRITE; +#else + for (uint32_t fMask = 1, i = 0; fMask < RT_BIT_32(HDA_MAX_STREAMS); i++, fMask <<= 1) + if (!(fChanged & fMask)) + { /* nothing */ } + else if (fNew & fMask) + { + Log3(("%Rfn: SSYNC bit %u set\n", pszCaller, i)); + /* See code in SDCTL around hdaR3StreamTimerMain call. */ + } + else + { + Log3(("%Rfn: SSYNC bit %u cleared\n", pszCaller, i)); + /* The next DMA timer callout will not do anything. */ + } +#endif + } + + HDA_REG(pThis, SSYNC) = fNew; + return VINF_SUCCESS; +} + +static VBOXSTRICTRC hdaRegWriteSSYNC(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + RT_NOREF(pDevIns); + return hdaRegWriteSSYNCWorker(pThis, iReg, u32Value, __FUNCTION__); +} + +static VBOXSTRICTRC hdaRegWriteNewSSYNC(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + RT_NOREF(pDevIns); + return hdaRegWriteSSYNCWorker(pThis, iReg, u32Value, __FUNCTION__); +} + +static VBOXSTRICTRC hdaRegWriteCORBRP(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + RT_NOREF(pDevIns, iReg); + if (u32Value & HDA_CORBRP_RST) + { + /* Do a CORB reset. */ + if (pThis->cbCorbBuf) + RT_ZERO(pThis->au32CorbBuf); + + LogRel2(("HDA: CORB reset\n")); + HDA_REG(pThis, CORBRP) = HDA_CORBRP_RST; /* Clears the pointer. */ + } + else + HDA_REG(pThis, CORBRP) &= ~HDA_CORBRP_RST; /* Only CORBRP_RST bit is writable. */ + + return VINF_SUCCESS; +} + +static VBOXSTRICTRC hdaRegWriteCORBCTL(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + VBOXSTRICTRC rc = hdaRegWriteU8(pDevIns, pThis, iReg, u32Value); + AssertRCSuccess(VBOXSTRICTRC_VAL(rc)); + + if (HDA_REG(pThis, CORBCTL) & HDA_CORBCTL_DMA) /* DMA engine started? */ + { +#ifdef IN_RING3 /** @todo do PDMDevHlpTaskTrigger everywhere? */ + rc = hdaR3CORBCmdProcess(pDevIns, pThis, PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATECC)); +#else + rc = PDMDevHlpTaskTrigger(pDevIns, pThis->hCorbDmaTask); + if (rc != VINF_SUCCESS && RT_SUCCESS(rc)) + rc = VINF_SUCCESS; +#endif + } + else + LogFunc(("CORB DMA not running, skipping\n")); + + return rc; +} + +static VBOXSTRICTRC hdaRegWriteCORBSIZE(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + RT_NOREF(pDevIns, iReg); + + if (!(HDA_REG(pThis, CORBCTL) & HDA_CORBCTL_DMA)) /* Ignore request if CORB DMA engine is (still) running. */ + { + u32Value = (u32Value & HDA_CORBSIZE_SZ); + + uint16_t cEntries; + switch (u32Value) + { + case 0: /* 8 byte; 2 entries. */ + cEntries = 2; + break; + case 1: /* 64 byte; 16 entries. */ + cEntries = 16; + break; + case 2: /* 1 KB; 256 entries. */ + cEntries = HDA_CORB_SIZE; /* default. */ + break; + default: + LogRel(("HDA: Guest tried to set an invalid CORB size (0x%x), keeping default\n", u32Value)); + u32Value = 2; + cEntries = HDA_CORB_SIZE; /* Use default size. */ + break; + } + + uint32_t cbCorbBuf = cEntries * HDA_CORB_ELEMENT_SIZE; + Assert(cbCorbBuf <= sizeof(pThis->au32CorbBuf)); /* paranoia */ + + if (cbCorbBuf != pThis->cbCorbBuf) + { + RT_ZERO(pThis->au32CorbBuf); /* Clear CORB when setting a new size. */ + pThis->cbCorbBuf = cbCorbBuf; + } + + LogFunc(("CORB buffer size is now %RU32 bytes (%u entries)\n", pThis->cbCorbBuf, pThis->cbCorbBuf / HDA_CORB_ELEMENT_SIZE)); + + HDA_REG(pThis, CORBSIZE) = u32Value; + } + else + LogFunc(("CORB DMA is (still) running, skipping\n")); + return VINF_SUCCESS; +} + +static VBOXSTRICTRC hdaRegWriteCORBSTS(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + RT_NOREF(pDevIns, iReg); + + uint32_t v = HDA_REG(pThis, CORBSTS); + HDA_REG(pThis, CORBSTS) &= ~(v & u32Value); + + return VINF_SUCCESS; +} + +static VBOXSTRICTRC hdaRegWriteCORBWP(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + VBOXSTRICTRC rc = hdaRegWriteU16(pDevIns, pThis, iReg, u32Value); + AssertRCSuccess(VBOXSTRICTRC_VAL(rc)); + +#ifdef IN_RING3 /** @todo do PDMDevHlpTaskTrigger everywhere? */ + return hdaR3CORBCmdProcess(pDevIns, pThis, PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATECC)); +#else + rc = PDMDevHlpTaskTrigger(pDevIns, pThis->hCorbDmaTask); + return RT_SUCCESS(rc) ? VINF_SUCCESS : rc; +#endif +} + +static VBOXSTRICTRC hdaRegWriteSDCBL(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + return hdaRegWriteU32(pDevIns, pThis, iReg, u32Value); +} + +static VBOXSTRICTRC hdaRegWriteSDCTL(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ +#ifdef IN_RING3 + /* Get the stream descriptor number. */ + const uint8_t uSD = HDA_SD_NUM_FROM_REG(pThis, CTL, iReg); + AssertReturn(uSD < RT_ELEMENTS(pThis->aStreams), VERR_INTERNAL_ERROR_3); /* paranoia^2: Bad g_aHdaRegMap. */ + + /* + * Extract the stream tag the guest wants to use for this specific + * stream descriptor (SDn). This only can happen if the stream is in a non-running + * state, so we're doing the lookup and assignment here. + * + * So depending on the guest OS, SD3 can use stream tag 4, for example. + */ + PHDASTATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATER3); + uint8_t uTag = (u32Value >> HDA_SDCTL_NUM_SHIFT) & HDA_SDCTL_NUM_MASK; + ASSERT_GUEST_MSG_RETURN(uTag < RT_ELEMENTS(pThisCC->aTags), + ("SD%RU8: Invalid stream tag %RU8 (u32Value=%#x)!\n", uSD, uTag, u32Value), + VINF_SUCCESS /* Always return success to the MMIO handler. */); + + PHDASTREAM const pStreamShared = &pThis->aStreams[uSD]; + PHDASTREAMR3 const pStreamR3 = &pThisCC->aStreams[uSD]; + + const bool fRun = RT_BOOL(u32Value & HDA_SDCTL_RUN); + const bool fReset = RT_BOOL(u32Value & HDA_SDCTL_SRST); + + /* If the run bit is set, we take the virtual-sync clock lock as well so we + can safely update timers via hdaR3TimerSet if necessary. We need to be + very careful with the fInReset and fInRun indicators here, as they may + change during the relocking if we need to acquire the clock lock. */ + const bool fNeedVirtualSyncClockLock = (u32Value & (HDA_SDCTL_RUN | HDA_SDCTL_SRST)) == HDA_SDCTL_RUN + && (HDA_REG_IND(pThis, iReg) & HDA_SDCTL_RUN) == 0; + if (fNeedVirtualSyncClockLock) + { + DEVHDA_UNLOCK(pDevIns, pThis); + DEVHDA_LOCK_BOTH_RETURN(pDevIns, pThis, pStreamShared, VINF_IOM_R3_MMIO_WRITE); + } + + const bool fInRun = RT_BOOL(HDA_REG_IND(pThis, iReg) & HDA_SDCTL_RUN); + const bool fInReset = RT_BOOL(HDA_REG_IND(pThis, iReg) & HDA_SDCTL_SRST); + + /*LogFunc(("[SD%RU8] fRun=%RTbool, fInRun=%RTbool, fReset=%RTbool, fInReset=%RTbool, %R[sdctl]\n", + uSD, fRun, fInRun, fReset, fInReset, u32Value));*/ + if (fInReset) + { + ASSERT_GUEST(!fReset); + ASSERT_GUEST(!fInRun && !fRun); + + /* Exit reset state. */ + ASMAtomicXchgBool(&pStreamShared->State.fInReset, false); + + /* Report that we're done resetting this stream by clearing SRST. */ + HDA_STREAM_REG(pThis, CTL, uSD) &= ~HDA_SDCTL_SRST; + + LogFunc(("[SD%RU8] Reset exit\n", uSD)); + } + else if (fReset) + { + /* ICH6 datasheet 18.2.33 says that RUN bit should be cleared before initiation of reset. */ + ASSERT_GUEST(!fInRun && !fRun); + + LogFunc(("[SD%RU8] Reset enter\n", uSD)); + + STAM_REL_PROFILE_START_NS(&pStreamR3->State.StatReset, a); + Assert(PDMDevHlpCritSectIsOwner(pDevIns, &pThis->CritSect)); + PAUDMIXSINK const pMixSink = pStreamR3->pMixSink ? pStreamR3->pMixSink->pMixSink : NULL; + if (pMixSink) + AudioMixerSinkLock(pMixSink); + + /* Deal with reset while running. */ + if (pStreamShared->State.fRunning) + { + int rc2 = hdaR3StreamEnable(pThis, pStreamShared, pStreamR3, false /* fEnable */); + AssertRC(rc2); Assert(!pStreamShared->State.fRunning); + pStreamShared->State.fRunning = false; + } + + hdaR3StreamReset(pThis, pThisCC, pStreamShared, pStreamR3, uSD); + + if (pMixSink) /* (FYI. pMixSink might not be what pStreamR3->pMixSink->pMixSink points at any longer) */ + AudioMixerSinkUnlock(pMixSink); + STAM_REL_PROFILE_STOP_NS(&pStreamR3->State.StatReset, a); + } + else + { + /* + * We enter here to change DMA states only. + */ + if (fInRun != fRun) + { + STAM_REL_PROFILE_START_NS((fRun ? &pStreamR3->State.StatStart : &pStreamR3->State.StatStop), r); + Assert(!fReset && !fInReset); /* (code change paranoia, currently impossible ) */ + LogFunc(("[SD%RU8] State changed (fRun=%RTbool)\n", uSD, fRun)); + + Assert(PDMDevHlpCritSectIsOwner(pDevIns, &pThis->CritSect)); + /** @todo bird: It's not clear to me when the pMixSink is actually + * assigned to the stream, so being paranoid till I find out... */ + PAUDMIXSINK const pMixSink = pStreamR3->pMixSink ? pStreamR3->pMixSink->pMixSink : NULL; + if (pMixSink) + AudioMixerSinkLock(pMixSink); + + int rc2 = VINF_SUCCESS; + if (fRun) + { + if (hdaGetDirFromSD(uSD) == PDMAUDIODIR_OUT) + { + const uint8_t uStripeCtl = ((u32Value >> HDA_SDCTL_STRIPE_SHIFT) & HDA_SDCTL_STRIPE_MASK) + 1; + LogFunc(("[SD%RU8] Using %RU8 SDOs (stripe control)\n", uSD, uStripeCtl)); + if (uStripeCtl > 1) + LogRel2(("HDA: Warning: Striping output over more than one SDO for stream #%RU8 currently is not implemented " \ + "(%RU8 SDOs requested)\n", uSD, uStripeCtl)); + } + + /* Assign new values. */ + LogFunc(("[SD%RU8] Using stream tag=%RU8\n", uSD, uTag)); + PHDATAG pTag = &pThisCC->aTags[uTag]; + pTag->uTag = uTag; + pTag->pStreamR3 = &pThisCC->aStreams[uSD]; + +# ifdef LOG_ENABLED + if (LogIsEnabled()) + { + PDMAUDIOPCMPROPS Props = { 0 }; + rc2 = hdaR3SDFMTToPCMProps(HDA_STREAM_REG(pThis, FMT, uSD), &Props); AssertRC(rc2); + LogFunc(("[SD%RU8] %RU32Hz, %RU8bit, %RU8 channel(s)\n", + uSD, Props.uHz, PDMAudioPropsSampleBits(&Props), PDMAudioPropsChannels(&Props))); + } +# endif + /* (Re-)initialize the stream with current values. */ + rc2 = hdaR3StreamSetUp(pDevIns, pThis, pStreamShared, pStreamR3, uSD); + if ( RT_SUCCESS(rc2) + /* Any vital stream change occurred so that we need to (re-)add the stream to our setup? + * Otherwise just skip this, as this costs a lot of performance. */ + /** @todo r=bird: hdaR3StreamSetUp does not return VINF_NO_CHANGE since r142810. */ + && rc2 != VINF_NO_CHANGE) + { + /* Remove the old stream from the device setup. */ + rc2 = hdaR3RemoveStream(pThisCC, &pStreamShared->State.Cfg); + AssertRC(rc2); + + /* Add the stream to the device setup. */ + rc2 = hdaR3AddStream(pThisCC, &pStreamShared->State.Cfg); + AssertRC(rc2); + } + } + + if (RT_SUCCESS(rc2)) + { + /* Enable/disable the stream. */ + rc2 = hdaR3StreamEnable(pThis, pStreamShared, pStreamR3, fRun /* fEnable */); + AssertRC(rc2); + + if (fRun) + { + /** @todo move this into a HDAStream.cpp function. */ + uint64_t tsNow; + if (hdaGetDirFromSD(uSD) == PDMAUDIODIR_OUT) + { + /* Output streams: Avoid going through the timer here by calling the stream's timer + function directly. Should speed up starting the stream transfers. */ + tsNow = hdaR3StreamTimerMain(pDevIns, pThis, pThisCC, pStreamShared, pStreamR3); + } + else + { + /* Input streams: Arm the timer and kick the AIO thread. */ + tsNow = PDMDevHlpTimerGet(pDevIns, pStreamShared->hTimer); + pStreamShared->State.tsTransferLast = tsNow; /* for WALCLK */ + + uint64_t tsTransferNext = tsNow + pStreamShared->State.aSchedule[0].cPeriodTicks; + pStreamShared->State.tsTransferNext = tsTransferNext; /* legacy */ + pStreamShared->State.cbCurDmaPeriod = pStreamShared->State.aSchedule[0].cbPeriod; + Log3Func(("[SD%RU8] tsTransferNext=%RU64 (in %RU64)\n", + pStreamShared->u8SD, tsTransferNext, tsTransferNext - tsNow)); + + int rc = PDMDevHlpTimerSet(pDevIns, pStreamShared->hTimer, tsTransferNext); + AssertRC(rc); + + /** @todo we should have a delayed AIO thread kick off, really... */ + if (pStreamR3->pMixSink && pStreamR3->pMixSink->pMixSink) + AudioMixerSinkSignalUpdateJob(pStreamR3->pMixSink->pMixSink); + else + AssertFailed(); + } + hdaR3StreamMarkStarted(pDevIns, pThis, pStreamShared, tsNow); + } + else + hdaR3StreamMarkStopped(pStreamShared); + } + + /* Make sure to leave the lock before (eventually) starting the timer. */ + if (pMixSink) + AudioMixerSinkUnlock(pMixSink); + STAM_REL_PROFILE_STOP_NS((fRun ? &pStreamR3->State.StatStart : &pStreamR3->State.StatStop), r); + } + } + + if (fNeedVirtualSyncClockLock) + PDMDevHlpTimerUnlockClock(pDevIns, pStreamShared->hTimer); /* Caller will unlock pThis->CritSect. */ + + return hdaRegWriteU24(pDevIns, pThis, iReg, u32Value); +#else /* !IN_RING3 */ + RT_NOREF(pDevIns, pThis, iReg, u32Value); + return VINF_IOM_R3_MMIO_WRITE; +#endif /* !IN_RING3 */ +} + +static VBOXSTRICTRC hdaRegWriteSDSTS(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + uint32_t v = HDA_REG_IND(pThis, iReg); + + /* Clear (zero) FIFOE, DESE and BCIS bits when writing 1 to it (6.2.33). */ + HDA_REG_IND(pThis, iReg) &= ~(u32Value & v); + + HDA_PROCESS_INTERRUPT(pDevIns, pThis); + + return VINF_SUCCESS; +} + +static VBOXSTRICTRC hdaRegWriteSDLVI(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + const size_t idxStream = HDA_SD_NUM_FROM_REG(pThis, LVI, iReg); + AssertReturn(idxStream < RT_ELEMENTS(pThis->aStreams), VERR_INTERNAL_ERROR_3); /* paranoia^2: Bad g_aHdaRegMap. */ + + ASSERT_GUEST_LOGREL_MSG(u32Value <= UINT8_MAX, /* Should be covered by the register write mask, but just to make sure. */ + ("LVI for stream #%zu must not be bigger than %RU8\n", idxStream, UINT8_MAX - 1)); + return hdaRegWriteU16(pDevIns, pThis, iReg, u32Value); +} + +/** + * Calculates the number of bytes of a FIFOW register. + * + * @return Number of bytes of a given FIFOW register. + * @param u16RegFIFOW FIFOW register to convert. + */ +uint8_t hdaSDFIFOWToBytes(uint16_t u16RegFIFOW) +{ + uint32_t cb; + switch (u16RegFIFOW) + { + case HDA_SDFIFOW_8B: cb = 8; break; + case HDA_SDFIFOW_16B: cb = 16; break; + case HDA_SDFIFOW_32B: cb = 32; break; + default: + AssertFailedStmt(cb = 32); /* Paranoia. */ + break; + } + + Assert(RT_IS_POWER_OF_TWO(cb)); + return cb; +} + +static VBOXSTRICTRC hdaRegWriteSDFIFOW(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + size_t const idxStream = HDA_SD_NUM_FROM_REG(pThis, FIFOW, iReg); + AssertReturn(idxStream < RT_ELEMENTS(pThis->aStreams), VERR_INTERNAL_ERROR_3); /* paranoia^2: Bad g_aHdaRegMap. */ + + if (RT_LIKELY(hdaGetDirFromSD((uint8_t)idxStream) == PDMAUDIODIR_IN)) /* FIFOW for input streams only. */ + { /* likely */ } + else + { +#ifndef IN_RING0 + LogRel(("HDA: Warning: Guest tried to write read-only FIFOW to output stream #%RU8, ignoring\n", idxStream)); + return VINF_SUCCESS; +#else + return VINF_IOM_R3_MMIO_WRITE; /* (Go to ring-3 for release logging.) */ +#endif + } + + uint16_t u16FIFOW = 0; + switch (u32Value) + { + case HDA_SDFIFOW_8B: + case HDA_SDFIFOW_16B: + case HDA_SDFIFOW_32B: + u16FIFOW = RT_LO_U16(u32Value); /* Only bits 2:0 are used; see ICH-6, 18.2.38. */ + break; + default: + ASSERT_GUEST_LOGREL_MSG_FAILED(("Guest tried writing unsupported FIFOW (0x%zx) to stream #%RU8, defaulting to 32 bytes\n", + u32Value, idxStream)); + u16FIFOW = HDA_SDFIFOW_32B; + break; + } + + pThis->aStreams[idxStream].u8FIFOW = hdaSDFIFOWToBytes(u16FIFOW); + LogFunc(("[SD%zu] Updating FIFOW to %RU8 bytes\n", idxStream, pThis->aStreams[idxStream].u8FIFOW)); + return hdaRegWriteU16(pDevIns, pThis, iReg, u16FIFOW); +} + +/** + * @note This method could be called for changing value on Output Streams only (ICH6 datasheet 18.2.39). + */ +static VBOXSTRICTRC hdaRegWriteSDFIFOS(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + uint8_t uSD = HDA_SD_NUM_FROM_REG(pThis, FIFOS, iReg); + + ASSERT_GUEST_LOGREL_MSG_RETURN(hdaGetDirFromSD(uSD) == PDMAUDIODIR_OUT, /* FIFOS for output streams only. */ + ("Guest tried writing read-only FIFOS to input stream #%RU8, ignoring\n", uSD), + VINF_SUCCESS); + + uint32_t u32FIFOS; + switch (u32Value) + { + case HDA_SDOFIFO_16B: + case HDA_SDOFIFO_32B: + case HDA_SDOFIFO_64B: + case HDA_SDOFIFO_128B: + case HDA_SDOFIFO_192B: + case HDA_SDOFIFO_256B: + u32FIFOS = u32Value; + break; + + default: + ASSERT_GUEST_LOGREL_MSG_FAILED(("Guest tried writing unsupported FIFOS (0x%x) to stream #%RU8, defaulting to 192 bytes\n", + u32Value, uSD)); + u32FIFOS = HDA_SDOFIFO_192B; + break; + } + + return hdaRegWriteU16(pDevIns, pThis, iReg, u32FIFOS); +} + +#ifdef IN_RING3 + +/** + * Adds an audio output stream to the device setup using the given configuration. + * + * @returns VBox status code. + * @param pThisCC The ring-3 HDA device state. + * @param pCfg Stream configuration to use for adding a stream. + */ +static int hdaR3AddStreamOut(PHDASTATER3 pThisCC, PPDMAUDIOSTREAMCFG pCfg) +{ + AssertPtrReturn(pCfg, VERR_INVALID_POINTER); + + AssertReturn(pCfg->enmDir == PDMAUDIODIR_OUT, VERR_INVALID_PARAMETER); + + LogFlowFunc(("Stream=%s\n", pCfg->szName)); + + int rc = VINF_SUCCESS; + + bool fUseFront = true; /* Always use front out by default. */ +# ifdef VBOX_WITH_AUDIO_HDA_51_SURROUND + bool fUseRear; + bool fUseCenter; + bool fUseLFE; + + fUseRear = fUseCenter = fUseLFE = false; + + /* + * Use commonly used setups for speaker configurations. + */ + + /** @todo Make the following configurable through mixer API and/or CFGM? */ + switch (PDMAudioPropsChannels(&pCfg->Props)) + { + case 3: /* 2.1: Front (Stereo) + LFE. */ + { + fUseLFE = true; + break; + } + + case 4: /* Quadrophonic: Front (Stereo) + Rear (Stereo). */ + { + fUseRear = true; + break; + } + + case 5: /* 4.1: Front (Stereo) + Rear (Stereo) + LFE. */ + { + fUseRear = true; + fUseLFE = true; + break; + } + + case 6: /* 5.1: Front (Stereo) + Rear (Stereo) + Center/LFE. */ + { + fUseRear = true; + fUseCenter = true; + fUseLFE = true; + break; + } + + default: /* Unknown; fall back to 2 front channels (stereo). */ + { + rc = VERR_NOT_SUPPORTED; + break; + } + } +# endif /* !VBOX_WITH_AUDIO_HDA_51_SURROUND */ + + if (rc == VERR_NOT_SUPPORTED) + { + LogRel2(("HDA: Warning: Unsupported channel count (%RU8), falling back to stereo channels (2)\n", + PDMAudioPropsChannels(&pCfg->Props) )); + + /* Fall back to 2 channels (see below in fUseFront block). */ + rc = VINF_SUCCESS; + } + + do + { + if (RT_FAILURE(rc)) + break; + + if (fUseFront) + { + RTStrPrintf(pCfg->szName, RT_ELEMENTS(pCfg->szName), "Front"); + + pCfg->enmPath = PDMAUDIOPATH_OUT_FRONT; + /// @todo PDMAudioPropsSetChannels(&pCfg->Props, 2); ? + + rc = hdaR3CodecAddStream(&pThisCC->Codec, PDMAUDIOMIXERCTL_FRONT, pCfg); + } + +# ifdef VBOX_WITH_AUDIO_HDA_51_SURROUND + if ( RT_SUCCESS(rc) + && (fUseCenter || fUseLFE)) + { + RTStrPrintf(pCfg->szName, RT_ELEMENTS(pCfg->szName), "Center/LFE"); + + pCfg->enmPath = PDMAUDIOPATH_OUT_CENTER_LFE; + PDMAudioPropsSetChannels(&pCfg->Props, fUseCenter && fUseLFE ? 2 : 1); + + rc = hdaR3CodecAddStream(&pThisCC->Codec, PDMAUDIOMIXERCTL_CENTER_LFE, pCfg); + } + + if ( RT_SUCCESS(rc) + && fUseRear) + { + RTStrPrintf(pCfg->szName, RT_ELEMENTS(pCfg->szName), "Rear"); + + pCfg->enmPath = PDMAUDIOPATH_OUT_REAR; + PDMAudioPropsSetChannels(&pCfg->Props, 2); + + rc = hdaR3CodecAddStream(&pThisCC->Codec, PDMAUDIOMIXERCTL_REAR, pCfg); + } +# endif /* VBOX_WITH_AUDIO_HDA_51_SURROUND */ + + } while (0); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Adds an audio input stream to the device setup using the given configuration. + * + * @returns VBox status code. + * @param pThisCC The ring-3 HDA device state. + * @param pCfg Stream configuration to use for adding a stream. + */ +static int hdaR3AddStreamIn(PHDASTATER3 pThisCC, PPDMAUDIOSTREAMCFG pCfg) +{ + AssertPtrReturn(pCfg, VERR_INVALID_POINTER); + + AssertReturn(pCfg->enmDir == PDMAUDIODIR_IN, VERR_INVALID_PARAMETER); + + LogFlowFunc(("Stream=%s enmPath=%ld\n", pCfg->szName, pCfg->enmPath)); + + int rc; + switch (pCfg->enmPath) + { + case PDMAUDIOPATH_IN_LINE: + rc = hdaR3CodecAddStream(&pThisCC->Codec, PDMAUDIOMIXERCTL_LINE_IN, pCfg); + break; +# ifdef VBOX_WITH_AUDIO_HDA_MIC_IN + case PDMAUDIOPATH_IN_MIC: + rc = hdaR3CodecAddStream(&pThisCC->Codec, PDMAUDIOMIXERCTL_MIC_IN, pCfg); + break; +# endif + default: + rc = VERR_NOT_SUPPORTED; + break; + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Adds an audio stream to the device setup using the given configuration. + * + * @returns VBox status code. + * @param pThisCC The ring-3 HDA device state. + * @param pCfg Stream configuration to use for adding a stream. + */ +static int hdaR3AddStream(PHDASTATER3 pThisCC, PPDMAUDIOSTREAMCFG pCfg) +{ + AssertPtrReturn(pCfg, VERR_INVALID_POINTER); + + LogFlowFuncEnter(); + + int rc; + switch (pCfg->enmDir) + { + case PDMAUDIODIR_OUT: + rc = hdaR3AddStreamOut(pThisCC, pCfg); + break; + + case PDMAUDIODIR_IN: + rc = hdaR3AddStreamIn(pThisCC, pCfg); + break; + + default: + rc = VERR_NOT_SUPPORTED; + AssertFailed(); + break; + } + + LogFlowFunc(("Returning %Rrc\n", rc)); + + return rc; +} + +/** + * Removes an audio stream from the device setup using the given configuration. + * + * Used by hdaRegWriteSDCTL(). + * + * @returns VBox status code. + * @param pThisCC The ring-3 HDA device state. + * @param pCfg Stream configuration to use for removing a stream. + */ +static int hdaR3RemoveStream(PHDASTATER3 pThisCC, PPDMAUDIOSTREAMCFG pCfg) +{ + AssertPtrReturn(pCfg, VERR_INVALID_POINTER); + + int rc = VINF_SUCCESS; + + PDMAUDIOMIXERCTL enmMixerCtl = PDMAUDIOMIXERCTL_UNKNOWN; + switch (pCfg->enmDir) + { + case PDMAUDIODIR_IN: + { + LogFlowFunc(("Stream=%s enmPath=%d (src)\n", pCfg->szName, pCfg->enmPath)); + + switch (pCfg->enmPath) + { + case PDMAUDIOPATH_UNKNOWN: break; + case PDMAUDIOPATH_IN_LINE: enmMixerCtl = PDMAUDIOMIXERCTL_LINE_IN; break; +# ifdef VBOX_WITH_AUDIO_HDA_MIC_IN + case PDMAUDIOPATH_IN_MIC: enmMixerCtl = PDMAUDIOMIXERCTL_MIC_IN; break; +# endif + default: + rc = VERR_NOT_SUPPORTED; + break; + } + break; + } + + case PDMAUDIODIR_OUT: + { + LogFlowFunc(("Stream=%s, enmPath=%d (dst)\n", pCfg->szName, pCfg->enmPath)); + + switch (pCfg->enmPath) + { + case PDMAUDIOPATH_UNKNOWN: break; + case PDMAUDIOPATH_OUT_FRONT: enmMixerCtl = PDMAUDIOMIXERCTL_FRONT; break; +# ifdef VBOX_WITH_AUDIO_HDA_51_SURROUND + case PDMAUDIOPATH_OUT_CENTER_LFE: enmMixerCtl = PDMAUDIOMIXERCTL_CENTER_LFE; break; + case PDMAUDIOPATH_OUT_REAR: enmMixerCtl = PDMAUDIOMIXERCTL_REAR; break; +# endif + default: + rc = VERR_NOT_SUPPORTED; + break; + } + break; + } + + default: + rc = VERR_NOT_SUPPORTED; + break; + } + + if ( RT_SUCCESS(rc) + && enmMixerCtl != PDMAUDIOMIXERCTL_UNKNOWN) + { + rc = hdaR3CodecRemoveStream(&pThisCC->Codec, enmMixerCtl, false /*fImmediate*/); + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +#endif /* IN_RING3 */ + +static VBOXSTRICTRC hdaRegWriteSDFMT(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ +#ifdef IN_RING3 + PDMAUDIOPCMPROPS Props; + int rc2 = hdaR3SDFMTToPCMProps(RT_LO_U16(u32Value), &Props); + AssertRC(rc2); + LogFunc(("[SD%RU8] Set to %#x (%RU32Hz, %RU8bit, %RU8 channel(s))\n", HDA_SD_NUM_FROM_REG(pThis, FMT, iReg), u32Value, + PDMAudioPropsHz(&Props), PDMAudioPropsSampleBits(&Props), PDMAudioPropsChannels(&Props))); + + /* + * Write the wanted stream format into the register in any case. + * + * This is important for e.g. MacOS guests, as those try to initialize streams which are not reported + * by the device emulation (wants 4 channels, only have 2 channels at the moment). + * + * When ignoring those (invalid) formats, this leads to MacOS thinking that the device is malfunctioning + * and therefore disabling the device completely. + */ + return hdaRegWriteU16(pDevIns, pThis, iReg, u32Value); +#else + RT_NOREF(pDevIns, pThis, iReg, u32Value); + return VINF_IOM_R3_MMIO_WRITE; +#endif +} + +/** + * Worker for writes to the BDPL and BDPU registers. + */ +DECLINLINE(VBOXSTRICTRC) hdaRegWriteSDBDPX(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value, uint8_t uSD) +{ + RT_NOREF(uSD); + return hdaRegWriteU32(pDevIns, pThis, iReg, u32Value); +} + +static VBOXSTRICTRC hdaRegWriteSDBDPL(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + return hdaRegWriteSDBDPX(pDevIns, pThis, iReg, u32Value, HDA_SD_NUM_FROM_REG(pThis, BDPL, iReg)); +} + +static VBOXSTRICTRC hdaRegWriteSDBDPU(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + return hdaRegWriteSDBDPX(pDevIns, pThis, iReg, u32Value, HDA_SD_NUM_FROM_REG(pThis, BDPU, iReg)); +} + +/** Skylake specific. */ +static VBOXSTRICTRC hdaRegReadSDnPIB(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t *pu32Value) +{ + uint8_t const uSD = HDA_SD_NUM_FROM_SKYLAKE_REG(DPIB, iReg); + LogFlowFunc(("uSD=%u -> SDnLPIB\n", uSD)); + return hdaRegReadLPIB(pDevIns, pThis, HDA_SD_TO_REG(LPIB, uSD), pu32Value); +} + +/** Skylake specific. */ +static VBOXSTRICTRC hdaRegReadSDnEFIFOS(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t *pu32Value) +{ + /** @todo This is not implemented as I have found no specs yet. */ + RT_NOREF(pDevIns, pThis, iReg); + LogFunc(("TODO - need register spec: uSD=%u\n", HDA_SD_NUM_FROM_SKYLAKE_REG(DPIB, iReg))); + *pu32Value = 256; + return VINF_SUCCESS; +} + + +static VBOXSTRICTRC hdaRegReadIRS(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t *pu32Value) +{ + /* regarding 3.4.3 we should mark IRS as busy in case CORB is active */ + if ( HDA_REG(pThis, CORBWP) != HDA_REG(pThis, CORBRP) + || (HDA_REG(pThis, CORBCTL) & HDA_CORBCTL_DMA)) + HDA_REG(pThis, IRS) = HDA_IRS_ICB; /* busy */ + + return hdaRegReadU32(pDevIns, pThis, iReg, pu32Value); +} + +static VBOXSTRICTRC hdaRegWriteIRS(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + RT_NOREF(pDevIns, iReg); + + /* + * If the guest set the ICB bit of IRS register, HDA should process the verb in IC register, + * write the response to IR register, and set the IRV (valid in case of success) bit of IRS register. + */ + if ( (u32Value & HDA_IRS_ICB) + && !(HDA_REG(pThis, IRS) & HDA_IRS_ICB)) + { +#ifdef IN_RING3 + uint32_t uCmd = HDA_REG(pThis, IC); + + if (HDA_REG(pThis, CORBWP) != HDA_REG(pThis, CORBRP)) + { + /* + * 3.4.3: Defines behavior of immediate Command status register. + */ + LogRel(("HDA: Guest attempted process immediate verb (%x) with active CORB\n", uCmd)); + return VINF_SUCCESS; + } + + HDA_REG(pThis, IRS) = HDA_IRS_ICB; /* busy */ + + PHDASTATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATER3); + uint64_t uResp = 0; + int rc2 = hdaR3CodecLookup(&pThisCC->Codec, HDA_CODEC_CMD(uCmd, 0 /* LUN */), &uResp); + if (RT_FAILURE(rc2)) + LogFunc(("Codec lookup failed with rc2=%Rrc\n", rc2)); + + HDA_REG(pThis, IR) = (uint32_t)uResp; /** @todo r=andy Do we need a 64-bit response? */ + HDA_REG(pThis, IRS) = HDA_IRS_IRV; /* result is ready */ + /** @todo r=michaln We just set the IRS value, why are we clearing unset bits? */ + HDA_REG(pThis, IRS) &= ~HDA_IRS_ICB; /* busy is clear */ + + return VINF_SUCCESS; +#else /* !IN_RING3 */ + return VINF_IOM_R3_MMIO_WRITE; +#endif /* !IN_RING3 */ + } + + /* + * Once the guest read the response, it should clear the IRV bit of the IRS register. + */ + HDA_REG(pThis, IRS) &= ~(u32Value & HDA_IRS_IRV); + return VINF_SUCCESS; +} + +static VBOXSTRICTRC hdaRegWriteRIRBWP(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + RT_NOREF(pDevIns, iReg); + + if (HDA_REG(pThis, CORBCTL) & HDA_CORBCTL_DMA) /* Ignore request if CORB DMA engine is (still) running. */ + LogFunc(("CORB DMA (still) running, skipping\n")); + else + { + if (u32Value & HDA_RIRBWP_RST) + { + /* Do a RIRB reset. */ + if (pThis->cbRirbBuf) + RT_ZERO(pThis->au64RirbBuf); + + LogRel2(("HDA: RIRB reset\n")); + + HDA_REG(pThis, RIRBWP) = 0; + } + /* The remaining bits are O, see 6.2.22. */ + } + return VINF_SUCCESS; +} + +static VBOXSTRICTRC hdaRegWriteRINTCNT(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + RT_NOREF(pDevIns); + if (HDA_REG(pThis, CORBCTL) & HDA_CORBCTL_DMA) /* Ignore request if CORB DMA engine is (still) running. */ + { + LogFunc(("CORB DMA is (still) running, skipping\n")); + return VINF_SUCCESS; + } + + VBOXSTRICTRC rc = hdaRegWriteU16(pDevIns, pThis, iReg, u32Value); + AssertRC(VBOXSTRICTRC_VAL(rc)); + + /** @todo r=bird: Shouldn't we make sure the HDASTATE::u16RespIntCnt is below + * the new RINTCNT value? Or alterantively, make the DMA look take + * this into account instead... I'll do the later for now. */ + + LogFunc(("Response interrupt count is now %RU8\n", HDA_REG(pThis, RINTCNT) & 0xFF)); + return rc; +} + +static VBOXSTRICTRC hdaRegWriteBase(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + RT_NOREF(pDevIns); + + VBOXSTRICTRC rc = hdaRegWriteU32(pDevIns, pThis, iReg, u32Value); + AssertRCSuccess(VBOXSTRICTRC_VAL(rc)); + + uint32_t const iRegMem = g_aHdaRegMap[iReg].idxReg; + switch (iReg) + { + case HDA_REG_CORBLBASE: + pThis->u64CORBBase &= UINT64_C(0xFFFFFFFF00000000); + pThis->u64CORBBase |= pThis->au32Regs[iRegMem]; + break; + case HDA_REG_CORBUBASE: + pThis->u64CORBBase &= UINT64_C(0x00000000FFFFFFFF); + pThis->u64CORBBase |= (uint64_t)pThis->au32Regs[iRegMem] << 32; + break; + case HDA_REG_RIRBLBASE: + pThis->u64RIRBBase &= UINT64_C(0xFFFFFFFF00000000); + pThis->u64RIRBBase |= pThis->au32Regs[iRegMem]; + break; + case HDA_REG_RIRBUBASE: + pThis->u64RIRBBase &= UINT64_C(0x00000000FFFFFFFF); + pThis->u64RIRBBase |= (uint64_t)pThis->au32Regs[iRegMem] << 32; + break; + case HDA_REG_DPLBASE: + pThis->u64DPBase = pThis->au32Regs[iRegMem] & DPBASE_ADDR_MASK; + Assert(pThis->u64DPBase % 128 == 0); /* Must be 128-byte aligned. */ + + /* Also make sure to handle the DMA position enable bit. */ + pThis->fDMAPosition = pThis->au32Regs[iRegMem] & RT_BIT_32(0); + +#ifndef IN_RING0 + LogRel(("HDA: DP base (lower) set: %#RGp\n", pThis->u64DPBase)); + LogRel(("HDA: DMA position buffer is %s\n", pThis->fDMAPosition ? "enabled" : "disabled")); +#else + return VINF_IOM_R3_MMIO_WRITE; /* (Go to ring-3 for release logging.) */ +#endif + break; + case HDA_REG_DPUBASE: + pThis->u64DPBase = RT_MAKE_U64(RT_LO_U32(pThis->u64DPBase) & DPBASE_ADDR_MASK, pThis->au32Regs[iRegMem]); +#ifndef IN_RING0 + LogRel(("HDA: DP base (upper) set: %#RGp\n", pThis->u64DPBase)); +#else + return VINF_IOM_R3_MMIO_WRITE; /* (Go to ring-3 for release logging.) */ +#endif + break; + default: + AssertMsgFailed(("Invalid index\n")); + break; + } + + LogFunc(("CORB base:%llx RIRB base: %llx DP base: %llx\n", + pThis->u64CORBBase, pThis->u64RIRBBase, pThis->u64DPBase)); + return rc; +} + +static VBOXSTRICTRC hdaRegWriteRIRBSTS(PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t iReg, uint32_t u32Value) +{ + RT_NOREF(pDevIns, iReg); + + uint8_t v = HDA_REG(pThis, RIRBSTS); + HDA_REG(pThis, RIRBSTS) &= ~(v & u32Value); + + HDA_PROCESS_INTERRUPT(pDevIns, pThis); + return VINF_SUCCESS; +} + +#ifdef IN_RING3 + +/** + * Retrieves a corresponding sink for a given mixer control. + * + * @return Pointer to the sink, NULL if no sink is found. + * @param pThisCC The ring-3 HDA device state. + * @param enmMixerCtl Mixer control to get the corresponding sink for. + */ +static PHDAMIXERSINK hdaR3MixerControlToSink(PHDASTATER3 pThisCC, PDMAUDIOMIXERCTL enmMixerCtl) +{ + PHDAMIXERSINK pSink; + + switch (enmMixerCtl) + { + case PDMAUDIOMIXERCTL_VOLUME_MASTER: + /* Fall through is intentional. */ + case PDMAUDIOMIXERCTL_FRONT: + pSink = &pThisCC->SinkFront; + break; +# ifdef VBOX_WITH_AUDIO_HDA_51_SURROUND + case PDMAUDIOMIXERCTL_CENTER_LFE: + pSink = &pThisCC->SinkCenterLFE; + break; + case PDMAUDIOMIXERCTL_REAR: + pSink = &pThisCC->SinkRear; + break; +# endif + case PDMAUDIOMIXERCTL_LINE_IN: + pSink = &pThisCC->SinkLineIn; + break; +# ifdef VBOX_WITH_AUDIO_HDA_MIC_IN + case PDMAUDIOMIXERCTL_MIC_IN: + pSink = &pThisCC->SinkMicIn; + break; +# endif + default: + AssertMsgFailed(("Unhandled mixer control\n")); + pSink = NULL; + break; + } + + return pSink; +} + +/** + * Adds a specific HDA driver to the driver chain. + * + * @returns VBox status code. + * @param pDevIns The HDA device instance. + * @param pThisCC The ring-3 HDA device state. + * @param pDrv HDA driver to add. + */ +static int hdaR3MixerAddDrv(PPDMDEVINS pDevIns, PHDASTATER3 pThisCC, PHDADRIVER pDrv) +{ + int rc = VINF_SUCCESS; + + PHDASTREAM pStream = pThisCC->SinkLineIn.pStreamShared; + if ( pStream + && AudioHlpStreamCfgIsValid(&pStream->State.Cfg)) + { + int rc2 = hdaR3MixerAddDrvStream(pDevIns, pThisCC->SinkLineIn.pMixSink, &pStream->State.Cfg, pDrv); + if (RT_SUCCESS(rc)) + rc = rc2; + } + +# ifdef VBOX_WITH_AUDIO_HDA_MIC_IN + pStream = pThisCC->SinkMicIn.pStreamShared; + if ( pStream + && AudioHlpStreamCfgIsValid(&pStream->State.Cfg)) + { + int rc2 = hdaR3MixerAddDrvStream(pDevIns, pThisCC->SinkMicIn.pMixSink, &pStream->State.Cfg, pDrv); + if (RT_SUCCESS(rc)) + rc = rc2; + } +# endif + + pStream = pThisCC->SinkFront.pStreamShared; + if ( pStream + && AudioHlpStreamCfgIsValid(&pStream->State.Cfg)) + { + int rc2 = hdaR3MixerAddDrvStream(pDevIns, pThisCC->SinkFront.pMixSink, &pStream->State.Cfg, pDrv); + if (RT_SUCCESS(rc)) + rc = rc2; + } + +# ifdef VBOX_WITH_AUDIO_HDA_51_SURROUND + pStream = pThisCC->SinkCenterLFE.pStreamShared; + if ( pStream + && AudioHlpStreamCfgIsValid(&pStream->State.Cfg)) + { + int rc2 = hdaR3MixerAddDrvStream(pDevIns, pThisCC->SinkCenterLFE.pMixSink, &pStream->State.Cfg, pDrv); + if (RT_SUCCESS(rc)) + rc = rc2; + } + + pStream = pThisCC->SinkRear.pStreamShared; + if ( pStream + && AudioHlpStreamCfgIsValid(&pStream->State.Cfg)) + { + int rc2 = hdaR3MixerAddDrvStream(pDevIns, pThisCC->SinkRear.pMixSink, &pStream->State.Cfg, pDrv); + if (RT_SUCCESS(rc)) + rc = rc2; + } +# endif + + return rc; +} + +/** + * Removes a specific HDA driver from the driver chain and destroys its + * associated streams. + * + * @param pDevIns The device instance. + * @param pThisCC The ring-3 HDA device state. + * @param pDrv HDA driver to remove. + */ +static void hdaR3MixerRemoveDrv(PPDMDEVINS pDevIns, PHDASTATER3 pThisCC, PHDADRIVER pDrv) +{ + AssertPtrReturnVoid(pDrv); + + if (pDrv->LineIn.pMixStrm) + { + AudioMixerSinkRemoveStream(pThisCC->SinkLineIn.pMixSink, pDrv->LineIn.pMixStrm); + AudioMixerStreamDestroy(pDrv->LineIn.pMixStrm, pDevIns, true /*fImmediate*/); + pDrv->LineIn.pMixStrm = NULL; + } + +# ifdef VBOX_WITH_AUDIO_HDA_MIC_IN + if (pDrv->MicIn.pMixStrm) + { + AudioMixerSinkRemoveStream(pThisCC->SinkMicIn.pMixSink, pDrv->MicIn.pMixStrm); + AudioMixerStreamDestroy(pDrv->MicIn.pMixStrm, pDevIns, true /*fImmediate*/); + pDrv->MicIn.pMixStrm = NULL; + } +# endif + + if (pDrv->Front.pMixStrm) + { + AudioMixerSinkRemoveStream(pThisCC->SinkFront.pMixSink, pDrv->Front.pMixStrm); + AudioMixerStreamDestroy(pDrv->Front.pMixStrm, pDevIns, true /*fImmediate*/); + pDrv->Front.pMixStrm = NULL; + } + +# ifdef VBOX_WITH_AUDIO_HDA_51_SURROUND + if (pDrv->CenterLFE.pMixStrm) + { + AudioMixerSinkRemoveStream(pThisCC->SinkCenterLFE.pMixSink, pDrv->CenterLFE.pMixStrm); + AudioMixerStreamDestroy(pDrv->CenterLFE.pMixStrm, pDevIns, true /*fImmediate*/); + pDrv->CenterLFE.pMixStrm = NULL; + } + + if (pDrv->Rear.pMixStrm) + { + AudioMixerSinkRemoveStream(pThisCC->SinkRear.pMixSink, pDrv->Rear.pMixStrm); + AudioMixerStreamDestroy(pDrv->Rear.pMixStrm, pDevIns, true /*fImmediate*/); + pDrv->Rear.pMixStrm = NULL; + } +# endif + + RTListNodeRemove(&pDrv->Node); +} + +/** + * Adds a driver stream to a specific mixer sink. + * + * @returns VBox status code (ignored by caller). + * @param pDevIns The HDA device instance. + * @param pMixSink Audio mixer sink to add audio streams to. + * @param pCfg Audio stream configuration to use for the audio + * streams to add. + * @param pDrv Driver stream to add. + */ +static int hdaR3MixerAddDrvStream(PPDMDEVINS pDevIns, PAUDMIXSINK pMixSink, PCPDMAUDIOSTREAMCFG pCfg, PHDADRIVER pDrv) +{ + AssertPtrReturn(pMixSink, VERR_INVALID_POINTER); + AssertPtrReturn(pCfg, VERR_INVALID_POINTER); + + LogFunc(("szSink=%s, szStream=%s, cChannels=%RU8\n", pMixSink->pszName, pCfg->szName, PDMAudioPropsChannels(&pCfg->Props))); + + /* + * Get the matching stream driver. + */ + PHDADRIVERSTREAM pDrvStream = NULL; + if (pCfg->enmDir == PDMAUDIODIR_IN) + { + LogFunc(("enmPath=%d (src)\n", pCfg->enmPath)); + switch (pCfg->enmPath) + { + case PDMAUDIOPATH_IN_LINE: + pDrvStream = &pDrv->LineIn; + break; +# ifdef VBOX_WITH_AUDIO_HDA_MIC_IN + case PDMAUDIOPATH_IN_MIC: + pDrvStream = &pDrv->MicIn; + break; +# endif + default: + LogFunc(("returns VERR_NOT_SUPPORTED - enmPath=%d\n", pCfg->enmPath)); + return VERR_NOT_SUPPORTED; + } + } + else if (pCfg->enmDir == PDMAUDIODIR_OUT) + { + LogFunc(("enmDst=%d %s (dst)\n", pCfg->enmPath, PDMAudioPathGetName(pCfg->enmPath))); + switch (pCfg->enmPath) + { + case PDMAUDIOPATH_OUT_FRONT: + pDrvStream = &pDrv->Front; + break; +# ifdef VBOX_WITH_AUDIO_HDA_51_SURROUND + case PDMAUDIOPATH_OUT_CENTER_LFE: + pDrvStream = &pDrv->CenterLFE; + break; + case PDMAUDIOPATH_OUT_REAR: + pDrvStream = &pDrv->Rear; + break; +# endif + default: + LogFunc(("returns VERR_NOT_SUPPORTED - enmPath=%d %s\n", pCfg->enmPath, PDMAudioPathGetName(pCfg->enmPath))); + return VERR_NOT_SUPPORTED; + } + } + else + AssertFailedReturn(VERR_NOT_SUPPORTED); + + LogFunc(("[LUN#%RU8] %s\n", pDrv->uLUN, pCfg->szName)); + + AssertPtr(pDrvStream); + AssertMsg(pDrvStream->pMixStrm == NULL, ("[LUN#%RU8] Driver stream already present when it must not\n", pDrv->uLUN)); + + PAUDMIXSTREAM pMixStrm = NULL; + int rc = AudioMixerSinkCreateStream(pMixSink, pDrv->pConnector, pCfg, pDevIns, &pMixStrm); + LogFlowFunc(("LUN#%RU8: Created stream \"%s\" for sink, rc=%Rrc\n", pDrv->uLUN, pCfg->szName, rc)); + if (RT_SUCCESS(rc)) + { + rc = AudioMixerSinkAddStream(pMixSink, pMixStrm); + LogFlowFunc(("LUN#%RU8: Added stream \"%s\" to sink, rc=%Rrc\n", pDrv->uLUN, pCfg->szName, rc)); + if (RT_FAILURE(rc)) + AudioMixerStreamDestroy(pMixStrm, pDevIns, true /*fImmediate*/); + } + + if (RT_SUCCESS(rc)) + pDrvStream->pMixStrm = pMixStrm; + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Adds all current driver streams to a specific mixer sink. + * + * @returns VBox status code. + * @param pDevIns The HDA device instance. + * @param pThisCC The ring-3 HDA device state. + * @param pMixSink Audio mixer sink to add stream to. + * @param pCfg Audio stream configuration to use for the audio streams + * to add. + */ +static int hdaR3MixerAddDrvStreams(PPDMDEVINS pDevIns, PHDASTATER3 pThisCC, PAUDMIXSINK pMixSink, PCPDMAUDIOSTREAMCFG pCfg) +{ + AssertPtrReturn(pMixSink, VERR_INVALID_POINTER); + AssertPtrReturn(pCfg, VERR_INVALID_POINTER); + + LogFunc(("Sink=%s, Stream=%s\n", pMixSink->pszName, pCfg->szName)); + + int rc; + if (AudioHlpStreamCfgIsValid(pCfg)) + { + rc = AudioMixerSinkSetFormat(pMixSink, &pCfg->Props, pCfg->Device.cMsSchedulingHint); + if (RT_SUCCESS(rc)) + { + PHDADRIVER pDrv; + RTListForEach(&pThisCC->lstDrv, pDrv, HDADRIVER, Node) + { + /* We ignore failures here because one non-working driver shouldn't + be allowed to spoil it for everyone else. */ + int rc2 = hdaR3MixerAddDrvStream(pDevIns, pMixSink, pCfg, pDrv); + if (RT_FAILURE(rc2)) + LogFunc(("Attaching stream failed with %Rrc (ignored)\n", rc2)); + } + } + } + else + rc = VERR_INVALID_PARAMETER; + return rc; +} + + +/** + * Adds a new audio stream to a specific mixer control. + * + * Depending on the mixer control the stream then gets assigned to one of the + * internal mixer sinks, which in turn then handle the mixing of all connected + * streams to that sink. + * + * @return VBox status code. + * @param pCodec The codec instance data. + * @param enmMixerCtl Mixer control to assign new stream to. + * @param pCfg Stream configuration for the new stream. + */ +DECLHIDDEN(int) hdaR3MixerAddStream(PHDACODECR3 pCodec, PDMAUDIOMIXERCTL enmMixerCtl, PCPDMAUDIOSTREAMCFG pCfg) +{ + PHDASTATER3 pThisCC = RT_FROM_MEMBER(pCodec, HDASTATER3, Codec); + AssertPtrReturn(pCfg, VERR_INVALID_POINTER); + + int rc; + PHDAMIXERSINK pSink = hdaR3MixerControlToSink(pThisCC, enmMixerCtl); + if (pSink) + { + rc = hdaR3MixerAddDrvStreams(pThisCC->pDevIns, pThisCC, pSink->pMixSink, pCfg); + + AssertPtr(pSink->pMixSink); + LogFlowFunc(("Sink=%s, Mixer control=%s\n", pSink->pMixSink->pszName, PDMAudioMixerCtlGetName(enmMixerCtl))); + } + else + rc = VERR_NOT_FOUND; + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Removes a specified mixer control from the HDA's mixer. + * + * @return VBox status code. + * @param pCodec The codec instance data. + * @param enmMixerCtl Mixer control to remove. + * @param fImmediate Whether the backend should be allowed to + * finished draining (@c false) or if it must be + * destroyed immediately (@c true). + */ +DECLHIDDEN(int) hdaR3MixerRemoveStream(PHDACODECR3 pCodec, PDMAUDIOMIXERCTL enmMixerCtl, bool fImmediate) +{ + PHDASTATER3 pThisCC = RT_FROM_MEMBER(pCodec, HDASTATER3, Codec); + int rc; + + PHDAMIXERSINK pSink = hdaR3MixerControlToSink(pThisCC, enmMixerCtl); + if (pSink) + { + PHDADRIVER pDrv; + RTListForEach(&pThisCC->lstDrv, pDrv, HDADRIVER, Node) + { + PAUDMIXSTREAM pMixStream = NULL; + switch (enmMixerCtl) + { + /* + * Input. + */ + case PDMAUDIOMIXERCTL_LINE_IN: + pMixStream = pDrv->LineIn.pMixStrm; + pDrv->LineIn.pMixStrm = NULL; + break; +# ifdef VBOX_WITH_AUDIO_HDA_MIC_IN + case PDMAUDIOMIXERCTL_MIC_IN: + pMixStream = pDrv->MicIn.pMixStrm; + pDrv->MicIn.pMixStrm = NULL; + break; +# endif + /* + * Output. + */ + case PDMAUDIOMIXERCTL_FRONT: + pMixStream = pDrv->Front.pMixStrm; + pDrv->Front.pMixStrm = NULL; + break; +# ifdef VBOX_WITH_AUDIO_HDA_51_SURROUND + case PDMAUDIOMIXERCTL_CENTER_LFE: + pMixStream = pDrv->CenterLFE.pMixStrm; + pDrv->CenterLFE.pMixStrm = NULL; + break; + case PDMAUDIOMIXERCTL_REAR: + pMixStream = pDrv->Rear.pMixStrm; + pDrv->Rear.pMixStrm = NULL; + break; +# endif + default: + AssertMsgFailed(("Mixer control %d not implemented\n", enmMixerCtl)); + break; + } + + if (pMixStream) + { + AudioMixerSinkRemoveStream(pSink->pMixSink, pMixStream); + AudioMixerStreamDestroy(pMixStream, pThisCC->pDevIns, fImmediate); + + pMixStream = NULL; + } + } + + AudioMixerSinkRemoveAllStreams(pSink->pMixSink); + rc = VINF_SUCCESS; + } + else + rc = VERR_NOT_FOUND; + + LogFunc(("Mixer control=%s, rc=%Rrc\n", PDMAudioMixerCtlGetName(enmMixerCtl), rc)); + return rc; +} + +/** + * Controls an input / output converter widget, that is, which converter is + * connected to which stream (and channel). + * + * @return VBox status code. + * @param pCodec The codec instance data. + * @param enmMixerCtl Mixer control to set SD stream number and channel for. + * @param uSD SD stream number (number + 1) to set. Set to 0 for unassign. + * @param uChannel Channel to set. Only valid if a valid SD stream number is specified. + * + * @note Is also called directly by the DevHDA code. + */ +DECLHIDDEN(int) hdaR3MixerControl(PHDACODECR3 pCodec, PDMAUDIOMIXERCTL enmMixerCtl, uint8_t uSD, uint8_t uChannel) +{ + PHDASTATER3 pThisCC = RT_FROM_MEMBER(pCodec, HDASTATER3, Codec); + PPDMDEVINS pDevIns = pThisCC->pDevIns; + PHDASTATE pThis = PDMDEVINS_2_DATA(pDevIns, PHDASTATE); + LogFunc(("enmMixerCtl=%s, uSD=%RU8, uChannel=%RU8\n", PDMAudioMixerCtlGetName(enmMixerCtl), uSD, uChannel)); + + if (uSD == 0) /* Stream number 0 is reserved. */ + { + Log2Func(("Invalid SDn (%RU8) number for mixer control '%s', ignoring\n", uSD, PDMAudioMixerCtlGetName(enmMixerCtl))); + return VINF_SUCCESS; + } + /* uChannel is optional. */ + + /* SDn0 starts as 1. */ + Assert(uSD); + uSD--; + +# ifndef VBOX_WITH_AUDIO_HDA_MIC_IN + /* Only SDI0 (Line-In) is supported. */ + if ( hdaGetDirFromSD(uSD) == PDMAUDIODIR_IN + && uSD >= 1) + { + LogRel2(("HDA: Dedicated Mic-In support not imlpemented / built-in (stream #%RU8), using Line-In (stream #0) instead\n", uSD)); + uSD = 0; + } +# endif + + int rc = VINF_SUCCESS; + + PHDAMIXERSINK pSink = hdaR3MixerControlToSink(pThisCC, enmMixerCtl); + if (pSink) + { + AssertPtr(pSink->pMixSink); + + /* If this an output stream, determine the correct SD#. */ + if ( uSD < HDA_MAX_SDI + && AudioMixerSinkGetDir(pSink->pMixSink) == PDMAUDIODIR_OUT) + uSD += HDA_MAX_SDI; + + /* Make 100% sure we got a good stream number before continuing. */ + AssertLogRelReturn(uSD < RT_ELEMENTS(pThisCC->aStreams), VERR_NOT_IMPLEMENTED); + + /* Detach the existing stream from the sink. */ + PHDASTREAM const pOldStreamShared = pSink->pStreamShared; + PHDASTREAMR3 const pOldStreamR3 = pSink->pStreamR3; + if ( pOldStreamShared + && pOldStreamR3 + && ( pOldStreamShared->u8SD != uSD + || pOldStreamShared->u8Channel != uChannel) + ) + { + LogFunc(("Sink '%s' was assigned to stream #%RU8 (channel %RU8) before\n", + pSink->pMixSink->pszName, pOldStreamShared->u8SD, pOldStreamShared->u8Channel)); + Assert(PDMDevHlpCritSectIsOwner(pDevIns, &pThis->CritSect)); + + /* Only disable the stream if the stream descriptor # has changed. */ + if (pOldStreamShared->u8SD != uSD) + hdaR3StreamEnable(pThis, pOldStreamShared, pOldStreamR3, false /*fEnable*/); + + if (pOldStreamR3->State.pAioRegSink) + { + AudioMixerSinkRemoveUpdateJob(pOldStreamR3->State.pAioRegSink, hdaR3StreamUpdateAsyncIoJob, pOldStreamR3); + pOldStreamR3->State.pAioRegSink = NULL; + } + + pOldStreamR3->pMixSink = NULL; + + + pSink->pStreamShared = NULL; + pSink->pStreamR3 = NULL; + } + + /* Attach the new stream to the sink. + * Enabling the stream will be done by the guest via a separate SDnCTL call then. */ + if (pSink->pStreamShared == NULL) + { + LogRel2(("HDA: Setting sink '%s' to stream #%RU8 (channel %RU8), mixer control=%s\n", + pSink->pMixSink->pszName, uSD, uChannel, PDMAudioMixerCtlGetName(enmMixerCtl))); + + PHDASTREAMR3 pStreamR3 = &pThisCC->aStreams[uSD]; + PHDASTREAM pStreamShared = &pThis->aStreams[uSD]; + Assert(PDMDevHlpCritSectIsOwner(pDevIns, &pThis->CritSect)); + + pSink->pStreamR3 = pStreamR3; + pSink->pStreamShared = pStreamShared; + + pStreamShared->u8Channel = uChannel; + pStreamR3->pMixSink = pSink; + + rc = VINF_SUCCESS; + } + } + else + rc = VERR_NOT_FOUND; + + if (RT_FAILURE(rc)) + LogRel(("HDA: Converter control for stream #%RU8 (channel %RU8) / mixer control '%s' failed with %Rrc, skipping\n", + uSD, uChannel, PDMAudioMixerCtlGetName(enmMixerCtl), rc)); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Sets the volume of a specified mixer control. + * + * @return IPRT status code. + * @param pCodec The codec instance data. + * @param enmMixerCtl Mixer control to set volume for. + * @param pVol Pointer to volume data to set. + */ +DECLHIDDEN(int) hdaR3MixerSetVolume(PHDACODECR3 pCodec, PDMAUDIOMIXERCTL enmMixerCtl, PPDMAUDIOVOLUME pVol) +{ + PHDASTATER3 pThisCC = RT_FROM_MEMBER(pCodec, HDASTATER3, Codec); + int rc; + + PHDAMIXERSINK pSink = hdaR3MixerControlToSink(pThisCC, enmMixerCtl); + if ( pSink + && pSink->pMixSink) + { + LogRel2(("HDA: Setting volume for mixer sink '%s' to fMuted=%RTbool auChannels=%.*Rhxs\n", + pSink->pMixSink->pszName, pVol->fMuted, sizeof(pVol->auChannels), pVol->auChannels)); + + /* Set the volume. + * We assume that the codec already converted it to the correct range. */ + rc = AudioMixerSinkSetVolume(pSink->pMixSink, pVol); + } + else + rc = VERR_NOT_FOUND; + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * @callback_method_impl{FNTMTIMERDEV, Main routine for the stream's timer.} + */ +static DECLCALLBACK(void) hdaR3Timer(PPDMDEVINS pDevIns, TMTIMERHANDLE hTimer, void *pvUser) +{ + PHDASTATE pThis = PDMDEVINS_2_DATA(pDevIns, PHDASTATE); + PHDASTATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATER3); + uintptr_t idxStream = (uintptr_t)pvUser; + AssertReturnVoid(idxStream < RT_ELEMENTS(pThis->aStreams)); + PHDASTREAM pStreamShared = &pThis->aStreams[idxStream]; + PHDASTREAMR3 pStreamR3 = &pThisCC->aStreams[idxStream]; + Assert(hTimer == pStreamShared->hTimer); + + Assert(PDMDevHlpCritSectIsOwner(pDevIns, &pThis->CritSect)); + Assert(PDMDevHlpTimerIsLockOwner(pDevIns, hTimer)); + + RT_NOREF(hTimer); + + hdaR3StreamTimerMain(pDevIns, pThis, pThisCC, pStreamShared, pStreamR3); +} + +/** + * Soft reset of the device triggered via GCTL. + * + * @param pDevIns The device instance. + * @param pThis The shared HDA device state. + * @param pThisCC The ring-3 HDA device state. + */ +static void hdaR3GCTLReset(PPDMDEVINS pDevIns, PHDASTATE pThis, PHDASTATER3 pThisCC) +{ + LogFlowFuncEnter(); + Assert(PDMDevHlpCritSectIsOwner(pDevIns, &pThis->CritSect)); + + /* + * Make sure all streams have stopped as these have both timers and + * asynchronous worker threads that would race us if we delay this work. + */ + for (size_t idxStream = 0; idxStream < RT_ELEMENTS(pThis->aStreams); idxStream++) + { + PHDASTREAM const pStreamShared = &pThis->aStreams[idxStream]; + PHDASTREAMR3 const pStreamR3 = &pThisCC->aStreams[idxStream]; + PAUDMIXSINK const pMixSink = pStreamR3->pMixSink ? pStreamR3->pMixSink->pMixSink : NULL; + if (pMixSink) + AudioMixerSinkLock(pMixSink); + + /* We're doing this unconditionally, hope that's not problematic in any way... */ + int rc = hdaR3StreamEnable(pThis, pStreamShared, &pThisCC->aStreams[idxStream], false /* fEnable */); + AssertLogRelMsg(RT_SUCCESS(rc) && !pStreamShared->State.fRunning, + ("Disabling stream #%u failed: %Rrc, fRunning=%d\n", idxStream, rc, pStreamShared->State.fRunning)); + pStreamShared->State.fRunning = false; + + hdaR3StreamReset(pThis, pThisCC, pStreamShared, &pThisCC->aStreams[idxStream], (uint8_t)idxStream); + + if (pMixSink) /* (FYI. pMixSink might not be what pStreamR3->pMixSink->pMixSink points at any longer) */ + AudioMixerSinkUnlock(pMixSink); + } + + /* + * Reset registers. + */ + HDA_REG(pThis, GCAP) = HDA_MAKE_GCAP(HDA_MAX_SDO, HDA_MAX_SDI, 0, 0, 1); /* see 6.2.1 */ + HDA_REG(pThis, VMIN) = 0x00; /* see 6.2.2 */ + HDA_REG(pThis, VMAJ) = 0x01; /* see 6.2.3 */ + HDA_REG(pThis, OUTPAY) = 0x003C; /* see 6.2.4 */ + HDA_REG(pThis, INPAY) = 0x001D; /* see 6.2.5 */ + HDA_REG(pThis, CORBSIZE) = 0x42; /* Up to 256 CORB entries see 6.2.1 */ + HDA_REG(pThis, RIRBSIZE) = 0x42; /* Up to 256 RIRB entries see 6.2.1 */ + HDA_REG(pThis, CORBRP) = 0x0; + HDA_REG(pThis, CORBWP) = 0x0; + HDA_REG(pThis, RIRBWP) = 0x0; + /* Some guests (like Haiku) don't set RINTCNT explicitly but expect an interrupt after each + * RIRB response -- so initialize RINTCNT to 1 by default. */ + HDA_REG(pThis, RINTCNT) = 0x1; + /* For newer devices, there is a capability list offset word at 0x14, linux read it, does + no checking and simply reads the dword it specifies. The list terminates when the lower + 16 bits are zero. See snd_hdac_bus_parse_capabilities. Table 5-2 in intel 341081-002 + specifies this to be 0xc00 and chaining with 0x800, 0x500 and 0x1f00. We just terminate + it at 0xc00 for now. */ + HDA_REG(pThis, LLCH) = 0xc00; + HDA_REG(pThis, MLCH) = 0x0; + HDA_REG(pThis, MLCD) = 0x0; + + /* + * Stop any audio currently playing and/or recording. + */ + pThisCC->SinkFront.pStreamShared = NULL; + pThisCC->SinkFront.pStreamR3 = NULL; + if (pThisCC->SinkFront.pMixSink) + AudioMixerSinkReset(pThisCC->SinkFront.pMixSink); +# ifdef VBOX_WITH_AUDIO_HDA_MIC_IN + pThisCC->SinkMicIn.pStreamShared = NULL; + pThisCC->SinkMicIn.pStreamR3 = NULL; + if (pThisCC->SinkMicIn.pMixSink) + AudioMixerSinkReset(pThisCC->SinkMicIn.pMixSink); +# endif + pThisCC->SinkLineIn.pStreamShared = NULL; + pThisCC->SinkLineIn.pStreamR3 = NULL; + if (pThisCC->SinkLineIn.pMixSink) + AudioMixerSinkReset(pThisCC->SinkLineIn.pMixSink); +# ifdef VBOX_WITH_AUDIO_HDA_51_SURROUND + pThisCC->SinkCenterLFE = NULL; + if (pThisCC->SinkCenterLFE.pMixSink) + AudioMixerSinkReset(pThisCC->SinkCenterLFE.pMixSink); + pThisCC->SinkRear.pStreamShared = NULL; + pThisCC->SinkRear.pStreamR3 = NULL; + if (pThisCC->SinkRear.pMixSink) + AudioMixerSinkReset(pThisCC->SinkRear.pMixSink); +# endif + + /* + * Reset the codec. + */ + hdaCodecReset(&pThisCC->Codec); + + /* + * Set some sensible defaults for which HDA sinks + * are connected to which stream number. + * + * We use SD0 for input and SD4 for output by default. + * These stream numbers can be changed by the guest dynamically lateron. + */ + ASMCompilerBarrier(); /* paranoia */ +# ifdef VBOX_WITH_AUDIO_HDA_MIC_IN + hdaR3MixerControl(&pThisCC->Codec, PDMAUDIOMIXERCTL_MIC_IN , 1 /* SD0 */, 0 /* Channel */); +# endif + hdaR3MixerControl(&pThisCC->Codec, PDMAUDIOMIXERCTL_LINE_IN , 1 /* SD0 */, 0 /* Channel */); + + hdaR3MixerControl(&pThisCC->Codec, PDMAUDIOMIXERCTL_FRONT , 5 /* SD4 */, 0 /* Channel */); +# ifdef VBOX_WITH_AUDIO_HDA_51_SURROUND + hdaR3MixerControl(&pThisCC->Codec, PDMAUDIOMIXERCTL_CENTER_LFE, 5 /* SD4 */, 0 /* Channel */); + hdaR3MixerControl(&pThisCC->Codec, PDMAUDIOMIXERCTL_REAR , 5 /* SD4 */, 0 /* Channel */); +# endif + ASMCompilerBarrier(); /* paranoia */ + + /* Reset CORB. */ + pThis->cbCorbBuf = HDA_CORB_SIZE * HDA_CORB_ELEMENT_SIZE; + RT_ZERO(pThis->au32CorbBuf); + + /* Reset RIRB. */ + pThis->cbRirbBuf = HDA_RIRB_SIZE * HDA_RIRB_ELEMENT_SIZE; + RT_ZERO(pThis->au64RirbBuf); + + /* Clear our internal response interrupt counter. */ + pThis->u16RespIntCnt = 0; + + /* Clear stream tags <-> objects mapping table. */ + RT_ZERO(pThisCC->aTags); + + /* Emulation of codec "wake up" (HDA spec 5.5.1 and 6.5). */ + HDA_REG(pThis, STATESTS) = 0x1; + + /* Reset the wall clock. */ + pThis->tsWalClkStart = PDMDevHlpTimerGet(pDevIns, pThis->aStreams[0].hTimer); + + LogFlowFuncLeave(); + LogRel(("HDA: Reset\n")); +} + +#else /* !IN_RING3 */ + +/** + * Checks if a dword read starting with @a idxRegDsc is safe. + * + * We can guarentee it only standard reader callbacks are used. + * @returns true if it will always succeed, false if it may return back to + * ring-3 or we're just not sure. + * @param idxRegDsc The first register descriptor in the DWORD being read. + */ +DECLINLINE(bool) hdaIsMultiReadSafeInRZ(unsigned idxRegDsc) +{ + int32_t cbLeft = 4; /* signed on purpose */ + do + { + if ( g_aHdaRegMap[idxRegDsc].pfnRead == hdaRegReadU24 + || g_aHdaRegMap[idxRegDsc].pfnRead == hdaRegReadU16 + || g_aHdaRegMap[idxRegDsc].pfnRead == hdaRegReadU8 + || g_aHdaRegMap[idxRegDsc].pfnRead == hdaRegReadUnimpl) + { /* okay */ } + else + { + Log4(("hdaIsMultiReadSafeInRZ: idxRegDsc=%u %s\n", idxRegDsc, g_aHdaRegMap[idxRegDsc].pszName)); + return false; + } + + idxRegDsc++; + if (idxRegDsc < RT_ELEMENTS(g_aHdaRegMap)) + cbLeft -= g_aHdaRegMap[idxRegDsc].off - g_aHdaRegMap[idxRegDsc - 1].off; + else + break; + } while (cbLeft > 0); + return true; +} + + +#endif /* !IN_RING3 */ + + +/* MMIO callbacks */ + +/** + * @callback_method_impl{FNIOMMMIONEWREAD, Looks up and calls the appropriate handler.} + * + * @note During implementation, we discovered so-called "forgotten" or "hole" + * registers whose description is not listed in the RPM, datasheet, or + * spec. + */ +static DECLCALLBACK(VBOXSTRICTRC) hdaMmioRead(PPDMDEVINS pDevIns, void *pvUser, RTGCPHYS off, void *pv, unsigned cb) +{ + PHDASTATE pThis = PDMDEVINS_2_DATA(pDevIns, PHDASTATE); + VBOXSTRICTRC rc; + RT_NOREF_PV(pvUser); + Assert(pThis->uAlignmentCheckMagic == HDASTATE_ALIGNMENT_CHECK_MAGIC); + + /* + * Look up and log. + */ + int idxRegDsc = hdaRegLookup(off); /* Register descriptor index. */ +#ifdef LOG_ENABLED + unsigned const cbLog = cb; + uint32_t offRegLog = (uint32_t)off; +# ifdef HDA_DEBUG_GUEST_RIP + if (LogIs6Enabled()) + { + PVMCPU pVCpu = (PVMCPU)PDMDevHlpGetVMCPU(pDevIns); + Log6Func(("cs:rip=%04x:%016RX64 rflags=%08RX32\n", CPUMGetGuestCS(pVCpu), CPUMGetGuestRIP(pVCpu), CPUMGetGuestEFlags(pVCpu))); + } +# endif +#endif + + Log3Func(("off=%#x cb=%#x\n", offRegLog, cb)); + Assert(cb == 4); Assert((off & 3) == 0); + + rc = PDMDevHlpCritSectEnter(pDevIns, &pThis->CritSect, VINF_IOM_R3_MMIO_READ); + if (rc == VINF_SUCCESS) + { + if (!(HDA_REG(pThis, GCTL) & HDA_GCTL_CRST) && idxRegDsc != HDA_REG_GCTL) + LogFunc(("Access to registers except GCTL is blocked while resetting\n")); + + if (idxRegDsc >= 0) + { + /* ASSUMES gapless DWORD at end of map. */ + if (g_aHdaRegMap[idxRegDsc].cb == 4) + { + /* + * Straight forward DWORD access. + */ + rc = g_aHdaRegMap[idxRegDsc].pfnRead(pDevIns, pThis, idxRegDsc, (uint32_t *)pv); + Log3Func((" Read %s => %x (%Rrc)\n", g_aHdaRegMap[idxRegDsc].pszName, *(uint32_t *)pv, VBOXSTRICTRC_VAL(rc))); + STAM_COUNTER_INC(&pThis->aStatRegReads[idxRegDsc]); + } +#ifndef IN_RING3 + else if (!hdaIsMultiReadSafeInRZ(idxRegDsc)) + + { + STAM_COUNTER_INC(&pThis->aStatRegReadsToR3[idxRegDsc]); + rc = VINF_IOM_R3_MMIO_READ; + } +#endif + else + { + /* + * Multi register read (unless there are trailing gaps). + * ASSUMES that only DWORD reads have sideeffects. + */ + STAM_COUNTER_INC(&pThis->CTX_SUFF_Z(StatRegMultiReads)); + Log4(("hdaMmioRead: multi read: %#x LB %#x %s\n", off, cb, g_aHdaRegMap[idxRegDsc].pszName)); + uint32_t u32Value = 0; + unsigned cbLeft = 4; + do + { + uint32_t const cbReg = g_aHdaRegMap[idxRegDsc].cb; + uint32_t u32Tmp = 0; + + rc = g_aHdaRegMap[idxRegDsc].pfnRead(pDevIns, pThis, idxRegDsc, &u32Tmp); + Log4Func((" Read %s[%db] => %x (%Rrc)*\n", g_aHdaRegMap[idxRegDsc].pszName, cbReg, u32Tmp, VBOXSTRICTRC_VAL(rc))); + STAM_COUNTER_INC(&pThis->aStatRegReads[idxRegDsc]); +#ifdef IN_RING3 + if (rc != VINF_SUCCESS) + break; +#else + AssertMsgBreak(rc == VINF_SUCCESS, ("rc=%Rrc - impossible, we sanitized the readers!\n", VBOXSTRICTRC_VAL(rc))); +#endif + u32Value |= (u32Tmp & g_afMasks[cbReg]) << ((4 - cbLeft) * 8); + + cbLeft -= cbReg; + off += cbReg; + idxRegDsc++; + } while (cbLeft > 0 && g_aHdaRegMap[idxRegDsc].off == off); + + if (rc == VINF_SUCCESS) + *(uint32_t *)pv = u32Value; + else + Assert(!IOM_SUCCESS(rc)); + } + } + else + { + LogRel(("HDA: Invalid read access @0x%x (bytes=%u)\n", (uint32_t)off, cb)); + Log3Func((" Hole at %x is accessed for read\n", offRegLog)); + STAM_COUNTER_INC(&pThis->StatRegUnknownReads); + rc = VINF_IOM_MMIO_UNUSED_FF; + } + + DEVHDA_UNLOCK(pDevIns, pThis); + + /* + * Log the outcome. + */ +#ifdef LOG_ENABLED + if (cbLog == 4) + Log3Func((" Returning @%#05x -> %#010x %Rrc\n", offRegLog, *(uint32_t *)pv, VBOXSTRICTRC_VAL(rc))); + else if (cbLog == 2) + Log3Func((" Returning @%#05x -> %#06x %Rrc\n", offRegLog, *(uint16_t *)pv, VBOXSTRICTRC_VAL(rc))); + else if (cbLog == 1) + Log3Func((" Returning @%#05x -> %#04x %Rrc\n", offRegLog, *(uint8_t *)pv, VBOXSTRICTRC_VAL(rc))); +#endif + } + else + { + if (idxRegDsc >= 0) + STAM_COUNTER_INC(&pThis->aStatRegReadsToR3[idxRegDsc]); + } + return rc; +} + + +DECLINLINE(VBOXSTRICTRC) hdaWriteReg(PPDMDEVINS pDevIns, PHDASTATE pThis, int idxRegDsc, uint32_t u32Value, char const *pszLog) +{ + if ( (HDA_REG(pThis, GCTL) & HDA_GCTL_CRST) + || idxRegDsc == HDA_REG_GCTL) + { /* likely */ } + else + { + Log(("hdaWriteReg: Warning: Access to %s is blocked while controller is in reset mode\n", g_aHdaRegMap[idxRegDsc].pszName)); +#if defined(IN_RING3) || defined(LOG_ENABLED) + LogRel2(("HDA: Warning: Access to register %s is blocked while controller is in reset mode\n", + g_aHdaRegMap[idxRegDsc].pszName)); +#endif + STAM_COUNTER_INC(&pThis->StatRegWritesBlockedByReset); + return VINF_SUCCESS; + } + + /* + * Handle RD (register description) flags. + */ + + /* For SDI / SDO: Check if writes to those registers are allowed while SDCTL's RUN bit is set. */ + if (idxRegDsc >= HDA_NUM_GENERAL_REGS) + { + /* + * Some OSes (like Win 10 AU) violate the spec by writing stuff to registers which are not supposed to be be touched + * while SDCTL's RUN bit is set. So just ignore those values. + */ + const uint32_t uSDCTL = HDA_STREAM_REG(pThis, CTL, HDA_SD_NUM_FROM_REG(pThis, CTL, idxRegDsc)); + if ( !(uSDCTL & HDA_SDCTL_RUN) + || (g_aHdaRegMap[idxRegDsc].fFlags & HDA_RD_F_SD_WRITE_RUN)) + { /* likely */ } + else + { + Log(("hdaWriteReg: Warning: Access to %s is blocked! %R[sdctl]\n", g_aHdaRegMap[idxRegDsc].pszName, uSDCTL)); +#if defined(IN_RING3) || defined(LOG_ENABLED) + LogRel2(("HDA: Warning: Access to register %s is blocked while the stream's RUN bit is set\n", + g_aHdaRegMap[idxRegDsc].pszName)); +#endif + STAM_COUNTER_INC(&pThis->StatRegWritesBlockedByRun); + return VINF_SUCCESS; + } + } + +#ifdef LOG_ENABLED + uint32_t const idxRegMem = g_aHdaRegMap[idxRegDsc].idxReg; + uint32_t const u32OldValue = pThis->au32Regs[idxRegMem]; +#endif + VBOXSTRICTRC rc = g_aHdaRegMap[idxRegDsc].pfnWrite(pDevIns, pThis, idxRegDsc, u32Value); + Log3Func(("Written value %#x to %s[%d byte]; %x => %x%s, rc=%d\n", u32Value, g_aHdaRegMap[idxRegDsc].pszName, + g_aHdaRegMap[idxRegDsc].cb, u32OldValue, pThis->au32Regs[idxRegMem], pszLog, VBOXSTRICTRC_VAL(rc))); +#ifndef IN_RING3 + if (rc == VINF_IOM_R3_MMIO_WRITE) + STAM_COUNTER_INC(&pThis->aStatRegWritesToR3[idxRegDsc]); + else +#endif + STAM_COUNTER_INC(&pThis->aStatRegWrites[idxRegDsc]); + + RT_NOREF(pszLog); + return rc; +} + + +/** + * @callback_method_impl{FNIOMMMIONEWWRITE, + * Looks up and calls the appropriate handler.} + */ +static DECLCALLBACK(VBOXSTRICTRC) hdaMmioWrite(PPDMDEVINS pDevIns, void *pvUser, RTGCPHYS off, void const *pv, unsigned cb) +{ + PHDASTATE pThis = PDMDEVINS_2_DATA(pDevIns, PHDASTATE); + RT_NOREF_PV(pvUser); + Assert(pThis->uAlignmentCheckMagic == HDASTATE_ALIGNMENT_CHECK_MAGIC); + + /* + * Look up and log the access. + */ + int idxRegDsc = hdaRegLookup(off); +#if defined(IN_RING3) || defined(LOG_ENABLED) + uint32_t idxRegMem = idxRegDsc != -1 ? g_aHdaRegMap[idxRegDsc].idxReg : UINT32_MAX; +#endif + uint64_t u64Value; + if (cb == 4) u64Value = *(uint32_t const *)pv; + else if (cb == 2) u64Value = *(uint16_t const *)pv; + else if (cb == 1) u64Value = *(uint8_t const *)pv; + else if (cb == 8) u64Value = *(uint64_t const *)pv; + else + ASSERT_GUEST_MSG_FAILED_RETURN(("cb=%u %.*Rhxs\n", cb, cb, pv), + PDMDevHlpDBGFStop(pDevIns, RT_SRC_POS, "odd write size: off=%RGp cb=%u\n", off, cb)); + + /* + * The behavior of accesses that aren't aligned on natural boundraries is + * undefined. Just reject them outright. + */ + ASSERT_GUEST_MSG_RETURN((off & (cb - 1)) == 0, ("off=%RGp cb=%u %.*Rhxs\n", off, cb, cb, pv), + PDMDevHlpDBGFStop(pDevIns, RT_SRC_POS, "misaligned write access: off=%RGp cb=%u\n", off, cb)); + +#ifdef LOG_ENABLED + uint32_t const u32LogOldValue = idxRegDsc >= 0 ? pThis->au32Regs[idxRegMem] : UINT32_MAX; +# ifdef HDA_DEBUG_GUEST_RIP + if (LogIs6Enabled()) + { + PVMCPU pVCpu = (PVMCPU)PDMDevHlpGetVMCPU(pDevIns); + Log6Func(("cs:rip=%04x:%016RX64 rflags=%08RX32\n", CPUMGetGuestCS(pVCpu), CPUMGetGuestRIP(pVCpu), CPUMGetGuestEFlags(pVCpu))); + } +# endif +#endif + + /* + * Try for a direct hit first. + */ + VBOXSTRICTRC rc; + if (idxRegDsc >= 0 && g_aHdaRegMap[idxRegDsc].cb == cb) + { + DEVHDA_LOCK_RETURN(pDevIns, pThis, VINF_IOM_R3_MMIO_WRITE); + + Log3Func(("@%#05x u%u=%#0*RX64 %s\n", (uint32_t)off, cb * 8, 2 + cb * 2, u64Value, g_aHdaRegMap[idxRegDsc].pszName)); + rc = hdaWriteReg(pDevIns, pThis, idxRegDsc, u64Value, ""); + Log3Func((" %#x -> %#x\n", u32LogOldValue, idxRegMem != UINT32_MAX ? pThis->au32Regs[idxRegMem] : UINT32_MAX)); + + DEVHDA_UNLOCK(pDevIns, pThis); + } + /* + * Sub-register access. Supply missing bits as needed. + */ + else if ( idxRegDsc >= 0 + && cb < g_aHdaRegMap[idxRegDsc].cb) + { + DEVHDA_LOCK_RETURN(pDevIns, pThis, VINF_IOM_R3_MMIO_WRITE); + + u64Value |= pThis->au32Regs[g_aHdaRegMap[idxRegDsc].idxReg] + & g_afMasks[g_aHdaRegMap[idxRegDsc].cb] + & ~g_afMasks[cb]; + Log4Func(("@%#05x u%u=%#0*RX64 cb=%#x cbReg=%x %s\n" + "hdaMmioWrite: Supplying missing bits (%#x): %#llx -> %#llx ...\n", + (uint32_t)off, cb * 8, 2 + cb * 2, u64Value, cb, g_aHdaRegMap[idxRegDsc].cb, g_aHdaRegMap[idxRegDsc].pszName, + g_afMasks[g_aHdaRegMap[idxRegDsc].cb] & ~g_afMasks[cb], u64Value & g_afMasks[cb], u64Value)); + rc = hdaWriteReg(pDevIns, pThis, idxRegDsc, u64Value, ""); + Log4Func((" %#x -> %#x\n", u32LogOldValue, idxRegMem != UINT32_MAX ? pThis->au32Regs[idxRegMem] : UINT32_MAX)); + STAM_COUNTER_INC(&pThis->CTX_SUFF_Z(StatRegSubWrite)); + + DEVHDA_UNLOCK(pDevIns, pThis); + } + /* + * Partial or multiple register access, loop thru the requested memory. + */ + else + { +#ifdef IN_RING3 + DEVHDA_LOCK_RETURN(pDevIns, pThis, VINF_IOM_R3_MMIO_WRITE); + + if (idxRegDsc == -1) + Log4Func(("@%#05x u32=%#010x cb=%d\n", (uint32_t)off, *(uint32_t const *)pv, cb)); + else if (g_aHdaRegMap[idxRegDsc].cb == cb) + Log4Func(("@%#05x u%u=%#0*RX64 %s\n", (uint32_t)off, cb * 8, 2 + cb * 2, u64Value, g_aHdaRegMap[idxRegDsc].pszName)); + else + Log4Func(("@%#05x u%u=%#0*RX64 %s - mismatch cbReg=%u\n", (uint32_t)off, cb * 8, 2 + cb * 2, u64Value, + g_aHdaRegMap[idxRegDsc].pszName, g_aHdaRegMap[idxRegDsc].cb)); + + /* + * If it's an access beyond the start of the register, shift the input + * value and fill in missing bits. Natural alignment rules means we + * will only see 1 or 2 byte accesses of this kind, so no risk of + * shifting out input values. + */ + if (idxRegDsc < 0) + { + idxRegDsc = hdaR3RegLookupWithin(off); + if (idxRegDsc != -1) + { + uint32_t const cbBefore = (uint32_t)off - g_aHdaRegMap[idxRegDsc].off; + Assert(cbBefore > 0 && cbBefore < 4); + off -= cbBefore; + idxRegMem = g_aHdaRegMap[idxRegDsc].idxReg; + u64Value <<= cbBefore * 8; + u64Value |= pThis->au32Regs[idxRegMem] & g_afMasks[cbBefore]; + Log4Func((" Within register, supplied %u leading bits: %#llx -> %#llx ...\n", + cbBefore * 8, ~(uint64_t)g_afMasks[cbBefore] & u64Value, u64Value)); + STAM_COUNTER_INC(&pThis->CTX_SUFF_Z(StatRegMultiWrites)); + } + else + STAM_COUNTER_INC(&pThis->StatRegUnknownWrites); + } + else + { + Log4(("hdaMmioWrite: multi write: %s\n", g_aHdaRegMap[idxRegDsc].pszName)); + STAM_COUNTER_INC(&pThis->CTX_SUFF_Z(StatRegMultiWrites)); + } + + /* Loop thru the write area, it may cover multiple registers. */ + rc = VINF_SUCCESS; + for (;;) + { + uint32_t cbReg; + if (idxRegDsc >= 0) + { + idxRegMem = g_aHdaRegMap[idxRegDsc].idxReg; + cbReg = g_aHdaRegMap[idxRegDsc].cb; + if (cb < cbReg) + { + u64Value |= pThis->au32Regs[idxRegMem] & g_afMasks[cbReg] & ~g_afMasks[cb]; + Log4Func((" Supplying missing bits (%#x): %#llx -> %#llx ...\n", + g_afMasks[cbReg] & ~g_afMasks[cb], u64Value & g_afMasks[cb], u64Value)); + } +# ifdef LOG_ENABLED + uint32_t uLogOldVal = pThis->au32Regs[idxRegMem]; +# endif + rc = hdaWriteReg(pDevIns, pThis, idxRegDsc, u64Value & g_afMasks[cbReg], "*"); + Log4Func((" %#x -> %#x\n", uLogOldVal, pThis->au32Regs[idxRegMem])); + } + else + { + LogRel(("HDA: Invalid write access @0x%x\n", (uint32_t)off)); + cbReg = 1; + } + if (rc != VINF_SUCCESS) + break; + if (cbReg >= cb) + break; + + /* Advance. */ + off += cbReg; + cb -= cbReg; + u64Value >>= cbReg * 8; + if (idxRegDsc == -1) + idxRegDsc = hdaRegLookup(off); + else + { + idxRegDsc++; + if ( (unsigned)idxRegDsc >= RT_ELEMENTS(g_aHdaRegMap) + || g_aHdaRegMap[idxRegDsc].off != off) + idxRegDsc = -1; + } + } + + DEVHDA_UNLOCK(pDevIns, pThis); + +#else /* !IN_RING3 */ + /* Take the simple way out. */ + rc = VINF_IOM_R3_MMIO_WRITE; +#endif /* !IN_RING3 */ + } + + return rc; +} + +#ifdef IN_RING3 + + +/********************************************************************************************************************************* +* Saved state * +*********************************************************************************************************************************/ + +/** + * @callback_method_impl{FNSSMFIELDGETPUT, + * Version 6 saves the IOC flag in HDABDLEDESC::fFlags as a bool} + */ +static DECLCALLBACK(int) +hdaR3GetPutTrans_HDABDLEDESC_fFlags_6(PSSMHANDLE pSSM, const struct SSMFIELD *pField, void *pvStruct, + uint32_t fFlags, bool fGetOrPut, void *pvUser) +{ + PPDMDEVINS pDevIns = (PPDMDEVINS)pvUser; + RT_NOREF(pSSM, pField, pvStruct, fFlags); + AssertReturn(fGetOrPut, VERR_INTERNAL_ERROR_4); + bool fIoc; + int rc = pDevIns->pHlpR3->pfnSSMGetBool(pSSM, &fIoc); + if (RT_SUCCESS(rc)) + { + PHDABDLEDESC pDesc = (PHDABDLEDESC)pvStruct; + pDesc->fFlags = fIoc ? HDA_BDLE_F_IOC : 0; + } + return rc; +} + + +/** + * @callback_method_impl{FNSSMFIELDGETPUT, + * Versions 1 thru 4 save the IOC flag in HDASTREAMSTATE::DescfFlags as a bool} + */ +static DECLCALLBACK(int) +hdaR3GetPutTrans_HDABDLE_Desc_fFlags_1thru4(PSSMHANDLE pSSM, const struct SSMFIELD *pField, void *pvStruct, + uint32_t fFlags, bool fGetOrPut, void *pvUser) +{ + PPDMDEVINS pDevIns = (PPDMDEVINS)pvUser; + RT_NOREF(pSSM, pField, pvStruct, fFlags); + AssertReturn(fGetOrPut, VERR_INTERNAL_ERROR_4); + bool fIoc; + int rc = pDevIns->pHlpR3->pfnSSMGetBool(pSSM, &fIoc); + if (RT_SUCCESS(rc)) + { + HDABDLELEGACY *pState = (HDABDLELEGACY *)pvStruct; + pState->Desc.fFlags = fIoc ? HDA_BDLE_F_IOC : 0; + } + return rc; +} + + +static int hdaR3SaveStream(PPDMDEVINS pDevIns, PSSMHANDLE pSSM, PHDASTREAM pStreamShared, PHDASTREAMR3 pStreamR3) +{ + PCPDMDEVHLPR3 pHlp = pDevIns->pHlpR3; +# ifdef LOG_ENABLED + PHDASTATE pThis = PDMDEVINS_2_DATA(pDevIns, PHDASTATE); +# endif + + Log2Func(("[SD%RU8]\n", pStreamShared->u8SD)); + + /* Save stream ID. */ + Assert(pStreamShared->u8SD < HDA_MAX_STREAMS); + int rc = pHlp->pfnSSMPutU8(pSSM, pStreamShared->u8SD); + AssertRCReturn(rc, rc); + + rc = pHlp->pfnSSMPutStructEx(pSSM, &pStreamShared->State, sizeof(pStreamShared->State), + 0 /*fFlags*/, g_aSSMStreamStateFields7, NULL); + AssertRCReturn(rc, rc); + + AssertCompile(sizeof(pStreamShared->State.idxCurBdle) == sizeof(uint8_t) && RT_ELEMENTS(pStreamShared->State.aBdl) == 256); + HDABDLEDESC TmpDesc = *(HDABDLEDESC *)&pStreamShared->State.aBdl[pStreamShared->State.idxCurBdle]; + rc = pHlp->pfnSSMPutStructEx(pSSM, &TmpDesc, sizeof(TmpDesc), 0 /*fFlags*/, g_aSSMBDLEDescFields7, NULL); + AssertRCReturn(rc, rc); + + HDABDLESTATELEGACY TmpState = { pStreamShared->State.idxCurBdle, 0, pStreamShared->State.offCurBdle, 0 }; + rc = pHlp->pfnSSMPutStructEx(pSSM, &TmpState, sizeof(TmpState), 0 /*fFlags*/, g_aSSMBDLEStateFields7, NULL); + AssertRCReturn(rc, rc); + + PAUDMIXSINK pSink = NULL; + uint32_t cbCircBuf = 0; + uint32_t cbCircBufUsed = 0; + if (pStreamR3->State.pCircBuf) + { + cbCircBuf = (uint32_t)RTCircBufSize(pStreamR3->State.pCircBuf); + + /* We take the AIO lock here and releases it after saving the buffer, + otherwise the AIO thread could race us reading out the buffer data. */ + pSink = pStreamR3->pMixSink ? pStreamR3->pMixSink->pMixSink : NULL; + if ( !pSink + || RT_SUCCESS(AudioMixerSinkTryLock(pSink))) + { + cbCircBufUsed = (uint32_t)RTCircBufUsed(pStreamR3->State.pCircBuf); + if (cbCircBufUsed == 0 && pSink) + AudioMixerSinkUnlock(pSink); + } + } + + pHlp->pfnSSMPutU32(pSSM, cbCircBuf); + rc = pHlp->pfnSSMPutU32(pSSM, cbCircBufUsed); + + if (cbCircBufUsed > 0) + { + /* HACK ALERT! We cannot remove data from the buffer (live snapshot), + we use RTCircBufOffsetRead and RTCircBufAcquireReadBlock + creatively to get at the other buffer segment in case + of a wraparound. */ + size_t const offBuf = RTCircBufOffsetRead(pStreamR3->State.pCircBuf); + void *pvBuf = NULL; + size_t cbBuf = 0; + RTCircBufAcquireReadBlock(pStreamR3->State.pCircBuf, cbCircBufUsed, &pvBuf, &cbBuf); + Assert(cbBuf); + rc = pHlp->pfnSSMPutMem(pSSM, pvBuf, cbBuf); + if (cbBuf < cbCircBufUsed) + rc = pHlp->pfnSSMPutMem(pSSM, (uint8_t *)pvBuf - offBuf, cbCircBufUsed - cbBuf); + RTCircBufReleaseReadBlock(pStreamR3->State.pCircBuf, 0 /* Don't advance read pointer! */); + + if (pSink) + AudioMixerSinkUnlock(pSink); + } + + Log2Func(("[SD%RU8] LPIB=%RU32, CBL=%RU32, LVI=%RU32\n", pStreamR3->u8SD, HDA_STREAM_REG(pThis, LPIB, pStreamShared->u8SD), + HDA_STREAM_REG(pThis, CBL, pStreamShared->u8SD), HDA_STREAM_REG(pThis, LVI, pStreamShared->u8SD))); + +#ifdef LOG_ENABLED + hdaR3BDLEDumpAll(pDevIns, pThis, pStreamShared->u64BDLBase, pStreamShared->u16LVI + 1); +#endif + + return rc; +} + +/** + * @callback_method_impl{FNSSMDEVSAVEEXEC} + */ +static DECLCALLBACK(int) hdaR3SaveExec(PPDMDEVINS pDevIns, PSSMHANDLE pSSM) +{ + PHDASTATE pThis = PDMDEVINS_2_DATA(pDevIns, PHDASTATE); + PHDASTATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATER3); + PCPDMDEVHLPR3 pHlp = pDevIns->pHlpR3; + + /* Save Codec nodes states. */ + hdaCodecSaveState(pDevIns, &pThisCC->Codec, pSSM); + + /* Save MMIO registers. */ + pHlp->pfnSSMPutU32(pSSM, RT_ELEMENTS(pThis->au32Regs)); + pHlp->pfnSSMPutMem(pSSM, pThis->au32Regs, sizeof(pThis->au32Regs)); + + /* Save controller-specifc internals. */ + pHlp->pfnSSMPutU64(pSSM, pThis->tsWalClkStart); + pHlp->pfnSSMPutU8(pSSM, pThis->u8IRQL); + + /* Save number of streams. */ + pHlp->pfnSSMPutU32(pSSM, HDA_MAX_STREAMS); + + /* Save stream states. */ + for (uint8_t i = 0; i < HDA_MAX_STREAMS; i++) + { + int rc = hdaR3SaveStream(pDevIns, pSSM, &pThis->aStreams[i], &pThisCC->aStreams[i]); + AssertRCReturn(rc, rc); + } + + return VINF_SUCCESS; +} + +/** + * @callback_method_impl{FNSSMDEVLOADDONE, + * Finishes stream setup and resuming.} + */ +static DECLCALLBACK(int) hdaR3LoadDone(PPDMDEVINS pDevIns, PSSMHANDLE pSSM) +{ + PHDASTATE pThis = PDMDEVINS_2_DATA(pDevIns, PHDASTATE); + PHDASTATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATER3); + LogFlowFuncEnter(); + + /* + * Enable all previously active streams. + */ + for (size_t i = 0; i < HDA_MAX_STREAMS; i++) + { + PHDASTREAM pStreamShared = &pThis->aStreams[i]; + + bool fActive = RT_BOOL(HDA_STREAM_REG(pThis, CTL, i) & HDA_SDCTL_RUN); + if (fActive) + { + PHDASTREAMR3 pStreamR3 = &pThisCC->aStreams[i]; + + /* (Re-)enable the stream. */ + int rc2 = hdaR3StreamEnable(pThis, pStreamShared, pStreamR3, true /* fEnable */); + AssertRC(rc2); + + /* Add the stream to the device setup. */ + rc2 = hdaR3AddStream(pThisCC, &pStreamShared->State.Cfg); + AssertRC(rc2); + + /* Use the LPIB to find the current scheduling position. If this isn't + exactly on a scheduling item adjust LPIB down to the start of the + current. This isn't entirely ideal, but it avoid the IRQ counting + issue if we round it upwards. (it is also a lot simpler) */ + uint32_t uLpib = HDA_STREAM_REG(pThis, LPIB, i); + AssertLogRelMsgStmt(uLpib < pStreamShared->u32CBL, ("LPIB=%#RX32 CBL=%#RX32\n", uLpib, pStreamShared->u32CBL), + HDA_STREAM_REG(pThis, LPIB, i) = uLpib = 0); + + uint32_t off = 0; + for (uint32_t j = 0; j < pStreamShared->State.cSchedule; j++) + { + AssertReturn(pStreamShared->State.aSchedule[j].cbPeriod >= 1 && pStreamShared->State.aSchedule[j].cLoops >= 1, + pDevIns->pHlpR3->pfnSSMSetLoadError(pSSM, VERR_INTERNAL_ERROR_2, RT_SRC_POS, + "Stream #%u, sched #%u: cbPeriod=%u cLoops=%u\n", + pStreamShared->u8SD, j, + pStreamShared->State.aSchedule[j].cbPeriod, + pStreamShared->State.aSchedule[j].cLoops)); + uint32_t cbCur = pStreamShared->State.aSchedule[j].cbPeriod + * pStreamShared->State.aSchedule[j].cLoops; + if (uLpib >= off + cbCur) + off += cbCur; + else + { + uint32_t const offDelta = uLpib - off; + uint32_t idxLoop = offDelta / pStreamShared->State.aSchedule[j].cbPeriod; + uint32_t offLoop = offDelta % pStreamShared->State.aSchedule[j].cbPeriod; + if (offLoop) + { + /** @todo somehow bake this into the DMA timer logic. */ + LogFunc(("stream #%u: LPIB=%#RX32; adjusting due to scheduling clash: -%#x (j=%u idxLoop=%u cbPeriod=%#x)\n", + pStreamShared->u8SD, uLpib, offLoop, j, idxLoop, pStreamShared->State.aSchedule[j].cbPeriod)); + uLpib -= offLoop; + HDA_STREAM_REG(pThis, LPIB, i) = uLpib; + } + pStreamShared->State.idxSchedule = (uint16_t)j; + pStreamShared->State.idxScheduleLoop = (uint16_t)idxLoop; + off = UINT32_MAX; + break; + } + } + Assert(off == UINT32_MAX); + + /* Now figure out the current BDLE and the offset within it. */ + off = 0; + for (uint32_t j = 0; j < pStreamShared->State.cBdles; j++) + if (uLpib >= off + pStreamShared->State.aBdl[j].cb) + off += pStreamShared->State.aBdl[j].cb; + else + { + pStreamShared->State.idxCurBdle = j; + pStreamShared->State.offCurBdle = uLpib - off; + off = UINT32_MAX; + break; + } + AssertReturn(off == UINT32_MAX, pDevIns->pHlpR3->pfnSSMSetLoadError(pSSM, VERR_INTERNAL_ERROR_3, RT_SRC_POS, + "Stream #%u: LPIB=%#RX32 not found in loaded BDL\n", + pStreamShared->u8SD, uLpib)); + + /* Avoid going through the timer here by calling the stream's timer function directly. + * Should speed up starting the stream transfers. */ + PDMDevHlpTimerLockClock2(pDevIns, pStreamShared->hTimer, &pThis->CritSect, VERR_IGNORED); + uint64_t tsNow = hdaR3StreamTimerMain(pDevIns, pThis, pThisCC, pStreamShared, pStreamR3); + PDMDevHlpTimerUnlockClock2(pDevIns, pStreamShared->hTimer, &pThis->CritSect); + + hdaR3StreamMarkStarted(pDevIns, pThis, pStreamShared, tsNow); + } + } + + LogFlowFuncLeave(); + return VINF_SUCCESS; +} + +/** + * Handles loading of all saved state versions older than the current one. + * + * @param pDevIns The device instance. + * @param pThis Pointer to the shared HDA state. + * @param pThisCC Pointer to the ring-3 HDA state. + * @param pSSM The saved state handle. + * @param uVersion Saved state version to load. + */ +static int hdaR3LoadExecLegacy(PPDMDEVINS pDevIns, PHDASTATE pThis, PHDASTATER3 pThisCC, PSSMHANDLE pSSM, uint32_t uVersion) +{ + PCPDMDEVHLPR3 pHlp = pDevIns->pHlpR3; + int rc; + + /* + * Load MMIO registers. + */ + uint32_t cRegs; + switch (uVersion) + { + case HDA_SAVED_STATE_VERSION_1: + /* Starting with r71199, we would save 112 instead of 113 + registers due to some code cleanups. This only affected trunk + builds in the 4.1 development period. */ + cRegs = 113; + if (pHlp->pfnSSMHandleRevision(pSSM) >= 71199) + { + uint32_t uVer = pHlp->pfnSSMHandleVersion(pSSM); + if ( VBOX_FULL_VERSION_GET_MAJOR(uVer) == 4 + && VBOX_FULL_VERSION_GET_MINOR(uVer) == 0 + && VBOX_FULL_VERSION_GET_BUILD(uVer) >= 51) + cRegs = 112; + } + break; + + case HDA_SAVED_STATE_VERSION_2: + case HDA_SAVED_STATE_VERSION_3: + cRegs = 112; + AssertCompile(RT_ELEMENTS(pThis->au32Regs) >= 112); + break; + + /* Since version 4 we store the register count to stay flexible. */ + case HDA_SAVED_STATE_VERSION_4: + case HDA_SAVED_STATE_VERSION_5: + case HDA_SAVED_STATE_VERSION_6: + rc = pHlp->pfnSSMGetU32(pSSM, &cRegs); + AssertRCReturn(rc, rc); + if (cRegs != RT_ELEMENTS(pThis->au32Regs)) + LogRel(("HDA: SSM version cRegs is %RU32, expected %RU32\n", cRegs, RT_ELEMENTS(pThis->au32Regs))); + break; + + default: + AssertLogRelMsgFailedReturn(("HDA: Internal Error! Didn't expect saved state version %RU32 ending up in hdaR3LoadExecLegacy!\n", + uVersion), VERR_INTERNAL_ERROR_5); + } + + if (cRegs >= RT_ELEMENTS(pThis->au32Regs)) + { + pHlp->pfnSSMGetMem(pSSM, pThis->au32Regs, sizeof(pThis->au32Regs)); + pHlp->pfnSSMSkip(pSSM, sizeof(uint32_t) * (cRegs - RT_ELEMENTS(pThis->au32Regs))); + } + else + pHlp->pfnSSMGetMem(pSSM, pThis->au32Regs, sizeof(uint32_t) * cRegs); + + /* Make sure to update the base addresses first before initializing any streams down below. */ + pThis->u64CORBBase = RT_MAKE_U64(HDA_REG(pThis, CORBLBASE), HDA_REG(pThis, CORBUBASE)); + pThis->u64RIRBBase = RT_MAKE_U64(HDA_REG(pThis, RIRBLBASE), HDA_REG(pThis, RIRBUBASE)); + pThis->u64DPBase = RT_MAKE_U64(HDA_REG(pThis, DPLBASE) & DPBASE_ADDR_MASK, HDA_REG(pThis, DPUBASE)); + + /* Also make sure to update the DMA position bit if this was enabled when saving the state. */ + pThis->fDMAPosition = RT_BOOL(HDA_REG(pThis, DPLBASE) & RT_BIT_32(0)); + + /* + * Load BDLEs (Buffer Descriptor List Entries) and DMA counters. + * + * Note: Saved states < v5 store LVI (u32BdleMaxCvi) for + * *every* BDLE state, whereas it only needs to be stored + * *once* for every stream. Most of the BDLE state we can + * get out of the registers anyway, so just ignore those values. + * + * Also, only the current BDLE was saved, regardless whether + * there were more than one (and there are at least two entries, + * according to the spec). + */ + switch (uVersion) + { + case HDA_SAVED_STATE_VERSION_1: + case HDA_SAVED_STATE_VERSION_2: + case HDA_SAVED_STATE_VERSION_3: + case HDA_SAVED_STATE_VERSION_4: + { + /* Only load the internal states. + * The rest will be initialized from the saved registers later. */ + + /* Note 1: Only the *current* BDLE for a stream was saved! */ + /* Note 2: The stream's saving order is/was fixed, so don't touch! */ + + HDABDLELEGACY BDLE; + + /* Output */ + PHDASTREAM pStreamShared = &pThis->aStreams[4]; + rc = hdaR3StreamSetUp(pDevIns, pThis, pStreamShared, &pThisCC->aStreams[4], 4 /* Stream descriptor, hardcoded */); + AssertRCReturn(rc, rc); + RT_ZERO(BDLE); + rc = pHlp->pfnSSMGetStructEx(pSSM, &BDLE, sizeof(BDLE), 0 /* fFlags */, g_aSSMStreamBdleFields1234, pDevIns); + AssertRCReturn(rc, rc); + pStreamShared->State.idxCurBdle = (uint8_t)BDLE.State.u32BDLIndex; /* not necessary */ + + /* Microphone-In */ + pStreamShared = &pThis->aStreams[2]; + rc = hdaR3StreamSetUp(pDevIns, pThis, pStreamShared, &pThisCC->aStreams[2], 2 /* Stream descriptor, hardcoded */); + AssertRCReturn(rc, rc); + rc = pHlp->pfnSSMGetStructEx(pSSM, &BDLE, sizeof(BDLE), 0 /* fFlags */, g_aSSMStreamBdleFields1234, pDevIns); + AssertRCReturn(rc, rc); + pStreamShared->State.idxCurBdle = (uint8_t)BDLE.State.u32BDLIndex; /* not necessary */ + + /* Line-In */ + pStreamShared = &pThis->aStreams[0]; + rc = hdaR3StreamSetUp(pDevIns, pThis, pStreamShared, &pThisCC->aStreams[0], 0 /* Stream descriptor, hardcoded */); + AssertRCReturn(rc, rc); + rc = pHlp->pfnSSMGetStructEx(pSSM, &BDLE, sizeof(BDLE), 0 /* fFlags */, g_aSSMStreamBdleFields1234, pDevIns); + AssertRCReturn(rc, rc); + pStreamShared->State.idxCurBdle = (uint8_t)BDLE.State.u32BDLIndex; /* not necessary */ + break; + } + + /* + * v5 & v6 - Since v5 we support flexible stream and BDLE counts. + */ + default: + { + /* Stream count. */ + uint32_t cStreams; + rc = pHlp->pfnSSMGetU32(pSSM, &cStreams); + AssertRCReturn(rc, rc); + if (cStreams > HDA_MAX_STREAMS) + return pHlp->pfnSSMSetLoadError(pSSM, VERR_SSM_DATA_UNIT_FORMAT_CHANGED, RT_SRC_POS, + N_("State contains %u streams while %u is the maximum supported"), + cStreams, HDA_MAX_STREAMS); + + /* Load stream states. */ + for (uint32_t i = 0; i < cStreams; i++) + { + uint8_t idStream; + rc = pHlp->pfnSSMGetU8(pSSM, &idStream); + AssertRCReturn(rc, rc); + + HDASTREAM StreamDummyShared; + HDASTREAMR3 StreamDummyR3; + PHDASTREAM pStreamShared = idStream < RT_ELEMENTS(pThis->aStreams) ? &pThis->aStreams[idStream] : &StreamDummyShared; + PHDASTREAMR3 pStreamR3 = idStream < RT_ELEMENTS(pThisCC->aStreams) ? &pThisCC->aStreams[idStream] : &StreamDummyR3; + AssertLogRelMsgStmt(idStream < RT_ELEMENTS(pThisCC->aStreams), + ("HDA stream ID=%RU8 not supported, skipping loadingit ...\n", idStream), + RT_ZERO(StreamDummyShared); RT_ZERO(StreamDummyR3)); + + rc = hdaR3StreamSetUp(pDevIns, pThis, pStreamShared, pStreamR3, idStream); + if (RT_FAILURE(rc)) + { + LogRel(("HDA: Stream #%RU32: Setting up of stream %RU8 failed, rc=%Rrc\n", i, idStream, rc)); + break; + } + + /* + * Load BDLEs (Buffer Descriptor List Entries) and DMA counters. + */ + if (uVersion == HDA_SAVED_STATE_VERSION_5) + { + struct V5HDASTREAMSTATE /* HDASTREAMSTATE + HDABDLE */ + { + uint16_t cBLDEs; + uint16_t uCurBDLE; + uint32_t u32BDLEIndex; + uint32_t cbBelowFIFOW; + uint32_t u32BufOff; + } Tmp; + static SSMFIELD const g_aV5State1Fields[] = + { + SSMFIELD_ENTRY(V5HDASTREAMSTATE, cBLDEs), + SSMFIELD_ENTRY(V5HDASTREAMSTATE, uCurBDLE), + SSMFIELD_ENTRY_TERM() + }; + rc = pHlp->pfnSSMGetStructEx(pSSM, &Tmp, sizeof(Tmp), 0 /* fFlags */, g_aV5State1Fields, NULL); + AssertRCReturn(rc, rc); + pStreamShared->State.idxCurBdle = (uint8_t)Tmp.uCurBDLE; /* not necessary */ + + for (uint16_t a = 0; a < Tmp.cBLDEs; a++) + { + static SSMFIELD const g_aV5State2Fields[] = + { + SSMFIELD_ENTRY(V5HDASTREAMSTATE, u32BDLEIndex), + SSMFIELD_ENTRY_OLD(au8FIFO, 256), + SSMFIELD_ENTRY(V5HDASTREAMSTATE, cbBelowFIFOW), + SSMFIELD_ENTRY_TERM() + }; + rc = pHlp->pfnSSMGetStructEx(pSSM, &Tmp, sizeof(Tmp), 0 /* fFlags */, g_aV5State2Fields, NULL); + AssertRCReturn(rc, rc); + } + } + else + { + rc = pHlp->pfnSSMGetStructEx(pSSM, &pStreamShared->State, sizeof(HDASTREAMSTATE), + 0 /* fFlags */, g_aSSMStreamStateFields6, NULL); + AssertRCReturn(rc, rc); + + HDABDLEDESC IgnDesc; + rc = pHlp->pfnSSMGetStructEx(pSSM, &IgnDesc, sizeof(IgnDesc), 0 /* fFlags */, g_aSSMBDLEDescFields6, pDevIns); + AssertRCReturn(rc, rc); + + HDABDLESTATELEGACY IgnState; + rc = pHlp->pfnSSMGetStructEx(pSSM, &IgnState, sizeof(IgnState), 0 /* fFlags */, g_aSSMBDLEStateFields6, NULL); + AssertRCReturn(rc, rc); + + Log2Func(("[SD%RU8] LPIB=%RU32, CBL=%RU32, LVI=%RU32\n", idStream, HDA_STREAM_REG(pThis, LPIB, idStream), + HDA_STREAM_REG(pThis, CBL, idStream), HDA_STREAM_REG(pThis, LVI, idStream))); +#ifdef LOG_ENABLED + hdaR3BDLEDumpAll(pDevIns, pThis, pStreamShared->u64BDLBase, pStreamShared->u16LVI + 1); +#endif + } + + } /* for cStreams */ + break; + } /* default */ + } + + return rc; +} + +/** + * @callback_method_impl{FNSSMDEVLOADEXEC} + */ +static DECLCALLBACK(int) hdaR3LoadExec(PPDMDEVINS pDevIns, PSSMHANDLE pSSM, uint32_t uVersion, uint32_t uPass) +{ + PHDASTATE pThis = PDMDEVINS_2_DATA(pDevIns, PHDASTATE); + PHDASTATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATER3); + PCPDMDEVHLPR3 pHlp = pDevIns->pHlpR3; + + Assert(uPass == SSM_PASS_FINAL); NOREF(uPass); + + LogRel2(("hdaR3LoadExec: uVersion=%RU32, uPass=0x%x\n", uVersion, uPass)); + + /* + * Load Codec nodes states. + */ + int rc = hdaR3CodecLoadState(pDevIns, &pThisCC->Codec, pSSM, uVersion); + if (RT_FAILURE(rc)) + { + LogRel(("HDA: Failed loading codec state (version %RU32, pass 0x%x), rc=%Rrc\n", uVersion, uPass, rc)); + return rc; + } + + if (uVersion <= HDA_SAVED_STATE_VERSION_6) /* Handle older saved states? */ + return hdaR3LoadExecLegacy(pDevIns, pThis, pThisCC, pSSM, uVersion); + + /* + * Load MMIO registers. + */ + uint32_t cRegs; + rc = pHlp->pfnSSMGetU32(pSSM, &cRegs); AssertRCReturn(rc, rc); + AssertRCReturn(rc, rc); + if (cRegs != RT_ELEMENTS(pThis->au32Regs)) + LogRel(("HDA: SSM version cRegs is %RU32, expected %RU32\n", cRegs, RT_ELEMENTS(pThis->au32Regs))); + + if (cRegs >= RT_ELEMENTS(pThis->au32Regs)) + { + pHlp->pfnSSMGetMem(pSSM, pThis->au32Regs, sizeof(pThis->au32Regs)); + rc = pHlp->pfnSSMSkip(pSSM, sizeof(uint32_t) * (cRegs - RT_ELEMENTS(pThis->au32Regs))); + AssertRCReturn(rc, rc); + } + else + { + rc = pHlp->pfnSSMGetMem(pSSM, pThis->au32Regs, sizeof(uint32_t) * cRegs); + AssertRCReturn(rc, rc); + } + + /* Make sure to update the base addresses first before initializing any streams down below. */ + pThis->u64CORBBase = RT_MAKE_U64(HDA_REG(pThis, CORBLBASE), HDA_REG(pThis, CORBUBASE)); + pThis->u64RIRBBase = RT_MAKE_U64(HDA_REG(pThis, RIRBLBASE), HDA_REG(pThis, RIRBUBASE)); + pThis->u64DPBase = RT_MAKE_U64(HDA_REG(pThis, DPLBASE) & DPBASE_ADDR_MASK, HDA_REG(pThis, DPUBASE)); + + /* Also make sure to update the DMA position bit if this was enabled when saving the state. */ + pThis->fDMAPosition = RT_BOOL(HDA_REG(pThis, DPLBASE) & RT_BIT_32(0)); + + /* + * Load controller-specific internals. + */ + if ( uVersion >= HDA_SAVED_STATE_WITHOUT_PERIOD + /* Don't annoy other team mates (forgot this for state v7): */ + || pHlp->pfnSSMHandleRevision(pSSM) >= 116273 + || pHlp->pfnSSMHandleVersion(pSSM) >= VBOX_FULL_VERSION_MAKE(5, 2, 0)) + { + pHlp->pfnSSMGetU64(pSSM, &pThis->tsWalClkStart); /* Was current wall clock */ + rc = pHlp->pfnSSMGetU8(pSSM, &pThis->u8IRQL); + AssertRCReturn(rc, rc); + + /* Convert the saved wall clock timestamp to a start timestamp. */ + if (uVersion < HDA_SAVED_STATE_WITHOUT_PERIOD && pThis->tsWalClkStart != 0) + { + uint64_t const cTimerTicksPerSec = PDMDevHlpTimerGetFreq(pDevIns, pThis->aStreams[0].hTimer); + AssertLogRel(cTimerTicksPerSec <= UINT32_MAX); + pThis->tsWalClkStart = ASMMultU64ByU32DivByU32(pThis->tsWalClkStart, + cTimerTicksPerSec, + 24000000 /* wall clock freq */); + pThis->tsWalClkStart = PDMDevHlpTimerGet(pDevIns, pThis->aStreams[0].hTimer) - pThis->tsWalClkStart; + } + } + + /* + * Load streams. + */ + uint32_t cStreams; + rc = pHlp->pfnSSMGetU32(pSSM, &cStreams); + AssertRCReturn(rc, rc); + if (cStreams > HDA_MAX_STREAMS) + return pHlp->pfnSSMSetLoadError(pSSM, VERR_SSM_DATA_UNIT_FORMAT_CHANGED, RT_SRC_POS, + N_("State contains %u streams while %u is the maximum supported"), + cStreams, HDA_MAX_STREAMS); + Log2Func(("cStreams=%RU32\n", cStreams)); + + /* Load stream states. */ + for (uint32_t i = 0; i < cStreams; i++) + { + uint8_t idStream; + rc = pHlp->pfnSSMGetU8(pSSM, &idStream); + AssertRCReturn(rc, rc); + + /* Paranoia. */ + AssertLogRelMsgReturn(idStream < HDA_MAX_STREAMS, + ("HDA: Saved state contains bogus stream ID %RU8 for stream #%RU8", idStream, i), + VERR_SSM_INVALID_STATE); + + HDASTREAM StreamDummyShared; + HDASTREAMR3 StreamDummyR3; + PHDASTREAM pStreamShared = idStream < RT_ELEMENTS(pThis->aStreams) ? &pThis->aStreams[idStream] : &StreamDummyShared; + PHDASTREAMR3 pStreamR3 = idStream < RT_ELEMENTS(pThisCC->aStreams) ? &pThisCC->aStreams[idStream] : &StreamDummyR3; + AssertLogRelMsgStmt(idStream < RT_ELEMENTS(pThisCC->aStreams), + ("HDA stream ID=%RU8 not supported, skipping loadingit ...\n", idStream), + RT_ZERO(StreamDummyShared); RT_ZERO(StreamDummyR3)); + + rc = PDMDevHlpCritSectEnter(pDevIns, &pThis->CritSect, VERR_IGNORED); /* timer code requires this */ + AssertRCReturn(rc, rc); + rc = hdaR3StreamSetUp(pDevIns, pThis, pStreamShared, pStreamR3, idStream); + PDMDevHlpCritSectLeave(pDevIns, &pThis->CritSect); + if (RT_FAILURE(rc)) + { + LogRel(("HDA: Stream #%RU8: Setting up failed, rc=%Rrc\n", idStream, rc)); + /* Continue. */ + } + + rc = pHlp->pfnSSMGetStructEx(pSSM, &pStreamShared->State, sizeof(HDASTREAMSTATE), + 0 /* fFlags */, g_aSSMStreamStateFields7, NULL); + AssertRCReturn(rc, rc); + + /* + * Load BDLEs (Buffer Descriptor List Entries) and DMA counters. + * Obsolete. Derived from LPID now. + */ + HDABDLEDESC IgnDesc; + rc = pHlp->pfnSSMGetStructEx(pSSM, &IgnDesc, sizeof(IgnDesc), 0 /* fFlags */, g_aSSMBDLEDescFields7, NULL); + AssertRCReturn(rc, rc); + + HDABDLESTATELEGACY IgnState; + rc = pHlp->pfnSSMGetStructEx(pSSM, &IgnState, sizeof(IgnState), 0 /* fFlags */, g_aSSMBDLEStateFields7, NULL); + AssertRCReturn(rc, rc); + + Log2Func(("[SD%RU8]\n", pStreamShared->u8SD)); + + /* + * Load period state if present. + */ + if (uVersion < HDA_SAVED_STATE_WITHOUT_PERIOD) + { + static SSMFIELD const s_aSSMStreamPeriodFields7[] = /* For the removed HDASTREAMPERIOD structure. */ + { + SSMFIELD_ENTRY_OLD(u64StartWalClk, sizeof(uint64_t)), + SSMFIELD_ENTRY_OLD(u64ElapsedWalClk, sizeof(uint64_t)), + SSMFIELD_ENTRY_OLD(cFramesTransferred, sizeof(uint32_t)), + SSMFIELD_ENTRY_OLD(cIntPending, sizeof(uint8_t)), /** @todo Not sure what we should for non-zero values on restore... ignoring it for now. */ + SSMFIELD_ENTRY_TERM() + }; + uint8_t bWhatever = 0; + rc = pHlp->pfnSSMGetStructEx(pSSM, &bWhatever, sizeof(bWhatever), 0 /* fFlags */, s_aSSMStreamPeriodFields7, NULL); + AssertRCReturn(rc, rc); + } + + /* + * Load internal DMA buffer. + */ + uint32_t cbCircBuf = 0; + pHlp->pfnSSMGetU32(pSSM, &cbCircBuf); /* cbCircBuf */ + uint32_t cbCircBufUsed = 0; + rc = pHlp->pfnSSMGetU32(pSSM, &cbCircBufUsed); /* cbCircBuf */ + AssertRCReturn(rc, rc); + + if (cbCircBuf) /* If 0, skip the buffer. */ + { + /* Paranoia. */ + AssertLogRelMsgReturn(cbCircBuf <= _32M, + ("HDA: Saved state contains bogus DMA buffer size (%RU32) for stream #%RU8", + cbCircBuf, idStream), + VERR_SSM_DATA_UNIT_FORMAT_CHANGED); + AssertLogRelMsgReturn(cbCircBufUsed <= cbCircBuf, + ("HDA: Saved state contains invalid DMA buffer usage (%RU32/%RU32) for stream #%RU8", + cbCircBufUsed, cbCircBuf, idStream), + VERR_SSM_DATA_UNIT_FORMAT_CHANGED); + + /* Do we need to cre-create the circular buffer do fit the data size? */ + if ( pStreamR3->State.pCircBuf + && cbCircBuf != (uint32_t)RTCircBufSize(pStreamR3->State.pCircBuf)) + { + RTCircBufDestroy(pStreamR3->State.pCircBuf); + pStreamR3->State.pCircBuf = NULL; + } + + rc = RTCircBufCreate(&pStreamR3->State.pCircBuf, cbCircBuf); + AssertRCReturn(rc, rc); + pStreamR3->State.StatDmaBufSize = cbCircBuf; + + if (cbCircBufUsed) + { + void *pvBuf = NULL; + size_t cbBuf = 0; + RTCircBufAcquireWriteBlock(pStreamR3->State.pCircBuf, cbCircBufUsed, &pvBuf, &cbBuf); + + AssertLogRelMsgReturn(cbBuf == cbCircBufUsed, ("cbBuf=%zu cbCircBufUsed=%zu\n", cbBuf, cbCircBufUsed), + VERR_INTERNAL_ERROR_3); + rc = pHlp->pfnSSMGetMem(pSSM, pvBuf, cbBuf); + AssertRCReturn(rc, rc); + pStreamShared->State.offWrite = cbCircBufUsed; + + RTCircBufReleaseWriteBlock(pStreamR3->State.pCircBuf, cbBuf); + + Assert(cbBuf == cbCircBufUsed); + } + } + + Log2Func(("[SD%RU8] LPIB=%RU32, CBL=%RU32, LVI=%RU32\n", idStream, HDA_STREAM_REG(pThis, LPIB, idStream), + HDA_STREAM_REG(pThis, CBL, idStream), HDA_STREAM_REG(pThis, LVI, idStream))); +#ifdef LOG_ENABLED + hdaR3BDLEDumpAll(pDevIns, pThis, pStreamShared->u64BDLBase, pStreamShared->u16LVI + 1); +#endif + /** @todo (Re-)initialize active periods? */ + + } /* for cStreams */ + + LogFlowFuncLeaveRC(rc); + return rc; +} + + +/********************************************************************************************************************************* +* IPRT format type handlers * +*********************************************************************************************************************************/ + +/** + * @callback_method_impl{FNRTSTRFORMATTYPE} + */ +static DECLCALLBACK(size_t) hdaR3StrFmtSDCTL(PFNRTSTROUTPUT pfnOutput, void *pvArgOutput, + const char *pszType, void const *pvValue, + int cchWidth, int cchPrecision, unsigned fFlags, + void *pvUser) +{ + RT_NOREF(pszType, cchWidth, cchPrecision, fFlags, pvUser); + uint32_t uSDCTL = (uint32_t)(uintptr_t)pvValue; + return RTStrFormat(pfnOutput, pvArgOutput, NULL, 0, + "SDCTL(raw:%#x, DIR:%s, TP:%RTbool, STRIPE:%x, DEIE:%RTbool, FEIE:%RTbool, IOCE:%RTbool, RUN:%RTbool, RESET:%RTbool)", + uSDCTL, + uSDCTL & HDA_SDCTL_DIR ? "OUT" : "IN", + RT_BOOL(uSDCTL & HDA_SDCTL_TP), + (uSDCTL & HDA_SDCTL_STRIPE_MASK) >> HDA_SDCTL_STRIPE_SHIFT, + RT_BOOL(uSDCTL & HDA_SDCTL_DEIE), + RT_BOOL(uSDCTL & HDA_SDCTL_FEIE), + RT_BOOL(uSDCTL & HDA_SDCTL_IOCE), + RT_BOOL(uSDCTL & HDA_SDCTL_RUN), + RT_BOOL(uSDCTL & HDA_SDCTL_SRST)); +} + +/** + * @callback_method_impl{FNRTSTRFORMATTYPE} + */ +static DECLCALLBACK(size_t) hdaR3StrFmtSDFIFOS(PFNRTSTROUTPUT pfnOutput, void *pvArgOutput, + const char *pszType, void const *pvValue, + int cchWidth, int cchPrecision, unsigned fFlags, + void *pvUser) +{ + RT_NOREF(pszType, cchWidth, cchPrecision, fFlags, pvUser); + uint32_t uSDFIFOS = (uint32_t)(uintptr_t)pvValue; + return RTStrFormat(pfnOutput, pvArgOutput, NULL, 0, "SDFIFOS(raw:%#x, sdfifos:%RU8 B)", uSDFIFOS, uSDFIFOS ? uSDFIFOS + 1 : 0); +} + +/** + * @callback_method_impl{FNRTSTRFORMATTYPE} + */ +static DECLCALLBACK(size_t) hdaR3StrFmtSDFIFOW(PFNRTSTROUTPUT pfnOutput, void *pvArgOutput, + const char *pszType, void const *pvValue, + int cchWidth, int cchPrecision, unsigned fFlags, + void *pvUser) +{ + RT_NOREF(pszType, cchWidth, cchPrecision, fFlags, pvUser); + uint32_t uSDFIFOW = (uint32_t)(uintptr_t)pvValue; + return RTStrFormat(pfnOutput, pvArgOutput, NULL, 0, "SDFIFOW(raw: %#0x, sdfifow:%d B)", uSDFIFOW, hdaSDFIFOWToBytes(uSDFIFOW)); +} + +/** + * @callback_method_impl{FNRTSTRFORMATTYPE} + */ +static DECLCALLBACK(size_t) hdaR3StrFmtSDSTS(PFNRTSTROUTPUT pfnOutput, void *pvArgOutput, + const char *pszType, void const *pvValue, + int cchWidth, int cchPrecision, unsigned fFlags, + void *pvUser) +{ + RT_NOREF(pszType, cchWidth, cchPrecision, fFlags, pvUser); + uint32_t uSdSts = (uint32_t)(uintptr_t)pvValue; + return RTStrFormat(pfnOutput, pvArgOutput, NULL, 0, + "SDSTS(raw:%#0x, fifordy:%RTbool, dese:%RTbool, fifoe:%RTbool, bcis:%RTbool)", + uSdSts, + RT_BOOL(uSdSts & HDA_SDSTS_FIFORDY), + RT_BOOL(uSdSts & HDA_SDSTS_DESE), + RT_BOOL(uSdSts & HDA_SDSTS_FIFOE), + RT_BOOL(uSdSts & HDA_SDSTS_BCIS)); +} + + +/********************************************************************************************************************************* +* Debug Info Item Handlers * +*********************************************************************************************************************************/ + +/** Worker for hdaR3DbgInfo. */ +static int hdaR3DbgLookupRegByName(const char *pszArgs) +{ + if (pszArgs && *pszArgs != '\0') + for (int iReg = 0; iReg < HDA_NUM_REGS; ++iReg) + if (!RTStrICmp(g_aHdaRegMap[iReg].pszName, pszArgs)) + return iReg; + return -1; +} + +/** Worker for hdaR3DbgInfo. */ +static void hdaR3DbgPrintRegister(PPDMDEVINS pDevIns, PHDASTATE pThis, PCDBGFINFOHLP pHlp, int iHdaIndex) +{ + /** @todo HDA_REG_IDX_NOMEM & GCAP both uses idxReg zero, no flag or anything + * to tell them appart. */ + if (g_aHdaRegMap[iHdaIndex].idxReg != 0 || g_aHdaRegMap[iHdaIndex].pfnRead != hdaRegReadWALCLK) + pHlp->pfnPrintf(pHlp, "%s: 0x%x\n", g_aHdaRegMap[iHdaIndex].pszName, pThis->au32Regs[g_aHdaRegMap[iHdaIndex].idxReg]); + else + { + uint64_t uWallNow = 0; + hdaQueryWallClock(pDevIns, pThis, false /*fDoDma*/, &uWallNow); + pHlp->pfnPrintf(pHlp, "%s: 0x%RX64\n", g_aHdaRegMap[iHdaIndex].pszName, uWallNow); + } +} + +/** + * @callback_method_impl{FNDBGFHANDLERDEV} + */ +static DECLCALLBACK(void) hdaR3DbgInfo(PPDMDEVINS pDevIns, PCDBGFINFOHLP pHlp, const char *pszArgs) +{ + PHDASTATE pThis = PDMDEVINS_2_DATA(pDevIns, PHDASTATE); + int idxReg = hdaR3DbgLookupRegByName(pszArgs); + if (idxReg != -1) + hdaR3DbgPrintRegister(pDevIns, pThis, pHlp, idxReg); + else + for (idxReg = 0; idxReg < HDA_NUM_REGS; ++idxReg) + hdaR3DbgPrintRegister(pDevIns, pThis, pHlp, idxReg); +} + +/** Worker for hdaR3DbgInfoStream. */ +static void hdaR3DbgPrintStream(PHDASTATE pThis, PCDBGFINFOHLP pHlp, int idxStream) +{ + char szTmp[PDMAUDIOSTRMCFGTOSTRING_MAX]; + PHDASTREAM const pStream = &pThis->aStreams[idxStream]; + pHlp->pfnPrintf(pHlp, "Stream #%d: %s\n", idxStream, PDMAudioStrmCfgToString(&pStream->State.Cfg, szTmp, sizeof(szTmp))); + pHlp->pfnPrintf(pHlp, " SD%dCTL : %R[sdctl]\n", idxStream, HDA_STREAM_REG(pThis, CTL, idxStream)); + pHlp->pfnPrintf(pHlp, " SD%dCTS : %R[sdsts]\n", idxStream, HDA_STREAM_REG(pThis, STS, idxStream)); + pHlp->pfnPrintf(pHlp, " SD%dFIFOS: %R[sdfifos]\n", idxStream, HDA_STREAM_REG(pThis, FIFOS, idxStream)); + pHlp->pfnPrintf(pHlp, " SD%dFIFOW: %R[sdfifow]\n", idxStream, HDA_STREAM_REG(pThis, FIFOW, idxStream)); + pHlp->pfnPrintf(pHlp, " Current BDLE%02u: %s%#RX64 LB %#x%s - off=%#x\n", pStream->State.idxCurBdle, "%%" /*vboxdbg phys prefix*/, + pStream->State.aBdl[pStream->State.idxCurBdle].GCPhys, pStream->State.aBdl[pStream->State.idxCurBdle].cb, + pStream->State.aBdl[pStream->State.idxCurBdle].fFlags ? " IOC" : "", pStream->State.offCurBdle); +} + +/** Worker for hdaR3DbgInfoBDL. */ +static void hdaR3DbgPrintBDL(PPDMDEVINS pDevIns, PHDASTATE pThis, PCDBGFINFOHLP pHlp, int idxStream) +{ + const PHDASTREAM pStream = &pThis->aStreams[idxStream]; + PCPDMAUDIOPCMPROPS pProps = &pStream->State.Cfg.Props; + uint64_t const u64BaseDMA = RT_MAKE_U64(HDA_STREAM_REG(pThis, BDPL, idxStream), + HDA_STREAM_REG(pThis, BDPU, idxStream)); + uint16_t const u16LVI = HDA_STREAM_REG(pThis, LVI, idxStream); + uint32_t const u32CBL = HDA_STREAM_REG(pThis, CBL, idxStream); + uint8_t const idxCurBdle = pStream->State.idxCurBdle; + pHlp->pfnPrintf(pHlp, "Stream #%d BDL: %s%#011RX64 LB %#x (LVI=%u)\n", idxStream, "%%" /*vboxdbg phys prefix*/, + u64BaseDMA, u16LVI * sizeof(HDABDLEDESC), u16LVI); + if (u64BaseDMA || idxCurBdle != 0 || pStream->State.aBdl[idxCurBdle].GCPhys != 0 || pStream->State.aBdl[idxCurBdle].cb != 0) + pHlp->pfnPrintf(pHlp, " Current: BDLE%03u: %s%#011RX64 LB %#x%s - off=%#x LPIB=%#RX32\n", + pStream->State.idxCurBdle, "%%" /*vboxdbg phys prefix*/, + pStream->State.aBdl[idxCurBdle].GCPhys, pStream->State.aBdl[idxCurBdle].cb, + pStream->State.aBdl[idxCurBdle].fFlags ? " IOC" : "", pStream->State.offCurBdle, + HDA_STREAM_REG(pThis, LPIB, idxStream)); + if (!u64BaseDMA) + return; + + /* + * The BDL: + */ + uint64_t cbTotal = 0; + for (uint16_t i = 0; i < u16LVI + 1; i++) + { + HDABDLEDESC bd = {0, 0, 0}; + PDMDevHlpPCIPhysRead(pDevIns, u64BaseDMA + i * sizeof(HDABDLEDESC), &bd, sizeof(bd)); + + char szFlags[64]; + szFlags[0] = '\0'; + if (bd.fFlags & ~HDA_BDLE_F_IOC) + RTStrPrintf(szFlags, sizeof(szFlags), " !!fFlags=%#x!!\n", bd.fFlags); + pHlp->pfnPrintf(pHlp, " %sBDLE%03u: %s%#011RX64 LB %#06x (%RU64 us) %s%s\n", idxCurBdle == i ? "=>" : " ", i, "%%", + bd.u64BufAddr, bd.u32BufSize, PDMAudioPropsBytesToMicro(pProps, bd.u32BufSize), + bd.fFlags & HDA_BDLE_F_IOC ? " IOC=1" : "", szFlags); + + if (memcmp(&bd, &pStream->State.aBdl[i], sizeof(bd)) != 0) + { + szFlags[0] = '\0'; + if (bd.fFlags & ~HDA_BDLE_F_IOC) + RTStrPrintf(szFlags, sizeof(szFlags), " !!fFlags=%#x!!\n", bd.fFlags); + pHlp->pfnPrintf(pHlp, " !!!loaded: %s%#011RX64 LB %#06x %s%s\n", "%%", pStream->State.aBdl[i].GCPhys, + pStream->State.aBdl[i].cb, pStream->State.aBdl[i].fFlags & HDA_BDLE_F_IOC ? " IOC=1" : "", szFlags); + } + + cbTotal += bd.u32BufSize; + } + pHlp->pfnPrintf(pHlp, " Total: %#RX64 bytes (%RU64), %RU64 ms\n", cbTotal, cbTotal, + PDMAudioPropsBytesToMilli(pProps, (uint32_t)cbTotal)); + if (cbTotal != u32CBL) + pHlp->pfnPrintf(pHlp, " Warning: %#RX64 bytes does not match CBL (%#RX64)!\n", cbTotal, u32CBL); + + /* + * The scheduling plan. + */ + uint16_t const idxSchedule = pStream->State.idxSchedule; + pHlp->pfnPrintf(pHlp, " Scheduling: %u items, %u prologue. Current: %u, loop %u.\n", pStream->State.cSchedule, + pStream->State.cSchedulePrologue, idxSchedule, pStream->State.idxScheduleLoop); + for (uint16_t i = 0; i < pStream->State.cSchedule; i++) + pHlp->pfnPrintf(pHlp, " %s#%02u: %#x bytes, %u loop%s, %RU32 ticks. BDLE%u thru BDLE%u\n", + i == idxSchedule ? "=>" : " ", i, + pStream->State.aSchedule[i].cbPeriod, pStream->State.aSchedule[i].cLoops, + pStream->State.aSchedule[i].cLoops == 1 ? "" : "s", + pStream->State.aSchedule[i].cPeriodTicks, pStream->State.aSchedule[i].idxFirst, + pStream->State.aSchedule[i].idxFirst + pStream->State.aSchedule[i].cEntries - 1); +} + +/** Used by hdaR3DbgInfoStream and hdaR3DbgInfoBDL. */ +static int hdaR3DbgLookupStrmIdx(PCDBGFINFOHLP pHlp, const char *pszArgs) +{ + if (pszArgs && *pszArgs) + { + int32_t idxStream; + int rc = RTStrToInt32Full(pszArgs, 0, &idxStream); + if (RT_SUCCESS(rc) && idxStream >= -1 && idxStream < HDA_MAX_STREAMS) + return idxStream; + pHlp->pfnPrintf(pHlp, "Argument '%s' is not a valid stream number!\n", pszArgs); + } + return -1; +} + +/** + * @callback_method_impl{FNDBGFHANDLERDEV, hdastream} + */ +static DECLCALLBACK(void) hdaR3DbgInfoStream(PPDMDEVINS pDevIns, PCDBGFINFOHLP pHlp, const char *pszArgs) +{ + PHDASTATE pThis = PDMDEVINS_2_DATA(pDevIns, PHDASTATE); + int idxStream = hdaR3DbgLookupStrmIdx(pHlp, pszArgs); + if (idxStream != -1) + hdaR3DbgPrintStream(pThis, pHlp, idxStream); + else + for (idxStream = 0; idxStream < HDA_MAX_STREAMS; ++idxStream) + hdaR3DbgPrintStream(pThis, pHlp, idxStream); +} + +/** + * @callback_method_impl{FNDBGFHANDLERDEV, hdabdl} + */ +static DECLCALLBACK(void) hdaR3DbgInfoBDL(PPDMDEVINS pDevIns, PCDBGFINFOHLP pHlp, const char *pszArgs) +{ + PHDASTATE pThis = PDMDEVINS_2_DATA(pDevIns, PHDASTATE); + int idxStream = hdaR3DbgLookupStrmIdx(pHlp, pszArgs); + if (idxStream != -1) + hdaR3DbgPrintBDL(pDevIns, pThis, pHlp, idxStream); + else + { + for (idxStream = 0; idxStream < HDA_MAX_STREAMS; ++idxStream) + hdaR3DbgPrintBDL(pDevIns, pThis, pHlp, idxStream); + idxStream = -1; + } + + /* + * DMA stream positions: + */ + uint64_t const uDPBase = pThis->u64DPBase & DPBASE_ADDR_MASK; + pHlp->pfnPrintf(pHlp, "DMA counters %#011RX64 LB %#x, %s:\n", uDPBase, HDA_MAX_STREAMS * 2 * sizeof(uint32_t), + pThis->fDMAPosition ? "enabled" : "disabled"); + if (uDPBase) + { + struct + { + uint32_t off, uReserved; + } aPositions[HDA_MAX_STREAMS]; + RT_ZERO(aPositions); + PDMDevHlpPCIPhysRead(pDevIns, uDPBase , &aPositions[0], sizeof(aPositions)); + + for (unsigned i = 0; i < RT_ELEMENTS(aPositions); i++) + if (idxStream == -1 || i == (unsigned)idxStream) /* lazy bird */ + { + char szReserved[64]; + szReserved[0] = '\0'; + if (aPositions[i].uReserved != 0) + RTStrPrintf(szReserved, sizeof(szReserved), " reserved=%#x", aPositions[i].uReserved); + pHlp->pfnPrintf(pHlp, " Stream #%u DMA @ %#x%s\n", i, aPositions[i].off, szReserved); + } + } +} + +/** + * @callback_method_impl{FNDBGFHANDLERDEV, hdcnodes} + */ +static DECLCALLBACK(void) hdaR3DbgInfoCodecNodes(PPDMDEVINS pDevIns, PCDBGFINFOHLP pHlp, const char *pszArgs) +{ + PHDASTATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATER3); + hdaR3CodecDbgListNodes(&pThisCC->Codec, pHlp, pszArgs); +} + +/** + * @callback_method_impl{FNDBGFHANDLERDEV, hdcselector} + */ +static DECLCALLBACK(void) hdaR3DbgInfoCodecSelector(PPDMDEVINS pDevIns, PCDBGFINFOHLP pHlp, const char *pszArgs) +{ + PHDASTATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATER3); + hdaR3CodecDbgSelector(&pThisCC->Codec, pHlp, pszArgs); +} + +/** + * @callback_method_impl{FNDBGFHANDLERDEV, hdamixer} + */ +static DECLCALLBACK(void) hdaR3DbgInfoMixer(PPDMDEVINS pDevIns, PCDBGFINFOHLP pHlp, const char *pszArgs) +{ + PHDASTATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATER3); + if (pThisCC->pMixer) + AudioMixerDebug(pThisCC->pMixer, pHlp, pszArgs); + else + pHlp->pfnPrintf(pHlp, "Mixer not available\n"); +} + + +/********************************************************************************************************************************* +* PDMIBASE * +*********************************************************************************************************************************/ + +/** + * @interface_method_impl{PDMIBASE,pfnQueryInterface} + */ +static DECLCALLBACK(void *) hdaR3QueryInterface(struct PDMIBASE *pInterface, const char *pszIID) +{ + PHDASTATER3 pThisCC = RT_FROM_MEMBER(pInterface, HDASTATER3, IBase); + + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIBASE, &pThisCC->IBase); + return NULL; +} + + +/********************************************************************************************************************************* +* PDMDEVREGR3 * +*********************************************************************************************************************************/ + +/** + * Worker for hdaR3Construct() and hdaR3Attach(). + * + * @returns VBox status code. + * @param pDevIns The device instance. + * @param pThis The shared HDA device state. + * @param pThisCC The ring-3 HDA device state. + * @param uLUN The logical unit which is being detached. + * @param ppDrv Attached driver instance on success. Optional. + */ +static int hdaR3AttachInternal(PPDMDEVINS pDevIns, PHDASTATE pThis, PHDASTATER3 pThisCC, unsigned uLUN, PHDADRIVER *ppDrv) +{ + PHDADRIVER pDrv = (PHDADRIVER)RTMemAllocZ(sizeof(HDADRIVER)); + AssertPtrReturn(pDrv, VERR_NO_MEMORY); + RTStrPrintf(pDrv->szDesc, sizeof(pDrv->szDesc), "Audio driver port (HDA) for LUN #%u", uLUN); + + PPDMIBASE pDrvBase; + int rc = PDMDevHlpDriverAttach(pDevIns, uLUN, &pThisCC->IBase, &pDrvBase, pDrv->szDesc); + if (RT_SUCCESS(rc)) + { + pDrv->pConnector = PDMIBASE_QUERY_INTERFACE(pDrvBase, PDMIAUDIOCONNECTOR); + AssertPtr(pDrv->pConnector); + if (RT_VALID_PTR(pDrv->pConnector)) + { + pDrv->pDrvBase = pDrvBase; + pDrv->pHDAStateShared = pThis; + pDrv->pHDAStateR3 = pThisCC; + pDrv->uLUN = uLUN; + + /* Attach to driver list if not attached yet. */ + if (!pDrv->fAttached) + { + RTListAppend(&pThisCC->lstDrv, &pDrv->Node); + pDrv->fAttached = true; + } + + if (ppDrv) + *ppDrv = pDrv; + + /* + * While we're here, give the windows backends a hint about our typical playback + * configuration. + * Note! If 48000Hz is advertised to the guest, add it here. + */ + if ( pDrv->pConnector + && pDrv->pConnector->pfnStreamConfigHint) + { + PDMAUDIOSTREAMCFG Cfg; + RT_ZERO(Cfg); + Cfg.enmDir = PDMAUDIODIR_OUT; + Cfg.enmPath = PDMAUDIOPATH_OUT_FRONT; + Cfg.Device.cMsSchedulingHint = 10; + Cfg.Backend.cFramesPreBuffering = UINT32_MAX; + PDMAudioPropsInit(&Cfg.Props, 2, true /*fSigned*/, 2, 44100); + RTStrPrintf(Cfg.szName, sizeof(Cfg.szName), "output 44.1kHz 2ch S16 (HDA config hint)"); + + pDrv->pConnector->pfnStreamConfigHint(pDrv->pConnector, &Cfg); /* (may trash CfgReq) */ + } + + LogFunc(("LUN#%u: returns VINF_SUCCESS (pCon=%p)\n", uLUN, pDrv->pConnector)); + return VINF_SUCCESS; + } + + rc = VERR_PDM_MISSING_INTERFACE_BELOW; + } + else if (rc == VERR_PDM_NO_ATTACHED_DRIVER) + LogFunc(("No attached driver for LUN #%u\n", uLUN)); + else + LogFunc(("Failed attaching driver for LUN #%u: %Rrc\n", uLUN, rc)); + RTMemFree(pDrv); + + LogFunc(("LUN#%u: rc=%Rrc\n", uLUN, rc)); + return rc; +} + + +/** + * @interface_method_impl{PDMDEVREG,pfnAttach} + */ +static DECLCALLBACK(int) hdaR3Attach(PPDMDEVINS pDevIns, unsigned uLUN, uint32_t fFlags) +{ + PHDASTATE pThis = PDMDEVINS_2_DATA(pDevIns, PHDASTATE); + PHDASTATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATER3); + RT_NOREF(fFlags); + LogFunc(("uLUN=%u, fFlags=0x%x\n", uLUN, fFlags)); + + DEVHDA_LOCK_RETURN(pDevIns, pThis, VERR_IGNORED); + + PHDADRIVER pDrv; + int rc = hdaR3AttachInternal(pDevIns, pThis, pThisCC, uLUN, &pDrv); + if (RT_SUCCESS(rc)) + { + int rc2 = hdaR3MixerAddDrv(pDevIns, pThisCC, pDrv); + if (RT_FAILURE(rc2)) + LogFunc(("hdaR3MixerAddDrv failed with %Rrc (ignored)\n", rc2)); + } + + DEVHDA_UNLOCK(pDevIns, pThis); + return rc; +} + + +/** + * Worker for hdaR3Detach that does all but free pDrv. + * + * This is called to let the device detach from a driver for a specified LUN + * at runtime. + * + * @param pDevIns The device instance. + * @param pThisCC The ring-3 HDA device state. + * @param pDrv Driver to detach from device. + */ +static void hdaR3DetachInternal(PPDMDEVINS pDevIns, PHDASTATER3 pThisCC, PHDADRIVER pDrv) +{ + /* Remove the driver from our list and destory it's associated streams. + This also will un-set the driver as a recording source (if associated). */ + hdaR3MixerRemoveDrv(pDevIns, pThisCC, pDrv); + LogFunc(("LUN#%u detached\n", pDrv->uLUN)); +} + + +/** + * @interface_method_impl{PDMDEVREG,pfnDetach} + */ +static DECLCALLBACK(void) hdaR3Detach(PPDMDEVINS pDevIns, unsigned iLUN, uint32_t fFlags) +{ + PHDASTATE pThis = PDMDEVINS_2_DATA(pDevIns, PHDASTATE); + PHDASTATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATER3); + RT_NOREF(fFlags); + LogFunc(("iLUN=%u, fFlags=%#x\n", iLUN, fFlags)); + + DEVHDA_LOCK(pDevIns, pThis); + + PHDADRIVER pDrv; + RTListForEach(&pThisCC->lstDrv, pDrv, HDADRIVER, Node) + { + if (pDrv->uLUN == iLUN) + { + hdaR3DetachInternal(pDevIns, pThisCC, pDrv); + RTMemFree(pDrv); + DEVHDA_UNLOCK(pDevIns, pThis); + return; + } + } + + DEVHDA_UNLOCK(pDevIns, pThis); + LogFunc(("LUN#%u was not found\n", iLUN)); +} + + +/** + * Powers off the device. + * + * @param pDevIns Device instance to power off. + */ +static DECLCALLBACK(void) hdaR3PowerOff(PPDMDEVINS pDevIns) +{ + PHDASTATE pThis = PDMDEVINS_2_DATA(pDevIns, PHDASTATE); + PHDASTATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATER3); + + DEVHDA_LOCK_RETURN_VOID(pDevIns, pThis); + + LogRel2(("HDA: Powering off ...\n")); + +/** @todo r=bird: What this "releasing references" and whatever here is + * referring to, is apparently that the device is destroyed after the + * drivers, so creating trouble as those structures have been torn down + * already... Reverse order, like we do for power off? Need a new + * PDMDEVREG flag. */ + + /* Ditto goes for the codec, which in turn uses the mixer. */ + hdaR3CodecPowerOff(&pThisCC->Codec); + + /* This is to prevent us from calling into the mixer and mixer sink code + after it has been destroyed below. */ + for (uint8_t i = 0; i < HDA_MAX_STREAMS; i++) + pThisCC->aStreams[i].State.pAioRegSink = NULL; /* don't need to remove, we're destorying it. */ + + /* + * Note: Destroy the mixer while powering off and *not* in hdaR3Destruct, + * giving the mixer the chance to release any references held to + * PDM audio streams it maintains. + */ + if (pThisCC->pMixer) + { + AudioMixerDestroy(pThisCC->pMixer, pDevIns); + pThisCC->pMixer = NULL; + } + + DEVHDA_UNLOCK(pDevIns, pThis); +} + + +/** + * @interface_method_impl{PDMDEVREG,pfnReset} + */ +static DECLCALLBACK(void) hdaR3Reset(PPDMDEVINS pDevIns) +{ + PHDASTATE pThis = PDMDEVINS_2_DATA(pDevIns, PHDASTATE); + PHDASTATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATER3); + + LogFlowFuncEnter(); + + DEVHDA_LOCK_RETURN_VOID(pDevIns, pThis); + + /* + * 18.2.6,7 defines that values of this registers might be cleared on power on/reset + * hdaR3Reset shouldn't affects these registers. + */ + HDA_REG(pThis, WAKEEN) = 0x0; + + hdaR3GCTLReset(pDevIns, pThis, pThisCC); + + /* Indicate that HDA is not in reset. The firmware is supposed to (un)reset HDA, + * but we can take a shortcut. + */ + HDA_REG(pThis, GCTL) = HDA_GCTL_CRST; + + DEVHDA_UNLOCK(pDevIns, pThis); +} + + +/** + * @interface_method_impl{PDMDEVREG,pfnDestruct} + */ +static DECLCALLBACK(int) hdaR3Destruct(PPDMDEVINS pDevIns) +{ + PDMDEV_CHECK_VERSIONS_RETURN_QUIET(pDevIns); /* this shall come first */ + PHDASTATE pThis = PDMDEVINS_2_DATA(pDevIns, PHDASTATE); + PHDASTATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATER3); + + if (PDMDevHlpCritSectIsInitialized(pDevIns, &pThis->CritSect)) + { + int rc = PDMDevHlpCritSectEnter(pDevIns, &pThis->CritSect, VERR_IGNORED); + AssertRC(rc); + } + + PHDADRIVER pDrv; + while (!RTListIsEmpty(&pThisCC->lstDrv)) + { + pDrv = RTListGetFirst(&pThisCC->lstDrv, HDADRIVER, Node); + + RTListNodeRemove(&pDrv->Node); + RTMemFree(pDrv); + } + + hdaCodecDestruct(&pThisCC->Codec); + + for (uint8_t i = 0; i < HDA_MAX_STREAMS; i++) + hdaR3StreamDestroy(&pThisCC->aStreams[i]); + + /* We don't always go via PowerOff, so make sure the mixer is destroyed. */ + if (pThisCC->pMixer) + { + AudioMixerDestroy(pThisCC->pMixer, pDevIns); + pThisCC->pMixer = NULL; + } + + if (PDMDevHlpCritSectIsInitialized(pDevIns, &pThis->CritSect)) + { + PDMDevHlpCritSectLeave(pDevIns, &pThis->CritSect); + PDMDevHlpCritSectDelete(pDevIns, &pThis->CritSect); + } + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMDEVREG,pfnConstruct} + */ +static DECLCALLBACK(int) hdaR3Construct(PPDMDEVINS pDevIns, int iInstance, PCFGMNODE pCfg) +{ + PDMDEV_CHECK_VERSIONS_RETURN(pDevIns); /* this shall come first */ + PHDASTATE pThis = PDMDEVINS_2_DATA(pDevIns, PHDASTATE); + PHDASTATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATER3); + PCPDMDEVHLPR3 pHlp = pDevIns->pHlpR3; + Assert(iInstance == 0); RT_NOREF(iInstance); + + /* + * Initialize the state sufficently to make the destructor work. + */ + pThis->uAlignmentCheckMagic = HDASTATE_ALIGNMENT_CHECK_MAGIC; + RTListInit(&pThisCC->lstDrv); + pThis->cbCorbBuf = HDA_CORB_SIZE * HDA_CORB_ELEMENT_SIZE; + pThis->cbRirbBuf = HDA_RIRB_SIZE * HDA_RIRB_ELEMENT_SIZE; + pThis->hCorbDmaTask = NIL_PDMTASKHANDLE; + + /** @todo r=bird: There are probably other things which should be + * initialized here before we start failing. */ + + /* + * Validate and read configuration. + */ + PDMDEV_VALIDATE_CONFIG_RETURN(pDevIns, + "BufSizeInMs" + "|BufSizeOutMs" + "|DebugEnabled" + "|DebugPathOut" + "|DeviceName", + ""); + + /** @devcfgm{hda,BufSizeInMs,uint16_t,0,2000,0,ms} + * The size of the DMA buffer for input streams expressed in milliseconds. */ + int rc = pHlp->pfnCFGMQueryU16Def(pCfg, "BufSizeInMs", &pThis->cMsCircBufIn, 0); + if (RT_FAILURE(rc)) + return PDMDEV_SET_ERROR(pDevIns, rc, + N_("HDA configuration error: failed to read 'BufSizeInMs' as 16-bit unsigned integer")); + if (pThis->cMsCircBufIn > 2000) + return PDMDEV_SET_ERROR(pDevIns, VERR_OUT_OF_RANGE, + N_("HDA configuration error: 'BufSizeInMs' is out of bound, max 2000 ms")); + + /** @devcfgm{hda,BufSizeOutMs,uint16_t,0,2000,0,ms} + * The size of the DMA buffer for output streams expressed in milliseconds. */ + rc = pHlp->pfnCFGMQueryU16Def(pCfg, "BufSizeOutMs", &pThis->cMsCircBufOut, 0); + if (RT_FAILURE(rc)) + return PDMDEV_SET_ERROR(pDevIns, rc, + N_("HDA configuration error: failed to read 'BufSizeOutMs' as 16-bit unsigned integer")); + if (pThis->cMsCircBufOut > 2000) + return PDMDEV_SET_ERROR(pDevIns, VERR_OUT_OF_RANGE, + N_("HDA configuration error: 'BufSizeOutMs' is out of bound, max 2000 ms")); + + rc = pHlp->pfnCFGMQueryBoolDef(pCfg, "DebugEnabled", &pThisCC->Dbg.fEnabled, false); + if (RT_FAILURE(rc)) + return PDMDEV_SET_ERROR(pDevIns, rc, + N_("HDA configuration error: failed to read debugging enabled flag as boolean")); + + rc = pHlp->pfnCFGMQueryStringAllocDef(pCfg, "DebugPathOut", &pThisCC->Dbg.pszOutPath, NULL); + if (RT_FAILURE(rc)) + return PDMDEV_SET_ERROR(pDevIns, rc, + N_("HDA configuration error: failed to read debugging output path flag as string")); + if (pThisCC->Dbg.fEnabled) + LogRel2(("HDA: Debug output will be saved to '%s'\n", pThisCC->Dbg.pszOutPath)); + + /** @devcfgm{hda,DeviceName,string} + * Override the default device/vendor IDs for the emulated device: + * - "" - default + * - "Intel ICH6" + * - "Intel Sunrise Point" - great for macOS 10.15 + */ + char szDeviceName[32]; + rc = pHlp->pfnCFGMQueryStringDef(pCfg, "DeviceName", szDeviceName, sizeof(szDeviceName), ""); + if (RT_FAILURE(rc)) + return PDMDEV_SET_ERROR(pDevIns, rc, N_("HDA configuration error: failed to read 'DeviceName' name string")); + enum + { + kDevice_Default, + kDevice_IntelIch6, + kDevice_IntelSunrisePoint /*skylake timeframe*/ + } enmDevice; + if (strcmp(szDeviceName, "") == 0) + enmDevice = kDevice_Default; + else if (strcmp(szDeviceName, "Intel ICH6") == 0) + enmDevice = kDevice_IntelIch6; + else if (strcmp(szDeviceName, "Intel Sunrise Point") == 0) + enmDevice = kDevice_IntelSunrisePoint; + else + return PDMDevHlpVMSetError(pDevIns, VERR_INVALID_PARAMETER, RT_SRC_POS, + N_("HDA configuration error: Unknown 'DeviceName' name '%s'"), szDeviceName); + + /* + * Use our own critical section for the device instead of the default + * one provided by PDM. This allows fine-grained locking in combination + * with TM when timer-specific stuff is being called in e.g. the MMIO handlers. + */ + rc = PDMDevHlpCritSectInit(pDevIns, &pThis->CritSect, RT_SRC_POS, "HDA"); + AssertRCReturn(rc, rc); + + rc = PDMDevHlpSetDeviceCritSect(pDevIns, PDMDevHlpCritSectGetNop(pDevIns)); + AssertRCReturn(rc, rc); + + /* + * Initialize data (most of it anyway). + */ + pThisCC->pDevIns = pDevIns; + /* IBase */ + pThisCC->IBase.pfnQueryInterface = hdaR3QueryInterface; + + /* PCI Device */ + PPDMPCIDEV pPciDev = pDevIns->apPciDevs[0]; + PDMPCIDEV_ASSERT_VALID(pDevIns, pPciDev); + + switch (enmDevice) + { + case kDevice_Default: + PDMPciDevSetVendorId(pPciDev, HDA_PCI_VENDOR_ID); + PDMPciDevSetDeviceId(pPciDev, HDA_PCI_DEVICE_ID); + break; + case kDevice_IntelIch6: /* Our default intel device. */ + PDMPciDevSetVendorId(pPciDev, 0x8086); + PDMPciDevSetDeviceId(pPciDev, 0x2668); + break; + case kDevice_IntelSunrisePoint: /* this is supported by more recent macOS version, at least 10.15 */ + PDMPciDevSetVendorId(pPciDev, 0x8086); + PDMPciDevSetDeviceId(pPciDev, 0x9d70); + break; + } + + PDMPciDevSetCommand( pPciDev, 0x0000); /* 04 rw,ro - pcicmd. */ + PDMPciDevSetStatus( pPciDev, VBOX_PCI_STATUS_CAP_LIST); /* 06 rwc?,ro? - pcists. */ + PDMPciDevSetRevisionId( pPciDev, 0x01); /* 08 ro - rid. */ + PDMPciDevSetClassProg( pPciDev, 0x00); /* 09 ro - pi. */ + PDMPciDevSetClassSub( pPciDev, 0x03); /* 0a ro - scc; 03 == HDA. */ + PDMPciDevSetClassBase( pPciDev, 0x04); /* 0b ro - bcc; 04 == multimedia. */ + PDMPciDevSetHeaderType( pPciDev, 0x00); /* 0e ro - headtyp. */ + PDMPciDevSetBaseAddress( pPciDev, 0, /* 10 rw - MMIO */ + false /* fIoSpace */, false /* fPrefetchable */, true /* f64Bit */, 0x00000000); + PDMPciDevSetInterruptLine( pPciDev, 0x00); /* 3c rw. */ + PDMPciDevSetInterruptPin( pPciDev, 0x01); /* 3d ro - INTA#. */ + +# if defined(HDA_AS_PCI_EXPRESS) + PDMPciDevSetCapabilityList(pPciDev, 0x80); +# elif defined(VBOX_WITH_MSI_DEVICES) + PDMPciDevSetCapabilityList(pPciDev, 0x60); +# else + PDMPciDevSetCapabilityList(pPciDev, 0x50); /* ICH6 datasheet 18.1.16 */ +# endif + + /// @todo r=michaln: If there are really no PDMPciDevSetXx for these, the + /// meaning of these values needs to be properly documented! + /* HDCTL off 0x40 bit 0 selects signaling mode (1-HDA, 0 - Ac97) 18.1.19 */ + PDMPciDevSetByte( pPciDev, 0x40, 0x01); + + /* Power Management */ + PDMPciDevSetByte( pPciDev, 0x50 + 0, VBOX_PCI_CAP_ID_PM); + PDMPciDevSetByte( pPciDev, 0x50 + 1, 0x0); /* next */ + PDMPciDevSetWord( pPciDev, 0x50 + 2, VBOX_PCI_PM_CAP_DSI | 0x02 /* version, PM1.1 */ ); + +# ifdef HDA_AS_PCI_EXPRESS + /* PCI Express */ + PDMPciDevSetByte( pPciDev, 0x80 + 0, VBOX_PCI_CAP_ID_EXP); /* PCI_Express */ + PDMPciDevSetByte( pPciDev, 0x80 + 1, 0x60); /* next */ + /* Device flags */ + PDMPciDevSetWord( pPciDev, 0x80 + 2, + 1 /* version */ + | (VBOX_PCI_EXP_TYPE_ROOT_INT_EP << 4) /* Root Complex Integrated Endpoint */ + | (100 << 9) /* MSI */ ); + /* Device capabilities */ + PDMPciDevSetDWord( pPciDev, 0x80 + 4, VBOX_PCI_EXP_DEVCAP_FLRESET); + /* Device control */ + PDMPciDevSetWord( pPciDev, 0x80 + 8, 0); + /* Device status */ + PDMPciDevSetWord( pPciDev, 0x80 + 10, 0); + /* Link caps */ + PDMPciDevSetDWord( pPciDev, 0x80 + 12, 0); + /* Link control */ + PDMPciDevSetWord( pPciDev, 0x80 + 16, 0); + /* Link status */ + PDMPciDevSetWord( pPciDev, 0x80 + 18, 0); + /* Slot capabilities */ + PDMPciDevSetDWord( pPciDev, 0x80 + 20, 0); + /* Slot control */ + PDMPciDevSetWord( pPciDev, 0x80 + 24, 0); + /* Slot status */ + PDMPciDevSetWord( pPciDev, 0x80 + 26, 0); + /* Root control */ + PDMPciDevSetWord( pPciDev, 0x80 + 28, 0); + /* Root capabilities */ + PDMPciDevSetWord( pPciDev, 0x80 + 30, 0); + /* Root status */ + PDMPciDevSetDWord( pPciDev, 0x80 + 32, 0); + /* Device capabilities 2 */ + PDMPciDevSetDWord( pPciDev, 0x80 + 36, 0); + /* Device control 2 */ + PDMPciDevSetQWord( pPciDev, 0x80 + 40, 0); + /* Link control 2 */ + PDMPciDevSetQWord( pPciDev, 0x80 + 48, 0); + /* Slot control 2 */ + PDMPciDevSetWord( pPciDev, 0x80 + 56, 0); +# endif /* HDA_AS_PCI_EXPRESS */ + + /* + * Register the PCI device. + */ + rc = PDMDevHlpPCIRegister(pDevIns, pPciDev); + AssertRCReturn(rc, rc); + + /** @todo r=bird: The IOMMMIO_FLAGS_READ_DWORD flag isn't entirely optimal, + * as several frequently used registers aren't dword sized. 6.0 and earlier + * will go to ring-3 to handle accesses to any such register, where-as 6.1 and + * later will do trivial register reads in ring-0. Real optimal code would use + * IOMMMIO_FLAGS_READ_PASSTHRU and do the necessary extra work to deal with + * anything the guest may throw at us. */ + rc = PDMDevHlpPCIIORegionCreateMmio(pDevIns, 0, 0x4000, PCI_ADDRESS_SPACE_MEM, hdaMmioWrite, hdaMmioRead, NULL /*pvUser*/, + IOMMMIO_FLAGS_READ_DWORD | IOMMMIO_FLAGS_WRITE_PASSTHRU, "HDA", &pThis->hMmio); + AssertRCReturn(rc, rc); + +# ifdef VBOX_WITH_MSI_DEVICES + PDMMSIREG MsiReg; + RT_ZERO(MsiReg); + MsiReg.cMsiVectors = 1; + MsiReg.iMsiCapOffset = 0x60; + MsiReg.iMsiNextOffset = 0x50; + rc = PDMDevHlpPCIRegisterMsi(pDevIns, &MsiReg); + if (RT_FAILURE(rc)) + { + /* That's OK, we can work without MSI */ + PDMPciDevSetCapabilityList(pPciDev, 0x50); + } +# endif + + /* Create task for continuing CORB DMA in ring-3. */ + rc = PDMDevHlpTaskCreate(pDevIns, PDMTASK_F_RZ, "HDA CORB DMA", + hdaR3CorbDmaTaskWorker, NULL /*pvUser*/, &pThis->hCorbDmaTask); + AssertRCReturn(rc,rc); + + rc = PDMDevHlpSSMRegisterEx(pDevIns, HDA_SAVED_STATE_VERSION, sizeof(*pThis), NULL /*pszBefore*/, + NULL /*pfnLivePrep*/, NULL /*pfnLiveExec*/, NULL /*pfnLiveVote*/, + NULL /*pfnSavePrep*/, hdaR3SaveExec, NULL /*pfnSaveDone*/, + NULL /*pfnLoadPrep*/, hdaR3LoadExec, hdaR3LoadDone); + AssertRCReturn(rc, rc); + + /* + * Attach drivers. We ASSUME they are configured consecutively without any + * gaps, so we stop when we hit the first LUN w/o a driver configured. + */ + for (unsigned iLun = 0; ; iLun++) + { + AssertBreak(iLun < UINT8_MAX); + LogFunc(("Trying to attach driver for LUN#%u ...\n", iLun)); + rc = hdaR3AttachInternal(pDevIns, pThis, pThisCC, iLun, NULL /* ppDrv */); + if (rc == VERR_PDM_NO_ATTACHED_DRIVER) + { + LogFunc(("cLUNs=%u\n", iLun)); + break; + } + AssertLogRelMsgReturn(RT_SUCCESS(rc), ("LUN#%u: rc=%Rrc\n", iLun, rc), rc); + } + + /* + * Create the mixer. + */ + uint32_t fMixer = AUDMIXER_FLAGS_NONE; + if (pThisCC->Dbg.fEnabled) + fMixer |= AUDMIXER_FLAGS_DEBUG; + rc = AudioMixerCreate("HDA Mixer", fMixer, &pThisCC->pMixer); + AssertRCReturn(rc, rc); + + /* + * Add mixer output sinks. + */ +# ifdef VBOX_WITH_AUDIO_HDA_51_SURROUND + rc = AudioMixerCreateSink(pThisCC->pMixer, "Front", + PDMAUDIODIR_OUT, pDevIns, &pThisCC->SinkFront.pMixSink); + AssertRCReturn(rc, rc); + rc = AudioMixerCreateSink(pThisCC->pMixer, "Center+Subwoofer", + PDMAUDIODIR_OUT, pDevIns, &pThisCC->SinkCenterLFE.pMixSink); + AssertRCReturn(rc, rc); + rc = AudioMixerCreateSink(pThisCC->pMixer, "Rear", + PDMAUDIODIR_OUT, pDevIns, &pThisCC->SinkRear.pMixSink); + AssertRCReturn(rc, rc); +# else + rc = AudioMixerCreateSink(pThisCC->pMixer, "PCM Output", + PDMAUDIODIR_OUT, pDevIns, &pThisCC->SinkFront.pMixSink); + AssertRCReturn(rc, rc); +# endif /* VBOX_WITH_AUDIO_HDA_51_SURROUND */ + + /* + * Add mixer input sinks. + */ + rc = AudioMixerCreateSink(pThisCC->pMixer, "Line In", + PDMAUDIODIR_IN, pDevIns, &pThisCC->SinkLineIn.pMixSink); + AssertRCReturn(rc, rc); +# ifdef VBOX_WITH_AUDIO_HDA_MIC_IN + rc = AudioMixerCreateSink(pThisCC->pMixer, "Microphone In", + PDMAUDIODIR_IN, pDevIns, &pThisCC->SinkMicIn.pMixSink); + AssertRCReturn(rc, rc); +# endif + + /* There is no master volume control. Set the master to max. */ + PDMAUDIOVOLUME Vol = PDMAUDIOVOLUME_INITIALIZER_MAX; + rc = AudioMixerSetMasterVolume(pThisCC->pMixer, &Vol); + AssertRCReturn(rc, rc); + + /* + * Initialize the codec. + */ + /* Construct the common + R3 codec part. */ + rc = hdaR3CodecConstruct(pDevIns, &pThisCC->Codec, 0 /* Codec index */, pCfg); + AssertRCReturn(rc, rc); + + /* ICH6 datasheet defines 0 values for SVID and SID (18.1.14-15), which together with values returned for + verb F20 should provide device/codec recognition. */ + Assert(pThisCC->Codec.Cfg.idVendor); + Assert(pThisCC->Codec.Cfg.idDevice); + PDMPciDevSetSubSystemVendorId(pPciDev, pThisCC->Codec.Cfg.idVendor); /* 2c ro - intel.) */ + PDMPciDevSetSubSystemId( pPciDev, pThisCC->Codec.Cfg.idDevice); /* 2e ro. */ + + /* + * Create the per stream timers and the asso. + * + * We must the critical section for the timers as the device has a + * noop section associated with it. + * + * Note: Use TMCLOCK_VIRTUAL_SYNC here, as the guest's HDA driver relies + * on exact (virtual) DMA timing and uses DMA Position Buffers + * instead of the LPIB registers. + */ + /** @todo r=bird: The need to use virtual sync is perhaps because TM + * doesn't schedule regular TMCLOCK_VIRTUAL timers as accurately as it + * should (VT-x preemption timer, etc). Hope to address that before + * long. @bugref{9943}. */ + static const char * const s_apszNames[] = + { "HDA SD0", "HDA SD1", "HDA SD2", "HDA SD3", "HDA SD4", "HDA SD5", "HDA SD6", "HDA SD7", }; + AssertCompile(RT_ELEMENTS(s_apszNames) == HDA_MAX_STREAMS); + for (size_t i = 0; i < HDA_MAX_STREAMS; i++) + { + rc = PDMDevHlpTimerCreate(pDevIns, TMCLOCK_VIRTUAL_SYNC, hdaR3Timer, (void *)(uintptr_t)i, + TMTIMER_FLAGS_NO_CRIT_SECT | TMTIMER_FLAGS_RING0, s_apszNames[i], &pThis->aStreams[i].hTimer); + AssertRCReturn(rc, rc); + + rc = PDMDevHlpTimerSetCritSect(pDevIns, pThis->aStreams[i].hTimer, &pThis->CritSect); + AssertRCReturn(rc, rc); + } + + /* + * Create all hardware streams. + */ + for (uint8_t i = 0; i < HDA_MAX_STREAMS; ++i) + { + rc = hdaR3StreamConstruct(&pThis->aStreams[i], &pThisCC->aStreams[i], pThis, pThisCC, i /* u8SD */); + AssertRCReturn(rc, rc); + } + + hdaR3Reset(pDevIns); + + /* + * Info items and string formatter types. The latter is non-optional as + * the info handles use (at least some of) the custom types and we cannot + * accept screwing formatting. + */ + PDMDevHlpDBGFInfoRegister(pDevIns, "hda", "HDA registers. (hda [register case-insensitive])", hdaR3DbgInfo); + PDMDevHlpDBGFInfoRegister(pDevIns, "hdabdl", + "HDA buffer descriptor list (BDL) and DMA stream positions. (hdabdl [stream number])", + hdaR3DbgInfoBDL); + PDMDevHlpDBGFInfoRegister(pDevIns, "hdastream", "HDA stream info. (hdastream [stream number])", hdaR3DbgInfoStream); + PDMDevHlpDBGFInfoRegister(pDevIns, "hdcnodes", "HDA codec nodes.", hdaR3DbgInfoCodecNodes); + PDMDevHlpDBGFInfoRegister(pDevIns, "hdcselector", "HDA codec's selector states [node number].", hdaR3DbgInfoCodecSelector); + PDMDevHlpDBGFInfoRegister(pDevIns, "hdamixer", "HDA mixer state.", hdaR3DbgInfoMixer); + + rc = RTStrFormatTypeRegister("sdctl", hdaR3StrFmtSDCTL, NULL); + AssertMsgReturn(RT_SUCCESS(rc) || rc == VERR_ALREADY_EXISTS, ("%Rrc\n", rc), rc); + rc = RTStrFormatTypeRegister("sdsts", hdaR3StrFmtSDSTS, NULL); + AssertMsgReturn(RT_SUCCESS(rc) || rc == VERR_ALREADY_EXISTS, ("%Rrc\n", rc), rc); + /** @todo the next two are rather pointless. */ + rc = RTStrFormatTypeRegister("sdfifos", hdaR3StrFmtSDFIFOS, NULL); + AssertMsgReturn(RT_SUCCESS(rc) || rc == VERR_ALREADY_EXISTS, ("%Rrc\n", rc), rc); + rc = RTStrFormatTypeRegister("sdfifow", hdaR3StrFmtSDFIFOW, NULL); + AssertMsgReturn(RT_SUCCESS(rc) || rc == VERR_ALREADY_EXISTS, ("%Rrc\n", rc), rc); + + /* + * Asserting sanity. + */ + AssertCompile(RT_ELEMENTS(pThis->au32Regs) < 256 /* assumption by HDAREGDESC::idxReg */); + for (unsigned i = 0; i < RT_ELEMENTS(g_aHdaRegMap); i++) + { + struct HDAREGDESC const *pReg = &g_aHdaRegMap[i]; + struct HDAREGDESC const *pNextReg = i + 1 < RT_ELEMENTS(g_aHdaRegMap) ? &g_aHdaRegMap[i + 1] : NULL; + + /* binary search order. */ + AssertReleaseMsg(!pNextReg || pReg->off + pReg->cb <= pNextReg->off, + ("[%#x] = {%#x LB %#x} vs. [%#x] = {%#x LB %#x}\n", + i, pReg->off, pReg->cb, i + 1, pNextReg->off, pNextReg->cb)); + + /* alignment. */ + AssertReleaseMsg( pReg->cb == 1 + || (pReg->cb == 2 && (pReg->off & 1) == 0) + || (pReg->cb == 3 && (pReg->off & 3) == 0) + || (pReg->cb == 4 && (pReg->off & 3) == 0), + ("[%#x] = {%#x LB %#x}\n", i, pReg->off, pReg->cb)); + + /* registers are packed into dwords - with 3 exceptions with gaps at the end of the dword. */ + AssertRelease(((pReg->off + pReg->cb) & 3) == 0 || pNextReg); + if (pReg->off & 3) + { + struct HDAREGDESC const *pPrevReg = i > 0 ? &g_aHdaRegMap[i - 1] : NULL; + AssertReleaseMsg(pPrevReg, ("[%#x] = {%#x LB %#x}\n", i, pReg->off, pReg->cb)); + if (pPrevReg) + AssertReleaseMsg(pPrevReg->off + pPrevReg->cb == pReg->off, + ("[%#x] = {%#x LB %#x} vs. [%#x] = {%#x LB %#x}\n", + i - 1, pPrevReg->off, pPrevReg->cb, i + 1, pReg->off, pReg->cb)); + } +#if 0 + if ((pReg->offset + pReg->size) & 3) + { + AssertReleaseMsg(pNextReg, ("[%#x] = {%#x LB %#x}\n", i, pReg->offset, pReg->size)); + if (pNextReg) + AssertReleaseMsg(pReg->offset + pReg->size == pNextReg->offset, + ("[%#x] = {%#x LB %#x} vs. [%#x] = {%#x LB %#x}\n", + i, pReg->offset, pReg->size, i + 1, pNextReg->offset, pNextReg->size)); + } +#endif + /* The final entry is a full DWORD, no gaps! Allows shortcuts. */ + AssertReleaseMsg(pNextReg || ((pReg->off + pReg->cb) & 3) == 0, + ("[%#x] = {%#x LB %#x}\n", i, pReg->off, pReg->cb)); + } + Assert(strcmp(g_aHdaRegMap[HDA_REG_SSYNC].pszName, "SSYNC") == 0); + Assert(strcmp(g_aHdaRegMap[HDA_REG_DPUBASE].pszName, "DPUBASE") == 0); + Assert(strcmp(g_aHdaRegMap[HDA_REG_MLCH].pszName, "MLCH") == 0); + Assert(strcmp(g_aHdaRegMap[HDA_REG_SD3DPIB].pszName, "SD3DPIB") == 0); + Assert(strcmp(g_aHdaRegMap[HDA_REG_SD7EFIFOS].pszName, "SD7EFIFOS") == 0); + + /* + * Register statistics. + */ +# ifdef VBOX_WITH_STATISTICS + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatIn, STAMTYPE_PROFILE, "Input", STAMUNIT_TICKS_PER_CALL, "Profiling input."); + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatOut, STAMTYPE_PROFILE, "Output", STAMUNIT_TICKS_PER_CALL, "Profiling output."); + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatBytesRead, STAMTYPE_COUNTER, "BytesRead" , STAMUNIT_BYTES, "Bytes read (DMA) from the guest."); + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatBytesWritten, STAMTYPE_COUNTER, "BytesWritten", STAMUNIT_BYTES, "Bytes written (DMA) to the guest."); +# ifdef VBOX_HDA_WITH_ON_REG_ACCESS_DMA + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatAccessDmaOutput, STAMTYPE_COUNTER, "AccessDmaOutput", STAMUNIT_COUNT, "Number of on-register-access DMA sub-transfers we've made."); + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatAccessDmaOutputToR3,STAMTYPE_COUNTER, "AccessDmaOutputToR3", STAMUNIT_COUNT, "Number of time the on-register-access DMA forced a ring-3 return."); +# endif + + AssertCompile(RT_ELEMENTS(g_aHdaRegMap) == HDA_NUM_REGS); + AssertCompile(RT_ELEMENTS(pThis->aStatRegReads) == HDA_NUM_REGS); + AssertCompile(RT_ELEMENTS(pThis->aStatRegReadsToR3) == HDA_NUM_REGS); + AssertCompile(RT_ELEMENTS(pThis->aStatRegWrites) == HDA_NUM_REGS); + AssertCompile(RT_ELEMENTS(pThis->aStatRegWritesToR3) == HDA_NUM_REGS); + for (size_t i = 0; i < RT_ELEMENTS(g_aHdaRegMap); i++) + { + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStatRegReads[i], STAMTYPE_COUNTER, STAMVISIBILITY_ALWAYS, STAMUNIT_OCCURENCES, + g_aHdaRegMap[i].pszDesc, "Regs/%03x-%s-Reads", g_aHdaRegMap[i].off, g_aHdaRegMap[i].pszName); + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStatRegReadsToR3[i], STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + g_aHdaRegMap[i].pszDesc, "Regs/%03x-%s-Reads-ToR3", g_aHdaRegMap[i].off, g_aHdaRegMap[i].pszName); + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStatRegWrites[i], STAMTYPE_COUNTER, STAMVISIBILITY_ALWAYS, STAMUNIT_OCCURENCES, + g_aHdaRegMap[i].pszDesc, "Regs/%03x-%s-Writes", g_aHdaRegMap[i].off, g_aHdaRegMap[i].pszName); + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStatRegWritesToR3[i], STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + g_aHdaRegMap[i].pszDesc, "Regs/%03x-%s-Writes-ToR3", g_aHdaRegMap[i].off, g_aHdaRegMap[i].pszName); + } + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatRegMultiReadsR3, STAMTYPE_COUNTER, "RegMultiReadsR3", STAMUNIT_OCCURENCES, "Register read not targeting just one register, handled in ring-3"); + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatRegMultiReadsRZ, STAMTYPE_COUNTER, "RegMultiReadsRZ", STAMUNIT_OCCURENCES, "Register read not targeting just one register, handled in ring-0"); + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatRegMultiWritesR3, STAMTYPE_COUNTER, "RegMultiWritesR3", STAMUNIT_OCCURENCES, "Register writes not targeting just one register, handled in ring-3"); + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatRegMultiWritesRZ, STAMTYPE_COUNTER, "RegMultiWritesRZ", STAMUNIT_OCCURENCES, "Register writes not targeting just one register, handled in ring-0"); + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatRegSubWriteR3, STAMTYPE_COUNTER, "RegSubWritesR3", STAMUNIT_OCCURENCES, "Trucated register writes, handled in ring-3"); + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatRegSubWriteRZ, STAMTYPE_COUNTER, "RegSubWritesRZ", STAMUNIT_OCCURENCES, "Trucated register writes, handled in ring-0"); + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatRegUnknownReads, STAMTYPE_COUNTER, "RegUnknownReads", STAMUNIT_OCCURENCES, "Reads of unknown registers."); + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatRegUnknownWrites, STAMTYPE_COUNTER, "RegUnknownWrites", STAMUNIT_OCCURENCES, "Writes to unknown registers."); + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatRegWritesBlockedByReset, STAMTYPE_COUNTER, "RegWritesBlockedByReset", STAMUNIT_OCCURENCES, "Writes blocked by pending reset (GCTL/CRST)"); + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatRegWritesBlockedByRun, STAMTYPE_COUNTER, "RegWritesBlockedByRun", STAMUNIT_OCCURENCES, "Writes blocked by byte RUN bit."); +# endif + + for (uint8_t idxStream = 0; idxStream < RT_ELEMENTS(pThisCC->aStreams); idxStream++) + { + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatDmaFlowProblems, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + "Number of internal DMA buffer problems.", "Stream%u/DMABufferProblems", idxStream); + if (hdaGetDirFromSD(idxStream) == PDMAUDIODIR_OUT) + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatDmaFlowErrors, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + "Number of internal DMA buffer overflows.", "Stream%u/DMABufferOverflows", idxStream); + else + { + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatDmaFlowErrors, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + "Number of internal DMA buffer underuns.", "Stream%u/DMABufferUnderruns", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatDmaFlowErrorBytes, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "Number of bytes of silence added to cope with underruns.", "Stream%u/DMABufferSilence", idxStream); + } + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatDmaSkippedPendingBcis, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + "DMA transfer period skipped because of BCIS pending.", "Stream%u/DMASkippedPendingBCIS", idxStream); + + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStreams[idxStream].State.offRead, STAMTYPE_U64, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "Virtual internal buffer read position.", "Stream%u/offRead", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStreams[idxStream].State.offWrite, STAMTYPE_U64, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "Virtual internal buffer write position.", "Stream%u/offWrite", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStreams[idxStream].State.cbCurDmaPeriod, STAMTYPE_U32, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "Bytes transfered per DMA timer callout.", "Stream%u/cbCurDmaPeriod", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, (void*)&pThis->aStreams[idxStream].State.fRunning, STAMTYPE_BOOL, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "True if the stream is in RUN mode.", "Stream%u/fRunning", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStreams[idxStream].State.Cfg.Props.uHz, STAMTYPE_U32, STAMVISIBILITY_USED, STAMUNIT_HZ, + "The stream frequency.", "Stream%u/Cfg/Hz", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStreams[idxStream].State.Cfg.Props.cbFrame, STAMTYPE_U8, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "The frame size.", "Stream%u/Cfg/FrameSize", idxStream); +#if 0 /** @todo this would require some callback or expansion. */ + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStreams[idxStream].State.Cfg.Props.cChannelsX, STAMTYPE_U8, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "The number of channels.", "Stream%u/Cfg/Channels-Host", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.Mapping.GuestProps.cChannels, STAMTYPE_U8, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "The number of channels.", "Stream%u/Cfg/Channels-Guest", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStreams[idxStream].State.Cfg.Props.cbSample, STAMTYPE_U8, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "The size of a sample (per channel).", "Stream%u/Cfg/cbSample", idxStream); +#endif + + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatDmaBufSize, STAMTYPE_U32, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "Size of the internal DMA buffer.", "Stream%u/DMABufSize", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatDmaBufUsed, STAMTYPE_U32, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "Number of bytes used in the internal DMA buffer.", "Stream%u/DMABufUsed", idxStream); + + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatStart, STAMTYPE_PROFILE, STAMVISIBILITY_USED, STAMUNIT_NS_PER_CALL, + "Starting the stream.", "Stream%u/Start", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatStop, STAMTYPE_PROFILE, STAMVISIBILITY_USED, STAMUNIT_NS_PER_CALL, + "Stopping the stream.", "Stream%u/Stop", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatReset, STAMTYPE_PROFILE, STAMVISIBILITY_USED, STAMUNIT_NS_PER_CALL, + "Resetting the stream.", "Stream%u/Reset", idxStream); + } + + return VINF_SUCCESS; +} + +#else /* !IN_RING3 */ + +/** + * @callback_method_impl{PDMDEVREGR0,pfnConstruct} + */ +static DECLCALLBACK(int) hdaRZConstruct(PPDMDEVINS pDevIns) +{ + PDMDEV_CHECK_VERSIONS_RETURN(pDevIns); /* this shall come first */ + PHDASTATE pThis = PDMDEVINS_2_DATA(pDevIns, PHDASTATE); + PHDASTATER0 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATER0); + + int rc = PDMDevHlpSetDeviceCritSect(pDevIns, PDMDevHlpCritSectGetNop(pDevIns)); + AssertRCReturn(rc, rc); + + rc = PDMDevHlpMmioSetUpContext(pDevIns, pThis->hMmio, hdaMmioWrite, hdaMmioRead, NULL /*pvUser*/); + AssertRCReturn(rc, rc); + +# if 0 /* Codec is not yet kosher enough for ring-0. @bugref{9890c64} */ + /* Construct the R0 codec part. */ + rc = hdaR0CodecConstruct(pDevIns, &pThis->Codec, &pThisCC->Codec); + AssertRCReturn(rc, rc); +# else + RT_NOREF(pThisCC); +# endif + + return VINF_SUCCESS; +} + +#endif /* !IN_RING3 */ + +/** + * The device registration structure. + */ +const PDMDEVREG g_DeviceHDA = +{ + /* .u32Version = */ PDM_DEVREG_VERSION, + /* .uReserved0 = */ 0, + /* .szName = */ "hda", + /* .fFlags = */ PDM_DEVREG_FLAGS_DEFAULT_BITS | PDM_DEVREG_FLAGS_RZ | PDM_DEVREG_FLAGS_NEW_STYLE + | PDM_DEVREG_FLAGS_FIRST_POWEROFF_NOTIFICATION /* stream clearnup with working drivers */, + /* .fClass = */ PDM_DEVREG_CLASS_AUDIO, + /* .cMaxInstances = */ 1, + /* .uSharedVersion = */ 42, + /* .cbInstanceShared = */ sizeof(HDASTATE), + /* .cbInstanceCC = */ CTX_EXPR(sizeof(HDASTATER3), sizeof(HDASTATER0), 0), + /* .cbInstanceRC = */ 0, + /* .cMaxPciDevices = */ 1, + /* .cMaxMsixVectors = */ 0, + /* .pszDescription = */ "Intel HD Audio Controller", +#if defined(IN_RING3) + /* .pszRCMod = */ "VBoxDDRC.rc", + /* .pszR0Mod = */ "VBoxDDR0.r0", + /* .pfnConstruct = */ hdaR3Construct, + /* .pfnDestruct = */ hdaR3Destruct, + /* .pfnRelocate = */ NULL, + /* .pfnMemSetup = */ NULL, + /* .pfnPowerOn = */ NULL, + /* .pfnReset = */ hdaR3Reset, + /* .pfnSuspend = */ NULL, + /* .pfnResume = */ NULL, + /* .pfnAttach = */ hdaR3Attach, + /* .pfnDetach = */ hdaR3Detach, + /* .pfnQueryInterface = */ NULL, + /* .pfnInitComplete = */ NULL, + /* .pfnPowerOff = */ hdaR3PowerOff, + /* .pfnSoftReset = */ NULL, + /* .pfnReserved0 = */ NULL, + /* .pfnReserved1 = */ NULL, + /* .pfnReserved2 = */ NULL, + /* .pfnReserved3 = */ NULL, + /* .pfnReserved4 = */ NULL, + /* .pfnReserved5 = */ NULL, + /* .pfnReserved6 = */ NULL, + /* .pfnReserved7 = */ NULL, +#elif defined(IN_RING0) + /* .pfnEarlyConstruct = */ NULL, + /* .pfnConstruct = */ hdaRZConstruct, + /* .pfnDestruct = */ NULL, + /* .pfnFinalDestruct = */ NULL, + /* .pfnRequest = */ NULL, + /* .pfnReserved0 = */ NULL, + /* .pfnReserved1 = */ NULL, + /* .pfnReserved2 = */ NULL, + /* .pfnReserved3 = */ NULL, + /* .pfnReserved4 = */ NULL, + /* .pfnReserved5 = */ NULL, + /* .pfnReserved6 = */ NULL, + /* .pfnReserved7 = */ NULL, +#elif defined(IN_RC) + /* .pfnConstruct = */ hdaRZConstruct, + /* .pfnReserved0 = */ NULL, + /* .pfnReserved1 = */ NULL, + /* .pfnReserved2 = */ NULL, + /* .pfnReserved3 = */ NULL, + /* .pfnReserved4 = */ NULL, + /* .pfnReserved5 = */ NULL, + /* .pfnReserved6 = */ NULL, + /* .pfnReserved7 = */ NULL, +#else +# error "Not in IN_RING3, IN_RING0 or IN_RC!" +#endif + /* .u32VersionEnd = */ PDM_DEVREG_VERSION +}; + +#endif /* !VBOX_DEVICE_STRUCT_TESTCASE */ + diff --git a/src/VBox/Devices/Audio/DevHda.h b/src/VBox/Devices/Audio/DevHda.h new file mode 100644 index 00000000..b8fe851f --- /dev/null +++ b/src/VBox/Devices/Audio/DevHda.h @@ -0,0 +1,918 @@ +/* $Id: DevHda.h $ */ +/** @file + * Intel HD Audio Controller Emulation - Structures. + */ + +/* + * Copyright (C) 2016-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 + */ + +#ifndef VBOX_INCLUDED_SRC_Audio_DevHda_h +#define VBOX_INCLUDED_SRC_Audio_DevHda_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include <iprt/path.h> +#include <VBox/vmm/pdmdev.h> +#include "AudioMixer.h" + +/* + * Compile time feature configuration. + */ + +/** @def VBOX_HDA_WITH_ON_REG_ACCESS_DMA + * Enables doing DMA work on certain register accesses (LPIB, WALCLK) in + * addition to the DMA timer. All but the last frame can be done during + * register accesses (as we don't wish to leave the DMA timer w/o work to + * do in case that upsets it). */ +#if defined(DOXYGEN_RUNNING) || 0 +# define VBOX_HDA_WITH_ON_REG_ACCESS_DMA +#endif + +#ifdef DEBUG_andy +/** Enables strict mode, which checks for stuff which isn't supposed to happen. + * Be prepared for assertions coming in! */ +//# define HDA_STRICT +#endif + +/** @def HDA_AS_PCI_EXPRESS + * Enables PCI express hardware. */ +#if defined(DOXYGEN_RUNNING) || 0 +# define HDA_AS_PCI_EXPRESS +#endif + +/** @def HDA_DEBUG_SILENCE + * To debug silence coming from the guest in form of audio gaps. + * Very crude implementation for now. + * @todo probably borked atm */ +#if defined(DOXYGEN_RUNNING) || 0 +# define HDA_DEBUG_SILENCE +#endif + + +/* + * Common pointer types. + */ +/** Pointer to an HDA stream (SDI / SDO). */ +typedef struct HDASTREAMR3 *PHDASTREAMR3; +/** Pointer to a shared HDA device state. */ +typedef struct HDASTATE *PHDASTATE; +/** Pointer to a ring-3 HDA device state. */ +typedef struct HDASTATER3 *PHDASTATER3; +/** Pointer to an HDA mixer sink definition (ring-3). */ +typedef struct HDAMIXERSINK *PHDAMIXERSINK; + + +/* + * The rest of the headers. + */ +#include "DevHdaStream.h" +#include "DevHdaCodec.h" + + + +/** @name Stream counts. + * + * At the moment we support 4 input + 4 output streams max, which is 8 in total. + * Bidirectional streams are currently *not* supported. + * + * @note When changing any of those values, be prepared for some saved state + * fixups / trouble! + * @{ + */ +#define HDA_MAX_SDI 4 +#define HDA_MAX_SDO 4 +#define HDA_MAX_STREAMS (HDA_MAX_SDI + HDA_MAX_SDO) +/** @} */ +AssertCompile(HDA_MAX_SDI <= HDA_MAX_SDO); + + +/** @defgroup grp_hda_regs HDA Register Definitions + * + * There are two variants for most register defines: + * - HDA_REG_XXX: Index into g_aHdaRegMap + * - HDA_RMX_XXX: Index into HDASTATE::au32Regs + * + * Use the HDA_REG and HDA_STREAM_REG macros to access registers where possible. + * + * @note The au32Regs[] layout is kept unchanged for saved state compatibility, + * thus the HDA_RMX_XXX assignments are for all purposes set in stone. + * + * @{ */ + +/** Number of general registers. */ +#define HDA_NUM_GENERAL_REGS 36 +/** Number of stream registers (10 registers per stream). */ +#define HDA_NUM_STREAM_REGS (HDA_MAX_STREAMS * 10 /* Each stream descriptor has 10 registers */) +/** Number of register after the stream registers. */ +#define HDA_NUM_POST_STREAM_REGS (2 + HDA_MAX_STREAMS * 2) +/** Number of total registers in the HDA's register map. */ +#define HDA_NUM_REGS (HDA_NUM_GENERAL_REGS + HDA_NUM_STREAM_REGS + HDA_NUM_POST_STREAM_REGS) +/** Total number of stream tags (channels). Index 0 is reserved / invalid. */ +#define HDA_MAX_TAGS 16 + + +/** Offset of the SD0 register map. */ +#define HDA_REG_DESC_SD0_BASE 0x80 + +/* Registers */ +#define HDA_REG_IND_NAME(x) HDA_REG_##x +#define HDA_MEM_IND_NAME(x) HDA_RMX_##x + +/** Direct register access by HDASTATE::au32Reg index. */ +#define HDA_REG_BY_IDX(a_pThis, a_idxReg) ((a_pThis)->au32Regs[(a_idxReg)]) + +/** Accesses register @a ShortRegNm. */ +#if defined(VBOX_STRICT) && defined(VBOX_HDA_CAN_ACCESS_REG_MAP) +# define HDA_REG(a_pThis, a_ShortRegNm) (*hdaStrictRegAccessor(a_pThis, HDA_REG_IND_NAME(a_ShortRegNm), HDA_MEM_IND_NAME(a_ShortRegNm))) +#else +# define HDA_REG(a_pThis, a_ShortRegNm) HDA_REG_BY_IDX(a_pThis, HDA_MEM_IND_NAME(a_ShortRegNm)) +#endif + +/** Indirect register access via g_aHdaRegMap[].idxReg. */ +#define HDA_REG_IND(a_pThis, a_idxMap) HDA_REG_BY_IDX(a_pThis, g_aHdaRegMap[(a_idxMap)].idxReg) + + +#define HDA_REG_GCAP 0 /* Range 0x00 - 0x01 */ +#define HDA_RMX_GCAP 0 +/** + * GCAP HDASpec 3.3.2 This macro encodes the following information about HDA in a compact manner: + * + * oss (15:12) - Number of output streams supported. + * iss (11:8) - Number of input streams supported. + * bss (7:3) - Number of bidirectional streams supported. + * bds (2:1) - Number of serial data out (SDO) signals supported. + * b64sup (0) - 64 bit addressing supported. + */ +#define HDA_MAKE_GCAP(oss, iss, bss, bds, b64sup) \ + ( (((oss) & 0xF) << 12) \ + | (((iss) & 0xF) << 8) \ + | (((bss) & 0x1F) << 3) \ + | (((bds) & 0x3) << 2) \ + | ((b64sup) & 1)) + +#define HDA_REG_VMIN 1 /* 0x02 */ +#define HDA_RMX_VMIN 1 + +#define HDA_REG_VMAJ 2 /* 0x03 */ +#define HDA_RMX_VMAJ 2 + +#define HDA_REG_OUTPAY 3 /* 0x04-0x05 */ +#define HDA_RMX_OUTPAY 3 + +#define HDA_REG_INPAY 4 /* 0x06-0x07 */ +#define HDA_RMX_INPAY 4 + +#define HDA_REG_GCTL 5 /* 0x08-0x0B */ +#define HDA_RMX_GCTL 5 +#define HDA_GCTL_UNSOL RT_BIT(8) /* Accept Unsolicited Response Enable */ +#define HDA_GCTL_FCNTRL RT_BIT(1) /* Flush Control */ +#define HDA_GCTL_CRST RT_BIT(0) /* Controller Reset */ + +#define HDA_REG_WAKEEN 6 /* 0x0C */ +#define HDA_RMX_WAKEEN 6 + +#define HDA_REG_STATESTS 7 /* 0x0E */ +#define HDA_RMX_STATESTS 7 +#define HDA_STATESTS_SCSF_MASK 0x7 /* State Change Status Flags (6.2.8). */ + +#define HDA_REG_GSTS 8 /* 0x10-0x11*/ +#define HDA_RMX_GSTS 8 +#define HDA_GSTS_FSTS RT_BIT(1) /* Flush Status */ + +#define HDA_REG_LLCH 9 /* 0x14 */ +#define HDA_RMX_LLCH 114 + +#define HDA_REG_OUTSTRMPAY 10 /* 0x18 */ +#define HDA_RMX_OUTSTRMPAY 112 + +#define HDA_REG_INSTRMPAY 11 /* 0x1a */ +#define HDA_RMX_INSTRMPAY 113 + +#define HDA_REG_INTCTL 12 /* 0x20 */ +#define HDA_RMX_INTCTL 9 +#define HDA_INTCTL_GIE RT_BIT(31) /* Global Interrupt Enable */ +#define HDA_INTCTL_CIE RT_BIT(30) /* Controller Interrupt Enable */ +/** Bits 0-29 correspond to streams 0-29. */ +#define HDA_STRMINT_MASK 0xFF /* Streams 0-7 implemented. Applies to INTCTL and INTSTS. */ + +#define HDA_REG_INTSTS 13 /* 0x24 */ +#define HDA_RMX_INTSTS 10 +#define HDA_INTSTS_GIS RT_BIT(31) /* Global Interrupt Status */ +#define HDA_INTSTS_CIS RT_BIT(30) /* Controller Interrupt Status */ + +#define HDA_REG_WALCLK 14 /* 0x30 */ +/* NB: HDA_RMX_WALCLK is not defined because the register is not stored in memory. */ + +/** + * @note The HDA specification defines a SSYNC register at offset 0x38. The ICH6/ICH9 + * datahseet defines SSYNC at offset 0x34. The Linux HDA driver matches the datasheet. + * See also https://mailman.alsa-project.org/pipermail/alsa-devel/2011-March/037819.html + */ +#define HDA_REG_SSYNC 15 /* 0x34 */ +#define HDA_RMX_SSYNC 12 + +#define HDA_REG_NEW_SSYNC 16 /* 0x38 */ +#define HDA_RMX_NEW_SSYNC HDA_RMX_SSYNC + +#define HDA_REG_CORBLBASE 17 /* 0x40 */ +#define HDA_RMX_CORBLBASE 13 + +#define HDA_REG_CORBUBASE 18 /* 0x44 */ +#define HDA_RMX_CORBUBASE 14 + +#define HDA_REG_CORBWP 19 /* 0x48 */ +#define HDA_RMX_CORBWP 15 + +#define HDA_REG_CORBRP 20 /* 0x4A */ +#define HDA_RMX_CORBRP 16 +#define HDA_CORBRP_RST RT_BIT(15) /* CORB Read Pointer Reset */ + +#define HDA_REG_CORBCTL 21 /* 0x4C */ +#define HDA_RMX_CORBCTL 17 +#define HDA_CORBCTL_DMA RT_BIT(1) /* Enable CORB DMA Engine */ +#define HDA_CORBCTL_CMEIE RT_BIT(0) /* CORB Memory Error Interrupt Enable */ + +#define HDA_REG_CORBSTS 22 /* 0x4D */ +#define HDA_RMX_CORBSTS 18 + +#define HDA_REG_CORBSIZE 23 /* 0x4E */ +#define HDA_RMX_CORBSIZE 19 +#define HDA_CORBSIZE_SZ_CAP 0xF0 +#define HDA_CORBSIZE_SZ 0x3 + +/** Number of CORB buffer entries. */ +#define HDA_CORB_SIZE 256 +/** CORB element size (in bytes). */ +#define HDA_CORB_ELEMENT_SIZE 4 +/** Number of RIRB buffer entries. */ +#define HDA_RIRB_SIZE 256 +/** RIRB element size (in bytes). */ +#define HDA_RIRB_ELEMENT_SIZE 8 + +#define HDA_REG_RIRBLBASE 24 /* 0x50 */ +#define HDA_RMX_RIRBLBASE 20 + +#define HDA_REG_RIRBUBASE 25 /* 0x54 */ +#define HDA_RMX_RIRBUBASE 21 + +#define HDA_REG_RIRBWP 26 /* 0x58 */ +#define HDA_RMX_RIRBWP 22 +#define HDA_RIRBWP_RST RT_BIT(15) /* RIRB Write Pointer Reset */ + +#define HDA_REG_RINTCNT 27 /* 0x5A */ +#define HDA_RMX_RINTCNT 23 + +/** Maximum number of Response Interrupts. */ +#define HDA_MAX_RINTCNT 256 + +#define HDA_REG_RIRBCTL 28 /* 0x5C */ +#define HDA_RMX_RIRBCTL 24 +#define HDA_RIRBCTL_ROIC RT_BIT(2) /* Response Overrun Interrupt Control */ +#define HDA_RIRBCTL_RDMAEN RT_BIT(1) /* RIRB DMA Enable */ +#define HDA_RIRBCTL_RINTCTL RT_BIT(0) /* Response Interrupt Control */ + +#define HDA_REG_RIRBSTS 29 /* 0x5D */ +#define HDA_RMX_RIRBSTS 25 +#define HDA_RIRBSTS_RIRBOIS RT_BIT(2) /* Response Overrun Interrupt Status */ +#define HDA_RIRBSTS_RINTFL RT_BIT(0) /* Response Interrupt Flag */ + +#define HDA_REG_RIRBSIZE 30 /* 0x5E */ +#define HDA_RMX_RIRBSIZE 26 + +#define HDA_REG_IC 31 /* 0x60 */ +#define HDA_RMX_IC 27 + +#define HDA_REG_IR 32 /* 0x64 */ +#define HDA_RMX_IR 28 + +#define HDA_REG_IRS 33 /* 0x68 */ +#define HDA_RMX_IRS 29 +#define HDA_IRS_IRV RT_BIT(1) /* Immediate Result Valid */ +#define HDA_IRS_ICB RT_BIT(0) /* Immediate Command Busy */ + +#define HDA_REG_DPLBASE 34 /* 0x70 */ +#define HDA_RMX_DPLBASE 30 + +#define HDA_REG_DPUBASE 35 /* 0x74 */ +#define HDA_RMX_DPUBASE 31 + +#define DPBASE_ADDR_MASK (~(uint64_t)0x7f) + +#define HDA_STREAM_REG_DEF(name, num) (HDA_REG_SD##num##name) +#define HDA_STREAM_RMX_DEF(name, num) (HDA_RMX_SD##num##name) +/** @note sdnum here _MUST_ be stream reg number [0,7]. */ +#if defined(VBOX_STRICT) && defined(VBOX_HDA_CAN_ACCESS_REG_MAP) +# define HDA_STREAM_REG(pThis, name, sdnum) (*hdaStrictStreamRegAccessor((pThis), HDA_REG_SD0##name, HDA_RMX_SD0##name, (sdnum))) +#else +# define HDA_STREAM_REG(pThis, name, sdnum) (HDA_REG_BY_IDX((pThis), HDA_RMX_SD0##name + (sdnum) * 10)) +#endif + +#define HDA_SD_NUM_FROM_REG(pThis, func, reg) ((reg - HDA_STREAM_REG_DEF(func, 0)) / 10) +#define HDA_SD_TO_REG(a_Name, uSD) (HDA_STREAM_REG_DEF(a_Name, 0) + (uSD) * 10) + +/** @todo Condense marcos! */ + +#define HDA_REG_SD0CTL HDA_NUM_GENERAL_REGS /* 0x80; other streams offset by 0x20 */ +#define HDA_RMX_SD0CTL 32 +#define HDA_RMX_SD1CTL (HDA_STREAM_RMX_DEF(CTL, 0) + 10) +#define HDA_RMX_SD2CTL (HDA_STREAM_RMX_DEF(CTL, 0) + 20) +#define HDA_RMX_SD3CTL (HDA_STREAM_RMX_DEF(CTL, 0) + 30) +#define HDA_RMX_SD4CTL (HDA_STREAM_RMX_DEF(CTL, 0) + 40) +#define HDA_RMX_SD5CTL (HDA_STREAM_RMX_DEF(CTL, 0) + 50) +#define HDA_RMX_SD6CTL (HDA_STREAM_RMX_DEF(CTL, 0) + 60) +#define HDA_RMX_SD7CTL (HDA_STREAM_RMX_DEF(CTL, 0) + 70) + +#define HDA_SDCTL_NUM_MASK 0xF +#define HDA_SDCTL_NUM_SHIFT 20 +#define HDA_SDCTL_DIR RT_BIT(19) /* Direction (Bidirectional streams only!) */ +#define HDA_SDCTL_TP RT_BIT(18) /* Traffic Priority (PCI Express) */ +#define HDA_SDCTL_STRIPE_MASK 0x3 +#define HDA_SDCTL_STRIPE_SHIFT 16 +#define HDA_SDCTL_DEIE RT_BIT(4) /* Descriptor Error Interrupt Enable */ +#define HDA_SDCTL_FEIE RT_BIT(3) /* FIFO Error Interrupt Enable */ +#define HDA_SDCTL_IOCE RT_BIT(2) /* Interrupt On Completion Enable */ +#define HDA_SDCTL_RUN RT_BIT(1) /* Stream Run */ +#define HDA_SDCTL_SRST RT_BIT(0) /* Stream Reset */ + +#define HDA_REG_SD0STS (HDA_NUM_GENERAL_REGS + 1) /* 0x83; other streams offset by 0x20 */ +#define HDA_RMX_SD0STS 33 +#define HDA_RMX_SD1STS (HDA_STREAM_RMX_DEF(STS, 0) + 10) +#define HDA_RMX_SD2STS (HDA_STREAM_RMX_DEF(STS, 0) + 20) +#define HDA_RMX_SD3STS (HDA_STREAM_RMX_DEF(STS, 0) + 30) +#define HDA_RMX_SD4STS (HDA_STREAM_RMX_DEF(STS, 0) + 40) +#define HDA_RMX_SD5STS (HDA_STREAM_RMX_DEF(STS, 0) + 50) +#define HDA_RMX_SD6STS (HDA_STREAM_RMX_DEF(STS, 0) + 60) +#define HDA_RMX_SD7STS (HDA_STREAM_RMX_DEF(STS, 0) + 70) + +#define HDA_SDSTS_FIFORDY RT_BIT(5) /* FIFO Ready */ +#define HDA_SDSTS_DESE RT_BIT(4) /* Descriptor Error */ +#define HDA_SDSTS_FIFOE RT_BIT(3) /* FIFO Error */ +#define HDA_SDSTS_BCIS RT_BIT(2) /* Buffer Completion Interrupt Status */ + +#define HDA_REG_SD0LPIB (HDA_NUM_GENERAL_REGS + 2) /* 0x84; other streams offset by 0x20 */ +#define HDA_REG_SD1LPIB (HDA_STREAM_REG_DEF(LPIB, 0) + 10) /* 0xA4 */ +#define HDA_REG_SD2LPIB (HDA_STREAM_REG_DEF(LPIB, 0) + 20) /* 0xC4 */ +#define HDA_REG_SD3LPIB (HDA_STREAM_REG_DEF(LPIB, 0) + 30) /* 0xE4 */ +#define HDA_REG_SD4LPIB (HDA_STREAM_REG_DEF(LPIB, 0) + 40) /* 0x104 */ +#define HDA_REG_SD5LPIB (HDA_STREAM_REG_DEF(LPIB, 0) + 50) /* 0x124 */ +#define HDA_REG_SD6LPIB (HDA_STREAM_REG_DEF(LPIB, 0) + 60) /* 0x144 */ +#define HDA_REG_SD7LPIB (HDA_STREAM_REG_DEF(LPIB, 0) + 70) /* 0x164 */ +#define HDA_RMX_SD0LPIB 34 +#define HDA_RMX_SD1LPIB (HDA_STREAM_RMX_DEF(LPIB, 0) + 10) +#define HDA_RMX_SD2LPIB (HDA_STREAM_RMX_DEF(LPIB, 0) + 20) +#define HDA_RMX_SD3LPIB (HDA_STREAM_RMX_DEF(LPIB, 0) + 30) +#define HDA_RMX_SD4LPIB (HDA_STREAM_RMX_DEF(LPIB, 0) + 40) +#define HDA_RMX_SD5LPIB (HDA_STREAM_RMX_DEF(LPIB, 0) + 50) +#define HDA_RMX_SD6LPIB (HDA_STREAM_RMX_DEF(LPIB, 0) + 60) +#define HDA_RMX_SD7LPIB (HDA_STREAM_RMX_DEF(LPIB, 0) + 70) + +#define HDA_REG_SD0CBL (HDA_NUM_GENERAL_REGS + 3) /* 0x88; other streams offset by 0x20 */ +#define HDA_RMX_SD0CBL 35 +#define HDA_RMX_SD1CBL (HDA_STREAM_RMX_DEF(CBL, 0) + 10) +#define HDA_RMX_SD2CBL (HDA_STREAM_RMX_DEF(CBL, 0) + 20) +#define HDA_RMX_SD3CBL (HDA_STREAM_RMX_DEF(CBL, 0) + 30) +#define HDA_RMX_SD4CBL (HDA_STREAM_RMX_DEF(CBL, 0) + 40) +#define HDA_RMX_SD5CBL (HDA_STREAM_RMX_DEF(CBL, 0) + 50) +#define HDA_RMX_SD6CBL (HDA_STREAM_RMX_DEF(CBL, 0) + 60) +#define HDA_RMX_SD7CBL (HDA_STREAM_RMX_DEF(CBL, 0) + 70) + +#define HDA_REG_SD0LVI (HDA_NUM_GENERAL_REGS + 4) /* 0x8C; other streams offset by 0x20 */ +#define HDA_RMX_SD0LVI 36 +#define HDA_RMX_SD1LVI (HDA_STREAM_RMX_DEF(LVI, 0) + 10) +#define HDA_RMX_SD2LVI (HDA_STREAM_RMX_DEF(LVI, 0) + 20) +#define HDA_RMX_SD3LVI (HDA_STREAM_RMX_DEF(LVI, 0) + 30) +#define HDA_RMX_SD4LVI (HDA_STREAM_RMX_DEF(LVI, 0) + 40) +#define HDA_RMX_SD5LVI (HDA_STREAM_RMX_DEF(LVI, 0) + 50) +#define HDA_RMX_SD6LVI (HDA_STREAM_RMX_DEF(LVI, 0) + 60) +#define HDA_RMX_SD7LVI (HDA_STREAM_RMX_DEF(LVI, 0) + 70) + +#define HDA_REG_SD0FIFOW (HDA_NUM_GENERAL_REGS + 5) /* 0x8E; other streams offset by 0x20 */ +#define HDA_RMX_SD0FIFOW 37 +#define HDA_RMX_SD1FIFOW (HDA_STREAM_RMX_DEF(FIFOW, 0) + 10) +#define HDA_RMX_SD2FIFOW (HDA_STREAM_RMX_DEF(FIFOW, 0) + 20) +#define HDA_RMX_SD3FIFOW (HDA_STREAM_RMX_DEF(FIFOW, 0) + 30) +#define HDA_RMX_SD4FIFOW (HDA_STREAM_RMX_DEF(FIFOW, 0) + 40) +#define HDA_RMX_SD5FIFOW (HDA_STREAM_RMX_DEF(FIFOW, 0) + 50) +#define HDA_RMX_SD6FIFOW (HDA_STREAM_RMX_DEF(FIFOW, 0) + 60) +#define HDA_RMX_SD7FIFOW (HDA_STREAM_RMX_DEF(FIFOW, 0) + 70) + +/* + * ICH6 datasheet defined limits for FIFOW values (18.2.38). + */ +#define HDA_SDFIFOW_8B 0x2 +#define HDA_SDFIFOW_16B 0x3 +#define HDA_SDFIFOW_32B 0x4 + +#define HDA_REG_SD0FIFOS (HDA_NUM_GENERAL_REGS + 6) /* 0x90; other streams offset by 0x20 */ +#define HDA_RMX_SD0FIFOS 38 +#define HDA_RMX_SD1FIFOS (HDA_STREAM_RMX_DEF(FIFOS, 0) + 10) +#define HDA_RMX_SD2FIFOS (HDA_STREAM_RMX_DEF(FIFOS, 0) + 20) +#define HDA_RMX_SD3FIFOS (HDA_STREAM_RMX_DEF(FIFOS, 0) + 30) +#define HDA_RMX_SD4FIFOS (HDA_STREAM_RMX_DEF(FIFOS, 0) + 40) +#define HDA_RMX_SD5FIFOS (HDA_STREAM_RMX_DEF(FIFOS, 0) + 50) +#define HDA_RMX_SD6FIFOS (HDA_STREAM_RMX_DEF(FIFOS, 0) + 60) +#define HDA_RMX_SD7FIFOS (HDA_STREAM_RMX_DEF(FIFOS, 0) + 70) + +/* The ICH6 datasheet defines limits for FIFOS registers (18.2.39). + Formula: size - 1 + Other values not listed are not supported. */ + +#define HDA_SDIFIFO_120B 0x77 /* 8-, 16-, 20-, 24-, 32-bit Input Streams */ +#define HDA_SDIFIFO_160B 0x9F /* 20-, 24-bit Input Streams Streams */ + +#define HDA_SDOFIFO_16B 0x0F /* 8-, 16-, 20-, 24-, 32-bit Output Streams */ +#define HDA_SDOFIFO_32B 0x1F /* 8-, 16-, 20-, 24-, 32-bit Output Streams */ +#define HDA_SDOFIFO_64B 0x3F /* 8-, 16-, 20-, 24-, 32-bit Output Streams */ +#define HDA_SDOFIFO_128B 0x7F /* 8-, 16-, 20-, 24-, 32-bit Output Streams */ +#define HDA_SDOFIFO_192B 0xBF /* 8-, 16-, 20-, 24-, 32-bit Output Streams */ +#define HDA_SDOFIFO_256B 0xFF /* 20-, 24-bit Output Streams */ + +#define HDA_REG_SD0FMT (HDA_NUM_GENERAL_REGS + 7) /* 0x92; other streams offset by 0x20 */ +#define HDA_RMX_SD0FMT 39 +#define HDA_RMX_SD1FMT (HDA_STREAM_RMX_DEF(FMT, 0) + 10) +#define HDA_RMX_SD2FMT (HDA_STREAM_RMX_DEF(FMT, 0) + 20) +#define HDA_RMX_SD3FMT (HDA_STREAM_RMX_DEF(FMT, 0) + 30) +#define HDA_RMX_SD4FMT (HDA_STREAM_RMX_DEF(FMT, 0) + 40) +#define HDA_RMX_SD5FMT (HDA_STREAM_RMX_DEF(FMT, 0) + 50) +#define HDA_RMX_SD6FMT (HDA_STREAM_RMX_DEF(FMT, 0) + 60) +#define HDA_RMX_SD7FMT (HDA_STREAM_RMX_DEF(FMT, 0) + 70) + +#define HDA_REG_SD0BDPL (HDA_NUM_GENERAL_REGS + 8) /* 0x98; other streams offset by 0x20 */ +#define HDA_RMX_SD0BDPL 40 +#define HDA_RMX_SD1BDPL (HDA_STREAM_RMX_DEF(BDPL, 0) + 10) +#define HDA_RMX_SD2BDPL (HDA_STREAM_RMX_DEF(BDPL, 0) + 20) +#define HDA_RMX_SD3BDPL (HDA_STREAM_RMX_DEF(BDPL, 0) + 30) +#define HDA_RMX_SD4BDPL (HDA_STREAM_RMX_DEF(BDPL, 0) + 40) +#define HDA_RMX_SD5BDPL (HDA_STREAM_RMX_DEF(BDPL, 0) + 50) +#define HDA_RMX_SD6BDPL (HDA_STREAM_RMX_DEF(BDPL, 0) + 60) +#define HDA_RMX_SD7BDPL (HDA_STREAM_RMX_DEF(BDPL, 0) + 70) + +#define HDA_REG_SD0BDPU (HDA_NUM_GENERAL_REGS + 9) /* 0x9C; other streams offset by 0x20 */ +#define HDA_RMX_SD0BDPU 41 +#define HDA_RMX_SD1BDPU (HDA_STREAM_RMX_DEF(BDPU, 0) + 10) +#define HDA_RMX_SD2BDPU (HDA_STREAM_RMX_DEF(BDPU, 0) + 20) +#define HDA_RMX_SD3BDPU (HDA_STREAM_RMX_DEF(BDPU, 0) + 30) +#define HDA_RMX_SD4BDPU (HDA_STREAM_RMX_DEF(BDPU, 0) + 40) +#define HDA_RMX_SD5BDPU (HDA_STREAM_RMX_DEF(BDPU, 0) + 50) +#define HDA_RMX_SD6BDPU (HDA_STREAM_RMX_DEF(BDPU, 0) + 60) +#define HDA_RMX_SD7BDPU (HDA_STREAM_RMX_DEF(BDPU, 0) + 70) + +#define HDA_CODEC_CAD_SHIFT 28 +/** Encodes the (required) LUN into a codec command. */ +#define HDA_CODEC_CMD(cmd, lun) ((cmd) | (lun << HDA_CODEC_CAD_SHIFT)) + +#define HDA_SDFMT_NON_PCM_SHIFT 15 +#define HDA_SDFMT_NON_PCM_MASK 0x1 +#define HDA_SDFMT_BASE_RATE_SHIFT 14 +#define HDA_SDFMT_BASE_RATE_MASK 0x1 +#define HDA_SDFMT_MULT_SHIFT 11 +#define HDA_SDFMT_MULT_MASK 0x7 +#define HDA_SDFMT_DIV_SHIFT 8 +#define HDA_SDFMT_DIV_MASK 0x7 +#define HDA_SDFMT_BITS_SHIFT 4 +#define HDA_SDFMT_BITS_MASK 0x7 +#define HDA_SDFMT_CHANNELS_MASK 0xF + +#define HDA_SDFMT_TYPE RT_BIT(15) +#define HDA_SDFMT_TYPE_PCM (0) +#define HDA_SDFMT_TYPE_NON_PCM (1) + +#define HDA_SDFMT_BASE RT_BIT(14) +#define HDA_SDFMT_BASE_48KHZ (0) +#define HDA_SDFMT_BASE_44KHZ (1) + +#define HDA_SDFMT_MULT_1X (0) +#define HDA_SDFMT_MULT_2X (1) +#define HDA_SDFMT_MULT_3X (2) +#define HDA_SDFMT_MULT_4X (3) + +#define HDA_SDFMT_DIV_1X (0) +#define HDA_SDFMT_DIV_2X (1) +#define HDA_SDFMT_DIV_3X (2) +#define HDA_SDFMT_DIV_4X (3) +#define HDA_SDFMT_DIV_5X (4) +#define HDA_SDFMT_DIV_6X (5) +#define HDA_SDFMT_DIV_7X (6) +#define HDA_SDFMT_DIV_8X (7) + +#define HDA_SDFMT_8_BIT (0) +#define HDA_SDFMT_16_BIT (1) +#define HDA_SDFMT_20_BIT (2) +#define HDA_SDFMT_24_BIT (3) +#define HDA_SDFMT_32_BIT (4) + +#define HDA_SDFMT_CHAN_MONO (0) +#define HDA_SDFMT_CHAN_STEREO (1) + +/** Emits a SDnFMT register format. + * Also being used in the codec's converter format. */ +#define HDA_SDFMT_MAKE(_afNonPCM, _aBaseRate, _aMult, _aDiv, _aBits, _aChan) \ + ( (((_afNonPCM) & HDA_SDFMT_NON_PCM_MASK) << HDA_SDFMT_NON_PCM_SHIFT) \ + | (((_aBaseRate) & HDA_SDFMT_BASE_RATE_MASK) << HDA_SDFMT_BASE_RATE_SHIFT) \ + | (((_aMult) & HDA_SDFMT_MULT_MASK) << HDA_SDFMT_MULT_SHIFT) \ + | (((_aDiv) & HDA_SDFMT_DIV_MASK) << HDA_SDFMT_DIV_SHIFT) \ + | (((_aBits) & HDA_SDFMT_BITS_MASK) << HDA_SDFMT_BITS_SHIFT) \ + | ( (_aChan) & HDA_SDFMT_CHANNELS_MASK)) + + +/* Post stream registers: */ +#define HDA_REG_MLCH (HDA_NUM_GENERAL_REGS + HDA_NUM_STREAM_REGS) /* 0xc00 */ +#define HDA_RMX_MLCH 115 +#define HDA_REG_MLCD (HDA_REG_MLCH + 1) /* 0xc04 */ +#define HDA_RMX_MLCD 116 + +/* Registers added/specific-to skylake/broxton: */ +#define HDA_SD_NUM_FROM_SKYLAKE_REG(a_Name, a_iMap) (((a_iMap) - HDA_STREAM_REG_DEF(a_Name, 0)) / 2) + +#define HDA_REG_SD0DPIB (HDA_REG_MLCD + 1) /* 0x1084 */ +#define HDA_REG_SD1DPIB (HDA_REG_SD0DPIB + 1*2) +#define HDA_REG_SD2DPIB (HDA_REG_SD0DPIB + 2*2) +#define HDA_REG_SD3DPIB (HDA_REG_SD0DPIB + 3*2) +#define HDA_REG_SD4DPIB (HDA_REG_SD0DPIB + 4*2) +#define HDA_REG_SD5DPIB (HDA_REG_SD0DPIB + 5*2) +#define HDA_REG_SD6DPIB (HDA_REG_SD0DPIB + 6*2) +#define HDA_REG_SD7DPIB (HDA_REG_SD0DPIB + 7*2) + +#define HDA_RMX_SD0DPIB HDA_RMX_SD0LPIB +#define HDA_RMX_SD1DPIB HDA_RMX_SD1LPIB +#define HDA_RMX_SD2DPIB HDA_RMX_SD2LPIB +#define HDA_RMX_SD3DPIB HDA_RMX_SD3LPIB +#define HDA_RMX_SD4DPIB HDA_RMX_SD4LPIB +#define HDA_RMX_SD5DPIB HDA_RMX_SD5LPIB +#define HDA_RMX_SD6DPIB HDA_RMX_SD6LPIB +#define HDA_RMX_SD7DPIB HDA_RMX_SD7LPIB + +#define HDA_REG_SD0EFIFOS (HDA_REG_SD0DPIB + 1) /* 0x1094 */ +#define HDA_REG_SD1EFIFOS (HDA_REG_SD0EFIFOS + 1*2) +#define HDA_REG_SD2EFIFOS (HDA_REG_SD0EFIFOS + 2*2) +#define HDA_REG_SD3EFIFOS (HDA_REG_SD0EFIFOS + 3*2) +#define HDA_REG_SD4EFIFOS (HDA_REG_SD0EFIFOS + 4*2) +#define HDA_REG_SD5EFIFOS (HDA_REG_SD0EFIFOS + 5*2) +#define HDA_REG_SD6EFIFOS (HDA_REG_SD0EFIFOS + 6*2) +#define HDA_REG_SD7EFIFOS (HDA_REG_SD0EFIFOS + 7*2) + +#define HDA_RMX_SD0EFIFOS 117 +#define HDA_RMX_SD1EFIFOS (HDA_RMX_SD0EFIFOS + 1) +#define HDA_RMX_SD2EFIFOS (HDA_RMX_SD0EFIFOS + 2) +#define HDA_RMX_SD3EFIFOS (HDA_RMX_SD0EFIFOS + 3) +#define HDA_RMX_SD4EFIFOS (HDA_RMX_SD0EFIFOS + 4) +#define HDA_RMX_SD5EFIFOS (HDA_RMX_SD0EFIFOS + 5) +#define HDA_RMX_SD6EFIFOS (HDA_RMX_SD0EFIFOS + 6) +#define HDA_RMX_SD7EFIFOS (HDA_RMX_SD0EFIFOS + 7) + +/** @} */ /* grp_hda_regs */ + + +/** + * Buffer descriptor list entry (BDLE). + * + * See 3.6.3 in HDA specs rev 1.0a (2010-06-17). + */ +typedef struct HDABDLEDESC +{ + /** Starting address of the actual buffer. Must be 128-bit aligned. */ + uint64_t u64BufAddr; + /** Size of the actual buffer (in bytes). */ + uint32_t u32BufSize; + /** HDA_BDLE_F_XXX. + * + * Bit 0: IOC - Interrupt on completion / HDA_BDLE_F_IOC. + * The controller will generate an interrupt when the last byte of the buffer + * has been fetched by the DMA engine. + * + * Bits 31:1 are reserved for further use and must be 0. */ + uint32_t fFlags; +} HDABDLEDESC, *PHDABDLEDESC; +AssertCompileSize(HDABDLEDESC, 16); /* Always 16 byte. Also must be aligned on 128-byte boundary. */ + +/** Interrupt on completion (IOC) flag. */ +#define HDA_BDLE_F_IOC RT_BIT(0) + + +/** + * HDA mixer sink definition (ring-3). + * + * Its purpose is to know which audio mixer sink is bound to which SDn + * (SDI/SDO) device stream. + * + * This is needed in order to handle interleaved streams (that is, multiple + * channels in one stream) or non-interleaved streams (each channel has a + * dedicated stream). + * + * This is only known to the actual device emulation level. + */ +typedef struct HDAMIXERSINK +{ + R3PTRTYPE(PHDASTREAM) pStreamShared; + R3PTRTYPE(PHDASTREAMR3) pStreamR3; + /** Pointer to the actual audio mixer sink. */ + R3PTRTYPE(PAUDMIXSINK) pMixSink; +} HDAMIXERSINK; + +/** + * Mapping a stream tag to an HDA stream (ring-3). + */ +typedef struct HDATAG +{ + /** Own stream tag. */ + uint8_t uTag; + uint8_t Padding[7]; + /** Pointer to associated stream. */ + R3PTRTYPE(PHDASTREAMR3) pStreamR3; +} HDATAG; +/** Pointer to a HDA stream tag mapping. */ +typedef HDATAG *PHDATAG; + +/** + * Shared ICH Intel HD audio controller state. + */ +typedef struct HDASTATE +{ + /** Critical section protecting the HDA state. */ + PDMCRITSECT CritSect; + /** Internal stream states (aligned on 64 byte boundrary). */ + HDASTREAM aStreams[HDA_MAX_STREAMS]; + /** The HDA's register set. */ + uint32_t au32Regs[HDA_NUM_REGS]; + /** CORB buffer base address. */ + uint64_t u64CORBBase; + /** RIRB buffer base address. */ + uint64_t u64RIRBBase; + /** DMA base address. + * Made out of DPLBASE + DPUBASE (3.3.32 + 3.3.33). */ + uint64_t u64DPBase; + /** Size in bytes of CORB buffer (#au32CorbBuf). */ + uint32_t cbCorbBuf; + /** Size in bytes of RIRB buffer (#au64RirbBuf). */ + uint32_t cbRirbBuf; + /** Response Interrupt Count (RINTCNT). */ + uint16_t u16RespIntCnt; + /** DMA position buffer enable bit. */ + bool fDMAPosition; + /** Current IRQ level. */ + uint8_t u8IRQL; + /** Config: Internal input DMA buffer size override, specified in milliseconds. + * Zero means default size according to buffer and stream config. + * @sa BufSizeInMs config value. */ + uint16_t cMsCircBufIn; + /** Config: Internal output DMA buffer size override, specified in milliseconds. + * Zero means default size according to buffer and stream config. + * @sa BufSizeOutMs config value. */ + uint16_t cMsCircBufOut; + /** The start time of the wall clock (WALCLK), measured on the virtual sync clock. */ + uint64_t tsWalClkStart; + /** CORB DMA task handle. + * We use this when there is stuff we cannot handle in ring-0. */ + PDMTASKHANDLE hCorbDmaTask; + /** The CORB buffer. */ + uint32_t au32CorbBuf[HDA_CORB_SIZE]; + /** Pointer to RIRB buffer. */ + uint64_t au64RirbBuf[HDA_RIRB_SIZE]; + + /** PCI Region \#0: 16KB of MMIO stuff. */ + IOMMMIOHANDLE hMmio; + +#ifdef VBOX_HDA_WITH_ON_REG_ACCESS_DMA + STAMCOUNTER StatAccessDmaOutput; + STAMCOUNTER StatAccessDmaOutputToR3; +#endif +#ifdef VBOX_WITH_STATISTICS + STAMPROFILE StatIn; + STAMPROFILE StatOut; + STAMCOUNTER StatBytesRead; + STAMCOUNTER StatBytesWritten; + + /** @name Register statistics. + * The array members run parallel to g_aHdaRegMap. + * @{ */ + STAMCOUNTER aStatRegReads[HDA_NUM_REGS]; + STAMCOUNTER aStatRegReadsToR3[HDA_NUM_REGS]; + STAMCOUNTER aStatRegWrites[HDA_NUM_REGS]; + STAMCOUNTER aStatRegWritesToR3[HDA_NUM_REGS]; + STAMCOUNTER StatRegMultiReadsRZ; + STAMCOUNTER StatRegMultiReadsR3; + STAMCOUNTER StatRegMultiWritesRZ; + STAMCOUNTER StatRegMultiWritesR3; + STAMCOUNTER StatRegSubWriteRZ; + STAMCOUNTER StatRegSubWriteR3; + STAMCOUNTER StatRegUnknownReads; + STAMCOUNTER StatRegUnknownWrites; + STAMCOUNTER StatRegWritesBlockedByReset; + STAMCOUNTER StatRegWritesBlockedByRun; + /** @} */ +#endif + +#ifdef DEBUG + /** Debug stuff. + * @todo Make STAM values out some of this? */ + struct + { +# if 0 /* unused */ + /** Timestamp (in ns) of the last timer callback (hdaTimer). + * Used to calculate the time actually elapsed between two timer callbacks. */ + uint64_t tsTimerLastCalledNs; +# endif + /** IRQ debugging information. */ + struct + { + /** Timestamp (in ns) of last processed (asserted / deasserted) IRQ. */ + uint64_t tsProcessedLastNs; + /** Timestamp (in ns) of last asserted IRQ. */ + uint64_t tsAssertedNs; +# if 0 /* unused */ + /** How many IRQs have been asserted already. */ + uint64_t cAsserted; + /** Accumulated elapsed time (in ns) of all IRQ being asserted. */ + uint64_t tsAssertedTotalNs; + /** Timestamp (in ns) of last deasserted IRQ. */ + uint64_t tsDeassertedNs; + /** How many IRQs have been deasserted already. */ + uint64_t cDeasserted; + /** Accumulated elapsed time (in ns) of all IRQ being deasserted. */ + uint64_t tsDeassertedTotalNs; +# endif + } IRQ; + } Dbg; +#endif + /** This is for checking that the build was correctly configured in all contexts. + * This is set to HDASTATE_ALIGNMENT_CHECK_MAGIC. */ + uint64_t uAlignmentCheckMagic; +} HDASTATE; +AssertCompileMemberAlignment(HDASTATE, aStreams, 64); +/** Pointer to a shared HDA device state. */ +typedef HDASTATE *PHDASTATE; + +/** Value for HDASTATE:uAlignmentCheckMagic. */ +#define HDASTATE_ALIGNMENT_CHECK_MAGIC UINT64_C(0x1298afb75893e059) + +/** + * Ring-0 ICH Intel HD audio controller state. + */ +typedef struct HDASTATER0 +{ +# if 0 /* Codec is not yet kosher enough for ring-0. @bugref{9890c64} */ + /** Pointer to HDA codec to use. */ + HDACODECR0 Codec; +# else + uint32_t u32Dummy; +# endif +} HDASTATER0; +/** Pointer to a ring-0 HDA device state. */ +typedef HDASTATER0 *PHDASTATER0; + +/** + * Ring-3 ICH Intel HD audio controller state. + */ +typedef struct HDASTATER3 +{ + /** Internal stream states. */ + HDASTREAMR3 aStreams[HDA_MAX_STREAMS]; + /** Mapping table between stream tags and stream states. */ + HDATAG aTags[HDA_MAX_TAGS]; + /** R3 Pointer to the device instance. */ + PPDMDEVINSR3 pDevIns; + /** The base interface for LUN\#0. */ + PDMIBASE IBase; + /** List of associated LUN drivers (HDADRIVER). */ + RTLISTANCHORR3 lstDrv; + /** The device' software mixer. */ + R3PTRTYPE(PAUDIOMIXER) pMixer; + /** HDA sink for (front) output. */ + HDAMIXERSINK SinkFront; +#ifdef VBOX_WITH_AUDIO_HDA_51_SURROUND + /** HDA sink for center / LFE output. */ + HDAMIXERSINK SinkCenterLFE; + /** HDA sink for rear output. */ + HDAMIXERSINK SinkRear; +#endif + /** HDA mixer sink for line input. */ + HDAMIXERSINK SinkLineIn; +#ifdef VBOX_WITH_AUDIO_HDA_MIC_IN + /** Audio mixer sink for microphone input. */ + HDAMIXERSINK SinkMicIn; +#endif + /** Debug stuff. */ + struct + { + /** Whether debugging is enabled or not. */ + bool fEnabled; + /** Path where to dump the debug output to. + * Can be NULL, in which the system's temporary directory will be used then. */ + R3PTRTYPE(char *) pszOutPath; + } Dbg; + /** Align the codec state on a cache line. */ + uint64_t au64Padding[3]; + /** The HDA codec state. */ + HDACODECR3 Codec; +} HDASTATER3; +AssertCompileMemberAlignment(HDASTATER3, Codec, 64); + + +/** Pointer to the context specific HDA state (HDASTATER3 or HDASTATER0). */ +typedef CTX_SUFF(PHDASTATE) PHDASTATECC; + + +/** @def HDA_PROCESS_INTERRUPT + * Wrapper around hdaProcessInterrupt that supplies the source function name + * string in logging builds. */ +#if defined(LOG_ENABLED) || defined(DOXYGEN_RUNNING) +void hdaProcessInterrupt(PPDMDEVINS pDevIns, PHDASTATE pThis, const char *pszSource); +# define HDA_PROCESS_INTERRUPT(a_pDevIns, a_pThis) hdaProcessInterrupt((a_pDevIns), (a_pThis), __FUNCTION__) +#else +void hdaProcessInterrupt(PPDMDEVINS pDevIns, PHDASTATE pThis); +# define HDA_PROCESS_INTERRUPT(a_pDevIns, a_pThis) hdaProcessInterrupt((a_pDevIns), (a_pThis)) +#endif + +/** + * Returns the audio direction of a specified stream descriptor. + * + * The register layout specifies that input streams (SDI) come first, + * followed by the output streams (SDO). So every stream ID below HDA_MAX_SDI + * is an input stream, whereas everything >= HDA_MAX_SDI is an output stream. + * + * @note SDnFMT register does not provide that information, so we have to judge + * for ourselves. + * + * @return Audio direction. + * @param uSD The stream number. + */ +DECLINLINE(PDMAUDIODIR) hdaGetDirFromSD(uint8_t uSD) +{ + if (uSD < HDA_MAX_SDI) + return PDMAUDIODIR_IN; + AssertReturn(uSD < HDA_MAX_STREAMS, PDMAUDIODIR_UNKNOWN); + return PDMAUDIODIR_OUT; +} + +/* Used by hdaR3StreamSetUp: */ +uint8_t hdaSDFIFOWToBytes(uint16_t u16RegFIFOW); + +#if defined(VBOX_STRICT) && defined(VBOX_HDA_CAN_ACCESS_REG_MAP) +/* Only in DevHda.cpp: */ +DECLINLINE(uint32_t *) hdaStrictRegAccessor(PHDASTATE pThis, uint32_t idxMap, uint32_t idxReg); +DECLINLINE(uint32_t *) hdaStrictStreamRegAccessor(PHDASTATE pThis, uint32_t idxMap0, uint32_t idxReg0, size_t idxStream); +#endif /* VBOX_STRICT && VBOX_HDA_CAN_ACCESS_REG_MAP */ + + +/** @name HDA device functions used by the codec. + * @{ */ +DECLHIDDEN(int) hdaR3MixerAddStream(PHDACODECR3 pCodec, PDMAUDIOMIXERCTL enmMixerCtl, PCPDMAUDIOSTREAMCFG pCfg); +DECLHIDDEN(int) hdaR3MixerRemoveStream(PHDACODECR3 pCodec, PDMAUDIOMIXERCTL enmMixerCtl, bool fImmediate); +DECLHIDDEN(int) hdaR3MixerControl(PHDACODECR3 pCodec, PDMAUDIOMIXERCTL enmMixerCtl, uint8_t uSD, uint8_t uChannel); +DECLHIDDEN(int) hdaR3MixerSetVolume(PHDACODECR3 pCodec, PDMAUDIOMIXERCTL enmMixerCtl, PPDMAUDIOVOLUME pVol); +/** @} */ + + +/** @name Saved state versions for the HDA device + * @{ */ +/** The current staved state version. + * @note Only for the registration call. Never used for tests. */ +#define HDA_SAVED_STATE_VERSION HDA_SAVED_STATE_WITHOUT_PERIOD + +/** Removed period and redefined wall clock. */ +#define HDA_SAVED_STATE_WITHOUT_PERIOD 8 +/** Added (Controller): Current wall clock value (this independent from WALCLK register value). + * Added (Controller): Current IRQ level. + * Added (Per stream): Ring buffer. This is optional and can be skipped if (not) needed. + * Added (Per stream): Struct g_aSSMStreamStateFields7. + * Added (Per stream): Struct g_aSSMStreamPeriodFields7. + * Added (Current BDLE per stream): Struct g_aSSMBDLEDescFields7. + * Added (Current BDLE per stream): Struct g_aSSMBDLEStateFields7. */ +#define HDA_SAVED_STATE_VERSION_7 7 +/** Saves the current BDLE state. + * @since 5.0.14 (r104839) */ +#define HDA_SAVED_STATE_VERSION_6 6 +/** Introduced dynamic number of streams + stream identifiers for serialization. + * Bug: Did not save the BDLE states correctly. + * Those will be skipped on load then. + * @since 5.0.12 (r104520) */ +#define HDA_SAVED_STATE_VERSION_5 5 +/** Since this version the number of MMIO registers can be flexible. */ +#define HDA_SAVED_STATE_VERSION_4 4 +#define HDA_SAVED_STATE_VERSION_3 3 +#define HDA_SAVED_STATE_VERSION_2 2 +#define HDA_SAVED_STATE_VERSION_1 1 +/** @} */ + +#endif /* !VBOX_INCLUDED_SRC_Audio_DevHda_h */ + diff --git a/src/VBox/Devices/Audio/DevHdaCodec.cpp b/src/VBox/Devices/Audio/DevHdaCodec.cpp new file mode 100644 index 00000000..59c86c9a --- /dev/null +++ b/src/VBox/Devices/Audio/DevHdaCodec.cpp @@ -0,0 +1,2893 @@ +/* $Id: DevHdaCodec.cpp $ */ +/** @file + * Intel HD Audio Controller Emulation - Codec, Sigmatel/IDT STAC9220. + * + * Implemented based on the Intel HD Audio specification and the + * Sigmatel/IDT STAC9220 datasheet. + */ + +/* + * Copyright (C) 2006-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 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_DEV_HDA_CODEC +#include <VBox/log.h> + +#include <VBox/AssertGuest.h> +#include <VBox/vmm/pdmdev.h> +#include <VBox/vmm/pdmaudioifs.h> +#include <VBox/vmm/pdmaudioinline.h> + +#include <iprt/assert.h> +#include <iprt/uuid.h> +#include <iprt/string.h> +#include <iprt/mem.h> +#include <iprt/asm.h> +#include <iprt/cpp/utils.h> + +#include "VBoxDD.h" +#include "AudioMixer.h" +#include "DevHda.h" + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +#define AMPLIFIER_IN 0 +#define AMPLIFIER_OUT 1 +#define AMPLIFIER_LEFT 1 +#define AMPLIFIER_RIGHT 0 +#define AMPLIFIER_REGISTER(amp, inout, side, index) ((amp)[30*(inout) + 15*(side) + (index)]) + + +/** @name STAC9220 - Nodes IDs / Names. + * @{ */ +#define STAC9220_NID_ROOT 0x0 /* Root node */ +#define STAC9220_NID_AFG 0x1 /* Audio Configuration Group */ +#define STAC9220_NID_DAC0 0x2 /* Out */ +#define STAC9220_NID_DAC1 0x3 /* Out */ +#define STAC9220_NID_DAC2 0x4 /* Out */ +#define STAC9220_NID_DAC3 0x5 /* Out */ +#define STAC9220_NID_ADC0 0x6 /* In */ +#define STAC9220_NID_ADC1 0x7 /* In */ +#define STAC9220_NID_SPDIF_OUT 0x8 /* Out */ +#define STAC9220_NID_SPDIF_IN 0x9 /* In */ +/** Also known as PIN_A. */ +#define STAC9220_NID_PIN_HEADPHONE0 0xA /* In, Out */ +#define STAC9220_NID_PIN_B 0xB /* In, Out */ +#define STAC9220_NID_PIN_C 0xC /* In, Out */ +/** Also known as PIN D. */ +#define STAC9220_NID_PIN_HEADPHONE1 0xD /* In, Out */ +#define STAC9220_NID_PIN_E 0xE /* In */ +#define STAC9220_NID_PIN_F 0xF /* In, Out */ +/** Also known as DIGOUT0. */ +#define STAC9220_NID_PIN_SPDIF_OUT 0x10 /* Out */ +/** Also known as DIGIN. */ +#define STAC9220_NID_PIN_SPDIF_IN 0x11 /* In */ +#define STAC9220_NID_ADC0_MUX 0x12 /* In */ +#define STAC9220_NID_ADC1_MUX 0x13 /* In */ +#define STAC9220_NID_PCBEEP 0x14 /* Out */ +#define STAC9220_NID_PIN_CD 0x15 /* In */ +#define STAC9220_NID_VOL_KNOB 0x16 +#define STAC9220_NID_AMP_ADC0 0x17 /* In */ +#define STAC9220_NID_AMP_ADC1 0x18 /* In */ +/* Only for STAC9221. */ +#define STAC9221_NID_ADAT_OUT 0x19 /* Out */ +#define STAC9221_NID_I2S_OUT 0x1A /* Out */ +#define STAC9221_NID_PIN_I2S_OUT 0x1B /* Out */ + +/** Number of total nodes emulated. */ +#define STAC9221_NUM_NODES 0x1C +/** @} */ + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ +/** + * A codec verb descriptor. + */ +typedef struct CODECVERB +{ + /** Verb. */ + uint32_t uVerb; + /** Verb mask. */ + uint32_t fMask; + /** + * Function pointer for implementation callback. + * + * This is always a valid pointer in ring-3, while elsewhere a NULL indicates + * that we must return to ring-3 to process it. + * + * @returns VBox status code (99.9% is VINF_SUCCESS, caller doesn't care much + * what you return at present). + * + * @param pThis The shared codec intance data. + * @param uCmd The command. + * @param puResp Where to return the response value. + * + * @thread EMT or task worker thread (see HDASTATE::hCorbDmaTask). + */ + DECLCALLBACKMEMBER(int, pfn, (PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp)); + /** Friendly name, for debugging. */ + const char *pszName; +} CODECVERB; +/** Pointer to a const codec verb descriptor. */ +typedef CODECVERB const *PCCODECVERB; + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** @name STAC9220 Node Classifications. + * @note Referenced through STAC9220WIDGET in the constructor below. + * @{ */ +static uint8_t const g_abStac9220Ports[] = { STAC9220_NID_PIN_HEADPHONE0, STAC9220_NID_PIN_B, STAC9220_NID_PIN_C, STAC9220_NID_PIN_HEADPHONE1, STAC9220_NID_PIN_E, STAC9220_NID_PIN_F, 0 }; +static uint8_t const g_abStac9220Dacs[] = { STAC9220_NID_DAC0, STAC9220_NID_DAC1, STAC9220_NID_DAC2, STAC9220_NID_DAC3, 0 }; +static uint8_t const g_abStac9220Adcs[] = { STAC9220_NID_ADC0, STAC9220_NID_ADC1, 0 }; +static uint8_t const g_abStac9220SpdifOuts[] = { STAC9220_NID_SPDIF_OUT, 0 }; +static uint8_t const g_abStac9220SpdifIns[] = { STAC9220_NID_SPDIF_IN, 0 }; +static uint8_t const g_abStac9220DigOutPins[] = { STAC9220_NID_PIN_SPDIF_OUT, 0 }; +static uint8_t const g_abStac9220DigInPins[] = { STAC9220_NID_PIN_SPDIF_IN, 0 }; +static uint8_t const g_abStac9220AdcVols[] = { STAC9220_NID_AMP_ADC0, STAC9220_NID_AMP_ADC1, 0 }; +static uint8_t const g_abStac9220AdcMuxs[] = { STAC9220_NID_ADC0_MUX, STAC9220_NID_ADC1_MUX, 0 }; +static uint8_t const g_abStac9220Pcbeeps[] = { STAC9220_NID_PCBEEP, 0 }; +static uint8_t const g_abStac9220Cds[] = { STAC9220_NID_PIN_CD, 0 }; +static uint8_t const g_abStac9220VolKnobs[] = { STAC9220_NID_VOL_KNOB, 0 }; +/** @} */ + +/** @name STAC 9221 Values. + * @note Referenced through STAC9220WIDGET in the constructor below + * @{ */ +/** @todo Is STAC9220_NID_SPDIF_IN really correct for reserved nodes? */ +static uint8_t const g_abStac9220Reserveds[] = { STAC9220_NID_SPDIF_IN, STAC9221_NID_ADAT_OUT, STAC9221_NID_I2S_OUT, STAC9221_NID_PIN_I2S_OUT, 0 }; +/** @} */ + + +/** SSM description of CODECCOMMONNODE. */ +static SSMFIELD const g_aCodecNodeFields[] = +{ + SSMFIELD_ENTRY( CODECSAVEDSTATENODE, Core.uID), + SSMFIELD_ENTRY_PAD_HC_AUTO(3, 3), + SSMFIELD_ENTRY( CODECSAVEDSTATENODE, Core.au32F00_param), + SSMFIELD_ENTRY( CODECSAVEDSTATENODE, Core.au32F02_param), + SSMFIELD_ENTRY( CODECSAVEDSTATENODE, au32Params), + SSMFIELD_ENTRY_TERM() +}; + +/** Backward compatibility with v1 of CODECCOMMONNODE. */ +static SSMFIELD const g_aCodecNodeFieldsV1[] = +{ + SSMFIELD_ENTRY( CODECSAVEDSTATENODE, Core.uID), + SSMFIELD_ENTRY_PAD_HC_AUTO(3, 7), + SSMFIELD_ENTRY_OLD_HCPTR(Core.name), + SSMFIELD_ENTRY( CODECSAVEDSTATENODE, Core.au32F00_param), + SSMFIELD_ENTRY( CODECSAVEDSTATENODE, Core.au32F02_param), + SSMFIELD_ENTRY( CODECSAVEDSTATENODE, au32Params), + SSMFIELD_ENTRY_TERM() +}; + + + +/********************************************************************************************************************************* +* STAC9220 Constructor / Reset * +*********************************************************************************************************************************/ + +/** + * Resets a single node of the codec. + * + * @param pThis HDA codec of node to reset. + * @param uNID Node ID to set node to. + * @param pNode Node to reset. + * @param fInReset Set if we're called from hdaCodecReset via + * stac9220Reset, clear if called from stac9220Construct. + */ +static void stac9220NodeReset(PHDACODECR3 pThis, uint8_t uNID, PCODECNODE pNode, bool const fInReset) +{ + LogFlowFunc(("NID=0x%x (%RU8)\n", uNID, uNID)); + + if ( !fInReset + && ( uNID != STAC9220_NID_ROOT + && uNID != STAC9220_NID_AFG) + ) + { + RT_ZERO(pNode->node); + } + + /* Set common parameters across all nodes. */ + pNode->node.uID = uNID; + pNode->node.uSD = 0; + + switch (uNID) + { + /* Root node. */ + case STAC9220_NID_ROOT: + { + /* Set the revision ID. */ + pNode->root.node.au32F00_param[0x02] = CODEC_MAKE_F00_02(0x1, 0x0, 0x3, 0x4, 0x0, 0x1); + break; + } + + /* + * AFG (Audio Function Group). + */ + case STAC9220_NID_AFG: + { + pNode->afg.node.au32F00_param[0x08] = CODEC_MAKE_F00_08(1, 0xd, 0xd); + /* We set the AFG's PCM capabitilies fixed to 16kHz, 22.5kHz + 44.1kHz, 16-bit signed. */ + pNode->afg.node.au32F00_param[0x0A] = CODEC_F00_0A_44_1KHZ /* 44.1 kHz */ + | CODEC_F00_0A_44_1KHZ_1_2X /* Messed up way of saying 22.05 kHz */ + | CODEC_F00_0A_48KHZ_1_3X /* Messed up way of saying 16 kHz. */ + | CODEC_F00_0A_16_BIT; /* 16-bit signed */ + /* Note! We do not set CODEC_F00_0A_48KHZ here because we end up with + S/PDIF output showing up in windows and it trying to configure + streams other than 0 and 4 and stuff going sideways in the + stream setup/removal area. */ + pNode->afg.node.au32F00_param[0x0B] = CODEC_F00_0B_PCM; + pNode->afg.node.au32F00_param[0x0C] = CODEC_MAKE_F00_0C(0x17) + | CODEC_F00_0C_CAP_BALANCED_IO + | CODEC_F00_0C_CAP_INPUT + | CODEC_F00_0C_CAP_OUTPUT + | CODEC_F00_0C_CAP_PRESENCE_DETECT + | CODEC_F00_0C_CAP_TRIGGER_REQUIRED + | CODEC_F00_0C_CAP_IMPENDANCE_SENSE; + + /* Default input amplifier capabilities. */ + pNode->node.au32F00_param[0x0D] = CODEC_MAKE_F00_0D(CODEC_AMP_CAP_MUTE, + CODEC_AMP_STEP_SIZE, + CODEC_AMP_NUM_STEPS, + CODEC_AMP_OFF_INITIAL); + /* Default output amplifier capabilities. */ + pNode->node.au32F00_param[0x12] = CODEC_MAKE_F00_12(CODEC_AMP_CAP_MUTE, + CODEC_AMP_STEP_SIZE, + CODEC_AMP_NUM_STEPS, + CODEC_AMP_OFF_INITIAL); + + pNode->afg.node.au32F00_param[0x11] = CODEC_MAKE_F00_11(1, 1, 0, 0, 4); + pNode->afg.node.au32F00_param[0x0F] = CODEC_F00_0F_D3 + | CODEC_F00_0F_D2 + | CODEC_F00_0F_D1 + | CODEC_F00_0F_D0; + + pNode->afg.u32F05_param = CODEC_MAKE_F05(0, 0, 0, CODEC_F05_D2, CODEC_F05_D2); /* PS-Act: D2, PS->Set D2. */ + pNode->afg.u32F08_param = 0; + pNode->afg.u32F17_param = 0; + break; + } + + /* + * DACs. + */ + case STAC9220_NID_DAC0: /* DAC0: Headphones 0 + 1 */ + case STAC9220_NID_DAC1: /* DAC1: PIN C */ + case STAC9220_NID_DAC2: /* DAC2: PIN B */ + case STAC9220_NID_DAC3: /* DAC3: PIN F */ + { + pNode->dac.u32A_param = CODEC_MAKE_A(HDA_SDFMT_TYPE_PCM, HDA_SDFMT_BASE_44KHZ, + HDA_SDFMT_MULT_1X, HDA_SDFMT_DIV_2X, HDA_SDFMT_16_BIT, + HDA_SDFMT_CHAN_STEREO); + + /* 7.3.4.6: Audio widget capabilities. */ + pNode->dac.node.au32F00_param[0x9] = CODEC_MAKE_F00_09(CODEC_F00_09_TYPE_AUDIO_OUTPUT, 13, 0) + | CODEC_F00_09_CAP_L_R_SWAP + | CODEC_F00_09_CAP_POWER_CTRL + | CODEC_F00_09_CAP_OUT_AMP_PRESENT + | CODEC_F00_09_CAP_STEREO; + + /* Connection list; must be 0 if the only connection for the widget is + * to the High Definition Audio Link. */ + pNode->dac.node.au32F00_param[0xE] = CODEC_MAKE_F00_0E(CODEC_F00_0E_LIST_NID_SHORT, 0 /* Entries */); + + pNode->dac.u32F05_param = CODEC_MAKE_F05(0, 0, 0, CODEC_F05_D3, CODEC_F05_D3); + + RT_ZERO(pNode->dac.B_params); + AMPLIFIER_REGISTER(pNode->dac.B_params, AMPLIFIER_OUT, AMPLIFIER_LEFT, 0) = 0x7F | RT_BIT(7); + AMPLIFIER_REGISTER(pNode->dac.B_params, AMPLIFIER_OUT, AMPLIFIER_RIGHT, 0) = 0x7F | RT_BIT(7); + break; + } + + /* + * ADCs. + */ + case STAC9220_NID_ADC0: /* Analog input. */ + { + pNode->node.au32F02_param[0] = STAC9220_NID_AMP_ADC0; + goto adc_init; + } + + case STAC9220_NID_ADC1: /* Analog input (CD). */ + { + pNode->node.au32F02_param[0] = STAC9220_NID_AMP_ADC1; + + /* Fall through is intentional. */ + adc_init: + + pNode->adc.u32A_param = CODEC_MAKE_A(HDA_SDFMT_TYPE_PCM, HDA_SDFMT_BASE_44KHZ, + HDA_SDFMT_MULT_1X, HDA_SDFMT_DIV_2X, HDA_SDFMT_16_BIT, + HDA_SDFMT_CHAN_STEREO); + + pNode->adc.u32F03_param = RT_BIT(0); + pNode->adc.u32F05_param = CODEC_MAKE_F05(0, 0, 0, CODEC_F05_D3, CODEC_F05_D3); /* PS-Act: D3 Set: D3 */ + + pNode->adc.node.au32F00_param[0x9] = CODEC_MAKE_F00_09(CODEC_F00_09_TYPE_AUDIO_INPUT, 0xD, 0) + | CODEC_F00_09_CAP_POWER_CTRL + | CODEC_F00_09_CAP_CONNECTION_LIST + | CODEC_F00_09_CAP_PROC_WIDGET + | CODEC_F00_09_CAP_STEREO; + /* Connection list entries. */ + pNode->adc.node.au32F00_param[0xE] = CODEC_MAKE_F00_0E(CODEC_F00_0E_LIST_NID_SHORT, 1 /* Entries */); + break; + } + + /* + * SP/DIF In/Out. + */ + case STAC9220_NID_SPDIF_OUT: + { + pNode->spdifout.u32A_param = CODEC_MAKE_A(HDA_SDFMT_TYPE_PCM, HDA_SDFMT_BASE_44KHZ, + HDA_SDFMT_MULT_1X, HDA_SDFMT_DIV_2X, HDA_SDFMT_16_BIT, + HDA_SDFMT_CHAN_STEREO); + pNode->spdifout.u32F06_param = 0; + pNode->spdifout.u32F0d_param = 0; + + pNode->spdifout.node.au32F00_param[0x9] = CODEC_MAKE_F00_09(CODEC_F00_09_TYPE_AUDIO_OUTPUT, 4, 0) + | CODEC_F00_09_CAP_DIGITAL + | CODEC_F00_09_CAP_FMT_OVERRIDE + | CODEC_F00_09_CAP_STEREO; + + /* Use a fixed format from AFG. */ + pNode->spdifout.node.au32F00_param[0xA] = pThis->aNodes[STAC9220_NID_AFG].node.au32F00_param[0xA]; + pNode->spdifout.node.au32F00_param[0xB] = CODEC_F00_0B_PCM; + break; + } + + case STAC9220_NID_SPDIF_IN: + { + pNode->spdifin.u32A_param = CODEC_MAKE_A(HDA_SDFMT_TYPE_PCM, HDA_SDFMT_BASE_44KHZ, + HDA_SDFMT_MULT_1X, HDA_SDFMT_DIV_2X, HDA_SDFMT_16_BIT, + HDA_SDFMT_CHAN_STEREO); + + pNode->spdifin.node.au32F00_param[0x9] = CODEC_MAKE_F00_09(CODEC_F00_09_TYPE_AUDIO_INPUT, 4, 0) + | CODEC_F00_09_CAP_DIGITAL + | CODEC_F00_09_CAP_CONNECTION_LIST + | CODEC_F00_09_CAP_FMT_OVERRIDE + | CODEC_F00_09_CAP_STEREO; + + /* Use a fixed format from AFG. */ + pNode->spdifin.node.au32F00_param[0xA] = pThis->aNodes[STAC9220_NID_AFG].node.au32F00_param[0xA]; + pNode->spdifin.node.au32F00_param[0xB] = CODEC_F00_0B_PCM; + + /* Connection list entries. */ + pNode->spdifin.node.au32F00_param[0xE] = CODEC_MAKE_F00_0E(CODEC_F00_0E_LIST_NID_SHORT, 1 /* Entries */); + pNode->spdifin.node.au32F02_param[0] = 0x11; + break; + } + + /* + * PINs / Ports. + */ + case STAC9220_NID_PIN_HEADPHONE0: /* Port A: Headphone in/out (front). */ + { + pNode->port.u32F09_param = CODEC_MAKE_F09_ANALOG(0 /*fPresent*/, CODEC_F09_ANALOG_NA); + + pNode->port.node.au32F00_param[0xC] = CODEC_MAKE_F00_0C(0x17) + | CODEC_F00_0C_CAP_INPUT + | CODEC_F00_0C_CAP_OUTPUT + | CODEC_F00_0C_CAP_HEADPHONE_AMP + | CODEC_F00_0C_CAP_PRESENCE_DETECT + | CODEC_F00_0C_CAP_TRIGGER_REQUIRED; + + /* Connection list entry 0: Goes to DAC0. */ + pNode->port.node.au32F02_param[0] = STAC9220_NID_DAC0; + + if (!fInReset) + pNode->port.u32F1c_param = CODEC_MAKE_F1C(CODEC_F1C_PORT_COMPLEX, + CODEC_F1C_LOCATION_FRONT, + CODEC_F1C_DEVICE_HP, + CODEC_F1C_CONNECTION_TYPE_1_8INCHES, + CODEC_F1C_COLOR_GREEN, + CODEC_F1C_MISC_NONE, + CODEC_F1C_ASSOCIATION_GROUP_1, 0x0 /* Seq */); + goto port_init; + } + + case STAC9220_NID_PIN_B: /* Port B: Rear CLFE (Center / Subwoofer). */ + { + pNode->port.u32F09_param = CODEC_MAKE_F09_ANALOG(1 /*fPresent*/, CODEC_F09_ANALOG_NA); + + pNode->port.node.au32F00_param[0xC] = CODEC_MAKE_F00_0C(0x17) + | CODEC_F00_0C_CAP_INPUT + | CODEC_F00_0C_CAP_OUTPUT + | CODEC_F00_0C_CAP_PRESENCE_DETECT + | CODEC_F00_0C_CAP_TRIGGER_REQUIRED; + + /* Connection list entry 0: Goes to DAC2. */ + pNode->port.node.au32F02_param[0] = STAC9220_NID_DAC2; + + if (!fInReset) + pNode->port.u32F1c_param = CODEC_MAKE_F1C(CODEC_F1C_PORT_COMPLEX, + CODEC_F1C_LOCATION_REAR, + CODEC_F1C_DEVICE_SPEAKER, + CODEC_F1C_CONNECTION_TYPE_1_8INCHES, + CODEC_F1C_COLOR_BLACK, + CODEC_F1C_MISC_NONE, + CODEC_F1C_ASSOCIATION_GROUP_0, 0x1 /* Seq */); + goto port_init; + } + + case STAC9220_NID_PIN_C: /* Rear Speaker. */ + { + pNode->port.u32F09_param = CODEC_MAKE_F09_ANALOG(1 /*fPresent*/, CODEC_F09_ANALOG_NA); + + pNode->port.node.au32F00_param[0xC] = CODEC_MAKE_F00_0C(0x17) + | CODEC_F00_0C_CAP_INPUT + | CODEC_F00_0C_CAP_OUTPUT + | CODEC_F00_0C_CAP_PRESENCE_DETECT + | CODEC_F00_0C_CAP_TRIGGER_REQUIRED; + + /* Connection list entry 0: Goes to DAC1. */ + pNode->port.node.au32F02_param[0x0] = STAC9220_NID_DAC1; + + if (!fInReset) + pNode->port.u32F1c_param = CODEC_MAKE_F1C(CODEC_F1C_PORT_COMPLEX, + CODEC_F1C_LOCATION_REAR, + CODEC_F1C_DEVICE_SPEAKER, + CODEC_F1C_CONNECTION_TYPE_1_8INCHES, + CODEC_F1C_COLOR_GREEN, + CODEC_F1C_MISC_NONE, + CODEC_F1C_ASSOCIATION_GROUP_0, 0x0 /* Seq */); + goto port_init; + } + + case STAC9220_NID_PIN_HEADPHONE1: /* Also known as PIN_D. */ + { + pNode->port.u32F09_param = CODEC_MAKE_F09_ANALOG(1 /*fPresent*/, CODEC_F09_ANALOG_NA); + + pNode->port.node.au32F00_param[0xC] = CODEC_MAKE_F00_0C(0x17) + | CODEC_F00_0C_CAP_INPUT + | CODEC_F00_0C_CAP_OUTPUT + | CODEC_F00_0C_CAP_HEADPHONE_AMP + | CODEC_F00_0C_CAP_PRESENCE_DETECT + | CODEC_F00_0C_CAP_TRIGGER_REQUIRED; + + /* Connection list entry 0: Goes to DAC1. */ + pNode->port.node.au32F02_param[0x0] = STAC9220_NID_DAC0; + + if (!fInReset) + pNode->port.u32F1c_param = CODEC_MAKE_F1C(CODEC_F1C_PORT_COMPLEX, + CODEC_F1C_LOCATION_FRONT, + CODEC_F1C_DEVICE_MIC, + CODEC_F1C_CONNECTION_TYPE_1_8INCHES, + CODEC_F1C_COLOR_PINK, + CODEC_F1C_MISC_NONE, + CODEC_F1C_ASSOCIATION_GROUP_15, 0x0 /* Ignored */); + /* Fall through is intentional. */ + + port_init: + + pNode->port.u32F07_param = CODEC_F07_IN_ENABLE + | CODEC_F07_OUT_ENABLE; + pNode->port.u32F08_param = 0; + + pNode->port.node.au32F00_param[0x9] = CODEC_MAKE_F00_09(CODEC_F00_09_TYPE_PIN_COMPLEX, 0, 0) + | CODEC_F00_09_CAP_CONNECTION_LIST + | CODEC_F00_09_CAP_UNSOL + | CODEC_F00_09_CAP_STEREO; + /* Connection list entries. */ + pNode->port.node.au32F00_param[0xE] = CODEC_MAKE_F00_0E(CODEC_F00_0E_LIST_NID_SHORT, 1 /* Entries */); + break; + } + + case STAC9220_NID_PIN_E: + { + pNode->port.u32F07_param = CODEC_F07_IN_ENABLE; + pNode->port.u32F08_param = 0; + /* If Line in is reported as enabled, OS X sees no speakers! Windows does + * not care either way, although Linux does. + */ + pNode->port.u32F09_param = CODEC_MAKE_F09_ANALOG(0 /* fPresent */, 0); + + pNode->port.node.au32F00_param[0x9] = CODEC_MAKE_F00_09(CODEC_F00_09_TYPE_PIN_COMPLEX, 0, 0) + | CODEC_F00_09_CAP_UNSOL + | CODEC_F00_09_CAP_STEREO; + + pNode->port.node.au32F00_param[0xC] = CODEC_F00_0C_CAP_INPUT + | CODEC_F00_0C_CAP_PRESENCE_DETECT; + + if (!fInReset) + pNode->port.u32F1c_param = CODEC_MAKE_F1C(CODEC_F1C_PORT_COMPLEX, + CODEC_F1C_LOCATION_REAR, + CODEC_F1C_DEVICE_LINE_IN, + CODEC_F1C_CONNECTION_TYPE_1_8INCHES, + CODEC_F1C_COLOR_BLUE, + CODEC_F1C_MISC_NONE, + CODEC_F1C_ASSOCIATION_GROUP_4, 0x1 /* Seq */); + break; + } + + case STAC9220_NID_PIN_F: + { + pNode->port.u32F07_param = CODEC_F07_IN_ENABLE | CODEC_F07_OUT_ENABLE; + pNode->port.u32F08_param = 0; + pNode->port.u32F09_param = CODEC_MAKE_F09_ANALOG(1 /* fPresent */, CODEC_F09_ANALOG_NA); + + pNode->port.node.au32F00_param[0x9] = CODEC_MAKE_F00_09(CODEC_F00_09_TYPE_PIN_COMPLEX, 0, 0) + | CODEC_F00_09_CAP_CONNECTION_LIST + | CODEC_F00_09_CAP_UNSOL + | CODEC_F00_09_CAP_OUT_AMP_PRESENT + | CODEC_F00_09_CAP_STEREO; + + pNode->port.node.au32F00_param[0xC] = CODEC_F00_0C_CAP_INPUT + | CODEC_F00_0C_CAP_OUTPUT; + + /* Connection list entry 0: Goes to DAC3. */ + pNode->port.node.au32F00_param[0xE] = CODEC_MAKE_F00_0E(CODEC_F00_0E_LIST_NID_SHORT, 1 /* Entries */); + pNode->port.node.au32F02_param[0x0] = STAC9220_NID_DAC3; + + if (!fInReset) + pNode->port.u32F1c_param = CODEC_MAKE_F1C(CODEC_F1C_PORT_COMPLEX, + CODEC_F1C_LOCATION_INTERNAL, + CODEC_F1C_DEVICE_SPEAKER, + CODEC_F1C_CONNECTION_TYPE_1_8INCHES, + CODEC_F1C_COLOR_ORANGE, + CODEC_F1C_MISC_NONE, + CODEC_F1C_ASSOCIATION_GROUP_0, 0x2 /* Seq */); + break; + } + + case STAC9220_NID_PIN_SPDIF_OUT: /* Rear SPDIF Out. */ + { + pNode->digout.u32F07_param = CODEC_F07_OUT_ENABLE; + pNode->digout.u32F09_param = 0; + + pNode->digout.node.au32F00_param[0x9] = CODEC_MAKE_F00_09(CODEC_F00_09_TYPE_PIN_COMPLEX, 0, 0) + | CODEC_F00_09_CAP_DIGITAL + | CODEC_F00_09_CAP_CONNECTION_LIST + | CODEC_F00_09_CAP_STEREO; + pNode->digout.node.au32F00_param[0xC] = CODEC_F00_0C_CAP_OUTPUT; + + /* Connection list entries. */ + pNode->digout.node.au32F00_param[0xE] = CODEC_MAKE_F00_0E(CODEC_F00_0E_LIST_NID_SHORT, 3 /* Entries */); + pNode->digout.node.au32F02_param[0x0] = RT_MAKE_U32_FROM_U8(STAC9220_NID_SPDIF_OUT, + STAC9220_NID_AMP_ADC0, STAC9221_NID_ADAT_OUT, 0); + if (!fInReset) + pNode->digout.u32F1c_param = CODEC_MAKE_F1C(CODEC_F1C_PORT_COMPLEX, + CODEC_F1C_LOCATION_REAR, + CODEC_F1C_DEVICE_SPDIF_OUT, + CODEC_F1C_CONNECTION_TYPE_DIN, + CODEC_F1C_COLOR_BLACK, + CODEC_F1C_MISC_NONE, + CODEC_F1C_ASSOCIATION_GROUP_2, 0x0 /* Seq */); + break; + } + + case STAC9220_NID_PIN_SPDIF_IN: + { + pNode->digin.u32F05_param = CODEC_MAKE_F05(0, 0, 0, CODEC_F05_D3, CODEC_F05_D3); /* PS-Act: D3 -> D3 */ + pNode->digin.u32F07_param = CODEC_F07_IN_ENABLE; + pNode->digin.u32F08_param = 0; + pNode->digin.u32F09_param = CODEC_MAKE_F09_DIGITAL(0, 0); + pNode->digin.u32F0c_param = 0; + + pNode->digin.node.au32F00_param[0x9] = CODEC_MAKE_F00_09(CODEC_F00_09_TYPE_PIN_COMPLEX, 3, 0) + | CODEC_F00_09_CAP_POWER_CTRL + | CODEC_F00_09_CAP_DIGITAL + | CODEC_F00_09_CAP_UNSOL + | CODEC_F00_09_CAP_STEREO; + + pNode->digin.node.au32F00_param[0xC] = CODEC_F00_0C_CAP_EAPD + | CODEC_F00_0C_CAP_INPUT + | CODEC_F00_0C_CAP_PRESENCE_DETECT; + if (!fInReset) + pNode->digin.u32F1c_param = CODEC_MAKE_F1C(CODEC_F1C_PORT_COMPLEX, + CODEC_F1C_LOCATION_REAR, + CODEC_F1C_DEVICE_SPDIF_IN, + CODEC_F1C_CONNECTION_TYPE_OTHER_DIGITAL, + CODEC_F1C_COLOR_BLACK, + CODEC_F1C_MISC_NONE, + CODEC_F1C_ASSOCIATION_GROUP_5, 0x0 /* Seq */); + break; + } + + case STAC9220_NID_ADC0_MUX: + { + pNode->adcmux.u32F01_param = 0; /* Connection select control index (STAC9220_NID_PIN_E). */ + goto adcmux_init; + } + + case STAC9220_NID_ADC1_MUX: + { + pNode->adcmux.u32F01_param = 1; /* Connection select control index (STAC9220_NID_PIN_CD). */ + /* Fall through is intentional. */ + + adcmux_init: + + pNode->adcmux.node.au32F00_param[0x9] = CODEC_MAKE_F00_09(CODEC_F00_09_TYPE_AUDIO_SELECTOR, 0, 0) + | CODEC_F00_09_CAP_CONNECTION_LIST + | CODEC_F00_09_CAP_AMP_FMT_OVERRIDE + | CODEC_F00_09_CAP_OUT_AMP_PRESENT + | CODEC_F00_09_CAP_STEREO; + + pNode->adcmux.node.au32F00_param[0xD] = CODEC_MAKE_F00_0D(0, 27, 4, 0); + + /* Connection list entries. */ + pNode->adcmux.node.au32F00_param[0xE] = CODEC_MAKE_F00_0E(CODEC_F00_0E_LIST_NID_SHORT, 7 /* Entries */); + pNode->adcmux.node.au32F02_param[0x0] = RT_MAKE_U32_FROM_U8(STAC9220_NID_PIN_E, + STAC9220_NID_PIN_CD, + STAC9220_NID_PIN_F, + STAC9220_NID_PIN_B); + pNode->adcmux.node.au32F02_param[0x4] = RT_MAKE_U32_FROM_U8(STAC9220_NID_PIN_C, + STAC9220_NID_PIN_HEADPHONE1, + STAC9220_NID_PIN_HEADPHONE0, + 0x0 /* Unused */); + + /* STAC 9220 v10 6.21-22.{4,5} both(left and right) out amplifiers initialized with 0. */ + RT_ZERO(pNode->adcmux.B_params); + break; + } + + case STAC9220_NID_PCBEEP: + { + pNode->pcbeep.u32F0a_param = 0; + + pNode->pcbeep.node.au32F00_param[0x9] = CODEC_MAKE_F00_09(CODEC_F00_09_TYPE_BEEP_GEN, 0, 0) + | CODEC_F00_09_CAP_AMP_FMT_OVERRIDE + | CODEC_F00_09_CAP_OUT_AMP_PRESENT; + pNode->pcbeep.node.au32F00_param[0xD] = CODEC_MAKE_F00_0D(0, 17, 3, 3); + + RT_ZERO(pNode->pcbeep.B_params); + break; + } + + case STAC9220_NID_PIN_CD: + { + pNode->cdnode.node.au32F00_param[0x9] = CODEC_MAKE_F00_09(CODEC_F00_09_TYPE_PIN_COMPLEX, 0, 0) + | CODEC_F00_09_CAP_STEREO; + pNode->cdnode.node.au32F00_param[0xC] = CODEC_F00_0C_CAP_INPUT; + + if (!fInReset) + pNode->cdnode.u32F1c_param = CODEC_MAKE_F1C(CODEC_F1C_PORT_FIXED, + CODEC_F1C_LOCATION_INTERNAL, + CODEC_F1C_DEVICE_CD, + CODEC_F1C_CONNECTION_TYPE_ATAPI, + CODEC_F1C_COLOR_UNKNOWN, + CODEC_F1C_MISC_NONE, + CODEC_F1C_ASSOCIATION_GROUP_4, 0x2 /* Seq */); + break; + } + + case STAC9220_NID_VOL_KNOB: + { + pNode->volumeKnob.u32F08_param = 0; + pNode->volumeKnob.u32F0f_param = 0x7f; + + pNode->volumeKnob.node.au32F00_param[0x9] = CODEC_MAKE_F00_09(CODEC_F00_09_TYPE_VOLUME_KNOB, 0, 0); + pNode->volumeKnob.node.au32F00_param[0xD] = RT_BIT(7) | 0x7F; + + /* Connection list entries. */ + pNode->volumeKnob.node.au32F00_param[0xE] = CODEC_MAKE_F00_0E(CODEC_F00_0E_LIST_NID_SHORT, 4 /* Entries */); + pNode->volumeKnob.node.au32F02_param[0x0] = RT_MAKE_U32_FROM_U8(STAC9220_NID_DAC0, + STAC9220_NID_DAC1, + STAC9220_NID_DAC2, + STAC9220_NID_DAC3); + break; + } + + case STAC9220_NID_AMP_ADC0: /* ADC0Vol */ + { + pNode->adcvol.node.au32F02_param[0] = STAC9220_NID_ADC0_MUX; + goto adcvol_init; + } + + case STAC9220_NID_AMP_ADC1: /* ADC1Vol */ + { + pNode->adcvol.node.au32F02_param[0] = STAC9220_NID_ADC1_MUX; + /* Fall through is intentional. */ + + adcvol_init: + + pNode->adcvol.node.au32F00_param[0x9] = CODEC_MAKE_F00_09(CODEC_F00_09_TYPE_AUDIO_SELECTOR, 0, 0) + | CODEC_F00_09_CAP_L_R_SWAP + | CODEC_F00_09_CAP_CONNECTION_LIST + | CODEC_F00_09_CAP_IN_AMP_PRESENT + | CODEC_F00_09_CAP_STEREO; + + + pNode->adcvol.node.au32F00_param[0xE] = CODEC_MAKE_F00_0E(CODEC_F00_0E_LIST_NID_SHORT, 1 /* Entries */); + + RT_ZERO(pNode->adcvol.B_params); + AMPLIFIER_REGISTER(pNode->adcvol.B_params, AMPLIFIER_IN, AMPLIFIER_LEFT, 0) = RT_BIT(7); + AMPLIFIER_REGISTER(pNode->adcvol.B_params, AMPLIFIER_IN, AMPLIFIER_RIGHT, 0) = RT_BIT(7); + break; + } + + /* + * STAC9221 nodes. + */ + + case STAC9221_NID_ADAT_OUT: + { + pNode->node.au32F00_param[0x9] = CODEC_MAKE_F00_09(CODEC_F00_09_TYPE_VENDOR_DEFINED, 3, 0) + | CODEC_F00_09_CAP_DIGITAL + | CODEC_F00_09_CAP_STEREO; + break; + } + + case STAC9221_NID_I2S_OUT: + { + pNode->node.au32F00_param[0x9] = CODEC_MAKE_F00_09(CODEC_F00_09_TYPE_AUDIO_OUTPUT, 3, 0) + | CODEC_F00_09_CAP_DIGITAL + | CODEC_F00_09_CAP_STEREO; + break; + } + + case STAC9221_NID_PIN_I2S_OUT: + { + pNode->node.au32F00_param[0x9] = CODEC_MAKE_F00_09(CODEC_F00_09_TYPE_PIN_COMPLEX, 0, 0) + | CODEC_F00_09_CAP_DIGITAL + | CODEC_F00_09_CAP_CONNECTION_LIST + | CODEC_F00_09_CAP_STEREO; + + pNode->node.au32F00_param[0xC] = CODEC_F00_0C_CAP_OUTPUT; + + /* Connection list entries. */ + pNode->node.au32F00_param[0xE] = CODEC_MAKE_F00_0E(CODEC_F00_0E_LIST_NID_SHORT, 1 /* Entries */); + pNode->node.au32F02_param[0] = STAC9221_NID_I2S_OUT; + + if (!fInReset) + pNode->reserved.u32F1c_param = CODEC_MAKE_F1C(CODEC_F1C_PORT_NO_PHYS, + CODEC_F1C_LOCATION_NA, + CODEC_F1C_DEVICE_LINE_OUT, + CODEC_F1C_CONNECTION_TYPE_UNKNOWN, + CODEC_F1C_COLOR_UNKNOWN, + CODEC_F1C_MISC_NONE, + CODEC_F1C_ASSOCIATION_GROUP_15, 0x0 /* Ignored */); + break; + } + + default: + AssertMsgFailed(("Node %RU8 not implemented\n", uNID)); + break; + } +} + + +/** + * Resets the codec with all its connected nodes. + * + * @param pThis HDA codec to reset. + */ +static void stac9220Reset(PHDACODECR3 pThis) +{ + AssertPtrReturnVoid(pThis->aNodes); + + LogRel(("HDA: Codec reset\n")); + + uint8_t const cTotalNodes = (uint8_t)RT_MIN(pThis->Cfg.cTotalNodes, RT_ELEMENTS(pThis->aNodes)); + for (uint8_t i = 0; i < cTotalNodes; i++) + stac9220NodeReset(pThis, i, &pThis->aNodes[i], true /*fInReset*/); +} + + +static int stac9220Construct(PHDACODECR3 pThis, HDACODECCFG *pCfg) +{ + /* + * Note: The Linux kernel uses "patch_stac922x" for the fixups, + * which in turn uses "ref922x_pin_configs" for the configuration + * defaults tweaking in sound/pci/hda/patch_sigmatel.c. + */ + pCfg->idVendor = 0x8384; /* SigmaTel */ + pCfg->idDevice = 0x7680; /* STAC9221 A1 */ + pCfg->bBSKU = 0x76; + pCfg->idAssembly = 0x80; + + AssertCompile(STAC9221_NUM_NODES <= RT_ELEMENTS(pThis->aNodes)); + pCfg->cTotalNodes = STAC9221_NUM_NODES; + pCfg->idxAdcVolsLineIn = STAC9220_NID_AMP_ADC0; + pCfg->idxDacLineOut = STAC9220_NID_DAC1; + + /* Copy over the node class lists and popuplate afNodeClassifications. */ +#define STAC9220WIDGET(a_Type) do { \ + AssertCompile(RT_ELEMENTS(g_abStac9220##a_Type##s) <= RT_ELEMENTS(pCfg->ab##a_Type##s)); \ + uint8_t *pbDst = (uint8_t *)&pCfg->ab##a_Type##s[0]; \ + uintptr_t i; \ + for (i = 0; i < RT_ELEMENTS(g_abStac9220##a_Type##s); i++) \ + { \ + uint8_t const idNode = g_abStac9220##a_Type##s[i]; \ + if (idNode == 0) \ + break; \ + AssertReturn(idNode < RT_ELEMENTS(pThis->aNodes), VERR_INTERNAL_ERROR_3); \ + pCfg->afNodeClassifications[idNode] |= RT_CONCAT(CODEC_NODE_CLS_,a_Type); \ + pbDst[i] = idNode; \ + } \ + Assert(i + 1 == RT_ELEMENTS(g_abStac9220##a_Type##s)); \ + for (; i < RT_ELEMENTS(pCfg->ab##a_Type##s); i++) \ + pbDst[i] = 0; \ + } while (0) + STAC9220WIDGET(Port); + STAC9220WIDGET(Dac); + STAC9220WIDGET(Adc); + STAC9220WIDGET(AdcVol); + STAC9220WIDGET(AdcMux); + STAC9220WIDGET(Pcbeep); + STAC9220WIDGET(SpdifIn); + STAC9220WIDGET(SpdifOut); + STAC9220WIDGET(DigInPin); + STAC9220WIDGET(DigOutPin); + STAC9220WIDGET(Cd); + STAC9220WIDGET(VolKnob); + STAC9220WIDGET(Reserved); +#undef STAC9220WIDGET + + /* + * Initialize all codec nodes. + * This is specific to the codec, so do this here. + * + * Note: Do *not* call stac9220Reset() here, as this would not + * initialize the node default configuration values then! + */ + for (uint8_t i = 0; i < STAC9221_NUM_NODES; i++) + stac9220NodeReset(pThis, i, &pThis->aNodes[i], false /*fInReset*/); + + /* Common root node initializers. */ + pThis->aNodes[STAC9220_NID_ROOT].root.node.au32F00_param[0] = CODEC_MAKE_F00_00(pCfg->idVendor, pCfg->idDevice); + pThis->aNodes[STAC9220_NID_ROOT].root.node.au32F00_param[4] = CODEC_MAKE_F00_04(0x1, 0x1); + + /* Common AFG node initializers. */ + pThis->aNodes[STAC9220_NID_AFG].afg.node.au32F00_param[0x4] = CODEC_MAKE_F00_04(0x2, STAC9221_NUM_NODES - 2); + pThis->aNodes[STAC9220_NID_AFG].afg.node.au32F00_param[0x5] = CODEC_MAKE_F00_05(1, CODEC_F00_05_AFG); + pThis->aNodes[STAC9220_NID_AFG].afg.node.au32F00_param[0xA] = CODEC_F00_0A_44_1KHZ | CODEC_F00_0A_16_BIT; + pThis->aNodes[STAC9220_NID_AFG].afg.u32F20_param = CODEC_MAKE_F20(pCfg->idVendor, pCfg->bBSKU, pCfg->idAssembly); + + return VINF_SUCCESS; +} + + +/********************************************************************************************************************************* +* Common Helpers * +*********************************************************************************************************************************/ + +/* + * Some generic predicate functions. + */ +#define HDA_CODEC_IS_NODE_OF_TYPE_FUNC(a_Type) \ + DECLINLINE(bool) hdaCodecIs##a_Type##Node(PHDACODECR3 pThis, uint8_t idNode) \ + { \ + Assert(idNode < RT_ELEMENTS(pThis->Cfg.afNodeClassifications)); \ + Assert( (memchr(&pThis->Cfg.RT_CONCAT3(ab,a_Type,s)[0], idNode, sizeof(pThis->Cfg.RT_CONCAT3(ab,a_Type,s))) != NULL) \ + == RT_BOOL(pThis->Cfg.afNodeClassifications[idNode] & RT_CONCAT(CODEC_NODE_CLS_,a_Type))); \ + return RT_BOOL(pThis->Cfg.afNodeClassifications[idNode] & RT_CONCAT(CODEC_NODE_CLS_,a_Type)); \ + } +/* hdaCodecIsPortNode */ +HDA_CODEC_IS_NODE_OF_TYPE_FUNC(Port) +/* hdaCodecIsDacNode */ +HDA_CODEC_IS_NODE_OF_TYPE_FUNC(Dac) +/* hdaCodecIsAdcVolNode */ +HDA_CODEC_IS_NODE_OF_TYPE_FUNC(AdcVol) +/* hdaCodecIsAdcNode */ +HDA_CODEC_IS_NODE_OF_TYPE_FUNC(Adc) +/* hdaCodecIsAdcMuxNode */ +HDA_CODEC_IS_NODE_OF_TYPE_FUNC(AdcMux) +/* hdaCodecIsPcbeepNode */ +HDA_CODEC_IS_NODE_OF_TYPE_FUNC(Pcbeep) +/* hdaCodecIsSpdifOutNode */ +HDA_CODEC_IS_NODE_OF_TYPE_FUNC(SpdifOut) +/* hdaCodecIsSpdifInNode */ +HDA_CODEC_IS_NODE_OF_TYPE_FUNC(SpdifIn) +/* hdaCodecIsDigInPinNode */ +HDA_CODEC_IS_NODE_OF_TYPE_FUNC(DigInPin) +/* hdaCodecIsDigOutPinNode */ +HDA_CODEC_IS_NODE_OF_TYPE_FUNC(DigOutPin) +/* hdaCodecIsCdNode */ +HDA_CODEC_IS_NODE_OF_TYPE_FUNC(Cd) +/* hdaCodecIsVolKnobNode */ +HDA_CODEC_IS_NODE_OF_TYPE_FUNC(VolKnob) +/* hdaCodecIsReservedNode */ +HDA_CODEC_IS_NODE_OF_TYPE_FUNC(Reserved) + + +/* + * Misc helpers. + */ +static int hdaR3CodecToAudVolume(PHDACODECR3 pThis, PCODECNODE pNode, AMPLIFIER *pAmp, PDMAUDIOMIXERCTL enmMixerCtl) +{ + RT_NOREF(pNode); + + uint8_t iDir; + switch (enmMixerCtl) + { + case PDMAUDIOMIXERCTL_VOLUME_MASTER: + case PDMAUDIOMIXERCTL_FRONT: + iDir = AMPLIFIER_OUT; + break; + case PDMAUDIOMIXERCTL_LINE_IN: + case PDMAUDIOMIXERCTL_MIC_IN: + iDir = AMPLIFIER_IN; + break; + default: + AssertMsgFailedReturn(("Invalid mixer control %RU32\n", enmMixerCtl), VERR_INVALID_PARAMETER); + break; + } + + int iMute; + iMute = AMPLIFIER_REGISTER(*pAmp, iDir, AMPLIFIER_LEFT, 0) & RT_BIT(7); + iMute |= AMPLIFIER_REGISTER(*pAmp, iDir, AMPLIFIER_RIGHT, 0) & RT_BIT(7); + iMute >>=7; + iMute &= 0x1; + + uint8_t bLeft = AMPLIFIER_REGISTER(*pAmp, iDir, AMPLIFIER_LEFT, 0) & 0x7f; + uint8_t bRight = AMPLIFIER_REGISTER(*pAmp, iDir, AMPLIFIER_RIGHT, 0) & 0x7f; + + /* + * The STAC9220 volume controls have 0 to -96dB attenuation range in 128 steps. + * We have 0 to -96dB range in 256 steps. HDA volume setting of 127 must map + * to 255 internally (0dB), while HDA volume setting of 0 (-96dB) should map + * to 1 (rather than zero) internally. + */ + bLeft = (bLeft + 1) * (2 * 255) / 256; + bRight = (bRight + 1) * (2 * 255) / 256; + + PDMAUDIOVOLUME Vol; + PDMAudioVolumeInitFromStereo(&Vol, RT_BOOL(iMute), bLeft, bRight); + + LogFunc(("[NID0x%02x] %RU8/%RU8%s\n", pNode->node.uID, bLeft, bRight, Vol.fMuted ? "- Muted!" : "")); + LogRel2(("HDA: Setting volume for mixer control '%s' to %RU8/%RU8%s\n", + PDMAudioMixerCtlGetName(enmMixerCtl), bLeft, bRight, Vol.fMuted ? "- Muted!" : "")); + + return hdaR3MixerSetVolume(pThis, enmMixerCtl, &Vol); +} + + +DECLINLINE(void) hdaCodecSetRegister(uint32_t *pu32Reg, uint32_t u32Cmd, uint8_t u8Offset, uint32_t mask) +{ + Assert((pu32Reg && u8Offset < 32)); + *pu32Reg &= ~(mask << u8Offset); + *pu32Reg |= (u32Cmd & mask) << u8Offset; +} + +DECLINLINE(void) hdaCodecSetRegisterU8(uint32_t *pu32Reg, uint32_t u32Cmd, uint8_t u8Offset) +{ + hdaCodecSetRegister(pu32Reg, u32Cmd, u8Offset, CODEC_VERB_8BIT_DATA); +} + +DECLINLINE(void) hdaCodecSetRegisterU16(uint32_t *pu32Reg, uint32_t u32Cmd, uint8_t u8Offset) +{ + hdaCodecSetRegister(pu32Reg, u32Cmd, u8Offset, CODEC_VERB_16BIT_DATA); +} + + +/********************************************************************************************************************************* +* Verb Processor Functions. * +*********************************************************************************************************************************/ +#if 0 /* unused */ + +/** + * @interface_method_impl{CODECVERB,pfn, Unimplemented} + */ +static DECLCALLBACK(int) vrbProcUnimplemented(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + RT_NOREF(pThis, uCmd); + LogFlowFunc(("uCmd(raw:%x: cad:%x, d:%c, nid:%x, verb:%x)\n", uCmd, + CODEC_CAD(uCmd), CODEC_DIRECT(uCmd) ? 'N' : 'Y', CODEC_NID(uCmd), CODEC_VERBDATA(uCmd))); + *puResp = 0; + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, ??? } + */ +static DECLCALLBACK(int) vrbProcBreak(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + int rc; + rc = vrbProcUnimplemented(pThis, uCmd, puResp); + *puResp |= CODEC_RESPONSE_UNSOLICITED; + return rc; +} + +#endif /* unused */ + +/** + * @interface_method_impl{CODECVERB,pfn, b-- } + */ +static DECLCALLBACK(int) vrbProcGetAmplifier(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + /* HDA spec 7.3.3.7 Note A */ + /** @todo If index out of range response should be 0. */ + uint8_t u8Index = CODEC_GET_AMP_DIRECTION(uCmd) == AMPLIFIER_OUT ? 0 : CODEC_GET_AMP_INDEX(uCmd); + + PCODECNODE pNode = &pThis->aNodes[CODEC_NID(uCmd)]; + if (hdaCodecIsDacNode(pThis, CODEC_NID(uCmd))) + *puResp = AMPLIFIER_REGISTER(pNode->dac.B_params, + CODEC_GET_AMP_DIRECTION(uCmd), + CODEC_GET_AMP_SIDE(uCmd), + u8Index); + else if (hdaCodecIsAdcVolNode(pThis, CODEC_NID(uCmd))) + *puResp = AMPLIFIER_REGISTER(pNode->adcvol.B_params, + CODEC_GET_AMP_DIRECTION(uCmd), + CODEC_GET_AMP_SIDE(uCmd), + u8Index); + else if (hdaCodecIsAdcMuxNode(pThis, CODEC_NID(uCmd))) + *puResp = AMPLIFIER_REGISTER(pNode->adcmux.B_params, + CODEC_GET_AMP_DIRECTION(uCmd), + CODEC_GET_AMP_SIDE(uCmd), + u8Index); + else if (hdaCodecIsPcbeepNode(pThis, CODEC_NID(uCmd))) + *puResp = AMPLIFIER_REGISTER(pNode->pcbeep.B_params, + CODEC_GET_AMP_DIRECTION(uCmd), + CODEC_GET_AMP_SIDE(uCmd), + u8Index); + else if (hdaCodecIsPortNode(pThis, CODEC_NID(uCmd))) + *puResp = AMPLIFIER_REGISTER(pNode->port.B_params, + CODEC_GET_AMP_DIRECTION(uCmd), + CODEC_GET_AMP_SIDE(uCmd), + u8Index); + else if (hdaCodecIsAdcNode(pThis, CODEC_NID(uCmd))) + *puResp = AMPLIFIER_REGISTER(pNode->adc.B_params, + CODEC_GET_AMP_DIRECTION(uCmd), + CODEC_GET_AMP_SIDE(uCmd), + u8Index); + else + LogRel2(("HDA: Warning: Unhandled get amplifier command: 0x%x (NID=0x%x [%RU8])\n", uCmd, CODEC_NID(uCmd), CODEC_NID(uCmd))); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, ??? } + */ +static DECLCALLBACK(int) vrbProcGetParameter(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + Assert((uCmd & CODEC_VERB_8BIT_DATA) < CODECNODE_F00_PARAM_LENGTH); + if ((uCmd & CODEC_VERB_8BIT_DATA) >= CODECNODE_F00_PARAM_LENGTH) + { + *puResp = 0; + + LogFlowFunc(("invalid F00 parameter %d\n", (uCmd & CODEC_VERB_8BIT_DATA))); + return VINF_SUCCESS; + } + + *puResp = pThis->aNodes[CODEC_NID(uCmd)].node.au32F00_param[uCmd & CODEC_VERB_8BIT_DATA]; + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, f01 } + */ +static DECLCALLBACK(int) vrbProcGetConSelectCtrl(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + if (hdaCodecIsAdcMuxNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].adcmux.u32F01_param; + else if (hdaCodecIsDigOutPinNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].digout.u32F01_param; + else if (hdaCodecIsPortNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].port.u32F01_param; + else if (hdaCodecIsAdcNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].adc.u32F01_param; + else if (hdaCodecIsAdcVolNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].adcvol.u32F01_param; + else + LogRel2(("HDA: Warning: Unhandled get connection select control command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 701 } + */ +static DECLCALLBACK(int) vrbProcSetConSelectCtrl(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + uint32_t *pu32Reg = NULL; + if (hdaCodecIsAdcMuxNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].adcmux.u32F01_param; + else if (hdaCodecIsDigOutPinNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].digout.u32F01_param; + else if (hdaCodecIsPortNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].port.u32F01_param; + else if (hdaCodecIsAdcNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].adc.u32F01_param; + else if (hdaCodecIsAdcVolNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].adcvol.u32F01_param; + else + LogRel2(("HDA: Warning: Unhandled set connection select control command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + + if (pu32Reg) + hdaCodecSetRegisterU8(pu32Reg, uCmd, 0); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, f07 } + */ +static DECLCALLBACK(int) vrbProcGetPinCtrl(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + if (hdaCodecIsPortNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].port.u32F07_param; + else if (hdaCodecIsDigOutPinNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].digout.u32F07_param; + else if (hdaCodecIsDigInPinNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].digin.u32F07_param; + else if (hdaCodecIsCdNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].cdnode.u32F07_param; + else if (hdaCodecIsPcbeepNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].pcbeep.u32F07_param; + else if (hdaCodecIsReservedNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].reserved.u32F07_param; + else + LogRel2(("HDA: Warning: Unhandled get pin control command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 707 } + */ +static DECLCALLBACK(int) vrbProcSetPinCtrl(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + uint32_t *pu32Reg = NULL; + if (hdaCodecIsPortNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].port.u32F07_param; + else if (hdaCodecIsDigInPinNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].digin.u32F07_param; + else if (hdaCodecIsDigOutPinNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].digout.u32F07_param; + else if (hdaCodecIsCdNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].cdnode.u32F07_param; + else if (hdaCodecIsPcbeepNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].pcbeep.u32F07_param; + else if ( hdaCodecIsReservedNode(pThis, CODEC_NID(uCmd)) + && CODEC_NID(uCmd) == 0x1b) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].reserved.u32F07_param; + else + LogRel2(("HDA: Warning: Unhandled set pin control command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + + if (pu32Reg) + hdaCodecSetRegisterU8(pu32Reg, uCmd, 0); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, f08 } + */ +static DECLCALLBACK(int) vrbProcGetUnsolicitedEnabled(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + if (hdaCodecIsPortNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].port.u32F08_param; + else if (hdaCodecIsDigInPinNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].digin.u32F08_param; + else if ((uCmd) == STAC9220_NID_AFG) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].afg.u32F08_param; + else if (hdaCodecIsVolKnobNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].volumeKnob.u32F08_param; + else if (hdaCodecIsDigOutPinNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].digout.u32F08_param; + else if (hdaCodecIsDigInPinNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].digin.u32F08_param; + else + LogRel2(("HDA: Warning: Unhandled get unsolicited enabled command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 708 } + */ +static DECLCALLBACK(int) vrbProcSetUnsolicitedEnabled(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + uint32_t *pu32Reg = NULL; + if (hdaCodecIsPortNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].port.u32F08_param; + else if (hdaCodecIsDigInPinNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].digin.u32F08_param; + else if (CODEC_NID(uCmd) == STAC9220_NID_AFG) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].afg.u32F08_param; + else if (hdaCodecIsVolKnobNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].volumeKnob.u32F08_param; + else if (hdaCodecIsDigInPinNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].digin.u32F08_param; + else if (hdaCodecIsDigOutPinNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].digout.u32F08_param; + else + LogRel2(("HDA: Warning: Unhandled set unsolicited enabled command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + + if (pu32Reg) + hdaCodecSetRegisterU8(pu32Reg, uCmd, 0); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, f09 } + */ +static DECLCALLBACK(int) vrbProcGetPinSense(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + if (hdaCodecIsPortNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].port.u32F09_param; + else if (hdaCodecIsDigInPinNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].digin.u32F09_param; + else + { + AssertFailed(); + LogRel2(("HDA: Warning: Unhandled get pin sense command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + } + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 709 } + */ +static DECLCALLBACK(int) vrbProcSetPinSense(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + uint32_t *pu32Reg = NULL; + if (hdaCodecIsPortNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].port.u32F09_param; + else if (hdaCodecIsDigInPinNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].digin.u32F09_param; + else + LogRel2(("HDA: Warning: Unhandled set pin sense command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + + if (pu32Reg) + hdaCodecSetRegisterU8(pu32Reg, uCmd, 0); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, ??? } + */ +static DECLCALLBACK(int) vrbProcGetConnectionListEntry(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + Assert((uCmd & CODEC_VERB_8BIT_DATA) < CODECNODE_F02_PARAM_LENGTH); + if ((uCmd & CODEC_VERB_8BIT_DATA) >= CODECNODE_F02_PARAM_LENGTH) + { + LogFlowFunc(("access to invalid F02 index %d\n", (uCmd & CODEC_VERB_8BIT_DATA))); + return VINF_SUCCESS; + } + *puResp = pThis->aNodes[CODEC_NID(uCmd)].node.au32F02_param[uCmd & CODEC_VERB_8BIT_DATA]; + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, f03 } + */ +static DECLCALLBACK(int) vrbProcGetProcessingState(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + if (hdaCodecIsAdcNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].adc.u32F03_param; + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 703 } + */ +static DECLCALLBACK(int) vrbProcSetProcessingState(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + if (hdaCodecIsAdcNode(pThis, CODEC_NID(uCmd))) + hdaCodecSetRegisterU8(&pThis->aNodes[CODEC_NID(uCmd)].adc.u32F03_param, uCmd, 0); + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, f0d } + */ +static DECLCALLBACK(int) vrbProcGetDigitalConverter(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + if (hdaCodecIsSpdifOutNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].spdifout.u32F0d_param; + else if (hdaCodecIsSpdifInNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].spdifin.u32F0d_param; + return VINF_SUCCESS; +} + + +static int codecSetDigitalConverter(PHDACODECR3 pThis, uint32_t uCmd, uint8_t u8Offset, uint64_t *puResp) +{ + *puResp = 0; + + if (hdaCodecIsSpdifOutNode(pThis, CODEC_NID(uCmd))) + hdaCodecSetRegisterU8(&pThis->aNodes[CODEC_NID(uCmd)].spdifout.u32F0d_param, uCmd, u8Offset); + else if (hdaCodecIsSpdifInNode(pThis, CODEC_NID(uCmd))) + hdaCodecSetRegisterU8(&pThis->aNodes[CODEC_NID(uCmd)].spdifin.u32F0d_param, uCmd, u8Offset); + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 70d } + */ +static DECLCALLBACK(int) vrbProcSetDigitalConverter1(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + return codecSetDigitalConverter(pThis, uCmd, 0, puResp); +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 70e } + */ +static DECLCALLBACK(int) vrbProcSetDigitalConverter2(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + return codecSetDigitalConverter(pThis, uCmd, 8, puResp); +} + + +/** + * @interface_method_impl{CODECVERB,pfn, f20 } + */ +static DECLCALLBACK(int) vrbProcGetSubId(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + Assert(CODEC_CAD(uCmd) == pThis->Cfg.id); + uint8_t const cTotalNodes = (uint8_t)RT_MIN(pThis->Cfg.cTotalNodes, RT_ELEMENTS(pThis->aNodes)); + Assert(CODEC_NID(uCmd) < cTotalNodes); + if (CODEC_NID(uCmd) >= cTotalNodes) + { + LogFlowFunc(("invalid node address %d\n", CODEC_NID(uCmd))); + *puResp = 0; + return VINF_SUCCESS; + } + if (CODEC_NID(uCmd) == STAC9220_NID_AFG) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].afg.u32F20_param; + else + *puResp = 0; + return VINF_SUCCESS; +} + + +static int codecSetSubIdX(PHDACODECR3 pThis, uint32_t uCmd, uint8_t u8Offset) +{ + Assert(CODEC_CAD(uCmd) == pThis->Cfg.id); + uint8_t const cTotalNodes = (uint8_t)RT_MIN(pThis->Cfg.cTotalNodes, RT_ELEMENTS(pThis->aNodes)); + Assert(CODEC_NID(uCmd) < cTotalNodes); + if (CODEC_NID(uCmd) >= cTotalNodes) + { + LogFlowFunc(("invalid node address %d\n", CODEC_NID(uCmd))); + return VINF_SUCCESS; + } + uint32_t *pu32Reg; + if (CODEC_NID(uCmd) == STAC9220_NID_AFG) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].afg.u32F20_param; + else + AssertFailedReturn(VINF_SUCCESS); + hdaCodecSetRegisterU8(pu32Reg, uCmd, u8Offset); + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 720 } + */ +static DECLCALLBACK(int) vrbProcSetSubId0(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + return codecSetSubIdX(pThis, uCmd, 0); +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 721 } + */ +static DECLCALLBACK(int) vrbProcSetSubId1(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + return codecSetSubIdX(pThis, uCmd, 8); +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 722 } + */ +static DECLCALLBACK(int) vrbProcSetSubId2(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + return codecSetSubIdX(pThis, uCmd, 16); +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 723 } + */ +static DECLCALLBACK(int) vrbProcSetSubId3(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + return codecSetSubIdX(pThis, uCmd, 24); +} + + +/** + * @interface_method_impl{CODECVERB,pfn, ??? } + */ +static DECLCALLBACK(int) vrbProcReset(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + Assert(CODEC_CAD(uCmd) == pThis->Cfg.id); + + if (pThis->Cfg.enmType == CODECTYPE_STAC9220) + { + Assert(CODEC_NID(uCmd) == STAC9220_NID_AFG); + + if (CODEC_NID(uCmd) == STAC9220_NID_AFG) + stac9220Reset(pThis); + } + else + AssertFailedReturn(VERR_NOT_IMPLEMENTED); + + *puResp = 0; + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, f05 } + */ +static DECLCALLBACK(int) vrbProcGetPowerState(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + if (CODEC_NID(uCmd) == STAC9220_NID_AFG) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].afg.u32F05_param; + else if (hdaCodecIsDacNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].dac.u32F05_param; + else if (hdaCodecIsAdcNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].adc.u32F05_param; + else if (hdaCodecIsDigInPinNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].digin.u32F05_param; + else if (hdaCodecIsDigOutPinNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].digout.u32F05_param; + else if (hdaCodecIsSpdifOutNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].spdifout.u32F05_param; + else if (hdaCodecIsSpdifInNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].spdifin.u32F05_param; + else if (hdaCodecIsReservedNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].reserved.u32F05_param; + else + LogRel2(("HDA: Warning: Unhandled get power state command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + + LogFunc(("[NID0x%02x]: fReset=%RTbool, fStopOk=%RTbool, Act=D%RU8, Set=D%RU8\n", + CODEC_NID(uCmd), CODEC_F05_IS_RESET(*puResp), CODEC_F05_IS_STOPOK(*puResp), CODEC_F05_ACT(*puResp), CODEC_F05_SET(*puResp))); + return VINF_SUCCESS; +} + +#if 1 + +/** + * @interface_method_impl{CODECVERB,pfn, 705 } + */ +static DECLCALLBACK(int) vrbProcSetPowerState(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + uint32_t *pu32Reg = NULL; + if (CODEC_NID(uCmd) == STAC9220_NID_AFG) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].afg.u32F05_param; + else if (hdaCodecIsDacNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].dac.u32F05_param; + else if (hdaCodecIsDigInPinNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].digin.u32F05_param; + else if (hdaCodecIsDigOutPinNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].digout.u32F05_param; + else if (hdaCodecIsAdcNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].adc.u32F05_param; + else if (hdaCodecIsSpdifOutNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].spdifout.u32F05_param; + else if (hdaCodecIsSpdifInNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].spdifin.u32F05_param; + else if (hdaCodecIsReservedNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].reserved.u32F05_param; + else + { + LogRel2(("HDA: Warning: Unhandled set power state command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + } + + if (!pu32Reg) + return VINF_SUCCESS; + + uint8_t uPwrCmd = CODEC_F05_SET (uCmd); + bool fReset = CODEC_F05_IS_RESET (*pu32Reg); + bool fStopOk = CODEC_F05_IS_STOPOK(*pu32Reg); +#ifdef LOG_ENABLED + bool fError = CODEC_F05_IS_ERROR (*pu32Reg); + uint8_t uPwrAct = CODEC_F05_ACT (*pu32Reg); + uint8_t uPwrSet = CODEC_F05_SET (*pu32Reg); + LogFunc(("[NID0x%02x] Cmd=D%RU8, fReset=%RTbool, fStopOk=%RTbool, fError=%RTbool, uPwrAct=D%RU8, uPwrSet=D%RU8\n", + CODEC_NID(uCmd), uPwrCmd, fReset, fStopOk, fError, uPwrAct, uPwrSet)); + LogFunc(("AFG: Act=D%RU8, Set=D%RU8\n", + CODEC_F05_ACT(pThis->aNodes[STAC9220_NID_AFG].afg.u32F05_param), + CODEC_F05_SET(pThis->aNodes[STAC9220_NID_AFG].afg.u32F05_param))); +#endif + + if (CODEC_NID(uCmd) == STAC9220_NID_AFG) + *pu32Reg = CODEC_MAKE_F05(fReset, fStopOk, 0, uPwrCmd /* PS-Act */, uPwrCmd /* PS-Set */); + + const uint8_t uAFGPwrAct = CODEC_F05_ACT(pThis->aNodes[STAC9220_NID_AFG].afg.u32F05_param); + if (uAFGPwrAct == CODEC_F05_D0) /* Only propagate power state if AFG is on (D0). */ + { + /* Propagate to all other nodes under this AFG. */ + LogFunc(("Propagating Act=D%RU8 (AFG), Set=D%RU8 to all AFG child nodes ...\n", uAFGPwrAct, uPwrCmd)); + +#define PROPAGATE_PWR_STATE(a_abList, a_Member) \ + do { \ + for (uintptr_t idxList = 0; idxList < RT_ELEMENTS(a_abList); idxList++) \ + { \ + uint8_t const idxNode = a_abList[idxList]; \ + if (idxNode) \ + { \ + pThis->aNodes[idxNode].a_Member.u32F05_param = CODEC_MAKE_F05(fReset, fStopOk, 0, uAFGPwrAct, uPwrCmd); \ + LogFunc(("\t[NID0x%02x]: Act=D%RU8, Set=D%RU8\n", idxNode, \ + CODEC_F05_ACT(pThis->aNodes[idxNode].a_Member.u32F05_param), \ + CODEC_F05_SET(pThis->aNodes[idxNode].a_Member.u32F05_param))); \ + } \ + else \ + break; \ + } \ + } while (0) + + PROPAGATE_PWR_STATE(pThis->Cfg.abDacs, dac); + PROPAGATE_PWR_STATE(pThis->Cfg.abAdcs, adc); + PROPAGATE_PWR_STATE(pThis->Cfg.abDigInPins, digin); + PROPAGATE_PWR_STATE(pThis->Cfg.abDigOutPins, digout); + PROPAGATE_PWR_STATE(pThis->Cfg.abSpdifIns, spdifin); + PROPAGATE_PWR_STATE(pThis->Cfg.abSpdifOuts, spdifout); + PROPAGATE_PWR_STATE(pThis->Cfg.abReserveds, reserved); + +#undef PROPAGATE_PWR_STATE + } + /* + * If this node is a reqular node (not the AFG one), adopt PS-Set of the AFG node + * as PS-Set of this node. PS-Act always is one level under PS-Set here. + */ + else + { + *pu32Reg = CODEC_MAKE_F05(fReset, fStopOk, 0, uAFGPwrAct, uPwrCmd); + } + + LogFunc(("[NID0x%02x] fReset=%RTbool, fStopOk=%RTbool, Act=D%RU8, Set=D%RU8\n", + CODEC_NID(uCmd), + CODEC_F05_IS_RESET(*pu32Reg), CODEC_F05_IS_STOPOK(*pu32Reg), CODEC_F05_ACT(*pu32Reg), CODEC_F05_SET(*pu32Reg))); + + return VINF_SUCCESS; +} + +#else + +DECLINLINE(void) codecPropogatePowerState(uint32_t *pu32F05_param) +{ + Assert(pu32F05_param); + if (!pu32F05_param) + return; + bool fReset = CODEC_F05_IS_RESET(*pu32F05_param); + bool fStopOk = CODEC_F05_IS_STOPOK(*pu32F05_param); + uint8_t u8SetPowerState = CODEC_F05_SET(*pu32F05_param); + *pu32F05_param = CODEC_MAKE_F05(fReset, fStopOk, 0, u8SetPowerState, u8SetPowerState); +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 705 } + */ +static DECLCALLBACK(int) vrbProcSetPowerState(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + Assert(CODEC_CAD(uCmd) == pThis->Cfg.id); + uint8_t const cTotalNodes = (uint8_t)RT_MIN(pThis->Cfg.cTotalNodes, RT_ELEMENTS(pThis->aNodes)); + Assert(CODEC_NID(uCmd) < cTotalNodes); + if (CODEC_NID(uCmd) >= cTotalNodes) + { + *puResp = 0; + LogFlowFunc(("invalid node address %d\n", CODEC_NID(uCmd))); + return VINF_SUCCESS; + } + *puResp = 0; + uint32_t *pu32Reg; + if (CODEC_NID(uCmd) == 1 /* AFG */) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].afg.u32F05_param; + else if (hdaCodecIsDacNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].dac.u32F05_param; + else if (hdaCodecIsDigInPinNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].digin.u32F05_param; + else if (hdaCodecIsAdcNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].adc.u32F05_param; + else if (hdaCodecIsSpdifOutNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].spdifout.u32F05_param; + else if (hdaCodecIsSpdifInNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].spdifin.u32F05_param; + else if (hdaCodecIsReservedNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].reserved.u32F05_param; + else + AssertFailedReturn(VINF_SUCCESS); + + bool fReset = CODEC_F05_IS_RESET(*pu32Reg); + bool fStopOk = CODEC_F05_IS_STOPOK(*pu32Reg); + + if (CODEC_NID(uCmd) != 1 /* AFG */) + { + /* + * We shouldn't propogate actual power state, which actual for AFG + */ + *pu32Reg = CODEC_MAKE_F05(fReset, fStopOk, 0, + CODEC_F05_ACT(pThis->aNodes[1].afg.u32F05_param), + CODEC_F05_SET(uCmd)); + } + + /* Propagate next power state only if AFG is on or verb modifies AFG power state */ + if ( CODEC_NID(uCmd) == 1 /* AFG */ + || !CODEC_F05_ACT(pThis->aNodes[1].afg.u32F05_param)) + { + *pu32Reg = CODEC_MAKE_F05(fReset, fStopOk, 0, CODEC_F05_SET(uCmd), CODEC_F05_SET(uCmd)); + if ( CODEC_NID(uCmd) == 1 /* AFG */ + && (CODEC_F05_SET(uCmd)) == CODEC_F05_D0) + { + /* now we're powered on AFG and may propogate power states on nodes */ + const uint8_t *pu8NodeIndex = &pThis->abDacs[0]; + while (*(++pu8NodeIndex)) + codecPropogatePowerState(&pThis->aNodes[*pu8NodeIndex].dac.u32F05_param); + + pu8NodeIndex = &pThis->abAdcs[0]; + while (*(++pu8NodeIndex)) + codecPropogatePowerState(&pThis->aNodes[*pu8NodeIndex].adc.u32F05_param); + + pu8NodeIndex = &pThis->abDigInPins[0]; + while (*(++pu8NodeIndex)) + codecPropogatePowerState(&pThis->aNodes[*pu8NodeIndex].digin.u32F05_param); + } + } + return VINF_SUCCESS; +} + +#endif + +/** + * @interface_method_impl{CODECVERB,pfn, f06 } + */ +static DECLCALLBACK(int) vrbProcGetStreamId(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + if (hdaCodecIsDacNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].dac.u32F06_param; + else if (hdaCodecIsAdcNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].adc.u32F06_param; + else if (hdaCodecIsSpdifInNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].spdifin.u32F06_param; + else if (hdaCodecIsSpdifOutNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].spdifout.u32F06_param; + else if (CODEC_NID(uCmd) == STAC9221_NID_I2S_OUT) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].reserved.u32F06_param; + else + LogRel2(("HDA: Warning: Unhandled get stream ID command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + + LogFlowFunc(("[NID0x%02x] Stream ID=%RU8, channel=%RU8\n", + CODEC_NID(uCmd), CODEC_F00_06_GET_STREAM_ID(uCmd), CODEC_F00_06_GET_CHANNEL_ID(uCmd))); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, a0 } + */ +static DECLCALLBACK(int) vrbProcGetConverterFormat(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + if (hdaCodecIsDacNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].dac.u32A_param; + else if (hdaCodecIsAdcNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].adc.u32A_param; + else if (hdaCodecIsSpdifOutNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].spdifout.u32A_param; + else if (hdaCodecIsSpdifInNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].spdifin.u32A_param; + else if (hdaCodecIsReservedNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].reserved.u32A_param; + else + LogRel2(("HDA: Warning: Unhandled get converter format command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, ??? - Also see section 3.7.1. } + */ +static DECLCALLBACK(int) vrbProcSetConverterFormat(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + if (hdaCodecIsDacNode(pThis, CODEC_NID(uCmd))) + hdaCodecSetRegisterU16(&pThis->aNodes[CODEC_NID(uCmd)].dac.u32A_param, uCmd, 0); + else if (hdaCodecIsAdcNode(pThis, CODEC_NID(uCmd))) + hdaCodecSetRegisterU16(&pThis->aNodes[CODEC_NID(uCmd)].adc.u32A_param, uCmd, 0); + else if (hdaCodecIsSpdifOutNode(pThis, CODEC_NID(uCmd))) + hdaCodecSetRegisterU16(&pThis->aNodes[CODEC_NID(uCmd)].spdifout.u32A_param, uCmd, 0); + else if (hdaCodecIsSpdifInNode(pThis, CODEC_NID(uCmd))) + hdaCodecSetRegisterU16(&pThis->aNodes[CODEC_NID(uCmd)].spdifin.u32A_param, uCmd, 0); + else + LogRel2(("HDA: Warning: Unhandled set converter format command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, f0c } + */ +static DECLCALLBACK(int) vrbProcGetEAPD_BTLEnabled(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + if (hdaCodecIsAdcVolNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].adcvol.u32F0c_param; + else if (hdaCodecIsDacNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].dac.u32F0c_param; + else if (hdaCodecIsDigInPinNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].digin.u32F0c_param; + else + LogRel2(("HDA: Warning: Unhandled get EAPD/BTL enabled command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 70c } + */ +static DECLCALLBACK(int) vrbProcSetEAPD_BTLEnabled(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + uint32_t *pu32Reg = NULL; + if (hdaCodecIsAdcVolNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].adcvol.u32F0c_param; + else if (hdaCodecIsDacNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].dac.u32F0c_param; + else if (hdaCodecIsDigInPinNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].digin.u32F0c_param; + else + LogRel2(("HDA: Warning: Unhandled set EAPD/BTL enabled command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + + if (pu32Reg) + hdaCodecSetRegisterU8(pu32Reg, uCmd, 0); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, f0f } + */ +static DECLCALLBACK(int) vrbProcGetVolumeKnobCtrl(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + if (hdaCodecIsVolKnobNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].volumeKnob.u32F0f_param; + else + LogRel2(("HDA: Warning: Unhandled get volume knob control command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 70f } + */ +static DECLCALLBACK(int) vrbProcSetVolumeKnobCtrl(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + uint32_t *pu32Reg = NULL; + if (hdaCodecIsVolKnobNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].volumeKnob.u32F0f_param; + else + LogRel2(("HDA: Warning: Unhandled set volume knob control command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + + if (pu32Reg) + hdaCodecSetRegisterU8(pu32Reg, uCmd, 0); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, f15 } + */ +static DECLCALLBACK(int) vrbProcGetGPIOData(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + RT_NOREF(pThis, uCmd); + *puResp = 0; + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 715 } + */ +static DECLCALLBACK(int) vrbProcSetGPIOData(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + RT_NOREF(pThis, uCmd); + *puResp = 0; + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, f16 } + */ +static DECLCALLBACK(int) vrbProcGetGPIOEnableMask(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + RT_NOREF(pThis, uCmd); + *puResp = 0; + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 716 } + */ +static DECLCALLBACK(int) vrbProcSetGPIOEnableMask(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + RT_NOREF(pThis, uCmd); + *puResp = 0; + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, f17 } + */ +static DECLCALLBACK(int) vrbProcGetGPIODirection(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + /* Note: this is true for ALC885. */ + if (CODEC_NID(uCmd) == STAC9220_NID_AFG) + *puResp = pThis->aNodes[1].afg.u32F17_param; + else + LogRel2(("HDA: Warning: Unhandled get GPIO direction command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 717 } + */ +static DECLCALLBACK(int) vrbProcSetGPIODirection(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + uint32_t *pu32Reg = NULL; + if (CODEC_NID(uCmd) == STAC9220_NID_AFG) + pu32Reg = &pThis->aNodes[1].afg.u32F17_param; + else + LogRel2(("HDA: Warning: Unhandled set GPIO direction command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + + if (pu32Reg) + hdaCodecSetRegisterU8(pu32Reg, uCmd, 0); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, f1c } + */ +static DECLCALLBACK(int) vrbProcGetConfig(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + if (hdaCodecIsPortNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].port.u32F1c_param; + else if (hdaCodecIsDigOutPinNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].digout.u32F1c_param; + else if (hdaCodecIsDigInPinNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].digin.u32F1c_param; + else if (hdaCodecIsPcbeepNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].pcbeep.u32F1c_param; + else if (hdaCodecIsCdNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].cdnode.u32F1c_param; + else if (hdaCodecIsReservedNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].reserved.u32F1c_param; + else + LogRel2(("HDA: Warning: Unhandled get config command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + + return VINF_SUCCESS; +} + +static int codecSetConfigX(PHDACODECR3 pThis, uint32_t uCmd, uint8_t u8Offset) +{ + uint32_t *pu32Reg = NULL; + if (hdaCodecIsPortNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].port.u32F1c_param; + else if (hdaCodecIsDigInPinNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].digin.u32F1c_param; + else if (hdaCodecIsDigOutPinNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].digout.u32F1c_param; + else if (hdaCodecIsCdNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].cdnode.u32F1c_param; + else if (hdaCodecIsPcbeepNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].pcbeep.u32F1c_param; + else if (hdaCodecIsReservedNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].reserved.u32F1c_param; + else + LogRel2(("HDA: Warning: Unhandled set config command (%RU8) for NID0x%02x: 0x%x\n", u8Offset, CODEC_NID(uCmd), uCmd)); + + if (pu32Reg) + hdaCodecSetRegisterU8(pu32Reg, uCmd, u8Offset); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 71c } + */ +static DECLCALLBACK(int) vrbProcSetConfig0(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + return codecSetConfigX(pThis, uCmd, 0); +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 71d } + */ +static DECLCALLBACK(int) vrbProcSetConfig1(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + return codecSetConfigX(pThis, uCmd, 8); +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 71e } + */ +static DECLCALLBACK(int) vrbProcSetConfig2(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + return codecSetConfigX(pThis, uCmd, 16); +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 71e } + */ +static DECLCALLBACK(int) vrbProcSetConfig3(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + return codecSetConfigX(pThis, uCmd, 24); +} + + +/** + * @interface_method_impl{CODECVERB,pfn, f04 } + */ +static DECLCALLBACK(int) vrbProcGetSDISelect(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + if (hdaCodecIsDacNode(pThis, CODEC_NID(uCmd))) + *puResp = pThis->aNodes[CODEC_NID(uCmd)].dac.u32F04_param; + else + LogRel2(("HDA: Warning: Unhandled get SDI select command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 704 } + */ +static DECLCALLBACK(int) vrbProcSetSDISelect(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + uint32_t *pu32Reg = NULL; + if (hdaCodecIsDacNode(pThis, CODEC_NID(uCmd))) + pu32Reg = &pThis->aNodes[CODEC_NID(uCmd)].dac.u32F04_param; + else + LogRel2(("HDA: Warning: Unhandled set SDI select command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + + if (pu32Reg) + hdaCodecSetRegisterU8(pu32Reg, uCmd, 0); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 3-- } + */ +static DECLCALLBACK(int) vrbProcR3SetAmplifier(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + PCODECNODE pNode = &pThis->aNodes[CODEC_NID(uCmd)]; + AMPLIFIER *pAmplifier = NULL; + if (hdaCodecIsDacNode(pThis, CODEC_NID(uCmd))) + pAmplifier = &pNode->dac.B_params; + else if (hdaCodecIsAdcVolNode(pThis, CODEC_NID(uCmd))) + pAmplifier = &pNode->adcvol.B_params; + else if (hdaCodecIsAdcMuxNode(pThis, CODEC_NID(uCmd))) + pAmplifier = &pNode->adcmux.B_params; + else if (hdaCodecIsPcbeepNode(pThis, CODEC_NID(uCmd))) + pAmplifier = &pNode->pcbeep.B_params; + else if (hdaCodecIsPortNode(pThis, CODEC_NID(uCmd))) + pAmplifier = &pNode->port.B_params; + else if (hdaCodecIsAdcNode(pThis, CODEC_NID(uCmd))) + pAmplifier = &pNode->adc.B_params; + else + LogRel2(("HDA: Warning: Unhandled set amplifier command: 0x%x (Payload=%RU16, NID=0x%x [%RU8])\n", + uCmd, CODEC_VERB_PAYLOAD16(uCmd), CODEC_NID(uCmd), CODEC_NID(uCmd))); + + if (!pAmplifier) + return VINF_SUCCESS; + + bool fIsOut = CODEC_SET_AMP_IS_OUT_DIRECTION(uCmd); + bool fIsIn = CODEC_SET_AMP_IS_IN_DIRECTION(uCmd); + bool fIsLeft = CODEC_SET_AMP_IS_LEFT_SIDE(uCmd); + bool fIsRight = CODEC_SET_AMP_IS_RIGHT_SIDE(uCmd); + uint8_t u8Index = CODEC_SET_AMP_INDEX(uCmd); + + if ( (!fIsLeft && !fIsRight) + || (!fIsOut && !fIsIn)) + return VINF_SUCCESS; + + LogFunc(("[NID0x%02x] fIsOut=%RTbool, fIsIn=%RTbool, fIsLeft=%RTbool, fIsRight=%RTbool, Idx=%RU8\n", + CODEC_NID(uCmd), fIsOut, fIsIn, fIsLeft, fIsRight, u8Index)); + + if (fIsIn) + { + if (fIsLeft) + hdaCodecSetRegisterU8(&LIFIER_REGISTER(*pAmplifier, AMPLIFIER_IN, AMPLIFIER_LEFT, u8Index), uCmd, 0); + if (fIsRight) + hdaCodecSetRegisterU8(&LIFIER_REGISTER(*pAmplifier, AMPLIFIER_IN, AMPLIFIER_RIGHT, u8Index), uCmd, 0); + + /* + * Check if the node ID is the one we use for controlling the line-in volume; + * with STAC9220 this is connected to STAC9220_NID_AMP_ADC0 (ID 0x17). + * + * If we don't do this check here, some guests like newer Ubuntus mute mic-in + * afterwards (connected to STAC9220_NID_AMP_ADC1 (ID 0x18)). This then would + * also mute line-in, which breaks audio recording. + * + * See STAC9220 V1.0 01/08, p. 30 + oem2ticketref:53. + */ + if (CODEC_NID(uCmd) == pThis->Cfg.idxAdcVolsLineIn) + hdaR3CodecToAudVolume(pThis, pNode, pAmplifier, PDMAUDIOMIXERCTL_LINE_IN); + +#ifdef VBOX_WITH_AUDIO_HDA_MIC_IN +# error "Implement mic-in volume / mute setting." + else if (CODEC_NID(uCmd) == pThis->Cfg.idxAdcVolsMicIn) + hdaR3CodecToAudVolume(pThis, pNode, pAmplifier, PDMAUDIOMIXERCTL_MIC_IN); +#endif + + } + if (fIsOut) + { + if (fIsLeft) + hdaCodecSetRegisterU8(&LIFIER_REGISTER(*pAmplifier, AMPLIFIER_OUT, AMPLIFIER_LEFT, u8Index), uCmd, 0); + if (fIsRight) + hdaCodecSetRegisterU8(&LIFIER_REGISTER(*pAmplifier, AMPLIFIER_OUT, AMPLIFIER_RIGHT, u8Index), uCmd, 0); + + if (CODEC_NID(uCmd) == pThis->Cfg.idxDacLineOut) + hdaR3CodecToAudVolume(pThis, pNode, pAmplifier, PDMAUDIOMIXERCTL_FRONT); + } + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{CODECVERB,pfn, 706 } + */ +static DECLCALLBACK(int) vrbProcR3SetStreamId(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + *puResp = 0; + + uint8_t uSD = CODEC_F00_06_GET_STREAM_ID(uCmd); + uint8_t uChannel = CODEC_F00_06_GET_CHANNEL_ID(uCmd); + + LogFlowFunc(("[NID0x%02x] Setting to stream ID=%RU8, channel=%RU8\n", + CODEC_NID(uCmd), uSD, uChannel)); + + ASSERT_GUEST_LOGREL_MSG_RETURN(uSD < HDA_MAX_STREAMS, + ("Setting stream ID #%RU8 is invalid\n", uSD), VERR_INVALID_PARAMETER); + + PDMAUDIODIR enmDir; + uint32_t *pu32Addr; + if (hdaCodecIsDacNode(pThis, CODEC_NID(uCmd))) + { + pu32Addr = &pThis->aNodes[CODEC_NID(uCmd)].dac.u32F06_param; + enmDir = PDMAUDIODIR_OUT; + } + else if (hdaCodecIsAdcNode(pThis, CODEC_NID(uCmd))) + { + pu32Addr = &pThis->aNodes[CODEC_NID(uCmd)].adc.u32F06_param; + enmDir = PDMAUDIODIR_IN; + } + else if (hdaCodecIsSpdifOutNode(pThis, CODEC_NID(uCmd))) + { + pu32Addr = &pThis->aNodes[CODEC_NID(uCmd)].spdifout.u32F06_param; + enmDir = PDMAUDIODIR_OUT; + } + else if (hdaCodecIsSpdifInNode(pThis, CODEC_NID(uCmd))) + { + pu32Addr = &pThis->aNodes[CODEC_NID(uCmd)].spdifin.u32F06_param; + enmDir = PDMAUDIODIR_IN; + } + else + { + enmDir = PDMAUDIODIR_UNKNOWN; + LogRel2(("HDA: Warning: Unhandled set stream ID command for NID0x%02x: 0x%x\n", CODEC_NID(uCmd), uCmd)); + return VINF_SUCCESS; + } + + /* Do we (re-)assign our input/output SDn (SDI/SDO) IDs? */ + pThis->aNodes[CODEC_NID(uCmd)].node.uSD = uSD; + pThis->aNodes[CODEC_NID(uCmd)].node.uChannel = uChannel; + + if (enmDir == PDMAUDIODIR_OUT) + { + /** @todo Check if non-interleaved streams need a different channel / SDn? */ + + /* Propagate to the controller. */ + hdaR3MixerControl(pThis, PDMAUDIOMIXERCTL_FRONT, uSD, uChannel); +# ifdef VBOX_WITH_AUDIO_HDA_51_SURROUND + hdaR3MixerControl(pThis, PDMAUDIOMIXERCTL_CENTER_LFE, uSD, uChannel); + hdaR3MixerControl(pThis, PDMAUDIOMIXERCTL_REAR, uSD, uChannel); +# endif + } + else if (enmDir == PDMAUDIODIR_IN) + { + hdaR3MixerControl(pThis, PDMAUDIOMIXERCTL_LINE_IN, uSD, uChannel); +# ifdef VBOX_WITH_AUDIO_HDA_MIC_IN + hdaR3MixerControl(pThis, PDMAUDIOMIXERCTL_MIC_IN, uSD, uChannel); +# endif + } + + hdaCodecSetRegisterU8(pu32Addr, uCmd, 0); + + return VINF_SUCCESS; +} + + + +/** + * HDA codec verb descriptors. + * + * @note This must be ordered by uVerb so we can do a binary lookup. + */ +static const CODECVERB g_aCodecVerbs[] = +{ + /* Verb Verb mask Callback Name + ---------- --------------------- ------------------------------------------------------------------- */ + { 0x00020000, CODEC_VERB_16BIT_CMD, vrbProcSetConverterFormat , "SetConverterFormat " }, + { 0x00030000, CODEC_VERB_16BIT_CMD, vrbProcR3SetAmplifier , "SetAmplifier " }, + { 0x00070100, CODEC_VERB_8BIT_CMD , vrbProcSetConSelectCtrl , "SetConSelectCtrl " }, + { 0x00070300, CODEC_VERB_8BIT_CMD , vrbProcSetProcessingState , "SetProcessingState " }, + { 0x00070400, CODEC_VERB_8BIT_CMD , vrbProcSetSDISelect , "SetSDISelect " }, + { 0x00070500, CODEC_VERB_8BIT_CMD , vrbProcSetPowerState , "SetPowerState " }, + { 0x00070600, CODEC_VERB_8BIT_CMD , vrbProcR3SetStreamId , "SetStreamId " }, + { 0x00070700, CODEC_VERB_8BIT_CMD , vrbProcSetPinCtrl , "SetPinCtrl " }, + { 0x00070800, CODEC_VERB_8BIT_CMD , vrbProcSetUnsolicitedEnabled , "SetUnsolicitedEnabled " }, + { 0x00070900, CODEC_VERB_8BIT_CMD , vrbProcSetPinSense , "SetPinSense " }, + { 0x00070C00, CODEC_VERB_8BIT_CMD , vrbProcSetEAPD_BTLEnabled , "SetEAPD_BTLEnabled " }, + { 0x00070D00, CODEC_VERB_8BIT_CMD , vrbProcSetDigitalConverter1 , "SetDigitalConverter1 " }, + { 0x00070E00, CODEC_VERB_8BIT_CMD , vrbProcSetDigitalConverter2 , "SetDigitalConverter2 " }, + { 0x00070F00, CODEC_VERB_8BIT_CMD , vrbProcSetVolumeKnobCtrl , "SetVolumeKnobCtrl " }, + { 0x00071500, CODEC_VERB_8BIT_CMD , vrbProcSetGPIOData , "SetGPIOData " }, + { 0x00071600, CODEC_VERB_8BIT_CMD , vrbProcSetGPIOEnableMask , "SetGPIOEnableMask " }, + { 0x00071700, CODEC_VERB_8BIT_CMD , vrbProcSetGPIODirection , "SetGPIODirection " }, + { 0x00071C00, CODEC_VERB_8BIT_CMD , vrbProcSetConfig0 , "SetConfig0 " }, + { 0x00071D00, CODEC_VERB_8BIT_CMD , vrbProcSetConfig1 , "SetConfig1 " }, + { 0x00071E00, CODEC_VERB_8BIT_CMD , vrbProcSetConfig2 , "SetConfig2 " }, + { 0x00071F00, CODEC_VERB_8BIT_CMD , vrbProcSetConfig3 , "SetConfig3 " }, + { 0x00072000, CODEC_VERB_8BIT_CMD , vrbProcSetSubId0 , "SetSubId0 " }, + { 0x00072100, CODEC_VERB_8BIT_CMD , vrbProcSetSubId1 , "SetSubId1 " }, + { 0x00072200, CODEC_VERB_8BIT_CMD , vrbProcSetSubId2 , "SetSubId2 " }, + { 0x00072300, CODEC_VERB_8BIT_CMD , vrbProcSetSubId3 , "SetSubId3 " }, + { 0x0007FF00, CODEC_VERB_8BIT_CMD , vrbProcReset , "Reset " }, + { 0x000A0000, CODEC_VERB_16BIT_CMD, vrbProcGetConverterFormat , "GetConverterFormat " }, + { 0x000B0000, CODEC_VERB_16BIT_CMD, vrbProcGetAmplifier , "GetAmplifier " }, + { 0x000F0000, CODEC_VERB_8BIT_CMD , vrbProcGetParameter , "GetParameter " }, + { 0x000F0100, CODEC_VERB_8BIT_CMD , vrbProcGetConSelectCtrl , "GetConSelectCtrl " }, + { 0x000F0200, CODEC_VERB_8BIT_CMD , vrbProcGetConnectionListEntry , "GetConnectionListEntry" }, + { 0x000F0300, CODEC_VERB_8BIT_CMD , vrbProcGetProcessingState , "GetProcessingState " }, + { 0x000F0400, CODEC_VERB_8BIT_CMD , vrbProcGetSDISelect , "GetSDISelect " }, + { 0x000F0500, CODEC_VERB_8BIT_CMD , vrbProcGetPowerState , "GetPowerState " }, + { 0x000F0600, CODEC_VERB_8BIT_CMD , vrbProcGetStreamId , "GetStreamId " }, + { 0x000F0700, CODEC_VERB_8BIT_CMD , vrbProcGetPinCtrl , "GetPinCtrl " }, + { 0x000F0800, CODEC_VERB_8BIT_CMD , vrbProcGetUnsolicitedEnabled , "GetUnsolicitedEnabled " }, + { 0x000F0900, CODEC_VERB_8BIT_CMD , vrbProcGetPinSense , "GetPinSense " }, + { 0x000F0C00, CODEC_VERB_8BIT_CMD , vrbProcGetEAPD_BTLEnabled , "GetEAPD_BTLEnabled " }, + { 0x000F0D00, CODEC_VERB_8BIT_CMD , vrbProcGetDigitalConverter , "GetDigitalConverter " }, + { 0x000F0F00, CODEC_VERB_8BIT_CMD , vrbProcGetVolumeKnobCtrl , "GetVolumeKnobCtrl " }, + { 0x000F1500, CODEC_VERB_8BIT_CMD , vrbProcGetGPIOData , "GetGPIOData " }, + { 0x000F1600, CODEC_VERB_8BIT_CMD , vrbProcGetGPIOEnableMask , "GetGPIOEnableMask " }, + { 0x000F1700, CODEC_VERB_8BIT_CMD , vrbProcGetGPIODirection , "GetGPIODirection " }, + { 0x000F1C00, CODEC_VERB_8BIT_CMD , vrbProcGetConfig , "GetConfig " }, + { 0x000F2000, CODEC_VERB_8BIT_CMD , vrbProcGetSubId , "GetSubId " }, + /** @todo Implement 0x7e7: IDT Set GPIO (STAC922x only). */ +}; + + +/** + * Implements codec lookup and will call the handler on the verb it finds, + * returning the handler response. + * + * @returns VBox status code (not strict). + */ +DECLHIDDEN(int) hdaR3CodecLookup(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp) +{ + /* + * Clear the return value and assert some sanity. + */ + AssertPtr(puResp); + *puResp = 0; + AssertPtr(pThis); + AssertMsgReturn(CODEC_CAD(uCmd) == pThis->Cfg.id, + ("Unknown codec address 0x%x\n", CODEC_CAD(uCmd)), + VERR_INVALID_PARAMETER); + uint32_t const uCmdData = CODEC_VERBDATA(uCmd); + AssertMsgReturn( uCmdData != 0 + && CODEC_NID(uCmd) < RT_MIN(pThis->Cfg.cTotalNodes, RT_ELEMENTS(pThis->aNodes)), + ("[NID0x%02x] Unknown / invalid node or data (0x%x)\n", CODEC_NID(uCmd), uCmdData), + VERR_INVALID_PARAMETER); + STAM_COUNTER_INC(&pThis->CTX_SUFF(StatLookups)); + + /* + * Do a binary lookup of the verb. + * Note! if we want other verb tables, add a table selector before the loop. + */ + size_t iFirst = 0; + size_t iEnd = RT_ELEMENTS(g_aCodecVerbs); + for (;;) + { + size_t const iCur = iFirst + (iEnd - iFirst) / 2; + uint32_t const uVerb = g_aCodecVerbs[iCur].uVerb; + if (uCmdData < uVerb) + { + if (iCur > iFirst) + iEnd = iCur; + else + break; + } + else if ((uCmdData & g_aCodecVerbs[iCur].fMask) != uVerb) + { + if (iCur + 1 < iEnd) + iFirst = iCur + 1; + else + break; + } + else + { + /* + * Found it! Run the callback and return. + */ + AssertPtrReturn(g_aCodecVerbs[iCur].pfn, VERR_INTERNAL_ERROR_5); /* Paranoia^2. */ + + int rc = g_aCodecVerbs[iCur].pfn(pThis, uCmd, puResp); + AssertRC(rc); + Log3Func(("[NID0x%02x] (0x%x) %s: 0x%x -> 0x%x\n", + CODEC_NID(uCmd), g_aCodecVerbs[iCur].uVerb, g_aCodecVerbs[iCur].pszName, CODEC_VERB_PAYLOAD8(uCmd), *puResp)); + return rc; + } + } + +#ifdef VBOX_STRICT + for (size_t i = 0; i < RT_ELEMENTS(g_aCodecVerbs); i++) + { + AssertMsg(i == 0 || g_aCodecVerbs[i - 1].uVerb < g_aCodecVerbs[i].uVerb, + ("i=%#x uVerb[-1]=%#x uVerb=%#x - buggy table!\n", i, g_aCodecVerbs[i - 1].uVerb, g_aCodecVerbs[i].uVerb)); + AssertMsg((uCmdData & g_aCodecVerbs[i].fMask) != g_aCodecVerbs[i].uVerb, + ("i=%#x uVerb=%#x uCmd=%#x - buggy binary search or table!\n", i, g_aCodecVerbs[i].uVerb, uCmd)); + } +#endif + LogFunc(("[NID0x%02x] Callback for %x not found\n", CODEC_NID(uCmd), CODEC_VERBDATA(uCmd))); + return VERR_NOT_FOUND; +} + + +/********************************************************************************************************************************* +* Debug * +*********************************************************************************************************************************/ +/** + * CODEC debug info item printing state. + */ +typedef struct CODECDEBUG +{ + /** DBGF info helpers. */ + PCDBGFINFOHLP pHlp; + /** Current recursion level. */ + uint8_t uLevel; + /** Pointer to codec state. */ + PHDACODECR3 pThis; +} CODECDEBUG; +/** Pointer to the debug info item printing state for the codec. */ +typedef CODECDEBUG *PCODECDEBUG; + +#define CODECDBG_INDENT pInfo->uLevel++; +#define CODECDBG_UNINDENT if (pInfo->uLevel) pInfo->uLevel--; + +#define CODECDBG_PRINT(...) pInfo->pHlp->pfnPrintf(pInfo->pHlp, __VA_ARGS__) +#define CODECDBG_PRINTI(...) codecDbgPrintf(pInfo, __VA_ARGS__) + + +/** Wrapper around DBGFINFOHLP::pfnPrintf that adds identation. */ +static void codecDbgPrintf(PCODECDEBUG pInfo, const char *pszFormat, ...) +{ + va_list va; + va_start(va, pszFormat); + pInfo->pHlp->pfnPrintf(pInfo->pHlp, "%*s%N", pInfo->uLevel * 4, "", pszFormat, &va); + va_end(va); +} + + +/** Power state */ +static void codecDbgPrintNodeRegF05(PCODECDEBUG pInfo, uint32_t u32Reg) +{ + codecDbgPrintf(pInfo, "Power (F05): fReset=%RTbool, fStopOk=%RTbool, Set=%RU8, Act=%RU8\n", + CODEC_F05_IS_RESET(u32Reg), CODEC_F05_IS_STOPOK(u32Reg), CODEC_F05_SET(u32Reg), CODEC_F05_ACT(u32Reg)); +} + + +static void codecDbgPrintNodeRegA(PCODECDEBUG pInfo, uint32_t u32Reg) +{ + codecDbgPrintf(pInfo, "RegA: %x\n", u32Reg); +} + + +static void codecDbgPrintNodeRegF00(PCODECDEBUG pInfo, uint32_t *paReg00) +{ + codecDbgPrintf(pInfo, "Parameters (F00):\n"); + + CODECDBG_INDENT + codecDbgPrintf(pInfo, "Connections: %RU8\n", CODEC_F00_0E_COUNT(paReg00[0xE])); + codecDbgPrintf(pInfo, "Amplifier Caps:\n"); + uint32_t uReg = paReg00[0xD]; + CODECDBG_INDENT + codecDbgPrintf(pInfo, "Input Steps=%02RU8, StepSize=%02RU8, StepOff=%02RU8, fCanMute=%RTbool\n", + CODEC_F00_0D_NUM_STEPS(uReg), + CODEC_F00_0D_STEP_SIZE(uReg), + CODEC_F00_0D_OFFSET(uReg), + RT_BOOL(CODEC_F00_0D_IS_CAP_MUTE(uReg))); + + uReg = paReg00[0x12]; + codecDbgPrintf(pInfo, "Output Steps=%02RU8, StepSize=%02RU8, StepOff=%02RU8, fCanMute=%RTbool\n", + CODEC_F00_12_NUM_STEPS(uReg), + CODEC_F00_12_STEP_SIZE(uReg), + CODEC_F00_12_OFFSET(uReg), + RT_BOOL(CODEC_F00_12_IS_CAP_MUTE(uReg))); + CODECDBG_UNINDENT + CODECDBG_UNINDENT +} + + +static void codecDbgPrintNodeAmp(PCODECDEBUG pInfo, uint32_t *paReg, uint8_t uIdx, uint8_t uDir) +{ +#define CODECDBG_AMP(reg, chan) \ + codecDbgPrintf(pInfo, "Amp %RU8 %s %s: In=%RTbool, Out=%RTbool, Left=%RTbool, Right=%RTbool, Idx=%RU8, fMute=%RTbool, uGain=%RU8\n", \ + uIdx, chan, uDir == AMPLIFIER_IN ? "In" : "Out", \ + RT_BOOL(CODEC_SET_AMP_IS_IN_DIRECTION(reg)), RT_BOOL(CODEC_SET_AMP_IS_OUT_DIRECTION(reg)), \ + RT_BOOL(CODEC_SET_AMP_IS_LEFT_SIDE(reg)), RT_BOOL(CODEC_SET_AMP_IS_RIGHT_SIDE(reg)), \ + CODEC_SET_AMP_INDEX(reg), RT_BOOL(CODEC_SET_AMP_MUTE(reg)), CODEC_SET_AMP_GAIN(reg)) + + uint32_t regAmp = AMPLIFIER_REGISTER(paReg, uDir, AMPLIFIER_LEFT, uIdx); + CODECDBG_AMP(regAmp, "Left"); + regAmp = AMPLIFIER_REGISTER(paReg, uDir, AMPLIFIER_RIGHT, uIdx); + CODECDBG_AMP(regAmp, "Right"); + +#undef CODECDBG_AMP +} + + +#if 0 /* unused */ +static void codecDbgPrintNodeConnections(PCODECDEBUG pInfo, PCODECNODE pNode) +{ + if (pNode->node.au32F00_param[0xE] == 0) /* Directly connected to HDA link. */ + { + codecDbgPrintf(pInfo, "[HDA LINK]\n"); + return; + } +} +#endif + + +static void codecDbgPrintNode(PCODECDEBUG pInfo, PCODECNODE pNode, bool fRecursive) +{ + codecDbgPrintf(pInfo, "Node 0x%02x (%02RU8): ", pNode->node.uID, pNode->node.uID); + + if (pNode->node.uID == STAC9220_NID_ROOT) + CODECDBG_PRINT("ROOT\n"); + else if (pNode->node.uID == STAC9220_NID_AFG) + { + CODECDBG_PRINT("AFG\n"); + CODECDBG_INDENT + codecDbgPrintNodeRegF00(pInfo, pNode->node.au32F00_param); + codecDbgPrintNodeRegF05(pInfo, pNode->afg.u32F05_param); + CODECDBG_UNINDENT + } + else if (hdaCodecIsPortNode(pInfo->pThis, pNode->node.uID)) + CODECDBG_PRINT("PORT\n"); + else if (hdaCodecIsDacNode(pInfo->pThis, pNode->node.uID)) + { + CODECDBG_PRINT("DAC\n"); + CODECDBG_INDENT + codecDbgPrintNodeRegF00(pInfo, pNode->node.au32F00_param); + codecDbgPrintNodeRegF05(pInfo, pNode->dac.u32F05_param); + codecDbgPrintNodeRegA (pInfo, pNode->dac.u32A_param); + codecDbgPrintNodeAmp (pInfo, pNode->dac.B_params, 0, AMPLIFIER_OUT); + CODECDBG_UNINDENT + } + else if (hdaCodecIsAdcVolNode(pInfo->pThis, pNode->node.uID)) + { + CODECDBG_PRINT("ADC VOLUME\n"); + CODECDBG_INDENT + codecDbgPrintNodeRegF00(pInfo, pNode->node.au32F00_param); + codecDbgPrintNodeRegA (pInfo, pNode->adcvol.u32A_params); + codecDbgPrintNodeAmp (pInfo, pNode->adcvol.B_params, 0, AMPLIFIER_IN); + CODECDBG_UNINDENT + } + else if (hdaCodecIsAdcNode(pInfo->pThis, pNode->node.uID)) + { + CODECDBG_PRINT("ADC\n"); + CODECDBG_INDENT + codecDbgPrintNodeRegF00(pInfo, pNode->node.au32F00_param); + codecDbgPrintNodeRegF05(pInfo, pNode->adc.u32F05_param); + codecDbgPrintNodeRegA (pInfo, pNode->adc.u32A_param); + codecDbgPrintNodeAmp (pInfo, pNode->adc.B_params, 0, AMPLIFIER_IN); + CODECDBG_UNINDENT + } + else if (hdaCodecIsAdcMuxNode(pInfo->pThis, pNode->node.uID)) + { + CODECDBG_PRINT("ADC MUX\n"); + CODECDBG_INDENT + codecDbgPrintNodeRegF00(pInfo, pNode->node.au32F00_param); + codecDbgPrintNodeRegA (pInfo, pNode->adcmux.u32A_param); + codecDbgPrintNodeAmp (pInfo, pNode->adcmux.B_params, 0, AMPLIFIER_IN); + CODECDBG_UNINDENT + } + else if (hdaCodecIsPcbeepNode(pInfo->pThis, pNode->node.uID)) + CODECDBG_PRINT("PC BEEP\n"); + else if (hdaCodecIsSpdifOutNode(pInfo->pThis, pNode->node.uID)) + CODECDBG_PRINT("SPDIF OUT\n"); + else if (hdaCodecIsSpdifInNode(pInfo->pThis, pNode->node.uID)) + CODECDBG_PRINT("SPDIF IN\n"); + else if (hdaCodecIsDigInPinNode(pInfo->pThis, pNode->node.uID)) + CODECDBG_PRINT("DIGITAL IN PIN\n"); + else if (hdaCodecIsDigOutPinNode(pInfo->pThis, pNode->node.uID)) + CODECDBG_PRINT("DIGITAL OUT PIN\n"); + else if (hdaCodecIsCdNode(pInfo->pThis, pNode->node.uID)) + CODECDBG_PRINT("CD\n"); + else if (hdaCodecIsVolKnobNode(pInfo->pThis, pNode->node.uID)) + CODECDBG_PRINT("VOLUME KNOB\n"); + else if (hdaCodecIsReservedNode(pInfo->pThis, pNode->node.uID)) + CODECDBG_PRINT("RESERVED\n"); + else + CODECDBG_PRINT("UNKNOWN TYPE 0x%x\n", pNode->node.uID); + + if (fRecursive) + { +#define CODECDBG_PRINT_CONLIST_ENTRY(_aNode, _aEntry) \ + if (cCnt >= _aEntry) \ + { \ + const uint8_t uID = RT_BYTE##_aEntry(_aNode->node.au32F02_param[0x0]); \ + if (pNode->node.uID == uID) \ + codecDbgPrintNode(pInfo, _aNode, false /* fRecursive */); \ + } + + /* Slow recursion, but this is debug stuff anyway. */ + for (uint8_t i = 0; i < pInfo->pThis->Cfg.cTotalNodes; i++) + { + const PCODECNODE pSubNode = &pInfo->pThis->aNodes[i]; + if (pSubNode->node.uID == pNode->node.uID) + continue; + + const uint8_t cCnt = CODEC_F00_0E_COUNT(pSubNode->node.au32F00_param[0xE]); + if (cCnt == 0) /* No connections present? Skip. */ + continue; + + CODECDBG_INDENT + CODECDBG_PRINT_CONLIST_ENTRY(pSubNode, 1) + CODECDBG_PRINT_CONLIST_ENTRY(pSubNode, 2) + CODECDBG_PRINT_CONLIST_ENTRY(pSubNode, 3) + CODECDBG_PRINT_CONLIST_ENTRY(pSubNode, 4) + CODECDBG_UNINDENT + } + +#undef CODECDBG_PRINT_CONLIST_ENTRY + } +} + + +/** + * Worker for hdaR3DbgInfoCodecNodes implementing the 'hdcnodes' info item. + */ +DECLHIDDEN(void) hdaR3CodecDbgListNodes(PHDACODECR3 pThis, PCDBGFINFOHLP pHlp, const char *pszArgs) +{ + RT_NOREF(pszArgs); + + pHlp->pfnPrintf(pHlp, "HDA LINK / INPUTS\n"); + + CODECDEBUG DbgInfo; + DbgInfo.pHlp = pHlp; + DbgInfo.pThis = pThis; + DbgInfo.uLevel = 0; + + PCODECDEBUG pInfo = &DbgInfo; + + CODECDBG_INDENT + for (uint8_t i = 0; i < pThis->Cfg.cTotalNodes; i++) + { + PCODECNODE pNode = &pThis->aNodes[i]; + + /* Start with all nodes which have connection entries set. */ + if (CODEC_F00_0E_COUNT(pNode->node.au32F00_param[0xE])) + codecDbgPrintNode(&DbgInfo, pNode, true /* fRecursive */); + } + CODECDBG_UNINDENT +} + + +/** + * Worker for hdaR3DbgInfoCodecSelector implementing the 'hdcselector' info item. + */ +DECLHIDDEN(void) hdaR3CodecDbgSelector(PHDACODECR3 pThis, PCDBGFINFOHLP pHlp, const char *pszArgs) +{ + RT_NOREF(pThis, pHlp, pszArgs); +} + + +#if 0 /* unused */ +static DECLCALLBACK(void) stac9220DbgNodes(PHDACODECR3 pThis, PCDBGFINFOHLP pHlp, const char *pszArgs) +{ + RT_NOREF(pszArgs); + uint8_t const cTotalNodes = RT_MIN(pThis->Cfg.cTotalNodes, RT_ELEMENTS(pThis->aNodes)); + for (uint8_t i = 1; i < cTotalNodes; i++) + { + PCODECNODE pNode = &pThis->aNodes[i]; + AMPLIFIER *pAmp = &pNode->dac.B_params; + + uint8_t lVol = AMPLIFIER_REGISTER(*pAmp, AMPLIFIER_OUT, AMPLIFIER_LEFT, 0) & 0x7f; + uint8_t rVol = AMPLIFIER_REGISTER(*pAmp, AMPLIFIER_OUT, AMPLIFIER_RIGHT, 0) & 0x7f; + + pHlp->pfnPrintf(pHlp, "0x%x: lVol=%RU8, rVol=%RU8\n", i, lVol, rVol); + } +} +#endif + + +/********************************************************************************************************************************* +* Stream and State Management * +*********************************************************************************************************************************/ + +int hdaR3CodecAddStream(PHDACODECR3 pThis, PDMAUDIOMIXERCTL enmMixerCtl, PPDMAUDIOSTREAMCFG pCfg) +{ + AssertPtrReturn(pThis, VERR_INVALID_POINTER); + AssertPtrReturn(pCfg, VERR_INVALID_POINTER); + + int rc = VINF_SUCCESS; + + switch (enmMixerCtl) + { + case PDMAUDIOMIXERCTL_VOLUME_MASTER: + case PDMAUDIOMIXERCTL_FRONT: +#ifdef VBOX_WITH_AUDIO_HDA_51_SURROUND + case PDMAUDIOMIXERCTL_CENTER_LFE: + case PDMAUDIOMIXERCTL_REAR: +#endif + break; + + case PDMAUDIOMIXERCTL_LINE_IN: +#ifdef VBOX_WITH_AUDIO_HDA_MIC_IN + case PDMAUDIOMIXERCTL_MIC_IN: +#endif + break; + + default: + AssertMsgFailed(("Mixer control %#x not implemented\n", enmMixerCtl)); + rc = VERR_NOT_IMPLEMENTED; + break; + } + + if (RT_SUCCESS(rc)) + rc = hdaR3MixerAddStream(pThis, enmMixerCtl, pCfg); + + LogFlowFuncLeaveRC(rc); + return rc; +} + + +int hdaR3CodecRemoveStream(PHDACODECR3 pThis, PDMAUDIOMIXERCTL enmMixerCtl, bool fImmediate) +{ + AssertPtrReturn(pThis, VERR_INVALID_POINTER); + + int rc = hdaR3MixerRemoveStream(pThis, enmMixerCtl, fImmediate); + + LogFlowFuncLeaveRC(rc); + return rc; +} + + +/** + * Saved the codec state. + * + * @returns VBox status code. + * @param pDevIns The device instance of the HDA device. + * @param pThis The codec instance data. + * @param pSSM The saved state handle. + */ +int hdaCodecSaveState(PPDMDEVINS pDevIns, PHDACODECR3 pThis, PSSMHANDLE pSSM) +{ + PCPDMDEVHLPR3 pHlp = pDevIns->pHlpR3; + AssertLogRelMsgReturn(pThis->Cfg.cTotalNodes == STAC9221_NUM_NODES, ("cTotalNodes=%#x, should be 0x1c", pThis->Cfg.cTotalNodes), + VERR_INTERNAL_ERROR); + pHlp->pfnSSMPutU32(pSSM, pThis->Cfg.cTotalNodes); + for (unsigned idxNode = 0; idxNode < pThis->Cfg.cTotalNodes; ++idxNode) + pHlp->pfnSSMPutStructEx(pSSM, &pThis->aNodes[idxNode].SavedState, sizeof(pThis->aNodes[idxNode].SavedState), + 0 /*fFlags*/, g_aCodecNodeFields, NULL /*pvUser*/); + return VINF_SUCCESS; +} + + +/** + * Loads the codec state. + * + * @returns VBox status code. + * @param pDevIns The device instance of the HDA device. + * @param pThis The codec instance data. + * @param pSSM The saved state handle. + * @param uVersion The state version. + */ +int hdaR3CodecLoadState(PPDMDEVINS pDevIns, PHDACODECR3 pThis, PSSMHANDLE pSSM, uint32_t uVersion) +{ + PCPDMDEVHLPR3 pHlp = pDevIns->pHlpR3; + PCSSMFIELD pFields = NULL; + uint32_t fFlags = 0; + if (uVersion >= HDA_SAVED_STATE_VERSION_4) + { + /* Since version 4 a flexible node count is supported. */ + uint32_t cNodes; + int rc2 = pHlp->pfnSSMGetU32(pSSM, &cNodes); + AssertRCReturn(rc2, rc2); + AssertReturn(cNodes == 0x1c, VERR_SSM_DATA_UNIT_FORMAT_CHANGED); + AssertReturn(pThis->Cfg.cTotalNodes == 0x1c, VERR_INTERNAL_ERROR); + + pFields = g_aCodecNodeFields; + fFlags = 0; + } + else if (uVersion >= HDA_SAVED_STATE_VERSION_2) + { + AssertReturn(pThis->Cfg.cTotalNodes == 0x1c, VERR_INTERNAL_ERROR); + pFields = g_aCodecNodeFields; + fFlags = SSMSTRUCT_FLAGS_MEM_BAND_AID_RELAXED; + } + else if (uVersion >= HDA_SAVED_STATE_VERSION_1) + { + AssertReturn(pThis->Cfg.cTotalNodes == 0x1c, VERR_INTERNAL_ERROR); + pFields = g_aCodecNodeFieldsV1; + fFlags = SSMSTRUCT_FLAGS_MEM_BAND_AID_RELAXED; + } + else + AssertFailedReturn(VERR_SSM_UNSUPPORTED_DATA_UNIT_VERSION); + + for (unsigned idxNode = 0; idxNode < pThis->Cfg.cTotalNodes; ++idxNode) + { + uint8_t idOld = pThis->aNodes[idxNode].SavedState.Core.uID; + int rc = pHlp->pfnSSMGetStructEx(pSSM, &pThis->aNodes[idxNode].SavedState, sizeof(pThis->aNodes[idxNode].SavedState), + fFlags, pFields, NULL); + AssertRCReturn(rc, rc); + AssertLogRelMsgReturn(idOld == pThis->aNodes[idxNode].SavedState.Core.uID, + ("loaded %#x, expected %#x\n", pThis->aNodes[idxNode].SavedState.Core.uID, idOld), + VERR_SSM_DATA_UNIT_FORMAT_CHANGED); + } + + /* + * Update stuff after changing the state. + */ + PCODECNODE pNode; + if (hdaCodecIsDacNode(pThis, pThis->Cfg.idxDacLineOut)) + { + pNode = &pThis->aNodes[pThis->Cfg.idxDacLineOut]; + hdaR3CodecToAudVolume(pThis, pNode, &pNode->dac.B_params, PDMAUDIOMIXERCTL_FRONT); + } + else if (hdaCodecIsSpdifOutNode(pThis, pThis->Cfg.idxDacLineOut)) + { + pNode = &pThis->aNodes[pThis->Cfg.idxDacLineOut]; + hdaR3CodecToAudVolume(pThis, pNode, &pNode->spdifout.B_params, PDMAUDIOMIXERCTL_FRONT); + } + + pNode = &pThis->aNodes[pThis->Cfg.idxAdcVolsLineIn]; + hdaR3CodecToAudVolume(pThis, pNode, &pNode->adcvol.B_params, PDMAUDIOMIXERCTL_LINE_IN); + + LogFlowFuncLeaveRC(VINF_SUCCESS); + return VINF_SUCCESS; +} + + +/** + * Powers off the codec (ring-3). + * + * @param pThis The codec data. + */ +void hdaR3CodecPowerOff(PHDACODECR3 pThis) +{ + LogFlowFuncEnter(); + LogRel2(("HDA: Powering off codec ...\n")); + + int rc2 = hdaR3CodecRemoveStream(pThis, PDMAUDIOMIXERCTL_FRONT, true /*fImmediate*/); + AssertRC(rc2); +#ifdef VBOX_WITH_AUDIO_HDA_51_SURROUND + rc2 = hdaR3CodecRemoveStream(pThis, PDMAUDIOMIXERCTL_CENTER_LFE, true /*fImmediate*/); + AssertRC(rc2); + rc2 = hdaR3CodecRemoveStream(pThis, PDMAUDIOMIXERCTL_REAR, true /*fImmediate*/); + AssertRC(rc2); +#endif + +#ifdef VBOX_WITH_AUDIO_HDA_MIC_IN + rc2 = hdaR3CodecRemoveStream(pThis, PDMAUDIOMIXERCTL_MIC_IN, true /*fImmediate*/); + AssertRC(rc2); +#endif + rc2 = hdaR3CodecRemoveStream(pThis, PDMAUDIOMIXERCTL_LINE_IN, true /*fImmediate*/); + AssertRC(rc2); +} + + +/** + * Constructs a codec (ring-3). + * + * @returns VBox status code. + * @param pDevIns The associated device instance. + * @param pThis The codec data. + * @param uLUN Device LUN to assign. + * @param pCfg CFGM node to use for configuration. + */ +int hdaR3CodecConstruct(PPDMDEVINS pDevIns, PHDACODECR3 pThis, uint16_t uLUN, PCFGMNODE pCfg) +{ + AssertPtrReturn(pDevIns, VERR_INVALID_POINTER); + AssertPtrReturn(pThis, VERR_INVALID_POINTER); + AssertPtrReturn(pCfg, VERR_INVALID_POINTER); + HDACODECCFG *pCodecCfg = (HDACODECCFG *)&pThis->Cfg; + + pCodecCfg->id = uLUN; + pCodecCfg->enmType = CODECTYPE_STAC9220; /** @todo Make this dynamic. */ + + int rc; + + switch (pCodecCfg->enmType) + { + case CODECTYPE_STAC9220: + { + rc = stac9220Construct(pThis, pCodecCfg); + AssertRCReturn(rc, rc); + break; + } + + default: + AssertFailedReturn(VERR_NOT_IMPLEMENTED); + break; + } + + /* + * Set initial volume. + */ + PCODECNODE pNode = &pThis->aNodes[pCodecCfg->idxDacLineOut]; + rc = hdaR3CodecToAudVolume(pThis, pNode, &pNode->dac.B_params, PDMAUDIOMIXERCTL_FRONT); + AssertRCReturn(rc, rc); + + pNode = &pThis->aNodes[pCodecCfg->idxAdcVolsLineIn]; + rc = hdaR3CodecToAudVolume(pThis, pNode, &pNode->adcvol.B_params, PDMAUDIOMIXERCTL_LINE_IN); + AssertRCReturn(rc, rc); + +#ifdef VBOX_WITH_AUDIO_HDA_MIC_IN +# error "Implement mic-in support!" +#endif + + /* + * Statistics + */ + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatLookupsR3, STAMTYPE_COUNTER, "Codec/LookupsR0", STAMUNIT_OCCURENCES, "Number of R0 codecLookup calls"); +#if 0 /* Codec is not yet kosher enough for ring-0. @bugref{9890c64} */ + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatLookupsR0, STAMTYPE_COUNTER, "Codec/LookupsR3", STAMUNIT_OCCURENCES, "Number of R3 codecLookup calls"); +#endif + + return rc; +} + + +/** + * Destructs a codec. + * + * @param pThis Codec to destruct. + */ +void hdaCodecDestruct(PHDACODECR3 pThis) +{ + LogFlowFuncEnter(); + + /* Nothing to do here atm. */ + RT_NOREF(pThis); +} + + +/** + * Resets a codec. + * + * @param pThis Codec to reset. + */ +void hdaCodecReset(PHDACODECR3 pThis) +{ + switch (pThis->Cfg.enmType) + { + case CODECTYPE_STAC9220: + stac9220Reset(pThis); + break; + + default: + AssertFailed(); + break; + } +} + diff --git a/src/VBox/Devices/Audio/DevHdaCodec.h b/src/VBox/Devices/Audio/DevHdaCodec.h new file mode 100644 index 00000000..e93320ee --- /dev/null +++ b/src/VBox/Devices/Audio/DevHdaCodec.h @@ -0,0 +1,909 @@ +/* $Id: DevHdaCodec.h $ */ +/** @file + * Intel HD Audio Controller Emulation - Codec, Sigmatel/IDT STAC9220. + */ + +/* + * Copyright (C) 2006-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 + */ + +#ifndef VBOX_INCLUDED_SRC_Audio_DevHdaCodec_h +#define VBOX_INCLUDED_SRC_Audio_DevHdaCodec_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#ifndef VBOX_INCLUDED_SRC_Audio_DevHda_h +# error "Only include DevHda.h!" +#endif + +#include <iprt/list.h> +#include "AudioMixer.h" + + +/** The ICH HDA (Intel) ring-3 codec state. */ +typedef struct HDACODECR3 *PHDACODECR3; + +/** + * Enumeration specifying the codec type to use. + */ +typedef enum CODECTYPE +{ + /** Invalid, do not use. */ + CODECTYPE_INVALID = 0, + /** SigmaTel 9220 (922x). */ + CODECTYPE_STAC9220, + /** Hack to blow the type up to 32-bit. */ + CODECTYPE_32BIT_HACK = 0x7fffffff +} CODECTYPE; + +/* PRM 5.3.1 */ +/** Codec address mask. */ +#define CODEC_CAD_MASK 0xF0000000 +/** Codec address shift. */ +#define CODEC_CAD_SHIFT 28 +#define CODEC_DIRECT_MASK RT_BIT(27) +/** Node ID mask. */ +#define CODEC_NID_MASK 0x07F00000 +/** Node ID shift. */ +#define CODEC_NID_SHIFT 20 +#define CODEC_VERBDATA_MASK 0x000FFFFF +#define CODEC_VERB_4BIT_CMD 0x000FFFF0 +#define CODEC_VERB_4BIT_DATA 0x0000000F +#define CODEC_VERB_8BIT_CMD 0x000FFF00 +#define CODEC_VERB_8BIT_DATA 0x000000FF +#define CODEC_VERB_16BIT_CMD 0x000F0000 +#define CODEC_VERB_16BIT_DATA 0x0000FFFF + +#define CODEC_CAD(cmd) (((cmd) & CODEC_CAD_MASK) >> CODEC_CAD_SHIFT) +#define CODEC_DIRECT(cmd) ((cmd) & CODEC_DIRECT_MASK) +#define CODEC_NID(cmd) ((((cmd) & CODEC_NID_MASK)) >> CODEC_NID_SHIFT) +#define CODEC_VERBDATA(cmd) ((cmd) & CODEC_VERBDATA_MASK) +#define CODEC_VERB_CMD(cmd, mask, x) (((cmd) & (mask)) >> (x)) +#define CODEC_VERB_CMD4(cmd) (CODEC_VERB_CMD((cmd), CODEC_VERB_4BIT_CMD, 4)) +#define CODEC_VERB_CMD8(cmd) (CODEC_VERB_CMD((cmd), CODEC_VERB_8BIT_CMD, 8)) +#define CODEC_VERB_CMD16(cmd) (CODEC_VERB_CMD((cmd), CODEC_VERB_16BIT_CMD, 16)) +#define CODEC_VERB_PAYLOAD4(cmd) ((cmd) & CODEC_VERB_4BIT_DATA) +#define CODEC_VERB_PAYLOAD8(cmd) ((cmd) & CODEC_VERB_8BIT_DATA) +#define CODEC_VERB_PAYLOAD16(cmd) ((cmd) & CODEC_VERB_16BIT_DATA) + +#define CODEC_VERB_GET_AMP_DIRECTION RT_BIT(15) +#define CODEC_VERB_GET_AMP_SIDE RT_BIT(13) +#define CODEC_VERB_GET_AMP_INDEX 0x7 + +/* HDA spec 7.3.3.7 NoteA */ +#define CODEC_GET_AMP_DIRECTION(cmd) (((cmd) & CODEC_VERB_GET_AMP_DIRECTION) >> 15) +#define CODEC_GET_AMP_SIDE(cmd) (((cmd) & CODEC_VERB_GET_AMP_SIDE) >> 13) +#define CODEC_GET_AMP_INDEX(cmd) (CODEC_GET_AMP_DIRECTION(cmd) ? 0 : ((cmd) & CODEC_VERB_GET_AMP_INDEX)) + +/* HDA spec 7.3.3.7 NoteC */ +#define CODEC_VERB_SET_AMP_OUT_DIRECTION RT_BIT(15) +#define CODEC_VERB_SET_AMP_IN_DIRECTION RT_BIT(14) +#define CODEC_VERB_SET_AMP_LEFT_SIDE RT_BIT(13) +#define CODEC_VERB_SET_AMP_RIGHT_SIDE RT_BIT(12) +#define CODEC_VERB_SET_AMP_INDEX (0x7 << 8) +#define CODEC_VERB_SET_AMP_MUTE RT_BIT(7) +/** Note: 7-bit value [6:0]. */ +#define CODEC_VERB_SET_AMP_GAIN 0x7F + +#define CODEC_SET_AMP_IS_OUT_DIRECTION(cmd) (((cmd) & CODEC_VERB_SET_AMP_OUT_DIRECTION) != 0) +#define CODEC_SET_AMP_IS_IN_DIRECTION(cmd) (((cmd) & CODEC_VERB_SET_AMP_IN_DIRECTION) != 0) +#define CODEC_SET_AMP_IS_LEFT_SIDE(cmd) (((cmd) & CODEC_VERB_SET_AMP_LEFT_SIDE) != 0) +#define CODEC_SET_AMP_IS_RIGHT_SIDE(cmd) (((cmd) & CODEC_VERB_SET_AMP_RIGHT_SIDE) != 0) +#define CODEC_SET_AMP_INDEX(cmd) (((cmd) & CODEC_VERB_SET_AMP_INDEX) >> 7) +#define CODEC_SET_AMP_MUTE(cmd) ((cmd) & CODEC_VERB_SET_AMP_MUTE) +#define CODEC_SET_AMP_GAIN(cmd) ((cmd) & CODEC_VERB_SET_AMP_GAIN) + +/* HDA spec 7.3.3.1 defines layout of configuration registers/verbs (0xF00) */ +/* VendorID (7.3.4.1) */ +#define CODEC_MAKE_F00_00(vendorID, deviceID) (((vendorID) << 16) | (deviceID)) +#define CODEC_F00_00_VENDORID(f00_00) (((f00_00) >> 16) & 0xFFFF) +#define CODEC_F00_00_DEVICEID(f00_00) ((f00_00) & 0xFFFF) + +/** RevisionID (7.3.4.2). */ +#define CODEC_MAKE_F00_02(majRev, minRev, venFix, venProg, stepFix, stepProg) \ + ( (((majRev) & 0xF) << 20) \ + | (((minRev) & 0xF) << 16) \ + | (((venFix) & 0xF) << 12) \ + | (((venProg) & 0xF) << 8) \ + | (((stepFix) & 0xF) << 4) \ + | ((stepProg) & 0xF)) + +/** Subordinate node count (7.3.4.3). */ +#define CODEC_MAKE_F00_04(startNodeNumber, totalNodeNumber) ((((startNodeNumber) & 0xFF) << 16)|((totalNodeNumber) & 0xFF)) +#define CODEC_F00_04_TO_START_NODE_NUMBER(f00_04) (((f00_04) >> 16) & 0xFF) +#define CODEC_F00_04_TO_NODE_COUNT(f00_04) ((f00_04) & 0xFF) +/* + * Function Group Type (7.3.4.4) + * 0 & [0x3-0x7f] are reserved types + * [0x80 - 0xff] are vendor defined function groups + */ +#define CODEC_MAKE_F00_05(UnSol, NodeType) (((UnSol) << 8)|(NodeType)) +#define CODEC_F00_05_UNSOL RT_BIT(8) +#define CODEC_F00_05_AFG (0x1) +#define CODEC_F00_05_MFG (0x2) +#define CODEC_F00_05_IS_UNSOL(f00_05) RT_BOOL((f00_05) & RT_BIT(8)) +#define CODEC_F00_05_GROUP(f00_05) ((f00_05) & 0xff) +/* Audio Function Group capabilities (7.3.4.5). */ +#define CODEC_MAKE_F00_08(BeepGen, InputDelay, OutputDelay) ((((BeepGen) & 0x1) << 16)| (((InputDelay) & 0xF) << 8) | ((OutputDelay) & 0xF)) +#define CODEC_F00_08_BEEP_GEN(f00_08) ((f00_08) & RT_BIT(16) + +/* Converter Stream, Channel (7.3.3.11). */ +#define CODEC_F00_06_GET_STREAM_ID(cmd) (((cmd) >> 4) & 0x0F) +#define CODEC_F00_06_GET_CHANNEL_ID(cmd) (((cmd) & 0x0F)) + +/* Widget Capabilities (7.3.4.6). */ +#define CODEC_MAKE_F00_09(type, delay, chan_ext) \ + ( (((type) & 0xF) << 20) \ + | (((delay) & 0xF) << 16) \ + | (((chan_ext) & 0xF) << 13)) +/* note: types 0x8-0xe are reserved */ +#define CODEC_F00_09_TYPE_AUDIO_OUTPUT (0x0) +#define CODEC_F00_09_TYPE_AUDIO_INPUT (0x1) +#define CODEC_F00_09_TYPE_AUDIO_MIXER (0x2) +#define CODEC_F00_09_TYPE_AUDIO_SELECTOR (0x3) +#define CODEC_F00_09_TYPE_PIN_COMPLEX (0x4) +#define CODEC_F00_09_TYPE_POWER_WIDGET (0x5) +#define CODEC_F00_09_TYPE_VOLUME_KNOB (0x6) +#define CODEC_F00_09_TYPE_BEEP_GEN (0x7) +#define CODEC_F00_09_TYPE_VENDOR_DEFINED (0xF) + +#define CODEC_F00_09_CAP_CP RT_BIT(12) +#define CODEC_F00_09_CAP_L_R_SWAP RT_BIT(11) +#define CODEC_F00_09_CAP_POWER_CTRL RT_BIT(10) +#define CODEC_F00_09_CAP_DIGITAL RT_BIT(9) +#define CODEC_F00_09_CAP_CONNECTION_LIST RT_BIT(8) +#define CODEC_F00_09_CAP_UNSOL RT_BIT(7) +#define CODEC_F00_09_CAP_PROC_WIDGET RT_BIT(6) +#define CODEC_F00_09_CAP_STRIPE RT_BIT(5) +#define CODEC_F00_09_CAP_FMT_OVERRIDE RT_BIT(4) +#define CODEC_F00_09_CAP_AMP_FMT_OVERRIDE RT_BIT(3) +#define CODEC_F00_09_CAP_OUT_AMP_PRESENT RT_BIT(2) +#define CODEC_F00_09_CAP_IN_AMP_PRESENT RT_BIT(1) +#define CODEC_F00_09_CAP_STEREO RT_BIT(0) + +#define CODEC_F00_09_TYPE(f00_09) (((f00_09) >> 20) & 0xF) + +#define CODEC_F00_09_IS_CAP_CP(f00_09) RT_BOOL((f00_09) & RT_BIT(12)) +#define CODEC_F00_09_IS_CAP_L_R_SWAP(f00_09) RT_BOOL((f00_09) & RT_BIT(11)) +#define CODEC_F00_09_IS_CAP_POWER_CTRL(f00_09) RT_BOOL((f00_09) & RT_BIT(10)) +#define CODEC_F00_09_IS_CAP_DIGITAL(f00_09) RT_BOOL((f00_09) & RT_BIT(9)) +#define CODEC_F00_09_IS_CAP_CONNECTION_LIST(f00_09) RT_BOOL((f00_09) & RT_BIT(8)) +#define CODEC_F00_09_IS_CAP_UNSOL(f00_09) RT_BOOL((f00_09) & RT_BIT(7)) +#define CODEC_F00_09_IS_CAP_PROC_WIDGET(f00_09) RT_BOOL((f00_09) & RT_BIT(6)) +#define CODEC_F00_09_IS_CAP_STRIPE(f00_09) RT_BOOL((f00_09) & RT_BIT(5)) +#define CODEC_F00_09_IS_CAP_FMT_OVERRIDE(f00_09) RT_BOOL((f00_09) & RT_BIT(4)) +#define CODEC_F00_09_IS_CAP_AMP_OVERRIDE(f00_09) RT_BOOL((f00_09) & RT_BIT(3)) +#define CODEC_F00_09_IS_CAP_OUT_AMP_PRESENT(f00_09) RT_BOOL((f00_09) & RT_BIT(2)) +#define CODEC_F00_09_IS_CAP_IN_AMP_PRESENT(f00_09) RT_BOOL((f00_09) & RT_BIT(1)) +#define CODEC_F00_09_IS_CAP_LSB(f00_09) RT_BOOL((f00_09) & RT_BIT(0)) + +/* Supported PCM size, rates (7.3.4.7) */ +#define CODEC_F00_0A_32_BIT RT_BIT(19) +#define CODEC_F00_0A_24_BIT RT_BIT(18) +#define CODEC_F00_0A_16_BIT RT_BIT(17) +#define CODEC_F00_0A_8_BIT RT_BIT(16) + +#define CODEC_F00_0A_48KHZ_MULT_8X RT_BIT(11) +#define CODEC_F00_0A_48KHZ_MULT_4X RT_BIT(10) +#define CODEC_F00_0A_44_1KHZ_MULT_4X RT_BIT(9) +#define CODEC_F00_0A_48KHZ_MULT_2X RT_BIT(8) +#define CODEC_F00_0A_44_1KHZ_MULT_2X RT_BIT(7) +#define CODEC_F00_0A_48KHZ RT_BIT(6) +#define CODEC_F00_0A_44_1KHZ RT_BIT(5) +/* 2/3 * 48kHz */ +#define CODEC_F00_0A_48KHZ_2_3X RT_BIT(4) +/* 1/2 * 44.1kHz */ +#define CODEC_F00_0A_44_1KHZ_1_2X RT_BIT(3) +/* 1/3 * 48kHz */ +#define CODEC_F00_0A_48KHZ_1_3X RT_BIT(2) +/* 1/4 * 44.1kHz */ +#define CODEC_F00_0A_44_1KHZ_1_4X RT_BIT(1) +/* 1/6 * 48kHz */ +#define CODEC_F00_0A_48KHZ_1_6X RT_BIT(0) + +/* Supported streams formats (7.3.4.8) */ +#define CODEC_F00_0B_AC3 RT_BIT(2) +#define CODEC_F00_0B_FLOAT32 RT_BIT(1) +#define CODEC_F00_0B_PCM RT_BIT(0) + +/* Pin Capabilities (7.3.4.9)*/ +#define CODEC_MAKE_F00_0C(vref_ctrl) (((vref_ctrl) & 0xFF) << 8) +#define CODEC_F00_0C_CAP_HBR RT_BIT(27) +#define CODEC_F00_0C_CAP_DP RT_BIT(24) +#define CODEC_F00_0C_CAP_EAPD RT_BIT(16) +#define CODEC_F00_0C_CAP_HDMI RT_BIT(7) +#define CODEC_F00_0C_CAP_BALANCED_IO RT_BIT(6) +#define CODEC_F00_0C_CAP_INPUT RT_BIT(5) +#define CODEC_F00_0C_CAP_OUTPUT RT_BIT(4) +#define CODEC_F00_0C_CAP_HEADPHONE_AMP RT_BIT(3) +#define CODEC_F00_0C_CAP_PRESENCE_DETECT RT_BIT(2) +#define CODEC_F00_0C_CAP_TRIGGER_REQUIRED RT_BIT(1) +#define CODEC_F00_0C_CAP_IMPENDANCE_SENSE RT_BIT(0) + +#define CODEC_F00_0C_IS_CAP_HBR(f00_0c) ((f00_0c) & RT_BIT(27)) +#define CODEC_F00_0C_IS_CAP_DP(f00_0c) ((f00_0c) & RT_BIT(24)) +#define CODEC_F00_0C_IS_CAP_EAPD(f00_0c) ((f00_0c) & RT_BIT(16)) +#define CODEC_F00_0C_IS_CAP_HDMI(f00_0c) ((f00_0c) & RT_BIT(7)) +#define CODEC_F00_0C_IS_CAP_BALANCED_IO(f00_0c) ((f00_0c) & RT_BIT(6)) +#define CODEC_F00_0C_IS_CAP_INPUT(f00_0c) ((f00_0c) & RT_BIT(5)) +#define CODEC_F00_0C_IS_CAP_OUTPUT(f00_0c) ((f00_0c) & RT_BIT(4)) +#define CODEC_F00_0C_IS_CAP_HP(f00_0c) ((f00_0c) & RT_BIT(3)) +#define CODEC_F00_0C_IS_CAP_PRESENCE_DETECT(f00_0c) ((f00_0c) & RT_BIT(2)) +#define CODEC_F00_0C_IS_CAP_TRIGGER_REQUIRED(f00_0c) ((f00_0c) & RT_BIT(1)) +#define CODEC_F00_0C_IS_CAP_IMPENDANCE_SENSE(f00_0c) ((f00_0c) & RT_BIT(0)) + +/* Input Amplifier capabilities (7.3.4.10). */ +#define CODEC_MAKE_F00_0D(mute_cap, step_size, num_steps, offset) \ + ( (((mute_cap) & UINT32_C(0x1)) << 31) \ + | (((step_size) & UINT32_C(0xFF)) << 16) \ + | (((num_steps) & UINT32_C(0xFF)) << 8) \ + | ((offset) & UINT32_C(0xFF))) + +#define CODEC_F00_0D_CAP_MUTE RT_BIT(7) + +#define CODEC_F00_0D_IS_CAP_MUTE(f00_0d) ( ( f00_0d) & RT_BIT(31)) +#define CODEC_F00_0D_STEP_SIZE(f00_0d) ((( f00_0d) & (0x7F << 16)) >> 16) +#define CODEC_F00_0D_NUM_STEPS(f00_0d) ((((f00_0d) & (0x7F << 8)) >> 8) + 1) +#define CODEC_F00_0D_OFFSET(f00_0d) ( (f00_0d) & 0x7F) + +/** Indicates that the amplifier can be muted. */ +#define CODEC_AMP_CAP_MUTE 0x1 +/** The amplifier's maximum number of steps. We want + * a ~90dB dynamic range, so 64 steps with 1.25dB each + * should do the trick. + * + * As we want to map our range to [0..128] values we can avoid + * multiplication and simply doing a shift later. + * + * Produces -96dB to +0dB. + * "0" indicates a step of 0.25dB, "127" indicates a step of 32dB. + */ +#define CODEC_AMP_NUM_STEPS 0x7F +/** The initial gain offset (and when doing a node reset). */ +#define CODEC_AMP_OFF_INITIAL 0x7F +/** The amplifier's gain step size. */ +#define CODEC_AMP_STEP_SIZE 0x2 + +/* Output Amplifier capabilities (7.3.4.10) */ +#define CODEC_MAKE_F00_12 CODEC_MAKE_F00_0D + +#define CODEC_F00_12_IS_CAP_MUTE(f00_12) CODEC_F00_0D_IS_CAP_MUTE(f00_12) +#define CODEC_F00_12_STEP_SIZE(f00_12) CODEC_F00_0D_STEP_SIZE(f00_12) +#define CODEC_F00_12_NUM_STEPS(f00_12) CODEC_F00_0D_NUM_STEPS(f00_12) +#define CODEC_F00_12_OFFSET(f00_12) CODEC_F00_0D_OFFSET(f00_12) + +/* Connection list lenght (7.3.4.11). */ +#define CODEC_MAKE_F00_0E(long_form, length) \ + ( (((long_form) & 0x1) << 7) \ + | ((length) & 0x7F)) +/* Indicates short-form NIDs. */ +#define CODEC_F00_0E_LIST_NID_SHORT 0 +/* Indicates long-form NIDs. */ +#define CODEC_F00_0E_LIST_NID_LONG 1 +#define CODEC_F00_0E_IS_LONG(f00_0e) RT_BOOL((f00_0e) & RT_BIT(7)) +#define CODEC_F00_0E_COUNT(f00_0e) ((f00_0e) & 0x7F) +/* Supported Power States (7.3.4.12) */ +#define CODEC_F00_0F_EPSS RT_BIT(31) +#define CODEC_F00_0F_CLKSTOP RT_BIT(30) +#define CODEC_F00_0F_S3D3 RT_BIT(29) +#define CODEC_F00_0F_D3COLD RT_BIT(4) +#define CODEC_F00_0F_D3 RT_BIT(3) +#define CODEC_F00_0F_D2 RT_BIT(2) +#define CODEC_F00_0F_D1 RT_BIT(1) +#define CODEC_F00_0F_D0 RT_BIT(0) + +/* Processing capabilities 7.3.4.13 */ +#define CODEC_MAKE_F00_10(num, benign) ((((num) & 0xFF) << 8) | ((benign) & 0x1)) +#define CODEC_F00_10_NUM(f00_10) (((f00_10) & (0xFF << 8)) >> 8) +#define CODEC_F00_10_BENING(f00_10) ((f00_10) & 0x1) + +/* GPIO count (7.3.4.14). */ +#define CODEC_MAKE_F00_11(wake, unsol, numgpi, numgpo, numgpio) \ + ( (((wake) & UINT32_C(0x1)) << 31) \ + | (((unsol) & UINT32_C(0x1)) << 30) \ + | (((numgpi) & UINT32_C(0xFF)) << 16) \ + | (((numgpo) & UINT32_C(0xFF)) << 8) \ + | ((numgpio) & UINT32_C(0xFF))) + +/* Processing States (7.3.3.4). */ +#define CODEC_F03_OFF (0) +#define CODEC_F03_ON RT_BIT(0) +#define CODEC_F03_BENING RT_BIT(1) +/* Power States (7.3.3.10). */ +#define CODEC_MAKE_F05(reset, stopok, error, act, set) \ + ( (((reset) & 0x1) << 10) \ + | (((stopok) & 0x1) << 9) \ + | (((error) & 0x1) << 8) \ + | (((act) & 0xF) << 4) \ + | ((set) & 0xF)) +#define CODEC_F05_D3COLD (4) +#define CODEC_F05_D3 (3) +#define CODEC_F05_D2 (2) +#define CODEC_F05_D1 (1) +#define CODEC_F05_D0 (0) + +#define CODEC_F05_IS_RESET(value) (((value) & RT_BIT(10)) != 0) +#define CODEC_F05_IS_STOPOK(value) (((value) & RT_BIT(9)) != 0) +#define CODEC_F05_IS_ERROR(value) (((value) & RT_BIT(8)) != 0) +#define CODEC_F05_ACT(value) (((value) & 0xF0) >> 4) +#define CODEC_F05_SET(value) (((value) & 0xF)) + +#define CODEC_F05_GE(p0, p1) ((p0) <= (p1)) +#define CODEC_F05_LE(p0, p1) ((p0) >= (p1)) + +/* Converter Stream, Channel (7.3.3.11). */ +#define CODEC_MAKE_F06(stream, channel) \ + ( (((stream) & 0xF) << 4) \ + | ((channel) & 0xF)) +#define CODEC_F06_STREAM(value) ((value) & 0xF0) +#define CODEC_F06_CHANNEL(value) ((value) & 0xF) + +/* Pin Widged Control (7.3.3.13). */ +#define CODEC_F07_VREF_HIZ (0) +#define CODEC_F07_VREF_50 (0x1) +#define CODEC_F07_VREF_GROUND (0x2) +#define CODEC_F07_VREF_80 (0x4) +#define CODEC_F07_VREF_100 (0x5) +#define CODEC_F07_IN_ENABLE RT_BIT(5) +#define CODEC_F07_OUT_ENABLE RT_BIT(6) +#define CODEC_F07_OUT_H_ENABLE RT_BIT(7) + +/* Volume Knob Control (7.3.3.29). */ +#define CODEC_F0F_IS_DIRECT RT_BIT(7) +#define CODEC_F0F_VOLUME (0x7F) + +/* Unsolicited enabled (7.3.3.14). */ +#define CODEC_MAKE_F08(enable, tag) ((((enable) & 1) << 7) | ((tag) & 0x3F)) + +/* Converter formats (7.3.3.8) and (3.7.1). */ +/* This is the same format as SDnFMT. */ +#define CODEC_MAKE_A HDA_SDFMT_MAKE + +#define CODEC_A_TYPE HDA_SDFMT_TYPE +#define CODEC_A_TYPE_PCM HDA_SDFMT_TYPE_PCM +#define CODEC_A_TYPE_NON_PCM HDA_SDFMT_TYPE_NON_PCM + +#define CODEC_A_BASE HDA_SDFMT_BASE +#define CODEC_A_BASE_48KHZ HDA_SDFMT_BASE_48KHZ +#define CODEC_A_BASE_44KHZ HDA_SDFMT_BASE_44KHZ + +/* Pin Sense (7.3.3.15). */ +#define CODEC_MAKE_F09_ANALOG(fPresent, impedance) \ +( (((fPresent) & 0x1) << 31) \ + | (((impedance) & UINT32_C(0x7FFFFFFF)))) +#define CODEC_F09_ANALOG_NA UINT32_C(0x7FFFFFFF) +#define CODEC_MAKE_F09_DIGITAL(fPresent, fELDValid) \ +( (((fPresent) & UINT32_C(0x1)) << 31) \ + | (((fELDValid) & UINT32_C(0x1)) << 30)) + +#define CODEC_MAKE_F0C(lrswap, eapd, btl) ((((lrswap) & 1) << 2) | (((eapd) & 1) << 1) | ((btl) & 1)) +#define CODEC_FOC_IS_LRSWAP(f0c) RT_BOOL((f0c) & RT_BIT(2)) +#define CODEC_FOC_IS_EAPD(f0c) RT_BOOL((f0c) & RT_BIT(1)) +#define CODEC_FOC_IS_BTL(f0c) RT_BOOL((f0c) & RT_BIT(0)) +/* HDA spec 7.3.3.31 defines layout of configuration registers/verbs (0xF1C) */ +/* Configuration's port connection */ +#define CODEC_F1C_PORT_MASK (0x3) +#define CODEC_F1C_PORT_SHIFT (30) + +#define CODEC_F1C_PORT_COMPLEX (0x0) +#define CODEC_F1C_PORT_NO_PHYS (0x1) +#define CODEC_F1C_PORT_FIXED (0x2) +#define CODEC_F1C_BOTH (0x3) + +/* Configuration default: connection */ +#define CODEC_F1C_PORT_MASK (0x3) +#define CODEC_F1C_PORT_SHIFT (30) + +/* Connected to a jack (1/8", ATAPI, ...). */ +#define CODEC_F1C_PORT_COMPLEX (0x0) +/* No physical connection. */ +#define CODEC_F1C_PORT_NO_PHYS (0x1) +/* Fixed function device (integrated speaker, integrated mic, ...). */ +#define CODEC_F1C_PORT_FIXED (0x2) +/* Both, a jack and an internal device are attached. */ +#define CODEC_F1C_BOTH (0x3) + +/* Configuration default: Location */ +#define CODEC_F1C_LOCATION_MASK (0x3F) +#define CODEC_F1C_LOCATION_SHIFT (24) + +/* [4:5] bits of location region means chassis attachment */ +#define CODEC_F1C_LOCATION_PRIMARY_CHASSIS (0) +#define CODEC_F1C_LOCATION_INTERNAL RT_BIT(4) +#define CODEC_F1C_LOCATION_SECONDRARY_CHASSIS RT_BIT(5) +#define CODEC_F1C_LOCATION_OTHER RT_BIT(5) + +/* [0:3] bits of location region means geometry location attachment */ +#define CODEC_F1C_LOCATION_NA (0) +#define CODEC_F1C_LOCATION_REAR (0x1) +#define CODEC_F1C_LOCATION_FRONT (0x2) +#define CODEC_F1C_LOCATION_LEFT (0x3) +#define CODEC_F1C_LOCATION_RIGTH (0x4) +#define CODEC_F1C_LOCATION_TOP (0x5) +#define CODEC_F1C_LOCATION_BOTTOM (0x6) +#define CODEC_F1C_LOCATION_SPECIAL_0 (0x7) +#define CODEC_F1C_LOCATION_SPECIAL_1 (0x8) +#define CODEC_F1C_LOCATION_SPECIAL_2 (0x9) + +/* Configuration default: Device type */ +#define CODEC_F1C_DEVICE_MASK (0xF) +#define CODEC_F1C_DEVICE_SHIFT (20) +#define CODEC_F1C_DEVICE_LINE_OUT (0) +#define CODEC_F1C_DEVICE_SPEAKER (0x1) +#define CODEC_F1C_DEVICE_HP (0x2) +#define CODEC_F1C_DEVICE_CD (0x3) +#define CODEC_F1C_DEVICE_SPDIF_OUT (0x4) +#define CODEC_F1C_DEVICE_DIGITAL_OTHER_OUT (0x5) +#define CODEC_F1C_DEVICE_MODEM_LINE_SIDE (0x6) +#define CODEC_F1C_DEVICE_MODEM_HANDSET_SIDE (0x7) +#define CODEC_F1C_DEVICE_LINE_IN (0x8) +#define CODEC_F1C_DEVICE_AUX (0x9) +#define CODEC_F1C_DEVICE_MIC (0xA) +#define CODEC_F1C_DEVICE_PHONE (0xB) +#define CODEC_F1C_DEVICE_SPDIF_IN (0xC) +#define CODEC_F1C_DEVICE_RESERVED (0xE) +#define CODEC_F1C_DEVICE_OTHER (0xF) + +/* Configuration default: Connection type */ +#define CODEC_F1C_CONNECTION_TYPE_MASK (0xF) +#define CODEC_F1C_CONNECTION_TYPE_SHIFT (16) + +#define CODEC_F1C_CONNECTION_TYPE_UNKNOWN (0) +#define CODEC_F1C_CONNECTION_TYPE_1_8INCHES (0x1) +#define CODEC_F1C_CONNECTION_TYPE_1_4INCHES (0x2) +#define CODEC_F1C_CONNECTION_TYPE_ATAPI (0x3) +#define CODEC_F1C_CONNECTION_TYPE_RCA (0x4) +#define CODEC_F1C_CONNECTION_TYPE_OPTICAL (0x5) +#define CODEC_F1C_CONNECTION_TYPE_OTHER_DIGITAL (0x6) +#define CODEC_F1C_CONNECTION_TYPE_ANALOG (0x7) +#define CODEC_F1C_CONNECTION_TYPE_DIN (0x8) +#define CODEC_F1C_CONNECTION_TYPE_XLR (0x9) +#define CODEC_F1C_CONNECTION_TYPE_RJ_11 (0xA) +#define CODEC_F1C_CONNECTION_TYPE_COMBO (0xB) +#define CODEC_F1C_CONNECTION_TYPE_OTHER (0xF) + +/* Configuration's color */ +#define CODEC_F1C_COLOR_MASK (0xF) +#define CODEC_F1C_COLOR_SHIFT (12) +#define CODEC_F1C_COLOR_UNKNOWN (0) +#define CODEC_F1C_COLOR_BLACK (0x1) +#define CODEC_F1C_COLOR_GREY (0x2) +#define CODEC_F1C_COLOR_BLUE (0x3) +#define CODEC_F1C_COLOR_GREEN (0x4) +#define CODEC_F1C_COLOR_RED (0x5) +#define CODEC_F1C_COLOR_ORANGE (0x6) +#define CODEC_F1C_COLOR_YELLOW (0x7) +#define CODEC_F1C_COLOR_PURPLE (0x8) +#define CODEC_F1C_COLOR_PINK (0x9) +#define CODEC_F1C_COLOR_RESERVED_0 (0xA) +#define CODEC_F1C_COLOR_RESERVED_1 (0xB) +#define CODEC_F1C_COLOR_RESERVED_2 (0xC) +#define CODEC_F1C_COLOR_RESERVED_3 (0xD) +#define CODEC_F1C_COLOR_WHITE (0xE) +#define CODEC_F1C_COLOR_OTHER (0xF) + +/* Configuration's misc */ +#define CODEC_F1C_MISC_MASK (0xF) +#define CODEC_F1C_MISC_SHIFT (8) +#define CODEC_F1C_MISC_NONE 0 +#define CODEC_F1C_MISC_JACK_NO_PRESENCE_DETECT RT_BIT(0) +#define CODEC_F1C_MISC_RESERVED_0 RT_BIT(1) +#define CODEC_F1C_MISC_RESERVED_1 RT_BIT(2) +#define CODEC_F1C_MISC_RESERVED_2 RT_BIT(3) + +/* Configuration default: Association */ +#define CODEC_F1C_ASSOCIATION_MASK (0xF) +#define CODEC_F1C_ASSOCIATION_SHIFT (4) + +/** Reserved; don't use. */ +#define CODEC_F1C_ASSOCIATION_INVALID 0x0 +#define CODEC_F1C_ASSOCIATION_GROUP_0 0x1 +#define CODEC_F1C_ASSOCIATION_GROUP_1 0x2 +#define CODEC_F1C_ASSOCIATION_GROUP_2 0x3 +#define CODEC_F1C_ASSOCIATION_GROUP_3 0x4 +#define CODEC_F1C_ASSOCIATION_GROUP_4 0x5 +#define CODEC_F1C_ASSOCIATION_GROUP_5 0x6 +#define CODEC_F1C_ASSOCIATION_GROUP_6 0x7 +#define CODEC_F1C_ASSOCIATION_GROUP_7 0x8 +/* Note: Windows OSes will treat group 15 (0xF) as single PIN devices. + * The sequence number associated with that group then will be ignored. */ +#define CODEC_F1C_ASSOCIATION_GROUP_15 0xF + +/* Configuration default: Association Sequence. */ +#define CODEC_F1C_SEQ_MASK (0xF) +#define CODEC_F1C_SEQ_SHIFT (0) + +/* Implementation identification (7.3.3.30). */ +#define CODEC_MAKE_F20(bmid, bsku, aid) \ + ( (((bmid) & 0xFFFF) << 16) \ + | (((bsku) & 0xFF) << 8) \ + | (((aid) & 0xFF)) \ + ) + +/* Macro definition helping in filling the configuration registers. */ +#define CODEC_MAKE_F1C(port_connectivity, location, device, connection_type, color, misc, association, sequence) \ + ( (((port_connectivity) & 0xF) << CODEC_F1C_PORT_SHIFT) \ + | (((location) & 0xF) << CODEC_F1C_LOCATION_SHIFT) \ + | (((device) & 0xF) << CODEC_F1C_DEVICE_SHIFT) \ + | (((connection_type) & 0xF) << CODEC_F1C_CONNECTION_TYPE_SHIFT) \ + | (((color) & 0xF) << CODEC_F1C_COLOR_SHIFT) \ + | (((misc) & 0xF) << CODEC_F1C_MISC_SHIFT) \ + | (((association) & 0xF) << CODEC_F1C_ASSOCIATION_SHIFT) \ + | (((sequence) & 0xF))) + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +/** The F00 parameter length (in dwords). */ +#define CODECNODE_F00_PARAM_LENGTH 20 +/** The F02 parameter length (in dwords). */ +#define CODECNODE_F02_PARAM_LENGTH 16 + +/* PRM 5.3.1 */ +#define CODEC_RESPONSE_UNSOLICITED RT_BIT_64(34) + +#define AMPLIFIER_SIZE 60 + +typedef uint32_t AMPLIFIER[AMPLIFIER_SIZE]; + +/** + * Common (or core) codec node structure. + */ +typedef struct CODECCOMMONNODE +{ + /** The node's ID. */ + uint8_t uID; + /** The node's name. */ + /** The SDn ID this node is assigned to. + * 0 means not assigned, 1 is SDn0. */ + uint8_t uSD; + /** The SDn's channel to use. + * Only valid if a valid SDn ID is set. */ + uint8_t uChannel; + /* PRM 5.3.6 */ + uint32_t au32F00_param[CODECNODE_F00_PARAM_LENGTH]; + uint32_t au32F02_param[CODECNODE_F02_PARAM_LENGTH]; +} CODECCOMMONNODE; +AssertCompile(CODECNODE_F00_PARAM_LENGTH == 20); /* saved state */ +AssertCompile(CODECNODE_F02_PARAM_LENGTH == 16); /* saved state */ +AssertCompileSize(CODECCOMMONNODE, (1 + 20 + 16) * sizeof(uint32_t)); +typedef CODECCOMMONNODE *PCODECCOMMONNODE; + +/** + * Compile time assertion on the expected node size. + */ +#define AssertNodeSize(a_Node, a_cParams) \ + AssertCompile((a_cParams) <= (60 + 6)); /* the max size - saved state */ \ + AssertCompile( sizeof(a_Node) - sizeof(CODECCOMMONNODE) \ + == ((a_cParams) * sizeof(uint32_t)) ) + +typedef struct ROOTCODECNODE +{ + CODECCOMMONNODE node; +} ROOTCODECNODE, *PROOTCODECNODE; +AssertNodeSize(ROOTCODECNODE, 0); + +typedef struct DACNODE +{ + CODECCOMMONNODE node; + uint32_t u32F0d_param; + uint32_t u32F04_param; + uint32_t u32F05_param; + uint32_t u32F06_param; + uint32_t u32F0c_param; + + uint32_t u32A_param; + AMPLIFIER B_params; + +} DACNODE, *PDACNODE; +AssertNodeSize(DACNODE, 6 + 60); + +typedef struct ADCNODE +{ + CODECCOMMONNODE node; + uint32_t u32F01_param; + uint32_t u32F03_param; + uint32_t u32F05_param; + uint32_t u32F06_param; + uint32_t u32F09_param; + + uint32_t u32A_param; + AMPLIFIER B_params; +} ADCNODE, *PADCNODE; +AssertNodeSize(DACNODE, 6 + 60); + +typedef struct SPDIFOUTNODE +{ + CODECCOMMONNODE node; + uint32_t u32F05_param; + uint32_t u32F06_param; + uint32_t u32F09_param; + uint32_t u32F0d_param; + + uint32_t u32A_param; + AMPLIFIER B_params; +} SPDIFOUTNODE, *PSPDIFOUTNODE; +AssertNodeSize(SPDIFOUTNODE, 5 + 60); + +typedef struct SPDIFINNODE +{ + CODECCOMMONNODE node; + uint32_t u32F05_param; + uint32_t u32F06_param; + uint32_t u32F09_param; + uint32_t u32F0d_param; + + uint32_t u32A_param; + AMPLIFIER B_params; +} SPDIFINNODE, *PSPDIFINNODE; +AssertNodeSize(SPDIFINNODE, 5 + 60); + +typedef struct AFGCODECNODE +{ + CODECCOMMONNODE node; + uint32_t u32F05_param; + uint32_t u32F08_param; + uint32_t u32F17_param; + uint32_t u32F20_param; +} AFGCODECNODE, *PAFGCODECNODE; +AssertNodeSize(AFGCODECNODE, 4); + +typedef struct PORTNODE +{ + CODECCOMMONNODE node; + uint32_t u32F01_param; + uint32_t u32F07_param; + uint32_t u32F08_param; + uint32_t u32F09_param; + uint32_t u32F1c_param; + AMPLIFIER B_params; +} PORTNODE, *PPORTNODE; +AssertNodeSize(PORTNODE, 5 + 60); + +typedef struct DIGOUTNODE +{ + CODECCOMMONNODE node; + uint32_t u32F01_param; + uint32_t u32F05_param; + uint32_t u32F07_param; + uint32_t u32F08_param; + uint32_t u32F09_param; + uint32_t u32F1c_param; +} DIGOUTNODE, *PDIGOUTNODE; +AssertNodeSize(DIGOUTNODE, 6); + +typedef struct DIGINNODE +{ + CODECCOMMONNODE node; + uint32_t u32F05_param; + uint32_t u32F07_param; + uint32_t u32F08_param; + uint32_t u32F09_param; + uint32_t u32F0c_param; + uint32_t u32F1c_param; + uint32_t u32F1e_param; +} DIGINNODE, *PDIGINNODE; +AssertNodeSize(DIGINNODE, 7); + +typedef struct ADCMUXNODE +{ + CODECCOMMONNODE node; + uint32_t u32F01_param; + + uint32_t u32A_param; + AMPLIFIER B_params; +} ADCMUXNODE, *PADCMUXNODE; +AssertNodeSize(ADCMUXNODE, 2 + 60); + +typedef struct PCBEEPNODE +{ + CODECCOMMONNODE node; + uint32_t u32F07_param; + uint32_t u32F0a_param; + + uint32_t u32A_param; + AMPLIFIER B_params; + uint32_t u32F1c_param; +} PCBEEPNODE, *PPCBEEPNODE; +AssertNodeSize(PCBEEPNODE, 3 + 60 + 1); + +typedef struct CDNODE +{ + CODECCOMMONNODE node; + uint32_t u32F07_param; + uint32_t u32F1c_param; +} CDNODE, *PCDNODE; +AssertNodeSize(CDNODE, 2); + +typedef struct VOLUMEKNOBNODE +{ + CODECCOMMONNODE node; + uint32_t u32F08_param; + uint32_t u32F0f_param; +} VOLUMEKNOBNODE, *PVOLUMEKNOBNODE; +AssertNodeSize(VOLUMEKNOBNODE, 2); + +typedef struct ADCVOLNODE +{ + CODECCOMMONNODE node; + uint32_t u32F0c_param; + uint32_t u32F01_param; + uint32_t u32A_params; + AMPLIFIER B_params; +} ADCVOLNODE, *PADCVOLNODE; +AssertNodeSize(ADCVOLNODE, 3 + 60); + +typedef struct RESNODE +{ + CODECCOMMONNODE node; + uint32_t u32F05_param; + uint32_t u32F06_param; + uint32_t u32F07_param; + uint32_t u32F1c_param; + + uint32_t u32A_param; +} RESNODE, *PRESNODE; +AssertNodeSize(RESNODE, 5); + +/** + * Used for the saved state. + */ +typedef struct CODECSAVEDSTATENODE +{ + CODECCOMMONNODE Core; + uint32_t au32Params[60 + 6]; +} CODECSAVEDSTATENODE; +AssertNodeSize(CODECSAVEDSTATENODE, 60 + 6); + +typedef union CODECNODE +{ + CODECCOMMONNODE node; + ROOTCODECNODE root; + AFGCODECNODE afg; + DACNODE dac; + ADCNODE adc; + SPDIFOUTNODE spdifout; + SPDIFINNODE spdifin; + PORTNODE port; + DIGOUTNODE digout; + DIGINNODE digin; + ADCMUXNODE adcmux; + PCBEEPNODE pcbeep; + CDNODE cdnode; + VOLUMEKNOBNODE volumeKnob; + ADCVOLNODE adcvol; + RESNODE reserved; + CODECSAVEDSTATENODE SavedState; +} CODECNODE, *PCODECNODE; +AssertNodeSize(CODECNODE, 60 + 6); + +#define CODEC_NODES_MAX 32 + +/** @name CODEC_NODE_CLS_XXX - node classification flags. + * @{ */ +#define CODEC_NODE_CLS_Port UINT16_C(0x0001) +#define CODEC_NODE_CLS_Dac UINT16_C(0x0002) +#define CODEC_NODE_CLS_AdcVol UINT16_C(0x0004) +#define CODEC_NODE_CLS_Adc UINT16_C(0x0008) +#define CODEC_NODE_CLS_AdcMux UINT16_C(0x0010) +#define CODEC_NODE_CLS_Pcbeep UINT16_C(0x0020) +#define CODEC_NODE_CLS_SpdifIn UINT16_C(0x0040) +#define CODEC_NODE_CLS_SpdifOut UINT16_C(0x0080) +#define CODEC_NODE_CLS_DigInPin UINT16_C(0x0100) +#define CODEC_NODE_CLS_DigOutPin UINT16_C(0x0200) +#define CODEC_NODE_CLS_Cd UINT16_C(0x0400) +#define CODEC_NODE_CLS_VolKnob UINT16_C(0x0800) +#define CODEC_NODE_CLS_Reserved UINT16_C(0x1000) +/** @} */ + +/** + * Codec configuration. + * + * This will not change after construction and is therefore kept in a const + * member of HDACODECR3 to encourage compiler optimizations and avoid accidental + * modification. + */ +typedef struct HDACODECCFG +{ + /** Codec implementation type. */ + CODECTYPE enmType; + /** Codec ID. */ + uint16_t id; + uint16_t idVendor; + uint16_t idDevice; + uint8_t bBSKU; + uint8_t idAssembly; + + uint8_t cTotalNodes; + uint8_t idxAdcVolsLineIn; + uint8_t idxDacLineOut; + + /** Align the lists below so they don't cross cache lines (assumes + * CODEC_NODES_MAX is 32). */ + uint8_t const abPadding1[CODEC_NODES_MAX - 15]; + + /** @name Node classifications. + * @note These are copies of the g_abStac9220Xxxx arrays in DevHdaCodec.cpp. + * They are used both for classifying a node and for processing a class of + * nodes. + * @{ */ + uint8_t abPorts[CODEC_NODES_MAX]; + uint8_t abDacs[CODEC_NODES_MAX]; + uint8_t abAdcVols[CODEC_NODES_MAX]; + uint8_t abAdcs[CODEC_NODES_MAX]; + uint8_t abAdcMuxs[CODEC_NODES_MAX]; + uint8_t abPcbeeps[CODEC_NODES_MAX]; + uint8_t abSpdifIns[CODEC_NODES_MAX]; + uint8_t abSpdifOuts[CODEC_NODES_MAX]; + uint8_t abDigInPins[CODEC_NODES_MAX]; + uint8_t abDigOutPins[CODEC_NODES_MAX]; + uint8_t abCds[CODEC_NODES_MAX]; + uint8_t abVolKnobs[CODEC_NODES_MAX]; + uint8_t abReserveds[CODEC_NODES_MAX]; + /** @} */ + + /** The CODEC_NODE_CLS_XXX flags for each node. */ + uint16_t afNodeClassifications[CODEC_NODES_MAX]; +} HDACODECCFG; +AssertCompileMemberAlignment(HDACODECCFG, abPorts, CODEC_NODES_MAX); +AssertCompileSizeAlignment(HDACODECCFG, 64); + + +/** + * HDA codec state (ring-3, no shared state). + */ +typedef struct HDACODECR3 +{ + /** The codec configuration - initialized at construction time. */ + HDACODECCFG const Cfg; + /** The state data for each node. */ + CODECNODE aNodes[CODEC_NODES_MAX]; + /** Statistics. */ + STAMCOUNTER StatLookupsR3; + /** Size alignment padding. */ + uint64_t const au64Padding1[7]; +} HDACODECR3; +AssertCompile(RT_IS_POWER_OF_TWO(CODEC_NODES_MAX)); +AssertCompileMemberAlignment(HDACODECR3, aNodes, 64); +AssertCompileSizeAlignment(HDACODECR3, 64); + + +/** @name HDA Codec API used by the device emulation. + * @{ */ +int hdaR3CodecConstruct(PPDMDEVINS pDevIns, PHDACODECR3 pThis, uint16_t uLUN, PCFGMNODE pCfg); +void hdaR3CodecPowerOff(PHDACODECR3 pThis); +int hdaR3CodecLoadState(PPDMDEVINS pDevIns, PHDACODECR3 pThis, PSSMHANDLE pSSM, uint32_t uVersion); +int hdaR3CodecAddStream(PHDACODECR3 pThis, PDMAUDIOMIXERCTL enmMixerCtl, PPDMAUDIOSTREAMCFG pCfg); +int hdaR3CodecRemoveStream(PHDACODECR3 pThis, PDMAUDIOMIXERCTL enmMixerCtl, bool fImmediate); + +int hdaCodecSaveState(PPDMDEVINS pDevIns, PHDACODECR3 pThis, PSSMHANDLE pSSM); +void hdaCodecDestruct(PHDACODECR3 pThis); +void hdaCodecReset(PHDACODECR3 pThis); + +DECLHIDDEN(int) hdaR3CodecLookup(PHDACODECR3 pThis, uint32_t uCmd, uint64_t *puResp); +DECLHIDDEN(void) hdaR3CodecDbgListNodes(PHDACODECR3 pThis, PCDBGFINFOHLP pHlp, const char *pszArgs); +DECLHIDDEN(void) hdaR3CodecDbgSelector(PHDACODECR3 pThis, PCDBGFINFOHLP pHlp, const char *pszArgs); +/** @} */ + +#endif /* !VBOX_INCLUDED_SRC_Audio_DevHdaCodec_h */ + diff --git a/src/VBox/Devices/Audio/DevHdaStream.cpp b/src/VBox/Devices/Audio/DevHdaStream.cpp new file mode 100644 index 00000000..5d9ffbac --- /dev/null +++ b/src/VBox/Devices/Audio/DevHdaStream.cpp @@ -0,0 +1,2540 @@ +/* $Id: DevHdaStream.cpp $ */ +/** @file + * Intel HD Audio Controller Emulation - Streams. + */ + +/* + * Copyright (C) 2017-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 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_DEV_HDA +#include <VBox/log.h> + +#include <iprt/mem.h> +#include <iprt/semaphore.h> +#include <iprt/zero.h> + +#include <VBox/AssertGuest.h> +#include <VBox/vmm/pdmdev.h> +#include <VBox/vmm/pdmaudioifs.h> +#include <VBox/vmm/pdmaudioinline.h> + +#include "AudioHlp.h" + +#include "DevHda.h" + +#ifdef VBOX_WITH_DTRACE +# include "dtrace/VBoxDD.h" +#endif + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ +#if defined(IN_RING3) || defined(VBOX_HDA_WITH_ON_REG_ACCESS_DMA) +static void hdaStreamSetPositionAbs(PHDASTREAM pStreamShared, PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t uLPIB); +#endif +#ifdef IN_RING3 +# ifdef VBOX_HDA_WITH_ON_REG_ACCESS_DMA +static void hdaR3StreamFlushDmaBounceBufferOutput(PHDASTREAM pStreamShared, PHDASTREAMR3 pStreamR3); +# endif +static uint32_t hdaR3StreamHandleDmaBufferOverrun(PHDASTREAM pStreamShared, PHDASTREAMR3 pStreamR3, PAUDMIXSINK pSink, + uint32_t cbNeeded, uint64_t nsNow, + const char *pszCaller, uint32_t const cbStreamFree); +static void hdaR3StreamUpdateDma(PPDMDEVINS pDevIns, PHDASTATE pThis, PHDASTATER3 pThisCC, + PHDASTREAM pStreamShared, PHDASTREAMR3 pStreamR3); +#endif + + +#ifdef IN_RING3 + +/** + * Creates an HDA stream. + * + * @returns VBox status code. + * @param pStreamShared The HDA stream to construct - shared bits. + * @param pStreamR3 The HDA stream to construct - ring-3 bits. + * @param pThis The shared HDA device instance. + * @param pThisCC The ring-3 HDA device instance. + * @param uSD Stream descriptor number to assign. + */ +int hdaR3StreamConstruct(PHDASTREAM pStreamShared, PHDASTREAMR3 pStreamR3, PHDASTATE pThis, PHDASTATER3 pThisCC, uint8_t uSD) +{ + pStreamR3->u8SD = uSD; + pStreamShared->u8SD = uSD; + pStreamR3->pMixSink = NULL; + pStreamR3->pHDAStateShared = pThis; + pStreamR3->pHDAStateR3 = pThisCC; + Assert(pStreamShared->hTimer != NIL_TMTIMERHANDLE); /* hdaR3Construct initalized this one already. */ + + pStreamShared->State.fInReset = false; + pStreamShared->State.fRunning = false; + + AssertPtr(pStreamR3->pHDAStateR3); + AssertPtr(pStreamR3->pHDAStateR3->pDevIns); + + const bool fIsInput = hdaGetDirFromSD(uSD) == PDMAUDIODIR_IN; + + if (fIsInput) + { + pStreamShared->State.Cfg.enmPath = PDMAUDIOPATH_UNKNOWN; + pStreamShared->State.Cfg.enmDir = PDMAUDIODIR_IN; + } + else + { + pStreamShared->State.Cfg.enmPath = PDMAUDIOPATH_UNKNOWN; + pStreamShared->State.Cfg.enmDir = PDMAUDIODIR_OUT; + } + + pStreamR3->Dbg.Runtime.fEnabled = pThisCC->Dbg.fEnabled; + + if (pStreamR3->Dbg.Runtime.fEnabled) + { + int rc2 = AudioHlpFileCreateF(&pStreamR3->Dbg.Runtime.pFileStream, AUDIOHLPFILE_FLAGS_NONE, AUDIOHLPFILETYPE_WAV, + pThisCC->Dbg.pszOutPath, AUDIOHLPFILENAME_FLAGS_NONE, 0 /*uInstance*/, + fIsInput ? "hdaStreamWriteSD%RU8" : "hdaStreamReadSD%RU8", uSD); + AssertRC(rc2); + + /* pFileDMARaw */ + rc2 = AudioHlpFileCreateF(&pStreamR3->Dbg.Runtime.pFileDMARaw, AUDIOHLPFILE_FLAGS_NONE, AUDIOHLPFILETYPE_WAV, + pThisCC->Dbg.pszOutPath, AUDIOHLPFILENAME_FLAGS_NONE, 0 /*uInstance*/, + fIsInput ? "hdaDMARawWriteSD%RU8" : "hdaDMARawReadSD%RU8", uSD); + AssertRC(rc2); + + /* pFileDMAMapped */ + rc2 = AudioHlpFileCreateF(&pStreamR3->Dbg.Runtime.pFileDMAMapped, AUDIOHLPFILE_FLAGS_NONE, AUDIOHLPFILETYPE_WAV, + pThisCC->Dbg.pszOutPath, AUDIOHLPFILENAME_FLAGS_NONE, 0 /*uInstance*/, + fIsInput ? "hdaDMAWriteMappedSD%RU8" : "hdaDMAReadMappedSD%RU8", uSD); + AssertRC(rc2); + + /* Delete stale debugging files from a former run. */ + AudioHlpFileDelete(pStreamR3->Dbg.Runtime.pFileStream); + AudioHlpFileDelete(pStreamR3->Dbg.Runtime.pFileDMARaw); + AudioHlpFileDelete(pStreamR3->Dbg.Runtime.pFileDMAMapped); + } + + return VINF_SUCCESS; +} + +/** + * Destroys an HDA stream. + * + * @param pStreamR3 The HDA stream to destroy - ring-3 bits. + */ +void hdaR3StreamDestroy(PHDASTREAMR3 pStreamR3) +{ + LogFlowFunc(("[SD%RU8] Destroying ...\n", pStreamR3->u8SD)); + int rc2; + + if (pStreamR3->State.pAioRegSink) + { + rc2 = AudioMixerSinkRemoveUpdateJob(pStreamR3->State.pAioRegSink, hdaR3StreamUpdateAsyncIoJob, pStreamR3); + AssertRC(rc2); + pStreamR3->State.pAioRegSink = NULL; + } + + if (pStreamR3->State.pCircBuf) + { + RTCircBufDestroy(pStreamR3->State.pCircBuf); + pStreamR3->State.pCircBuf = NULL; + pStreamR3->State.StatDmaBufSize = 0; + pStreamR3->State.StatDmaBufUsed = 0; + } + + if (pStreamR3->Dbg.Runtime.fEnabled) + { + AudioHlpFileDestroy(pStreamR3->Dbg.Runtime.pFileStream); + pStreamR3->Dbg.Runtime.pFileStream = NULL; + + AudioHlpFileDestroy(pStreamR3->Dbg.Runtime.pFileDMARaw); + pStreamR3->Dbg.Runtime.pFileDMARaw = NULL; + + AudioHlpFileDestroy(pStreamR3->Dbg.Runtime.pFileDMAMapped); + pStreamR3->Dbg.Runtime.pFileDMAMapped = NULL; + } + + LogFlowFuncLeave(); +} + + +/** + * Converts an HDA stream's SDFMT register into a given PCM properties structure. + * + * @returns VBox status code. + * @param u16SDFMT The HDA stream's SDFMT value to convert. + * @param pProps PCM properties structure to hold converted result on success. + */ +int hdaR3SDFMTToPCMProps(uint16_t u16SDFMT, PPDMAUDIOPCMPROPS pProps) +{ + AssertPtrReturn(pProps, VERR_INVALID_POINTER); + +# define EXTRACT_VALUE(v, mask, shift) ((v & ((mask) << (shift))) >> (shift)) + + int rc = VINF_SUCCESS; + + uint32_t u32Hz = EXTRACT_VALUE(u16SDFMT, HDA_SDFMT_BASE_RATE_MASK, HDA_SDFMT_BASE_RATE_SHIFT) + ? 44100 : 48000; + uint32_t u32HzMult = 1; + uint32_t u32HzDiv = 1; + + switch (EXTRACT_VALUE(u16SDFMT, HDA_SDFMT_MULT_MASK, HDA_SDFMT_MULT_SHIFT)) + { + case 0: u32HzMult = 1; break; + case 1: u32HzMult = 2; break; + case 2: u32HzMult = 3; break; + case 3: u32HzMult = 4; break; + default: + LogFunc(("Unsupported multiplier %x\n", + EXTRACT_VALUE(u16SDFMT, HDA_SDFMT_MULT_MASK, HDA_SDFMT_MULT_SHIFT))); + rc = VERR_NOT_SUPPORTED; + break; + } + switch (EXTRACT_VALUE(u16SDFMT, HDA_SDFMT_DIV_MASK, HDA_SDFMT_DIV_SHIFT)) + { + case 0: u32HzDiv = 1; break; + case 1: u32HzDiv = 2; break; + case 2: u32HzDiv = 3; break; + case 3: u32HzDiv = 4; break; + case 4: u32HzDiv = 5; break; + case 5: u32HzDiv = 6; break; + case 6: u32HzDiv = 7; break; + case 7: u32HzDiv = 8; break; + default: + LogFunc(("Unsupported divisor %x\n", + EXTRACT_VALUE(u16SDFMT, HDA_SDFMT_DIV_MASK, HDA_SDFMT_DIV_SHIFT))); + rc = VERR_NOT_SUPPORTED; + break; + } + + uint8_t cbSample = 0; + switch (EXTRACT_VALUE(u16SDFMT, HDA_SDFMT_BITS_MASK, HDA_SDFMT_BITS_SHIFT)) + { + case 0: + cbSample = 1; + break; + case 1: + cbSample = 2; + break; + case 4: + cbSample = 4; + break; + default: + AssertMsgFailed(("Unsupported bits per sample %x\n", + EXTRACT_VALUE(u16SDFMT, HDA_SDFMT_BITS_MASK, HDA_SDFMT_BITS_SHIFT))); + rc = VERR_NOT_SUPPORTED; + break; + } + + if (RT_SUCCESS(rc)) + { + PDMAudioPropsInit(pProps, cbSample, true /*fSigned*/, (u16SDFMT & 0xf) + 1 /*cChannels*/, u32Hz * u32HzMult / u32HzDiv); + /** @todo is there anything we need to / can do about channel assignments? */ + } + +# undef EXTRACT_VALUE + return rc; +} + +# ifdef LOG_ENABLED +void hdaR3BDLEDumpAll(PPDMDEVINS pDevIns, PHDASTATE pThis, uint64_t u64BDLBase, uint16_t cBDLE) +{ + LogFlowFunc(("BDLEs @ 0x%x (%RU16):\n", u64BDLBase, cBDLE)); + if (!u64BDLBase) + return; + + uint32_t cbBDLE = 0; + for (uint16_t i = 0; i < cBDLE; i++) + { + HDABDLEDESC bd; + PDMDevHlpPhysRead(pDevIns, u64BDLBase + i * sizeof(HDABDLEDESC), &bd, sizeof(bd)); + + LogFunc(("\t#%03d BDLE(adr:0x%llx, size:%RU32, ioc:%RTbool)\n", + i, bd.u64BufAddr, bd.u32BufSize, bd.fFlags & HDA_BDLE_F_IOC)); + + cbBDLE += bd.u32BufSize; + } + + LogFlowFunc(("Total: %RU32 bytes\n", cbBDLE)); + + if (!pThis->u64DPBase) /* No DMA base given? Bail out. */ + return; + + LogFlowFunc(("DMA counters:\n")); + + for (int i = 0; i < cBDLE; i++) + { + uint32_t uDMACnt; + PDMDevHlpPhysRead(pDevIns, (pThis->u64DPBase & DPBASE_ADDR_MASK) + (i * 2 * sizeof(uint32_t)), + &uDMACnt, sizeof(uDMACnt)); + + LogFlowFunc(("\t#%03d DMA @ 0x%x\n", i , uDMACnt)); + } +} +# endif /* LOG_ENABLED */ + + +/** + * Appends a item to the scheduler. + * + * @returns VBox status code. + * @param pStreamShared The stream which scheduler should be modified. + * @param cbCur The period length in guest bytes. + * @param cbMaxPeriod The max period in guest bytes. + * @param idxLastBdle The last BDLE in the period. + * @param pProps The PCM properties. + * @param pcbBorrow Where to account for bytes borrowed across buffers + * to align scheduling items on frame boundraries. + */ +static int hdaR3StreamAddScheduleItem(PHDASTREAM pStreamShared, uint32_t cbCur, uint32_t cbMaxPeriod, + uint32_t idxLastBdle, PCPDMAUDIOPCMPROPS pProps, uint32_t *pcbBorrow) +{ + /* Check that we've got room (shouldn't ever be a problem). */ + size_t idx = pStreamShared->State.cSchedule; + AssertLogRelReturn(idx + 1 < RT_ELEMENTS(pStreamShared->State.aSchedule), VERR_INTERNAL_ERROR_5); + + /* Figure out the BDLE range for this period. */ + uint32_t const idxFirstBdle = idx == 0 ? 0 + : RT_MIN((uint32_t)( pStreamShared->State.aSchedule[idx - 1].idxFirst + + pStreamShared->State.aSchedule[idx - 1].cEntries), + idxLastBdle); + + pStreamShared->State.aSchedule[idx].idxFirst = (uint8_t)idxFirstBdle; + pStreamShared->State.aSchedule[idx].cEntries = idxLastBdle >= idxFirstBdle + ? idxLastBdle - idxFirstBdle + 1 + : pStreamShared->State.cBdles - idxFirstBdle + idxLastBdle + 1; + + /* Deal with borrowing due to unaligned IOC buffers. */ + uint32_t const cbBorrowed = *pcbBorrow; + if (cbBorrowed < cbCur) + cbCur -= cbBorrowed; + else + { + /* Note. We can probably gloss over this, but it's not a situation a sane guest would put us, so don't bother for now. */ + ASSERT_GUEST_MSG_FAILED(("#%u: cbBorrow=%#x cbCur=%#x BDLE[%u..%u]\n", + pStreamShared->u8SD, cbBorrowed, cbCur, idxFirstBdle, idxLastBdle)); + LogRelMax(32, ("HDA: Stream #%u has a scheduling error: cbBorrow=%#x cbCur=%#x BDLE[%u..%u]\n", + pStreamShared->u8SD, cbBorrowed, cbCur, idxFirstBdle, idxLastBdle)); + return VERR_OUT_OF_RANGE; + } + + uint32_t cbCurAligned = PDMAudioPropsRoundUpBytesToFrame(pProps, cbCur); + *pcbBorrow = cbCurAligned - cbCur; + + /* Do we need to split up the period? */ + if (cbCurAligned <= cbMaxPeriod) + { + pStreamShared->State.aSchedule[idx].cbPeriod = cbCurAligned; + pStreamShared->State.aSchedule[idx].cLoops = 1; + } + else + { + /* Reduce till we've below the threshold. */ + uint32_t cbLoop = cbCurAligned; + do + cbLoop = cbLoop / 2; + while (cbLoop > cbMaxPeriod); + cbLoop = PDMAudioPropsRoundUpBytesToFrame(pProps, cbLoop); + + /* Complete the scheduling item. */ + pStreamShared->State.aSchedule[idx].cbPeriod = cbLoop; + pStreamShared->State.aSchedule[idx].cLoops = cbCurAligned / cbLoop; + + /* If there is a remainder, add it as a separate entry (this is + why the schedule must be more than twice the size of the BDL).*/ + cbCurAligned %= cbLoop; + if (cbCurAligned) + { + pStreamShared->State.aSchedule[idx + 1] = pStreamShared->State.aSchedule[idx]; + idx++; + pStreamShared->State.aSchedule[idx].cbPeriod = cbCurAligned; + pStreamShared->State.aSchedule[idx].cLoops = 1; + } + } + + /* Done. */ + pStreamShared->State.cSchedule = (uint16_t)(idx + 1); + + return VINF_SUCCESS; +} + +/** + * Creates the DMA timer schedule for the stream + * + * This is called from the stream setup code. + * + * @returns VBox status code. + * @param pStreamShared The stream to create a schedule for. The BDL + * must be loaded. + * @param cSegments Number of BDL segments. + * @param cBufferIrqs Number of the BDLEs with IOC=1. + * @param cbTotal The total BDL length in guest bytes. + * @param cbMaxPeriod Max period in guest bytes. This is in case the + * guest want to play the whole "Der Ring des + * Nibelungen" cycle in one go. + * @param cTimerTicksPerSec The DMA timer frequency. + * @param pProps The PCM properties. + */ +static int hdaR3StreamCreateSchedule(PHDASTREAM pStreamShared, uint32_t cSegments, uint32_t cBufferIrqs, uint32_t cbTotal, + uint32_t cbMaxPeriod, uint64_t cTimerTicksPerSec, PCPDMAUDIOPCMPROPS pProps) +{ + int rc; + + /* + * Reset scheduling state. + */ + RT_ZERO(pStreamShared->State.aSchedule); + pStreamShared->State.cSchedule = 0; + pStreamShared->State.cSchedulePrologue = 0; + pStreamShared->State.idxSchedule = 0; + pStreamShared->State.idxScheduleLoop = 0; + + /* + * Do the basic schedule compilation. + */ + uint32_t cPotentialPrologue = 0; + uint32_t cbBorrow = 0; + uint32_t cbCur = 0; + uint32_t cbMin = UINT32_MAX; + pStreamShared->State.aSchedule[0].idxFirst = 0; + for (uint32_t i = 0; i < cSegments; i++) + { + cbCur += pStreamShared->State.aBdl[i].cb; + if (pStreamShared->State.aBdl[i].cb < cbMin) + cbMin = pStreamShared->State.aBdl[i].cb; + if (pStreamShared->State.aBdl[i].fFlags & HDA_BDLE_F_IOC) + { + rc = hdaR3StreamAddScheduleItem(pStreamShared, cbCur, cbMaxPeriod, i, pProps, &cbBorrow); + ASSERT_GUEST_RC_RETURN(rc, rc); + + if (cPotentialPrologue == 0) + cPotentialPrologue = pStreamShared->State.cSchedule; + cbCur = 0; + } + } + + /* + * Deal with any loose ends. + */ + if (cbCur && cBufferIrqs == 0) + { + /* + * No IOC. Vista ends up here, typically with three buffers configured. + * + * The perferred option here is to aim at processing one average BDLE with + * each DMA timer period, since that best matches how we update LPIB at + * present. + * + * The second alternative is to divide the whole span up into 3-4 periods + * to try increase our chances of keeping ahead of the guest. We may need + * to pick this if there are too few buffer descriptor or they are too small. + * + * However, what we probably should be doing is to do real DMA work whenever + * the guest reads a DMA related register (like LPIB) and just do 3-4 DMA + * timer periods, however we'll be postponing the DMA timer every time we + * return to ring-3 and signal the AIO, so in the end we'd probably not use + * the timer callback at all. (This is assuming a small shared per-stream + * buffer for keeping the DMA data in and that it's size will force a return + * to ring-3 often enough to keep the AIO thread going at a reasonable rate.) + */ + Assert(cbCur == cbTotal); + + /* Match the BDLEs 1:1 if there are 3 or more and that the smallest one + is at least 5ms big. */ + if (cSegments >= 3 && PDMAudioPropsBytesToMilli(pProps, cbMin) >= 5 /*ms*/) + { + for (uint32_t i = 0; i < cSegments; i++) + { + rc = hdaR3StreamAddScheduleItem(pStreamShared, pStreamShared->State.aBdl[i].cb, cbMaxPeriod, i, pProps, &cbBorrow); + ASSERT_GUEST_RC_RETURN(rc, rc); + } + } + /* Otherwise, just divide the work into 3 or 4 portions and hope for the best. + It seems, though, that this only really work for windows vista if we avoid + working accross buffer lines. */ + /** @todo This can be simplified/relaxed/uncluttered if we do DMA work when LPIB + * is read, assuming ofc that LPIB is read before each buffer update. */ + else + { + uint32_t const cPeriods = cSegments != 3 && PDMAudioPropsBytesToMilli(pProps, cbCur) >= 4 * 5 /*ms*/ + ? 4 : cSegments != 2 ? 3 : 2; + uint32_t const cbPeriod = PDMAudioPropsFloorBytesToFrame(pProps, cbCur / cPeriods); + uint32_t iBdle = 0; + uint32_t offBdle = 0; + for (uint32_t iPeriod = 0; iPeriod < cPeriods; iPeriod++) + { + if (iPeriod + 1 < cPeriods) + { + offBdle += cbPeriod; + while (iBdle < cSegments && offBdle >= pStreamShared->State.aBdl[iBdle].cb) + offBdle -= pStreamShared->State.aBdl[iBdle++].cb; + rc = hdaR3StreamAddScheduleItem(pStreamShared, cbPeriod, cbMaxPeriod, offBdle != 0 ? iBdle : iBdle - 1, + pProps, &cbBorrow); + } + else + rc = hdaR3StreamAddScheduleItem(pStreamShared, cbCur - iPeriod * cbPeriod, cbMaxPeriod, cSegments - 1, + pProps, &cbBorrow); + ASSERT_GUEST_RC_RETURN(rc, rc); + } + + } + } + else if (cbCur) + { + /* The last BDLE didn't have IOC set, so we must continue processing + from the start till we hit one that has. */ + uint32_t i; + for (i = 0; i < cSegments; i++) + { + cbCur += pStreamShared->State.aBdl[i].cb; + if (pStreamShared->State.aBdl[i].fFlags & HDA_BDLE_F_IOC) + break; + } + rc = hdaR3StreamAddScheduleItem(pStreamShared, cbCur, cbMaxPeriod, i, pProps, &cbBorrow); + ASSERT_GUEST_RC_RETURN(rc, rc); + + /* The initial scheduling items covering the wrap around area are + considered a prologue and must not repeated later. */ + Assert(cPotentialPrologue); + pStreamShared->State.cSchedulePrologue = (uint8_t)cPotentialPrologue; + } + + AssertLogRelMsgReturn(cbBorrow == 0, ("HDA: Internal scheduling error on stream #%u: cbBorrow=%#x cbTotal=%#x cbCur=%#x\n", + pStreamShared->u8SD, cbBorrow, cbTotal, cbCur), + VERR_INTERNAL_ERROR_3); + + /* + * If there is just one BDLE with IOC set, we have to make sure + * we've got at least two periods scheduled, otherwise there is + * a very good chance the guest will overwrite the start of the + * buffer before we ever get around to reading it. + */ + if (cBufferIrqs == 1) + { + uint32_t i = pStreamShared->State.cSchedulePrologue; + Assert(i < pStreamShared->State.cSchedule); + if ( i + 1 == pStreamShared->State.cSchedule + && pStreamShared->State.aSchedule[i].cLoops == 1) + { + uint32_t const cbFirstHalf = PDMAudioPropsFloorBytesToFrame(pProps, pStreamShared->State.aSchedule[i].cbPeriod / 2); + uint32_t const cbOtherHalf = pStreamShared->State.aSchedule[i].cbPeriod - cbFirstHalf; + pStreamShared->State.aSchedule[i].cbPeriod = cbFirstHalf; + if (cbFirstHalf == cbOtherHalf) + pStreamShared->State.aSchedule[i].cLoops = 2; + else + { + pStreamShared->State.aSchedule[i + 1] = pStreamShared->State.aSchedule[i]; + pStreamShared->State.aSchedule[i].cbPeriod = cbOtherHalf; + pStreamShared->State.cSchedule++; + } + } + } + + /* + * Go over the schduling entries and calculate the timer ticks for each period. + */ + LogRel2(("HDA: Stream #%u schedule: %u items, %u prologue\n", + pStreamShared->u8SD, pStreamShared->State.cSchedule, pStreamShared->State.cSchedulePrologue)); + uint64_t const cbPerSec = PDMAudioPropsFramesToBytes(pProps, pProps->uHz); + for (uint32_t i = 0; i < pStreamShared->State.cSchedule; i++) + { + uint64_t const cTicks = ASMMultU64ByU32DivByU32(cTimerTicksPerSec, pStreamShared->State.aSchedule[i].cbPeriod, cbPerSec); + AssertLogRelMsgReturn((uint32_t)cTicks == cTicks, ("cTicks=%RU64 (%#RX64)\n", cTicks, cTicks), VERR_INTERNAL_ERROR_4); + pStreamShared->State.aSchedule[i].cPeriodTicks = RT_MAX((uint32_t)cTicks, 16); + LogRel2(("HDA: #%u: %u ticks / %u bytes, %u loops, BDLE%u L %u\n", i, pStreamShared->State.aSchedule[i].cPeriodTicks, + pStreamShared->State.aSchedule[i].cbPeriod, pStreamShared->State.aSchedule[i].cLoops, + pStreamShared->State.aSchedule[i].idxFirst, pStreamShared->State.aSchedule[i].cEntries)); + } + + return VINF_SUCCESS; +} + + +/** + * Sets up ((re-)iniitalizes) an HDA stream. + * + * @returns VBox status code. VINF_NO_CHANGE if the stream does not need + * be set-up again because the stream's (hardware) parameters did + * not change. + * @param pDevIns The device instance. + * @param pThis The shared HDA device state (for HW register + * parameters). + * @param pStreamShared HDA stream to set up, shared portion. + * @param pStreamR3 HDA stream to set up, ring-3 portion. + * @param uSD Stream descriptor number to assign it. + */ +int hdaR3StreamSetUp(PPDMDEVINS pDevIns, PHDASTATE pThis, PHDASTREAM pStreamShared, PHDASTREAMR3 pStreamR3, uint8_t uSD) +{ + /* This must be valid all times. */ + AssertReturn(uSD < HDA_MAX_STREAMS, VERR_INVALID_PARAMETER); + + /* These member can only change on data corruption, despite what the code does further down (bird). */ + AssertReturn(pStreamShared->u8SD == uSD, VERR_WRONG_ORDER); + AssertReturn(pStreamR3->u8SD == uSD, VERR_WRONG_ORDER); + + const uint64_t u64BDLBase = RT_MAKE_U64(HDA_STREAM_REG(pThis, BDPL, uSD), + HDA_STREAM_REG(pThis, BDPU, uSD)); + const uint16_t u16LVI = HDA_STREAM_REG(pThis, LVI, uSD); + const uint32_t u32CBL = HDA_STREAM_REG(pThis, CBL, uSD); + const uint8_t u8FIFOS = HDA_STREAM_REG(pThis, FIFOS, uSD) + 1; + uint8_t u8FIFOW = hdaSDFIFOWToBytes(HDA_STREAM_REG(pThis, FIFOW, uSD)); + const uint16_t u16FMT = HDA_STREAM_REG(pThis, FMT, uSD); + + /* Is the bare minimum set of registers configured for the stream? + * If not, bail out early, as there's nothing to do here for us (yet). */ + if ( !u64BDLBase + || !u16LVI + || !u32CBL + || !u8FIFOS + || !u8FIFOW + || !u16FMT) + { + LogFunc(("[SD%RU8] Registers not set up yet, skipping (re-)initialization\n", uSD)); + return VINF_SUCCESS; + } + + /* + * Convert the config to PDM PCM properties and configure the stream. + */ + PPDMAUDIOSTREAMCFG pCfg = &pStreamShared->State.Cfg; + int rc = hdaR3SDFMTToPCMProps(u16FMT, &pCfg->Props); + if (RT_SUCCESS(rc)) + pCfg->enmDir = hdaGetDirFromSD(uSD); + else + { + LogRelMax(32, ("HDA: Warning: Format 0x%x for stream #%RU8 not supported\n", HDA_STREAM_REG(pThis, FMT, uSD), uSD)); + return rc; + } + + ASSERT_GUEST_LOGREL_MSG_RETURN( PDMAudioPropsFrameSize(&pCfg->Props) > 0 + && u32CBL % PDMAudioPropsFrameSize(&pCfg->Props) == 0, + ("CBL for stream #%RU8 does not align to frame size (u32CBL=%u cbFrameSize=%u)\n", + uSD, u32CBL, PDMAudioPropsFrameSize(&pCfg->Props)), + VERR_INVALID_PARAMETER); + + /* Make sure the guest behaves regarding the stream's FIFO. */ + ASSERT_GUEST_LOGREL_MSG_STMT(u8FIFOW <= u8FIFOS, + ("Guest tried setting a bigger FIFOW (%RU8) than FIFOS (%RU8), limiting\n", u8FIFOW, u8FIFOS), + u8FIFOW = u8FIFOS /* ASSUMES that u8FIFOS has been validated. */); + + pStreamShared->u8SD = uSD; + + /* Update all register copies so that we later know that something has changed. */ + pStreamShared->u64BDLBase = u64BDLBase; + pStreamShared->u16LVI = u16LVI; + pStreamShared->u32CBL = u32CBL; + pStreamShared->u8FIFOS = u8FIFOS; + pStreamShared->u8FIFOW = u8FIFOW; + pStreamShared->u16FMT = u16FMT; + + /* The the stream's name, based on the direction. */ + switch (pCfg->enmDir) + { + case PDMAUDIODIR_IN: +# ifdef VBOX_WITH_AUDIO_HDA_MIC_IN +# error "Implement me!" +# else + pCfg->enmPath = PDMAUDIOPATH_IN_LINE; + RTStrCopy(pCfg->szName, sizeof(pCfg->szName), "Line In"); +# endif + break; + + case PDMAUDIODIR_OUT: + /* Destination(s) will be set in hdaR3AddStreamOut(), + * based on the channels / stream layout. */ + break; + + default: + AssertFailedReturn(VERR_NOT_SUPPORTED); + break; + } + + LogRel2(("HDA: Stream #%RU8 DMA @ 0x%x (%RU32 bytes = %RU64ms total)\n", uSD, pStreamShared->u64BDLBase, + pStreamShared->u32CBL, PDMAudioPropsBytesToMilli(&pCfg->Props, pStreamShared->u32CBL))); + + /* + * Load the buffer descriptor list. + * + * Section 3.6.2 states that "the BDL should not be modified unless the RUN + * bit is 0", so it should be within the specs to read it once here and not + * re-read any BDLEs later. + */ + /* Reset BDL state. */ + RT_ZERO(pStreamShared->State.aBdl); + pStreamShared->State.offCurBdle = 0; + pStreamShared->State.idxCurBdle = 0; + + uint32_t /*const*/ cTransferFragments = (pStreamShared->u16LVI & 0xff) + 1; + if (cTransferFragments <= 1) + LogRel(("HDA: Warning: Stream #%RU8 transfer buffer count invalid: (%RU16)! Buggy guest audio driver!\n", uSD, pStreamShared->u16LVI)); + AssertLogRelReturn(cTransferFragments <= RT_ELEMENTS(pStreamShared->State.aBdl), VERR_INTERNAL_ERROR_5); + pStreamShared->State.cBdles = cTransferFragments; + + /* Load them. */ + rc = PDMDevHlpPCIPhysRead(pDevIns, u64BDLBase, pStreamShared->State.aBdl, + sizeof(pStreamShared->State.aBdl[0]) * cTransferFragments); + AssertRC(rc); + + /* Check what we just loaded. Refuse overly large buffer lists. */ + uint64_t cbTotal = 0; + uint32_t cBufferIrqs = 0; + for (uint32_t i = 0; i < cTransferFragments; i++) + { + if (pStreamShared->State.aBdl[i].fFlags & HDA_BDLE_F_IOC) + cBufferIrqs++; + cbTotal += pStreamShared->State.aBdl[i].cb; + } + ASSERT_GUEST_STMT_RETURN(cbTotal < _2G, + LogRelMax(32, ("HDA: Error: Stream #%u is configured with an insane amount of buffer space - refusing do work with it: %RU64 (%#RX64) bytes.\n", + uSD, cbTotal, cbTotal)), + VERR_NOT_SUPPORTED); + ASSERT_GUEST_STMT_RETURN(cbTotal == u32CBL, + LogRelMax(32, ("HDA: Warning: Stream #%u has a mismatch between CBL and configured buffer space: %RU32 (%#RX32) vs %RU64 (%#RX64)\n", + uSD, u32CBL, u32CBL, cbTotal, cbTotal)), + VERR_NOT_SUPPORTED); + + /* + * Create a DMA timer schedule. + */ + rc = hdaR3StreamCreateSchedule(pStreamShared, cTransferFragments, cBufferIrqs, (uint32_t)cbTotal, + PDMAudioPropsMilliToBytes(&pCfg->Props, 100 /** @todo make configurable */), + PDMDevHlpTimerGetFreq(pDevIns, pStreamShared->hTimer), &pCfg->Props); + if (RT_FAILURE(rc)) + return rc; + + pStreamShared->State.cbCurDmaPeriod = pStreamShared->State.aSchedule[0].cbPeriod; + + /* + * Calculate the transfer Hz for use in the circular buffer calculation + * and the average period for the scheduling hint. + */ + uint32_t cbMaxPeriod = 0; + uint32_t cbMinPeriod = UINT32_MAX; + uint64_t cTicks = 0; + uint32_t cPeriods = 0; + for (uint32_t i = pStreamShared->State.cSchedulePrologue; i < pStreamShared->State.cSchedule; i++) + { + uint32_t cbPeriod = pStreamShared->State.aSchedule[i].cbPeriod; + cbMaxPeriod = RT_MAX(cbMaxPeriod, cbPeriod); + cbMinPeriod = RT_MIN(cbMinPeriod, cbPeriod); + cPeriods += pStreamShared->State.aSchedule[i].cLoops; + cTicks += pStreamShared->State.aSchedule[i].cPeriodTicks * pStreamShared->State.aSchedule[i].cLoops; + } + /* Only consider the prologue in relation to the max period. */ + for (uint32_t i = 0; i < pStreamShared->State.cSchedulePrologue; i++) + cbMaxPeriod = RT_MAX(cbMaxPeriod, pStreamShared->State.aSchedule[i].cbPeriod); + + AssertLogRelReturn(cPeriods > 0, VERR_INTERNAL_ERROR_3); + uint64_t const cbTransferPerSec = RT_MAX(PDMAudioPropsFramesToBytes(&pCfg->Props, pCfg->Props.uHz), + 4096 /* zero div prevention: min is 6kHz, picked 4k in case I'm mistaken */); + unsigned uTransferHz = cbTransferPerSec * 1000 / cbMaxPeriod; + LogRel2(("HDA: Stream #%RU8 needs a %u.%03u Hz timer rate (period: %u..%u host bytes)\n", + uSD, uTransferHz / 1000, uTransferHz % 1000, cbMinPeriod, cbMaxPeriod)); + uTransferHz /= 1000; + + if (uTransferHz > 400) /* Anything above 400 Hz looks fishy -- tell the user. */ + LogRelMax(32, ("HDA: Warning: Calculated transfer Hz rate for stream #%RU8 looks incorrect (%u), please re-run with audio debug mode and report a bug\n", + uSD, uTransferHz)); + + pStreamShared->State.cbAvgTransfer = (uint32_t)(cbTotal + cPeriods - 1) / cPeriods; + + /* Calculate the average scheduling period length in nanoseconds. */ + uint64_t const cTimerResolution = PDMDevHlpTimerGetFreq(pDevIns, pStreamShared->hTimer); + Assert(cTimerResolution <= UINT32_MAX); + uint64_t const cNsPerPeriod = ASMMultU64ByU32DivByU32(cTicks / cPeriods, RT_NS_1SEC, cTimerResolution); + AssertLogRelReturn(cNsPerPeriod > 0, VERR_INTERNAL_ERROR_3); + + /* For input streams we must determin a pre-buffering requirement. + We use the initial delay as a basis here, though we must have at + least two max periods worth of data queued up due to the way we + work the AIO thread. */ + pStreamShared->State.fInputPreBuffered = false; + pStreamShared->State.cbInputPreBuffer = cbMaxPeriod * 2; + + /* + * Set up data transfer stuff. + */ + /* Set I/O scheduling hint for the backends. */ + pCfg->Device.cMsSchedulingHint = cNsPerPeriod > RT_NS_1MS ? (cNsPerPeriod + RT_NS_1MS / 2) / RT_NS_1MS : 1; + LogRel2(("HDA: Stream #%RU8 set scheduling hint for the backends to %RU32ms\n", uSD, pCfg->Device.cMsSchedulingHint)); + + /* Make sure to also update the stream's DMA counter (based on its current LPIB value). */ + /** @todo r=bird: We use LPIB as-is here, so if it's not zero we have to + * locate the right place in the schedule and whatnot... + * + * This is a similar scenario as when loading state saved, btw. + */ + if (HDA_STREAM_REG(pThis, LPIB, uSD) != 0) + LogRel2(("HDA: Warning! Stream #%RU8 is set up with LPIB=%#RX32 instead of zero!\n", uSD, HDA_STREAM_REG(pThis, LPIB, uSD))); + hdaStreamSetPositionAbs(pStreamShared, pDevIns, pThis, HDA_STREAM_REG(pThis, LPIB, uSD)); + +# ifdef LOG_ENABLED + hdaR3BDLEDumpAll(pDevIns, pThis, pStreamShared->u64BDLBase, pStreamShared->u16LVI + 1); +# endif + + /* + * Set up internal ring buffer. + */ + + /* (Re-)Allocate the stream's internal DMA buffer, + * based on the timing *and* PCM properties we just got above. */ + if (pStreamR3->State.pCircBuf) + { + RTCircBufDestroy(pStreamR3->State.pCircBuf); + pStreamR3->State.pCircBuf = NULL; + pStreamR3->State.StatDmaBufSize = 0; + pStreamR3->State.StatDmaBufUsed = 0; + } + pStreamShared->State.offWrite = 0; + pStreamShared->State.offRead = 0; + + /* + * The default internal ring buffer size must be: + * + * - Large enough for at least three periodic DMA transfers. + * + * It is critically important that we don't experience underruns + * in the DMA OUT code, because it will cause the buffer processing + * to get skewed and possibly overlap with what the guest is updating. + * At the time of writing (2021-03-05) there is no code for getting + * back into sync there. + * + * - Large enough for at least three I/O scheduling hints. + * + * We want to lag behind a DMA period or two, but there must be + * sufficent space for the AIO thread to get schedule and shuffle + * data thru the mixer and onto the host audio hardware. + * + * - Both above with plenty to spare. + * + * So, just take the longest of the two periods and multipling it by 6. + * We aren't not talking about very large base buffers heres, so size isn't + * an issue. + * + * Note: Use pCfg->Props as PCM properties here, as we only want to store the + * samples we actually need, in other words, skipping the interleaved + * channels we don't support / need to save space. + */ + uint32_t cbCircBuf = PDMAudioPropsMilliToBytes(&pCfg->Props, RT_MS_1SEC * 6 / uTransferHz); + LogRel2(("HDA: Stream #%RU8 default ring buffer size is %RU32 bytes / %RU64 ms\n", + uSD, cbCircBuf, PDMAudioPropsBytesToMilli(&pCfg->Props, cbCircBuf))); + + uint32_t msCircBufCfg = hdaGetDirFromSD(uSD) == PDMAUDIODIR_IN ? pThis->cMsCircBufIn : pThis->cMsCircBufOut; + if (msCircBufCfg) /* Anything set via CFGM? */ + { + cbCircBuf = PDMAudioPropsMilliToBytes(&pCfg->Props, msCircBufCfg); + LogRel2(("HDA: Stream #%RU8 is using a custom ring buffer size of %RU32 bytes / %RU64 ms\n", + uSD, cbCircBuf, PDMAudioPropsBytesToMilli(&pCfg->Props, cbCircBuf))); + } + + /* Serious paranoia: */ + ASSERT_GUEST_LOGREL_MSG_STMT(cbCircBuf % PDMAudioPropsFrameSize(&pCfg->Props) == 0, + ("Ring buffer size (%RU32) for stream #%RU8 not aligned to the (host) frame size (%RU8)\n", + cbCircBuf, uSD, PDMAudioPropsFrameSize(&pCfg->Props)), + rc = VERR_INVALID_PARAMETER); + ASSERT_GUEST_LOGREL_MSG_STMT(cbCircBuf, ("Ring buffer size for stream #%RU8 is invalid\n", uSD), + rc = VERR_INVALID_PARAMETER); + if (RT_SUCCESS(rc)) + { + rc = RTCircBufCreate(&pStreamR3->State.pCircBuf, cbCircBuf); + if (RT_SUCCESS(rc)) + { + pStreamR3->State.StatDmaBufSize = cbCircBuf; + + /* + * Forward the timer frequency hint to TM as well for better accuracy on + * systems w/o preemption timers (also good for 'info timers'). + */ + PDMDevHlpTimerSetFrequencyHint(pDevIns, pStreamShared->hTimer, uTransferHz); + } + } + + if (RT_FAILURE(rc)) + LogRelMax(32, ("HDA: Initializing stream #%RU8 failed with %Rrc\n", uSD, rc)); + +# ifdef VBOX_WITH_DTRACE + VBOXDD_HDA_STREAM_SETUP((uint32_t)uSD, rc, pStreamShared->State.Cfg.Props.uHz, + pStreamShared->State.aSchedule[pStreamShared->State.cSchedule - 1].cPeriodTicks, + pStreamShared->State.aSchedule[pStreamShared->State.cSchedule - 1].cbPeriod); +# endif + return rc; +} + + +/** + * Worker for hdaR3StreamReset(). + * + * @returns The default mixer sink, NULL if none found. + * @param pThisCC The ring-3 HDA device state. + * @param uSD SD# to return mixer sink for. + * NULL if not found / handled. + */ +static PHDAMIXERSINK hdaR3GetDefaultSink(PHDASTATER3 pThisCC, uint8_t uSD) +{ + if (hdaGetDirFromSD(uSD) == PDMAUDIODIR_IN) + { + const uint8_t uFirstSDI = 0; + + if (uSD == uFirstSDI) /* First SDI. */ + return &pThisCC->SinkLineIn; +# ifdef VBOX_WITH_AUDIO_HDA_MIC_IN + if (uSD == uFirstSDI + 1) + return &pThisCC->SinkMicIn; +# else + /* If we don't have a dedicated Mic-In sink, use the always present Line-In sink. */ + return &pThisCC->SinkLineIn; +# endif + } + else + { + const uint8_t uFirstSDO = HDA_MAX_SDI; + + if (uSD == uFirstSDO) + return &pThisCC->SinkFront; +# ifdef VBOX_WITH_AUDIO_HDA_51_SURROUND + if (uSD == uFirstSDO + 1) + return &pThisCC->SinkCenterLFE; + if (uSD == uFirstSDO + 2) + return &pThisCC->SinkRear; +# endif + } + + return NULL; +} + + +/** + * Resets an HDA stream. + * + * @param pThis The shared HDA device state. + * @param pThisCC The ring-3 HDA device state. + * @param pStreamShared HDA stream to reset (shared). + * @param pStreamR3 HDA stream to reset (ring-3). + * @param uSD Stream descriptor (SD) number to use for this stream. + */ +void hdaR3StreamReset(PHDASTATE pThis, PHDASTATER3 pThisCC, PHDASTREAM pStreamShared, PHDASTREAMR3 pStreamR3, uint8_t uSD) +{ + LogFunc(("[SD%RU8] Reset\n", uSD)); + + /* + * Assert some sanity. + */ + AssertPtr(pThis); + AssertPtr(pStreamShared); + AssertPtr(pStreamR3); + Assert(uSD < HDA_MAX_STREAMS); + Assert(pStreamShared->u8SD == uSD); + Assert(pStreamR3->u8SD == uSD); + AssertMsg(!pStreamShared->State.fRunning, ("[SD%RU8] Cannot reset stream while in running state\n", uSD)); + + /* + * Set reset state. + */ + Assert(ASMAtomicReadBool(&pStreamShared->State.fInReset) == false); /* No nested calls. */ + ASMAtomicXchgBool(&pStreamShared->State.fInReset, true); + + /* + * Second, initialize the registers. + */ + /* See 6.2.33: Clear on reset. */ + HDA_STREAM_REG(pThis, STS, uSD) = 0; + /* According to the ICH6 datasheet, 0x40000 is the default value for stream descriptor register 23:20 + * bits are reserved for stream number 18.2.33, resets SDnCTL except SRST bit. */ + HDA_STREAM_REG(pThis, CTL, uSD) = HDA_SDCTL_TP | (HDA_STREAM_REG(pThis, CTL, uSD) & HDA_SDCTL_SRST); + /* ICH6 defines default values (120 bytes for input and 192 bytes for output descriptors) of FIFO size. 18.2.39. */ + HDA_STREAM_REG(pThis, FIFOS, uSD) = hdaGetDirFromSD(uSD) == PDMAUDIODIR_IN ? HDA_SDIFIFO_120B : HDA_SDOFIFO_192B; + /* See 18.2.38: Always defaults to 0x4 (32 bytes). */ + HDA_STREAM_REG(pThis, FIFOW, uSD) = HDA_SDFIFOW_32B; + HDA_STREAM_REG(pThis, LPIB, uSD) = 0; + HDA_STREAM_REG(pThis, CBL, uSD) = 0; + HDA_STREAM_REG(pThis, LVI, uSD) = 0; + HDA_STREAM_REG(pThis, FMT, uSD) = 0; + HDA_STREAM_REG(pThis, BDPU, uSD) = 0; + HDA_STREAM_REG(pThis, BDPL, uSD) = 0; + + /* Assign the default mixer sink to the stream. */ + pStreamR3->pMixSink = hdaR3GetDefaultSink(pThisCC, uSD); + if (pStreamR3->State.pAioRegSink) + { + int rc2 = AudioMixerSinkRemoveUpdateJob(pStreamR3->State.pAioRegSink, hdaR3StreamUpdateAsyncIoJob, pStreamR3); + AssertRC(rc2); + pStreamR3->State.pAioRegSink = NULL; + } + + /* Reset transfer stuff. */ + pStreamShared->State.cTransferPendingInterrupts = 0; + pStreamShared->State.tsTransferLast = 0; + pStreamShared->State.tsTransferNext = 0; + + /* Initialize timestamps. */ + pStreamShared->State.tsLastTransferNs = 0; + pStreamShared->State.tsLastReadNs = 0; + pStreamShared->State.tsStart = 0; + + RT_ZERO(pStreamShared->State.aBdl); + RT_ZERO(pStreamShared->State.aSchedule); + pStreamShared->State.offCurBdle = 0; + pStreamShared->State.cBdles = 0; + pStreamShared->State.idxCurBdle = 0; + pStreamShared->State.cSchedulePrologue = 0; + pStreamShared->State.cSchedule = 0; + pStreamShared->State.idxSchedule = 0; + pStreamShared->State.idxScheduleLoop = 0; + pStreamShared->State.fInputPreBuffered = false; + + if (pStreamR3->State.pCircBuf) + RTCircBufReset(pStreamR3->State.pCircBuf); + pStreamShared->State.offWrite = 0; + pStreamShared->State.offRead = 0; + + /* Report that we're done resetting this stream. */ + HDA_STREAM_REG(pThis, CTL, uSD) = 0; + +# ifdef VBOX_WITH_DTRACE + VBOXDD_HDA_STREAM_RESET((uint32_t)uSD); +# endif + LogFunc(("[SD%RU8] Reset\n", uSD)); + + /* Exit reset mode. */ + ASMAtomicXchgBool(&pStreamShared->State.fInReset, false); +} + +/** + * Enables or disables an HDA audio stream. + * + * @returns VBox status code. + * @param pThis The shared HDA device state. + * @param pStreamShared HDA stream to enable or disable - shared bits. + * @param pStreamR3 HDA stream to enable or disable - ring-3 bits. + * @param fEnable Whether to enable or disble the stream. + */ +int hdaR3StreamEnable(PHDASTATE pThis, PHDASTREAM pStreamShared, PHDASTREAMR3 pStreamR3, bool fEnable) +{ + AssertPtr(pStreamR3); + AssertPtr(pStreamShared); + + LogFunc(("[SD%RU8] fEnable=%RTbool, pMixSink=%p\n", pStreamShared->u8SD, fEnable, pStreamR3->pMixSink)); + + /* First, enable or disable the stream and the stream's sink, if any. */ + int rc = VINF_SUCCESS; + PAUDMIXSINK const pSink = pStreamR3->pMixSink ? pStreamR3->pMixSink->pMixSink : NULL; + if (pSink) + { + if (fEnable) + { + if (pStreamR3->State.pAioRegSink != pSink) + { + if (pStreamR3->State.pAioRegSink) + { + rc = AudioMixerSinkRemoveUpdateJob(pStreamR3->State.pAioRegSink, hdaR3StreamUpdateAsyncIoJob, pStreamR3); + AssertRC(rc); + } + rc = AudioMixerSinkAddUpdateJob(pSink, hdaR3StreamUpdateAsyncIoJob, pStreamR3, + pStreamShared->State.Cfg.Device.cMsSchedulingHint); + AssertLogRelRC(rc); + pStreamR3->State.pAioRegSink = RT_SUCCESS(rc) ? pSink : NULL; + } + rc = AudioMixerSinkStart(pSink); + } + else + rc = AudioMixerSinkDrainAndStop(pSink, + pStreamR3->State.pCircBuf ? (uint32_t)RTCircBufUsed(pStreamR3->State.pCircBuf) : 0); + } + if ( RT_SUCCESS(rc) + && fEnable + && pStreamR3->Dbg.Runtime.fEnabled) + { + Assert(AudioHlpPcmPropsAreValidAndSupported(&pStreamShared->State.Cfg.Props)); + + if (fEnable) + { + if (!AudioHlpFileIsOpen(pStreamR3->Dbg.Runtime.pFileStream)) + { + int rc2 = AudioHlpFileOpen(pStreamR3->Dbg.Runtime.pFileStream, AUDIOHLPFILE_DEFAULT_OPEN_FLAGS, + &pStreamShared->State.Cfg.Props); + AssertRC(rc2); + } + + if (!AudioHlpFileIsOpen(pStreamR3->Dbg.Runtime.pFileDMARaw)) + { + int rc2 = AudioHlpFileOpen(pStreamR3->Dbg.Runtime.pFileDMARaw, AUDIOHLPFILE_DEFAULT_OPEN_FLAGS, + &pStreamShared->State.Cfg.Props); + AssertRC(rc2); + } + + if (!AudioHlpFileIsOpen(pStreamR3->Dbg.Runtime.pFileDMAMapped)) + { + int rc2 = AudioHlpFileOpen(pStreamR3->Dbg.Runtime.pFileDMAMapped, AUDIOHLPFILE_DEFAULT_OPEN_FLAGS, + &pStreamShared->State.Cfg.Props); + AssertRC(rc2); + } + } + } + + if (RT_SUCCESS(rc)) + { + if (fEnable) + pStreamShared->State.tsTransferLast = 0; /* Make sure it's not stale and messes up WALCLK calculations. */ + pStreamShared->State.fRunning = fEnable; + + /* + * Set the FIFORDY bit when we start running and clear it when stopping. + * + * This prevents Linux from timing out in snd_hdac_stream_sync when starting + * a stream. Technically, Linux also uses the SSYNC feature there, but we + * can get away with just setting the FIFORDY bit for now. + */ + if (fEnable) + HDA_STREAM_REG(pThis, STS, pStreamShared->u8SD) |= HDA_SDSTS_FIFORDY; + else + HDA_STREAM_REG(pThis, STS, pStreamShared->u8SD) &= ~HDA_SDSTS_FIFORDY; + } + + LogFunc(("[SD%RU8] rc=%Rrc\n", pStreamShared->u8SD, rc)); + return rc; +} + +/** + * Marks the stream as started. + * + * Used after the stream has been enabled and the DMA timer has been armed. + */ +void hdaR3StreamMarkStarted(PPDMDEVINS pDevIns, PHDASTATE pThis, PHDASTREAM pStreamShared, uint64_t tsNow) +{ + pStreamShared->State.tsLastReadNs = RTTimeNanoTS(); + pStreamShared->State.tsStart = tsNow; + Log3Func(("#%u: tsStart=%RU64 tsLastReadNs=%RU64\n", + pStreamShared->u8SD, pStreamShared->State.tsStart, pStreamShared->State.tsLastReadNs)); + RT_NOREF(pDevIns, pThis); +} + +/** + * Marks the stream as stopped. + */ +void hdaR3StreamMarkStopped(PHDASTREAM pStreamShared) +{ + Log3Func(("#%u\n", pStreamShared->u8SD)); + RT_NOREF(pStreamShared); +} + +#endif /* IN_RING3 */ +#if defined(IN_RING3) || defined(VBOX_HDA_WITH_ON_REG_ACCESS_DMA) + +/** + * Updates an HDA stream's current read or write buffer position (depending on the stream type) by + * setting its associated LPIB register and DMA position buffer (if enabled) to an absolute value. + * + * @param pStreamShared HDA stream to update read / write position for (shared). + * @param pDevIns The device instance. + * @param pThis The shared HDA device state. + * @param uLPIB Absolute position (in bytes) to set current read / write position to. + */ +static void hdaStreamSetPositionAbs(PHDASTREAM pStreamShared, PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t uLPIB) +{ + AssertPtrReturnVoid(pStreamShared); + AssertMsgStmt(uLPIB <= pStreamShared->u32CBL, ("%#x\n", uLPIB), uLPIB = pStreamShared->u32CBL); + + Log3Func(("[SD%RU8] LPIB=%RU32 (DMA Position Buffer Enabled: %RTbool)\n", pStreamShared->u8SD, uLPIB, pThis->fDMAPosition)); + + /* Update LPIB in any case. */ + HDA_STREAM_REG(pThis, LPIB, pStreamShared->u8SD) = uLPIB; + + /* Do we need to tell the current DMA position? */ + if (pThis->fDMAPosition) + { + /* + * Linux switched to using the position buffers some time during 2.6.x. + * 2.6.12 used LPIB, 2.6.17 defaulted to DMA position buffers, between + * the two version things were being changing quite a bit. + * + * Since 2.6.17, they will treat a zero DMA position value during the first + * period/IRQ as reason to fall back to LPIB mode (see azx_position_ok in + * 2.6.27+, and azx_pcm_pointer before that). They later also added + * UINT32_MAX to the values causing same. + * + * Since 2.6.35 azx_position_ok will read the wall clock register before + * determining the position. + */ + int rc2 = PDMDevHlpPCIPhysWrite(pDevIns, + pThis->u64DPBase + (pStreamShared->u8SD * 2 * sizeof(uint32_t)), + (void *)&uLPIB, sizeof(uint32_t)); + AssertRC(rc2); + } +} + + +/** + * Updates an HDA stream's current read or write buffer position (depending on the stream type) by + * adding a value to its associated LPIB register and DMA position buffer (if enabled). + * + * @note Handles automatic CBL wrap-around. + * + * @param pStreamShared HDA stream to update read / write position for (shared). + * @param pDevIns The device instance. + * @param pThis The shared HDA device state. + * @param cbToAdd Position (in bytes) to add to the current read / write position. + */ +static void hdaStreamSetPositionAdd(PHDASTREAM pStreamShared, PPDMDEVINS pDevIns, PHDASTATE pThis, uint32_t cbToAdd) +{ + if (cbToAdd) /* No need to update anything if 0. */ + { + uint32_t const uCBL = pStreamShared->u32CBL; + if (uCBL) /* paranoia */ + { + uint32_t uNewLpid = HDA_STREAM_REG(pThis, LPIB, pStreamShared->u8SD) + cbToAdd; +# if 1 /** @todo r=bird: this is wrong according to the spec */ + uNewLpid %= uCBL; +# else + /* The spec says it goes to CBL then wraps arpimd to 1, not back to zero. See 3.3.37. */ + if (uNewLpid > uCBL) + uNewLpid %= uCBL; +# endif + hdaStreamSetPositionAbs(pStreamShared, pDevIns, pThis, uNewLpid); + } + } +} + +#endif /* IN_RING3 || VBOX_HDA_WITH_ON_REG_ACCESS_DMA */ +#ifdef IN_RING3 + +/** + * Retrieves the available size of (buffered) audio data (in bytes) of a given HDA stream. + * + * @returns Available data (in bytes). + * @param pStreamR3 HDA stream to retrieve size for (ring-3). + */ +static uint32_t hdaR3StreamGetUsed(PHDASTREAMR3 pStreamR3) +{ + AssertPtrReturn(pStreamR3, 0); + + if (pStreamR3->State.pCircBuf) + return (uint32_t)RTCircBufUsed(pStreamR3->State.pCircBuf); + return 0; +} + +/** + * Retrieves the free size of audio data (in bytes) of a given HDA stream. + * + * @returns Free data (in bytes). + * @param pStreamR3 HDA stream to retrieve size for (ring-3). + */ +static uint32_t hdaR3StreamGetFree(PHDASTREAMR3 pStreamR3) +{ + AssertPtrReturn(pStreamR3, 0); + + if (pStreamR3->State.pCircBuf) + return (uint32_t)RTCircBufFree(pStreamR3->State.pCircBuf); + return 0; +} + +#endif /* IN_RING3 */ +#if defined(IN_RING3) || defined(VBOX_HDA_WITH_ON_REG_ACCESS_DMA) + +/** + * Get the current address and number of bytes left in the current BDLE. + * + * @returns The current physical address. + * @param pStreamShared The stream to check. + * @param pcbLeft The number of bytes left at the returned address. + */ +DECLINLINE(RTGCPHYS) hdaStreamDmaBufGet(PHDASTREAM pStreamShared, uint32_t *pcbLeft) +{ + uint8_t idxBdle = pStreamShared->State.idxCurBdle; + AssertStmt(idxBdle < pStreamShared->State.cBdles, idxBdle = 0); + + uint32_t const cbCurBdl = pStreamShared->State.aBdl[idxBdle].cb; + uint32_t offCurBdle = pStreamShared->State.offCurBdle; + AssertStmt(pStreamShared->State.offCurBdle <= cbCurBdl, offCurBdle = cbCurBdl); + + *pcbLeft = cbCurBdl - offCurBdle; + return pStreamShared->State.aBdl[idxBdle].GCPhys + offCurBdle; +} + +/** + * Checks if the current BDLE is completed. + * + * @retval true if complete + * @retval false if not. + * @param pStreamShared The stream to check. + */ +DECLINLINE(bool) hdaStreamDmaBufIsComplete(PHDASTREAM pStreamShared) +{ + uint8_t const idxBdle = pStreamShared->State.idxCurBdle; + AssertReturn(idxBdle < pStreamShared->State.cBdles, true); + + uint32_t const cbCurBdl = pStreamShared->State.aBdl[idxBdle].cb; + uint32_t const offCurBdle = pStreamShared->State.offCurBdle; + Assert(offCurBdle <= cbCurBdl); + return offCurBdle >= cbCurBdl; +} + +/** + * Checks if the current BDLE needs a completion IRQ. + * + * @retval true if IRQ is needed. + * @retval false if not. + * @param pStreamShared The stream to check. + */ +DECLINLINE(bool) hdaStreamDmaBufNeedsIrq(PHDASTREAM pStreamShared) +{ + uint8_t const idxBdle = pStreamShared->State.idxCurBdle; + AssertReturn(idxBdle < pStreamShared->State.cBdles, false); + return (pStreamShared->State.aBdl[idxBdle].fFlags & HDA_BDLE_F_IOC) != 0; +} + +/** + * Advances the DMA engine to the next BDLE. + * + * @param pStreamShared The stream which DMA engine is to be updated. + */ +DECLINLINE(void) hdaStreamDmaBufAdvanceToNext(PHDASTREAM pStreamShared) +{ + uint8_t idxBdle = pStreamShared->State.idxCurBdle; + Assert(pStreamShared->State.offCurBdle == pStreamShared->State.aBdl[idxBdle].cb); + + if (idxBdle < pStreamShared->State.cBdles - 1) + idxBdle++; + else + idxBdle = 0; + pStreamShared->State.idxCurBdle = idxBdle; + pStreamShared->State.offCurBdle = 0; +} + +#endif /* defined(IN_RING3) || defined(VBOX_HDA_WITH_ON_REG_ACCESS_DMA) */ +#ifdef IN_RING3 + +/** + * Common do-DMA prologue code. + * + * @retval true if DMA processing can take place + * @retval false if caller should return immediately. + * @param pThis The shared HDA device state. + * @param pStreamShared HDA stream to update (shared). + * @param pStreamR3 HDA stream to update (ring-3). + * @param uSD The stream ID (for asserting). + * @param tsNowNs The current RTTimeNano() value. + * @param pszFunction The function name (for logging). + */ +DECLINLINE(bool) hdaR3StreamDoDmaPrologue(PHDASTATE pThis, PHDASTREAM pStreamShared, PHDASTREAMR3 pStreamR3, uint8_t uSD, + uint64_t tsNowNs, const char *pszFunction) +{ + RT_NOREF(uSD, pszFunction); + + /* + * Check if we should skip town... + */ + /* Stream not running (anymore)? */ + if (pStreamShared->State.fRunning) + { /* likely */ } + else + { + Log3(("%s: [SD%RU8] Not running, skipping transfer\n", pszFunction, uSD)); + return false; + } + + if (!(HDA_STREAM_REG(pThis, STS, uSD) & HDA_SDSTS_BCIS)) + { /* likely */ } + else + { + /** @todo r=bird: This is a bit fishy. We should make effort the reschedule + * the transfer immediately after the guest clears the interrupt. + * The same fishy code is present in AC'97 with just a little + * explanation as here, see @bugref{9890#c95}. + * + * The reasoning is probably that the developer noticed some windows + * versions don't like having their BCIS interrupts bundled. There were + * comments to that effect elsewhere, probably as a result of a fixed + * uTimerHz approach to DMA scheduling. However, pausing DMA for a + * period isn't going to help us with the host backends, as they don't + * pause and will want samples ASAP. So, we should at least unpause + * DMA as quickly as we possible when BCIS is cleared. We might even + * not skip it iff the DMA work here doesn't involve raising any IOC, + * which is possible although unlikely. */ + Log3(("%s: [SD%RU8] BCIS bit set, skipping transfer\n", pszFunction, uSD)); + STAM_REL_COUNTER_INC(&pStreamR3->State.StatDmaSkippedPendingBcis); + Log(("%s: [SD%RU8] BCIS bit set, skipping transfer\n", pszFunction, uSD)); +# ifdef HDA_STRICT + /* Timing emulation bug or guest is misbehaving -- let me know. */ + AssertMsgFailed(("%s: BCIS bit for stream #%RU8 still set when it shouldn't\n", pszFunction, uSD)); +# endif + return false; + } + + /* + * Stream sanity checks. + */ + /* Register sanity checks. */ + Assert(uSD < HDA_MAX_STREAMS); + Assert(pStreamShared->u64BDLBase); + Assert(pStreamShared->u32CBL); + Assert(pStreamShared->u8FIFOS); + + /* State sanity checks. */ + Assert(ASMAtomicReadBool(&pStreamShared->State.fInReset) == false); + Assert(ASMAtomicReadBool(&pStreamShared->State.fRunning)); + + /* + * Some timestamp stuff for logging/debugging. + */ + /*const uint64_t tsNowNs = RTTimeNanoTS();*/ + Log3(("%s: [SD%RU8] tsDeltaNs=%'RU64 ns\n", pszFunction, uSD, tsNowNs - pStreamShared->State.tsLastTransferNs)); + pStreamShared->State.tsLastTransferNs = tsNowNs; + + return true; +} + +/** + * Common do-DMA epilogue. + * + * @param pDevIns The device instance. + * @param pStreamShared The HDA stream (shared). + * @param pStreamR3 The HDA stream (ring-3). + */ +DECLINLINE(void) hdaR3StreamDoDmaEpilogue(PPDMDEVINS pDevIns, PHDASTREAM pStreamShared, PHDASTREAMR3 pStreamR3) +{ + /* + * We must update this in the epilogue rather than in the prologue + * as it is used for WALCLK calculation and we must make sure the + * guest doesn't think we've processed the current period till we + * actually have. + */ + pStreamShared->State.tsTransferLast = PDMDevHlpTimerGet(pDevIns, pStreamShared->hTimer); + + /* + * Update the buffer statistics. + */ + pStreamR3->State.StatDmaBufUsed = (uint32_t)RTCircBufUsed(pStreamR3->State.pCircBuf); +} + +#endif /* IN_RING3 */ + +#if defined(IN_RING3) || defined(VBOX_HDA_WITH_ON_REG_ACCESS_DMA) +/** + * Completes a BDLE at the end of a DMA loop iteration, if possible. + * + * @retval true if buffer completed and new loaded. + * @retval false if buffer not completed. + * @param pDevIns The device instance. + * @param pThis The shared HDA device state. + * @param pStreamShared HDA stream to update (shared). + * @param pszFunction The function name (for logging). + */ +DECLINLINE(bool) hdaStreamDoDmaMaybeCompleteBuffer(PPDMDEVINS pDevIns, PHDASTATE pThis, + PHDASTREAM pStreamShared, const char *pszFunction) +{ + RT_NOREF(pszFunction); + + /* + * Is the buffer descriptor complete. + */ + if (hdaStreamDmaBufIsComplete(pStreamShared)) + { + Log3(("%s: [SD%RU8] Completed BDLE%u %#RX64 LB %#RX32 fFlags=%#x\n", pszFunction, pStreamShared->u8SD, + pStreamShared->State.idxCurBdle, pStreamShared->State.aBdl[pStreamShared->State.idxCurBdle].GCPhys, + pStreamShared->State.aBdl[pStreamShared->State.idxCurBdle].cb, + pStreamShared->State.aBdl[pStreamShared->State.idxCurBdle].fFlags)); + + /* Does the current BDLE require an interrupt to be sent? */ + if (hdaStreamDmaBufNeedsIrq(pStreamShared)) + { + /* If the IOCE ("Interrupt On Completion Enable") bit of the SDCTL + register is set we need to generate an interrupt. */ + if (HDA_STREAM_REG(pThis, CTL, pStreamShared->u8SD) & HDA_SDCTL_IOCE) + { + /* Assert the interrupt before actually fetching the next BDLE below. */ + pStreamShared->State.cTransferPendingInterrupts = 1; + Log3(("%s: [SD%RU8] Scheduling interrupt\n", pszFunction, pStreamShared->u8SD)); + + /* Trigger an interrupt first and let hdaRegWriteSDSTS() deal with + * ending / beginning of a period. */ + /** @todo r=bird: What does the above comment mean? */ + HDA_STREAM_REG(pThis, STS, pStreamShared->u8SD) |= HDA_SDSTS_BCIS; + HDA_PROCESS_INTERRUPT(pDevIns, pThis); + } + } + + /* + * Advance to the next BDLE. + */ + hdaStreamDmaBufAdvanceToNext(pStreamShared); + return true; + } + + Log3(("%s: [SD%RU8] Incomplete BDLE%u %#RX64 LB %#RX32 fFlags=%#x: off=%#RX32\n", pszFunction, pStreamShared->u8SD, + pStreamShared->State.idxCurBdle, pStreamShared->State.aBdl[pStreamShared->State.idxCurBdle].GCPhys, + pStreamShared->State.aBdl[pStreamShared->State.idxCurBdle].cb, + pStreamShared->State.aBdl[pStreamShared->State.idxCurBdle].fFlags, pStreamShared->State.offCurBdle)); + return false; +} +#endif /* IN_RING3 || VBOX_HDA_WITH_ON_REG_ACCESS_DMA */ + +#ifdef IN_RING3 + +/** + * Does DMA transfer for an HDA input stream. + * + * Reads audio data from the HDA stream's internal DMA buffer and writing to + * guest memory. + * + * @param pDevIns The device instance. + * @param pThis The shared HDA device state. + * @param pStreamShared HDA stream to update (shared). + * @param pStreamR3 HDA stream to update (ring-3). + * @param cbToConsume The max amount of data to consume from the + * internal DMA buffer. The caller will make sure + * this is always the transfer size fo the current + * period (unless something is seriously wrong). + * @param fWriteSilence Whether to feed the guest silence rather than + * fetching bytes from the internal DMA buffer. + * This is set initially while we pre-buffer a + * little bit of input, so we can better handle + * time catch-ups and other schduling fun. + * @param tsNowNs The current RTTimeNano() value. + * + * @remarks Caller owns the stream lock. + */ +static void hdaR3StreamDoDmaInput(PPDMDEVINS pDevIns, PHDASTATE pThis, PHDASTREAM pStreamShared, + PHDASTREAMR3 pStreamR3, uint32_t const cbToConsume, bool fWriteSilence, uint64_t tsNowNs) +{ + uint8_t const uSD = pStreamShared->u8SD; + LogFlowFunc(("ENTER - #%u cbToConsume=%#x%s\n", uSD, cbToConsume, fWriteSilence ? " silence" : "")); + + /* + * Common prologue. + */ + if (hdaR3StreamDoDmaPrologue(pThis, pStreamShared, pStreamR3, uSD, tsNowNs, "hdaR3StreamDoDmaInput")) + { /* likely */ } + else + return; + + /* + * + * The DMA copy loop. + * + * Note! Unaligned BDLEs shouldn't be a problem since the circular buffer + * doesn't care about alignment. Only, we have to read the rest + * of the incomplete frame from it ASAP. + */ + PRTCIRCBUF pCircBuf = pStreamR3->State.pCircBuf; + uint32_t cbLeft = cbToConsume; + Assert(cbLeft == pStreamShared->State.cbCurDmaPeriod); + Assert(PDMAudioPropsIsSizeAligned(&pStreamShared->State.Cfg.Props, cbLeft)); + + while (cbLeft > 0) + { + STAM_PROFILE_START(&pThis->StatIn, a); + + /* + * Figure out how much we can read & write in this iteration. + */ + uint32_t cbChunk = 0; + RTGCPHYS GCPhys = hdaStreamDmaBufGet(pStreamShared, &cbChunk); + + if (cbChunk <= cbLeft) + { /* very likely */ } + else + cbChunk = cbLeft; + + uint32_t cbWritten = 0; + if (!fWriteSilence) + { + /* + * Write the host data directly into the guest buffers. + */ + while (cbChunk > 0) + { + /* Grab internal DMA buffer space and read into it. */ + void /*const*/ *pvBufSrc; + size_t cbBufSrc; + RTCircBufAcquireReadBlock(pCircBuf, cbChunk, &pvBufSrc, &cbBufSrc); + AssertBreakStmt(cbBufSrc, RTCircBufReleaseReadBlock(pCircBuf, 0)); + + int rc2 = PDMDevHlpPCIPhysWrite(pDevIns, GCPhys, pvBufSrc, cbBufSrc); + AssertRC(rc2); + +# ifdef HDA_DEBUG_SILENCE + fix me if relevant; +# endif + if (RT_LIKELY(!pStreamR3->Dbg.Runtime.pFileDMARaw)) + { /* likely */ } + else + AudioHlpFileWrite(pStreamR3->Dbg.Runtime.pFileDMARaw, pvBufSrc, cbBufSrc); + +# ifdef VBOX_WITH_DTRACE + VBOXDD_HDA_STREAM_DMA_IN((uint32_t)uSD, (uint32_t)cbBufSrc, pStreamShared->State.offRead); +# endif + pStreamShared->State.offRead += cbBufSrc; + RTCircBufReleaseReadBlock(pCircBuf, cbBufSrc); + STAM_COUNTER_ADD(&pThis->StatBytesWritten, cbBufSrc); + + /* advance */ + cbChunk -= (uint32_t)cbBufSrc; + cbWritten += (uint32_t)cbBufSrc; + GCPhys += cbBufSrc; + pStreamShared->State.offCurBdle += (uint32_t)cbBufSrc; + } + } + /* + * Write silence. Since we only do signed formats, we can use the zero + * buffers from IPRT as source here. + */ + else + { + Assert(PDMAudioPropsIsSigned(&pStreamShared->State.Cfg.Props)); + while (cbChunk > 0) + { + /* Write it to the guest buffer. */ + uint32_t cbToWrite = RT_MIN(sizeof(g_abRTZero64K), cbChunk); + int rc2 = PDMDevHlpPCIPhysWrite(pDevIns, GCPhys, g_abRTZero64K, cbToWrite); + AssertRC(rc2); + STAM_COUNTER_ADD(&pThis->StatBytesWritten, cbToWrite); + + /* advance */ + cbWritten += cbToWrite; + cbChunk -= cbToWrite; + GCPhys += cbToWrite; + pStreamShared->State.offCurBdle += cbToWrite; + } + } + + cbLeft -= cbWritten; + STAM_PROFILE_STOP(&pThis->StatIn, a); + + /* + * Complete the buffer if necessary (common with the output DMA code). + * + * Must update the DMA position before we do this as the buffer IRQ may + * fire on another vCPU and run in parallel to us, although it is very + * unlikely it can make much progress as long as we're sitting on the + * lock, it could still read the DMA position (Linux won't, as it reads + * WALCLK and possibly SDnSTS before the DMA position). + */ + hdaStreamSetPositionAdd(pStreamShared, pDevIns, pThis, cbWritten); + hdaStreamDoDmaMaybeCompleteBuffer(pDevIns, pThis, pStreamShared, "hdaR3StreamDoDmaInput"); + } + + Assert(cbLeft == 0); /* There shall be no break statements in the above loop, so cbLeft is always zero here! */ + + /* + * Common epilogue. + */ + hdaR3StreamDoDmaEpilogue(pDevIns, pStreamShared, pStreamR3); + + /* + * Log and leave. + */ + Log3Func(("LEAVE - [SD%RU8] %#RX32/%#RX32 @ %#RX64 - cTransferPendingInterrupts=%RU8\n", + uSD, cbToConsume, pStreamShared->State.cbCurDmaPeriod, pStreamShared->State.offRead - cbToConsume, + pStreamShared->State.cTransferPendingInterrupts)); +} + + +/** + * Input streams: Pulls data from the mixer, putting it in the internal DMA + * buffer. + * + * @param pStreamShared HDA stream to update (shared). + * @param pStreamR3 HDA stream to update (ring-3 bits). + * @param pSink The mixer sink to pull from. + */ +static void hdaR3StreamPullFromMixer(PHDASTREAM pStreamShared, PHDASTREAMR3 pStreamR3, PAUDMIXSINK pSink) +{ +# ifdef LOG_ENABLED + uint64_t const offWriteOld = pStreamShared->State.offWrite; +# endif + pStreamShared->State.offWrite = AudioMixerSinkTransferToCircBuf(pSink, + pStreamR3->State.pCircBuf, + pStreamShared->State.offWrite, + pStreamR3->u8SD, + pStreamR3->Dbg.Runtime.fEnabled + ? pStreamR3->Dbg.Runtime.pFileStream : NULL); + + Log3Func(("[SD%RU8] transferred=%#RX64 bytes -> @%#RX64\n", pStreamR3->u8SD, + pStreamShared->State.offWrite - offWriteOld, pStreamShared->State.offWrite)); + + /* Update buffer stats. */ + pStreamR3->State.StatDmaBufUsed = (uint32_t)RTCircBufUsed(pStreamR3->State.pCircBuf); +} + + +/** + * Does DMA transfer for an HDA output stream. + * + * This transfers one DMA timer period worth of data from the guest and into the + * internal DMA buffer. + * + * @param pDevIns The device instance. + * @param pThis The shared HDA device state. + * @param pStreamShared HDA stream to update (shared). + * @param pStreamR3 HDA stream to update (ring-3). + * @param cbToProduce The max amount of data to produce (i.e. put into + * the circular buffer). Unless something is going + * seriously wrong, this will always be transfer + * size for the current period. + * @param tsNowNs The current RTTimeNano() value. + * + * @remarks Caller owns the stream lock. + */ +static void hdaR3StreamDoDmaOutput(PPDMDEVINS pDevIns, PHDASTATE pThis, PHDASTREAM pStreamShared, + PHDASTREAMR3 pStreamR3, uint32_t const cbToProduce, uint64_t tsNowNs) +{ + uint8_t const uSD = pStreamShared->u8SD; + LogFlowFunc(("ENTER - #%u cbToProduce=%#x\n", uSD, cbToProduce)); + + /* + * Common prologue. + */ + if (hdaR3StreamDoDmaPrologue(pThis, pStreamShared, pStreamR3, uSD, tsNowNs, "hdaR3StreamDoDmaOutput")) + { /* likely */ } + else + return; + + /* + * + * The DMA copy loop. + * + * Note! Unaligned BDLEs shouldn't be a problem since the circular buffer + * doesn't care about alignment. Only, we have to write the rest + * of the incomplete frame to it ASAP. + */ + PRTCIRCBUF pCircBuf = pStreamR3->State.pCircBuf; + uint32_t cbLeft = cbToProduce; +# ifdef VBOX_HDA_WITH_ON_REG_ACCESS_DMA + Assert(cbLeft <= pStreamShared->State.cbCurDmaPeriod); /* a little pointless with the DMA'ing on LPIB read. */ +# else + Assert(cbLeft == pStreamShared->State.cbCurDmaPeriod); +# endif + Assert(PDMAudioPropsIsSizeAligned(&pStreamShared->State.Cfg.Props, cbLeft)); + + while (cbLeft > 0) + { + STAM_PROFILE_START(&pThis->StatOut, a); + + /* + * Figure out how much we can read & write in this iteration. + */ + uint32_t cbChunk = 0; + RTGCPHYS GCPhys = hdaStreamDmaBufGet(pStreamShared, &cbChunk); + + if (cbChunk <= cbLeft) + { /* very likely */ } + else + cbChunk = cbLeft; + + /* + * Read the guest data directly into the internal DMA buffer. + */ + uint32_t cbRead = 0; + while (cbChunk > 0) + { + /* Grab internal DMA buffer space and read into it. */ + void *pvBufDst; + size_t cbBufDst; + RTCircBufAcquireWriteBlock(pCircBuf, cbChunk, &pvBufDst, &cbBufDst); + AssertBreakStmt(cbBufDst, RTCircBufReleaseWriteBlock(pCircBuf, 0)); + + int rc2 = PDMDevHlpPCIPhysRead(pDevIns, GCPhys, pvBufDst, cbBufDst); + AssertRC(rc2); + +# ifdef HDA_DEBUG_SILENCE + fix me if relevant; +# endif + if (RT_LIKELY(!pStreamR3->Dbg.Runtime.pFileDMARaw)) + { /* likely */ } + else + AudioHlpFileWrite(pStreamR3->Dbg.Runtime.pFileDMARaw, pvBufDst, cbBufDst); + +# ifdef VBOX_WITH_DTRACE + VBOXDD_HDA_STREAM_DMA_OUT((uint32_t)uSD, (uint32_t)cbBufDst, pStreamShared->State.offWrite); +# endif + pStreamShared->State.offWrite += cbBufDst; + RTCircBufReleaseWriteBlock(pCircBuf, cbBufDst); + STAM_COUNTER_ADD(&pThis->StatBytesRead, cbBufDst); + + /* advance */ + cbChunk -= (uint32_t)cbBufDst; + cbRead += (uint32_t)cbBufDst; + GCPhys += cbBufDst; + pStreamShared->State.offCurBdle += (uint32_t)cbBufDst; + } + + cbLeft -= cbRead; + STAM_PROFILE_STOP(&pThis->StatOut, a); + + /* + * Complete the buffer if necessary (common with the input DMA code). + * + * Must update the DMA position before we do this as the buffer IRQ may + * fire on another vCPU and run in parallel to us, although it is very + * unlikely it can make much progress as long as we're sitting on the + * lock, it could still read the DMA position (Linux won't, as it reads + * WALCLK and possibly SDnSTS before the DMA position). + */ + hdaStreamSetPositionAdd(pStreamShared, pDevIns, pThis, cbRead); + hdaStreamDoDmaMaybeCompleteBuffer(pDevIns, pThis, pStreamShared, "hdaR3StreamDoDmaOutput"); + } + + Assert(cbLeft == 0); /* There shall be no break statements in the above loop, so cbLeft is always zero here! */ + + /* + * Common epilogue. + */ + hdaR3StreamDoDmaEpilogue(pDevIns, pStreamShared, pStreamR3); + + /* + * Log and leave. + */ + Log3Func(("LEAVE - [SD%RU8] %#RX32/%#RX32 @ %#RX64 - cTransferPendingInterrupts=%RU8\n", + uSD, cbToProduce, pStreamShared->State.cbCurDmaPeriod, pStreamShared->State.offWrite - cbToProduce, + pStreamShared->State.cTransferPendingInterrupts)); +} + +#endif /* IN_RING3 */ +#ifdef VBOX_HDA_WITH_ON_REG_ACCESS_DMA + +/** + * Do DMA output transfer on LPIB/WALCLK register access. + * + * @returns VINF_SUCCESS or VINF_IOM_R3_MMIO_READ. + * @param pDevIns The device instance. + * @param pThis The shared instance data. + * @param pStreamShared The shared stream data. + * @param tsNow The current time on the timer clock. + * @param cbToTransfer How much to transfer. + */ +VBOXSTRICTRC hdaStreamDoOnAccessDmaOutput(PPDMDEVINS pDevIns, PHDASTATE pThis, PHDASTREAM pStreamShared, + uint64_t tsNow, uint32_t cbToTransfer) +{ + AssertReturn(cbToTransfer > 0, VINF_SUCCESS); + int rc = VINF_SUCCESS; + + /* + * Check if we're exceeding the available buffer, go to ring-3 to + * handle that (we would perhaps always take this path when in ring-3). + */ + uint32_t cbDma = pStreamShared->State.cbDma; + ASMCompilerBarrier(); + if ( cbDma >= sizeof(pStreamShared->State.abDma) /* paranoia */ + || cbToTransfer >= sizeof(pStreamShared->State.abDma) /* paranoia */ + || cbDma + cbToTransfer > sizeof(pStreamShared->State.abDma)) + { +# ifndef IN_RING3 + STAM_REL_COUNTER_INC(&pThis->StatAccessDmaOutputToR3); + LogFlowFunc(("[SD%RU8] out of DMA buffer space (%#x, need %#x) -> VINF_IOM_R3_MMIO_READ\n", + pStreamShared->u8SD, sizeof(pStreamShared->State.abDma) - pStreamShared->State.cbDma, cbToTransfer)); + return VINF_IOM_R3_MMIO_READ; +# else /* IN_RING3 */ + /* + * Flush the bounce buffer, then do direct transfers to the + * internal DMA buffer (updates LPIB). + */ + PHDASTATER3 const pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATER3); + uintptr_t const idxStream = pStreamShared->u8SD; + AssertReturn(idxStream < RT_ELEMENTS(pThisCC->aStreams), VERR_INTERNAL_ERROR_4); + PHDASTREAMR3 const pStreamR3 = &pThisCC->aStreams[idxStream]; + + hdaR3StreamFlushDmaBounceBufferOutput(pStreamShared, pStreamR3); + + uint32_t cbStreamFree = hdaR3StreamGetFree(pStreamR3); + if (cbStreamFree >= cbToTransfer) + { /* likely */ } + else + { + PAUDMIXSINK pSink = pStreamR3->pMixSink ? pStreamR3->pMixSink->pMixSink : NULL; + if (pSink) + cbStreamFree = hdaR3StreamHandleDmaBufferOverrun(pStreamShared, pStreamR3, pSink, cbToTransfer, RTTimeNanoTS(), + "hdaStreamDoOnAccessDmaOutput", cbStreamFree); + else + { + LogFunc(("[SD%RU8] No sink and insufficient internal DMA buffer space (%#x) - won't do anything\n", + pStreamShared->u8SD, cbStreamFree)); + return VINF_SUCCESS; + } + cbToTransfer = RT_MIN(cbToTransfer, cbStreamFree); + if (cbToTransfer < PDMAudioPropsFrameSize(&pStreamShared->State.Cfg.Props)) + { + LogFunc(("[SD%RU8] No internal DMA buffer space (%#x) - won't do anything\n", pStreamShared->u8SD, cbStreamFree)); + return VINF_SUCCESS; + } + } + hdaR3StreamDoDmaOutput(pDevIns, pThis, pStreamShared, pStreamR3, cbToTransfer, RTTimeNanoTS()); + pStreamShared->State.cbDmaTotal += cbToTransfer; +# endif /* IN_RING3 */ + } + else + { + /* + * Transfer into the DMA bounce buffer. + */ + LogFlowFunc(("[SD%RU8] Transfering %#x bytes to DMA bounce buffer (cbDma=%#x cbDmaTotal=%#x) (%p/%u)\n", + pStreamShared->u8SD, cbToTransfer, cbDma, pStreamShared->State.cbDmaTotal, pStreamShared, pStreamShared->u8SD)); + uint32_t cbLeft = cbToTransfer; + do + { + uint32_t cbChunk = 0; + RTGCPHYS GCPhys = hdaStreamDmaBufGet(pStreamShared, &cbChunk); + + bool fMustAdvanceBuffer; + if (cbLeft < cbChunk) + { + fMustAdvanceBuffer = false; + cbChunk = cbLeft; + } + else + fMustAdvanceBuffer = true; + + /* Read the guest data directly into the DMA bounce buffer. */ + int rc2 = PDMDevHlpPCIPhysRead(pDevIns, GCPhys, &pStreamShared->State.abDma[cbDma], cbChunk); + AssertRC(rc2); + + /* We update offWrite and StatBytesRead here even if we haven't moved the data + to the internal DMA buffer yet, because we want the dtrace even to fire here. */ +# ifdef VBOX_WITH_DTRACE + VBOXDD_HDA_STREAM_DMA_OUT((uint32_t)pStreamShared->u8SD, cbChunk, pStreamShared->State.offWrite); +# endif + pStreamShared->State.offWrite += cbChunk; + STAM_COUNTER_ADD(&pThis->StatBytesRead, cbChunk); + + /* advance */ + pStreamShared->State.offCurBdle += cbChunk; + pStreamShared->State.cbDmaTotal += cbChunk; + cbDma += cbChunk; + pStreamShared->State.cbDma = cbDma; + cbLeft -= cbChunk; + Log6Func(("cbLeft=%#x cbDma=%#x cbDmaTotal=%#x offCurBdle=%#x idxCurBdle=%#x (%p/%u)\n", + cbLeft, cbDma, pStreamShared->State.cbDmaTotal, pStreamShared->State.offCurBdle, + pStreamShared->State.idxCurBdle, pStreamShared, pStreamShared->u8SD)); + + /* Next buffer. */ + bool fAdvanced = hdaStreamDoDmaMaybeCompleteBuffer(pDevIns, pThis, pStreamShared, "hdaStreamDoOnAccessDmaOutput"); + AssertMsgStmt(fMustAdvanceBuffer == fAdvanced, ("%d %d\n", fMustAdvanceBuffer, fAdvanced), rc = VERR_INTERNAL_ERROR_3); + } while (cbLeft > 0); + + /* + * Advance LPIB and update the last transfer time (for WALCLK). + */ + pStreamShared->State.tsTransferLast = tsNow; + hdaStreamSetPositionAdd(pStreamShared, pDevIns, pThis, cbToTransfer - cbLeft); + } + +# ifdef VBOX_STRICT + uint32_t idxSched = pStreamShared->State.idxSchedule; + AssertStmt(idxSched < RT_MIN(RT_ELEMENTS(pStreamShared->State.aSchedule), pStreamShared->State.cSchedule), idxSched = 0); + uint32_t const cbPeriod = pStreamShared->State.aSchedule[idxSched].cbPeriod; + AssertMsg(pStreamShared->State.cbDmaTotal < cbPeriod, ("%#x vs %#x\n", pStreamShared->State.cbDmaTotal, cbPeriod)); +# endif + + STAM_REL_COUNTER_INC(&pThis->StatAccessDmaOutput); + return rc; +} + + +/** + * Consider doing DMA output transfer on LPIB/WALCLK register access. + * + * @returns VINF_SUCCESS or VINF_IOM_R3_MMIO_READ. + * @param pDevIns The device instance. + * @param pThis The shared instance data. + * @param pStreamShared The shared stream data. + * @param tsNow The current time on the timer clock. Used to do the + * calculation. + */ +VBOXSTRICTRC hdaStreamMaybeDoOnAccessDmaOutput(PPDMDEVINS pDevIns, PHDASTATE pThis, PHDASTREAM pStreamShared, uint64_t tsNow) +{ + Assert(pStreamShared->State.fRunning); /* caller should check this */ + + /* + * Calculate where the DMA engine should be according to the clock, if we can. + */ + uint32_t const cbFrame = PDMAudioPropsFrameSize(&pStreamShared->State.Cfg.Props); + uint32_t const cbPeriod = pStreamShared->State.cbCurDmaPeriod; + if (cbPeriod > cbFrame) + { + AssertMsg(pStreamShared->State.cbDmaTotal < cbPeriod, ("%#x vs %#x\n", pStreamShared->State.cbDmaTotal, cbPeriod)); + uint64_t const tsTransferNext = pStreamShared->State.tsTransferNext; + uint32_t cbFuture; + if (tsNow < tsTransferNext) + { + /** @todo ASSUMES nanosecond clock ticks, need to make this + * resolution independent. */ + cbFuture = PDMAudioPropsNanoToBytes(&pStreamShared->State.Cfg.Props, tsTransferNext - tsNow); + cbFuture = RT_MIN(cbFuture, cbPeriod - cbFrame); + } + else + { + /* We've hit/overshot the timer deadline. Return to ring-3 if we're + not already there to increase the chance that we'll help expidite + the timer. If we're already in ring-3, do all but the last frame. */ +# ifndef IN_RING3 + LogFunc(("[SD%RU8] DMA period expired: tsNow=%RU64 >= tsTransferNext=%RU64 -> VINF_IOM_R3_MMIO_READ\n", + tsNow, tsTransferNext)); + return VINF_IOM_R3_MMIO_READ; +# else + cbFuture = cbPeriod - cbFrame; + LogFunc(("[SD%RU8] DMA period expired: tsNow=%RU64 >= tsTransferNext=%RU64 -> cbFuture=%#x (cbPeriod=%#x - cbFrame=%#x)\n", + tsNow, tsTransferNext, cbFuture, cbPeriod, cbFrame)); +# endif + } + uint32_t const offNow = PDMAudioPropsFloorBytesToFrame(&pStreamShared->State.Cfg.Props, cbPeriod - cbFuture); + + /* + * Should we transfer a little? Minimum is 64 bytes (semi-random, + * suspect real hardware might be doing some cache aligned stuff, + * which might soon get complicated if you take unaligned buffers + * into consideration and which cache line size (128 bytes is just + * as likely as 64 or 32 bytes)). + */ + uint32_t cbDmaTotal = pStreamShared->State.cbDmaTotal; + if (cbDmaTotal + 64 <= offNow) + { +# ifdef LOG_ENABLED + uint32_t const uOldLpib = HDA_STREAM_REG(pThis, CBL, pStreamShared->u8SD); +# endif + VBOXSTRICTRC rcStrict = hdaStreamDoOnAccessDmaOutput(pDevIns, pThis, pStreamShared, tsNow, offNow - cbDmaTotal); + LogFlowFunc(("[SD%RU8] LPIB=%#RX32 -> LPIB=%#RX32 offNow=%#x rcStrict=%Rrc\n", pStreamShared->u8SD, + uOldLpib, HDA_STREAM_REG(pThis, LPIB, pStreamShared->u8SD), offNow, VBOXSTRICTRC_VAL(rcStrict) )); + return rcStrict; + } + + /* + * Do nothing. + */ + LogFlowFunc(("[SD%RU8] Skipping DMA transfer: cbDmaTotal=%#x offNow=%#x\n", pStreamShared->u8SD, cbDmaTotal, offNow)); + } + else + LogFunc(("[SD%RU8] cbPeriod=%#x <= cbFrame=%#x\n", pStreamShared->u8SD, cbPeriod, cbFrame)); + return VINF_SUCCESS; +} + +#endif /* VBOX_HDA_WITH_ON_REG_ACCESS_DMA */ +#ifdef IN_RING3 + +/** + * Output streams: Pushes data to the mixer. + * + * @param pStreamShared HDA stream to update (shared bits). + * @param pStreamR3 HDA stream to update (ring-3 bits). + * @param pSink The mixer sink to push to. + * @param nsNow The current RTTimeNanoTS() value. + */ +static void hdaR3StreamPushToMixer(PHDASTREAM pStreamShared, PHDASTREAMR3 pStreamR3, PAUDMIXSINK pSink, uint64_t nsNow) +{ +# ifdef LOG_ENABLED + uint64_t const offReadOld = pStreamShared->State.offRead; +# endif + pStreamShared->State.offRead = AudioMixerSinkTransferFromCircBuf(pSink, + pStreamR3->State.pCircBuf, + pStreamShared->State.offRead, + pStreamR3->u8SD, + pStreamR3->Dbg.Runtime.fEnabled + ? pStreamR3->Dbg.Runtime.pFileStream : NULL); + + Assert(nsNow >= pStreamShared->State.tsLastReadNs); + Log3Func(("[SD%RU8] nsDeltaLastRead=%RI64 transferred=%#RX64 bytes -> @%#RX64\n", pStreamR3->u8SD, + nsNow - pStreamShared->State.tsLastReadNs, pStreamShared->State.offRead - offReadOld, pStreamShared->State.offRead)); + RT_NOREF(pStreamShared, nsNow); + + /* Update buffer stats. */ + pStreamR3->State.StatDmaBufUsed = (uint32_t)RTCircBufUsed(pStreamR3->State.pCircBuf); +} + + +/** + * Deals with a DMA buffer overrun. + * + * Makes sure we return with @a cbNeeded bytes of free space in pCircBuf. + * + * @returns Number of bytes free in the internal DMA buffer. + * @param pStreamShared The shared data for the HDA stream. + * @param pStreamR3 The ring-3 data for the HDA stream. + * @param pSink The mixer sink (valid). + * @param cbNeeded How much space we need (in bytes). + * @param nsNow Current RTNanoTimeTS() timestamp. + * @param cbStreamFree The current amount of free buffer space. + * @param pszCaller The caller (for logging). + */ +static uint32_t hdaR3StreamHandleDmaBufferOverrun(PHDASTREAM pStreamShared, PHDASTREAMR3 pStreamR3, PAUDMIXSINK pSink, + uint32_t cbNeeded, uint64_t nsNow, + const char *pszCaller, uint32_t const cbStreamFree) +{ + STAM_REL_COUNTER_INC(&pStreamR3->State.StatDmaFlowProblems); + Log(("%s: Warning! Stream #%u has insufficient space free: %#x bytes, need %#x. Will try move data out of the buffer...\n", + pszCaller, pStreamShared->u8SD, cbStreamFree, cbNeeded)); + RT_NOREF(pszCaller, cbStreamFree); + + int rc = AudioMixerSinkTryLock(pSink); + if (RT_SUCCESS(rc)) + { + hdaR3StreamPushToMixer(pStreamShared, pStreamR3, pSink, nsNow); + AudioMixerSinkUpdate(pSink, 0, 0); + AudioMixerSinkUnlock(pSink); + } + else + RTThreadYield(); + + uint32_t const cbRet = hdaR3StreamGetFree(pStreamR3); + Log(("%s: Gained %u bytes.\n", pszCaller, cbRet - cbStreamFree)); + if (cbRet >= cbNeeded) + return cbRet; + + /* + * Unable to make sufficient space. Drop the whole buffer content. + * + * This is needed in order to keep the device emulation running at a + * constant rate, at the cost of losing valid (but too much) data. + */ + STAM_REL_COUNTER_INC(&pStreamR3->State.StatDmaFlowErrors); + LogRel2(("HDA: Warning: Hit stream #%RU8 overflow, dropping %u bytes of audio data (%s)\n", + pStreamShared->u8SD, hdaR3StreamGetUsed(pStreamR3), pszCaller)); +# ifdef HDA_STRICT + AssertMsgFailed(("Hit stream #%RU8 overflow -- timing bug?\n", pStreamShared->u8SD)); +# endif +/** + * + * @todo r=bird: I don't think RTCircBufReset is entirely safe w/o + * owning the AIO lock. See the note in the documentation about it not being + * multi-threading aware (safe). Wish I'd verified this code much earlier. + * Sigh^3! + * + */ + RTCircBufReset(pStreamR3->State.pCircBuf); + pStreamShared->State.offWrite = 0; + pStreamShared->State.offRead = 0; + return hdaR3StreamGetFree(pStreamR3); +} + + +# ifdef VBOX_HDA_WITH_ON_REG_ACCESS_DMA +/** + * Flushes the DMA bounce buffer content to the internal DMA buffer. + * + * @param pStreamShared The shared data of the stream to have its DMA bounce + * buffer flushed. + * @param pStreamR3 The ring-3 stream data for same. + */ +static void hdaR3StreamFlushDmaBounceBufferOutput(PHDASTREAM pStreamShared, PHDASTREAMR3 pStreamR3) +{ + uint32_t cbDma = pStreamShared->State.cbDma; + LogFlowFunc(("cbDma=%#x\n", cbDma)); + if (cbDma) + { + AssertReturnVoid(cbDma <= sizeof(pStreamShared->State.abDma)); + PRTCIRCBUF const pCircBuf = pStreamR3->State.pCircBuf; + if (pCircBuf) + { + uint32_t offDma = 0; + while (offDma < cbDma) + { + uint32_t const cbSrcLeft = cbDma - offDma; + + /* + * Grab a chunk of the internal DMA buffer. + */ + void *pvBufDst = NULL; + size_t cbBufDst = 0; + RTCircBufAcquireWriteBlock(pCircBuf, cbSrcLeft, &pvBufDst, &cbBufDst); + if (cbBufDst > 0) + { /* likely */ } + else + { + /* We've got buffering trouble. */ + RTCircBufReleaseWriteBlock(pCircBuf, 0); + + PAUDMIXSINK pSink = pStreamR3->pMixSink ? pStreamR3->pMixSink->pMixSink : NULL; + if (pSink) + hdaR3StreamHandleDmaBufferOverrun(pStreamShared, pStreamR3, pSink, cbSrcLeft, RTTimeNanoTS(), + "hdaR3StreamFlushDmaBounceBufferOutput", 0 /*cbStreamFree*/); + else + { + LogFunc(("Stream #%u has no sink. Dropping the rest of the data\n", pStreamR3->u8SD)); + break; + } + + RTCircBufAcquireWriteBlock(pCircBuf, cbSrcLeft, &pvBufDst, &cbBufDst); + AssertBreakStmt(cbBufDst, RTCircBufReleaseWriteBlock(pCircBuf, 0)); + } + + /* + * Copy the samples into it and write it to the debug file if open. + * + * We do not fire the dtrace probe here nor update offRead as that was + * done already (not sure that was a good idea?). + */ + memcpy(pvBufDst, &pStreamShared->State.abDma[offDma], cbBufDst); + + if (RT_LIKELY(!pStreamR3->Dbg.Runtime.pFileDMARaw)) + { /* likely */ } + else + AudioHlpFileWrite(pStreamR3->Dbg.Runtime.pFileDMARaw, pvBufDst, cbBufDst); + + RTCircBufReleaseWriteBlock(pCircBuf, cbBufDst); + + offDma += (uint32_t)cbBufDst; + } + } + + /* + * Mark the buffer empty. + */ + pStreamShared->State.cbDma = 0; + } +} +# endif /* VBOX_HDA_WITH_ON_REG_ACCESS_DMA */ + + +/** + * The stream's main function when called by the timer. + * + * @note This function also will be called without timer invocation when + * starting (enabling) the stream to minimize startup latency. + * + * @returns Current timer time if the timer is enabled, otherwise zero. + * @param pDevIns The device instance. + * @param pThis The shared HDA device state. + * @param pThisCC The ring-3 HDA device state. + * @param pStreamShared HDA stream to update (shared bits). + * @param pStreamR3 HDA stream to update (ring-3 bits). + */ +uint64_t hdaR3StreamTimerMain(PPDMDEVINS pDevIns, PHDASTATE pThis, PHDASTATER3 pThisCC, + PHDASTREAM pStreamShared, PHDASTREAMR3 pStreamR3) +{ + Assert(PDMDevHlpCritSectIsOwner(pDevIns, &pThis->CritSect)); + Assert(PDMDevHlpTimerIsLockOwner(pDevIns, pStreamShared->hTimer)); + + /* Do the work: */ + hdaR3StreamUpdateDma(pDevIns, pThis, pThisCC, pStreamShared, pStreamR3); + + /* Re-arm the timer if the sink is still active: */ + if ( pStreamShared->State.fRunning + && pStreamR3->pMixSink + && AudioMixerSinkIsActive(pStreamR3->pMixSink->pMixSink)) + { + /* Advance the schduling: */ + uint32_t idxSched = pStreamShared->State.idxSchedule; + AssertStmt(idxSched < RT_ELEMENTS(pStreamShared->State.aSchedule), idxSched = 0); + uint32_t idxLoop = pStreamShared->State.idxScheduleLoop + 1; + if (idxLoop >= pStreamShared->State.aSchedule[idxSched].cLoops) + { + idxSched += 1; + if ( idxSched >= pStreamShared->State.cSchedule + || idxSched >= RT_ELEMENTS(pStreamShared->State.aSchedule) /*paranoia^2*/) + { + idxSched = pStreamShared->State.cSchedulePrologue; + AssertStmt(idxSched < RT_ELEMENTS(pStreamShared->State.aSchedule), idxSched = 0); + } + pStreamShared->State.idxSchedule = idxSched; + idxLoop = 0; + } + pStreamShared->State.idxScheduleLoop = (uint16_t)idxLoop; + + /* Do the actual timer re-arming. */ + uint64_t const tsNow = PDMDevHlpTimerGet(pDevIns, pStreamShared->hTimer); /* (For virtual sync this remains the same for the whole callout IIRC) */ + uint64_t const tsTransferNext = tsNow + pStreamShared->State.aSchedule[idxSched].cPeriodTicks; + Log3Func(("[SD%RU8] fSinkActive=true, tsTransferNext=%RU64 (in %RU64)\n", + pStreamShared->u8SD, tsTransferNext, tsTransferNext - tsNow)); + int rc = PDMDevHlpTimerSet(pDevIns, pStreamShared->hTimer, tsTransferNext); + AssertRC(rc); + + /* Some legacy stuff: */ + pStreamShared->State.tsTransferNext = tsTransferNext; + pStreamShared->State.cbCurDmaPeriod = pStreamShared->State.aSchedule[idxSched].cbPeriod; + + return tsNow; + } + + Log3Func(("[SD%RU8] fSinkActive=false\n", pStreamShared->u8SD)); + return 0; +} + + +/** + * Updates a HDA stream by doing DMA transfers. + * + * Will do mixer transfers too to try fix an overrun/underrun situation. + * + * The host sink(s) set the overall pace (bird: no it doesn't, the DMA timer + * does - we just hope like heck it matches the speed at which the *backend* + * host audio driver processes samples). + * + * @param pDevIns The device instance. + * @param pThis The shared HDA device state. + * @param pThisCC The ring-3 HDA device state. + * @param pStreamShared HDA stream to update (shared bits). + * @param pStreamR3 HDA stream to update (ring-3 bits). + */ +static void hdaR3StreamUpdateDma(PPDMDEVINS pDevIns, PHDASTATE pThis, PHDASTATER3 pThisCC, + PHDASTREAM pStreamShared, PHDASTREAMR3 pStreamR3) +{ + RT_NOREF(pThisCC); + int rc2; + + /* + * Make sure we're running and got an active mixer sink. + */ + if (RT_LIKELY(pStreamShared->State.fRunning)) + { /* likely */ } + else + return; + + PAUDMIXSINK pSink = NULL; + if (pStreamR3->pMixSink) + pSink = pStreamR3->pMixSink->pMixSink; + if (RT_LIKELY(AudioMixerSinkIsActive(pSink))) + { /* likely */ } + else + return; + + /* + * Get scheduling info common to both input and output streams. + */ + const uint64_t tsNowNs = RTTimeNanoTS(); + uint32_t idxSched = pStreamShared->State.idxSchedule; + AssertStmt(idxSched < RT_MIN(RT_ELEMENTS(pStreamShared->State.aSchedule), pStreamShared->State.cSchedule), idxSched = 0); + uint32_t cbPeriod = pStreamShared->State.aSchedule[idxSched].cbPeriod; + + /* + * Output streams (SDO). + */ + if (hdaGetDirFromSD(pStreamShared->u8SD) == PDMAUDIODIR_OUT) + { +# ifdef VBOX_HDA_WITH_ON_REG_ACCESS_DMA + /* Subtract already transferred bytes and flush the DMA bounce buffer. */ + uint32_t cbDmaTotal = pStreamShared->State.cbDmaTotal; + if (cbDmaTotal > 0) + { + AssertStmt(cbDmaTotal < cbPeriod, cbDmaTotal = cbPeriod); + cbPeriod -= cbDmaTotal; + pStreamShared->State.cbDmaTotal = 0; + hdaR3StreamFlushDmaBounceBufferOutput(pStreamShared, pStreamR3); + } + else + Assert(pStreamShared->State.cbDma == 0); +# endif + + /* + * Check how much room we have in our DMA buffer. There should be at + * least one period worth of space there or we're in an overflow situation. + */ + uint32_t cbStreamFree = hdaR3StreamGetFree(pStreamR3); + if (cbStreamFree >= cbPeriod) + { /* likely */ } + else + cbStreamFree = hdaR3StreamHandleDmaBufferOverrun(pStreamShared, pStreamR3, pSink, cbPeriod, tsNowNs, + "hdaR3StreamUpdateDma", cbStreamFree); + + /* + * Do the DMA transfer. + */ + uint64_t const offWriteBefore = pStreamShared->State.offWrite; + hdaR3StreamDoDmaOutput(pDevIns, pThis, pStreamShared, pStreamR3, RT_MIN(cbStreamFree, cbPeriod), tsNowNs); + + /* + * Should we push data to down thru the mixer to and to the host drivers? + */ + bool fKickAioThread = pStreamShared->State.offWrite > offWriteBefore + || hdaR3StreamGetFree(pStreamR3) < pStreamShared->State.cbAvgTransfer * 2; + + Log3Func(("msDelta=%RU64 (vs %u) cbStreamFree=%#x (vs %#x) => fKickAioThread=%RTbool\n", + (tsNowNs - pStreamShared->State.tsLastReadNs) / RT_NS_1MS, + pStreamShared->State.Cfg.Device.cMsSchedulingHint, cbStreamFree, + pStreamShared->State.cbAvgTransfer * 2, fKickAioThread)); + + if (fKickAioThread) + { + /* Notify the async I/O worker thread that there's work to do. */ + Log5Func(("Notifying AIO thread\n")); + rc2 = AudioMixerSinkSignalUpdateJob(pSink); + AssertRC(rc2); + /* Update last read timestamp for logging/debugging. */ + pStreamShared->State.tsLastReadNs = tsNowNs; + } + } + /* + * Input stream (SDI). + */ + else + { + Assert(hdaGetDirFromSD(pStreamShared->u8SD) == PDMAUDIODIR_IN); + + /* + * See how much data we've got buffered... + */ + bool fWriteSilence = false; + uint32_t cbStreamUsed = hdaR3StreamGetUsed(pStreamR3); + if (pStreamShared->State.fInputPreBuffered && cbStreamUsed >= cbPeriod) + { /*likely*/ } + /* + * Because it may take a while for the input stream to get going (at + * least with pulseaudio), we feed the guest silence till we've + * pre-buffer a reasonable amount of audio. + */ + else if (!pStreamShared->State.fInputPreBuffered) + { + if (cbStreamUsed < pStreamShared->State.cbInputPreBuffer) + { + Log3(("hdaR3StreamUpdateDma: Pre-buffering (got %#x out of %#x bytes)...\n", + cbStreamUsed, pStreamShared->State.cbInputPreBuffer)); + fWriteSilence = true; + } + else + { + Log3(("hdaR3StreamUpdateDma: Completed pre-buffering (got %#x, needed %#x bytes).\n", + cbStreamUsed, pStreamShared->State.cbInputPreBuffer)); + pStreamShared->State.fInputPreBuffered = true; + fWriteSilence = true; /* For now, just do the most conservative thing. */ + } + cbStreamUsed = cbPeriod; + } + /* + * When we're low on data, we must really try fetch some ourselves + * as buffer underruns must not happen. + */ + else + { + /** @todo We're ending up here to frequently with pulse audio at least (just + * watch the stream stats in the statistcs viewer, and way to often we + * have to inject silence bytes. I suspect part of the problem is + * that the HDA device require a much better latency than what the + * pulse audio is configured for by default (10 ms vs 150ms). */ + STAM_REL_COUNTER_INC(&pStreamR3->State.StatDmaFlowProblems); + Log(("hdaR3StreamUpdateDma: Warning! Stream #%u has insufficient data available: %u bytes, need %u. Will try move pull more data into the buffer...\n", + pStreamShared->u8SD, cbStreamUsed, cbPeriod)); + int rc = AudioMixerSinkTryLock(pSink); + if (RT_SUCCESS(rc)) + { + AudioMixerSinkUpdate(pSink, cbStreamUsed, cbPeriod); + hdaR3StreamPullFromMixer(pStreamShared, pStreamR3, pSink); + AudioMixerSinkUnlock(pSink); + } + else + RTThreadYield(); + Log(("hdaR3StreamUpdateDma: Gained %u bytes.\n", hdaR3StreamGetUsed(pStreamR3) - cbStreamUsed)); + cbStreamUsed = hdaR3StreamGetUsed(pStreamR3); + if (cbStreamUsed < cbPeriod) + { + /* Unable to find sufficient input data by simple prodding. + In order to keep a constant byte stream following thru the DMA + engine into the guest, we will try again and then fall back on + filling the gap with silence. */ + uint32_t cbSilence = 0; + do + { + AudioMixerSinkLock(pSink); + + cbStreamUsed = hdaR3StreamGetUsed(pStreamR3); + if (cbStreamUsed < cbPeriod) + { + hdaR3StreamPullFromMixer(pStreamShared, pStreamR3, pSink); + cbStreamUsed = hdaR3StreamGetUsed(pStreamR3); + while (cbStreamUsed < cbPeriod) + { + void *pvDstBuf; + size_t cbDstBuf; + RTCircBufAcquireWriteBlock(pStreamR3->State.pCircBuf, cbPeriod - cbStreamUsed, + &pvDstBuf, &cbDstBuf); + RT_BZERO(pvDstBuf, cbDstBuf); + RTCircBufReleaseWriteBlock(pStreamR3->State.pCircBuf, cbDstBuf); + cbSilence += (uint32_t)cbDstBuf; + cbStreamUsed += (uint32_t)cbDstBuf; + } + } + + AudioMixerSinkUnlock(pSink); + } while (cbStreamUsed < cbPeriod); + if (cbSilence > 0) + { + STAM_REL_COUNTER_INC(&pStreamR3->State.StatDmaFlowErrors); + STAM_REL_COUNTER_ADD(&pStreamR3->State.StatDmaFlowErrorBytes, cbSilence); + LogRel2(("HDA: Warning: Stream #%RU8 underrun, added %u bytes of silence (%u us)\n", pStreamShared->u8SD, + cbSilence, PDMAudioPropsBytesToMicro(&pStreamShared->State.Cfg.Props, cbSilence))); + } + } + } + + /* + * Do the DMA'ing. + */ + if (cbStreamUsed) + hdaR3StreamDoDmaInput(pDevIns, pThis, pStreamShared, pStreamR3, + RT_MIN(cbStreamUsed, cbPeriod), fWriteSilence, tsNowNs); + + /* + * We should always kick the AIO thread. + */ + /** @todo This isn't entirely ideal. If we get into an underrun situation, + * we ideally want the AIO thread to run right before the DMA timer + * rather than right after it ran. */ + Log5Func(("Notifying AIO thread\n")); + rc2 = AudioMixerSinkSignalUpdateJob(pSink); + AssertRC(rc2); + pStreamShared->State.tsLastReadNs = tsNowNs; + } +} + + +/** + * @callback_method_impl{FNAUDMIXSINKUPDATE} + * + * For output streams this moves data from the internal DMA buffer (in which + * hdaR3StreamUpdateDma put it), thru the mixer and to the various backend audio + * devices. + * + * For input streams this pulls data from the backend audio device(s), thru the + * mixer and puts it in the internal DMA buffer ready for hdaR3StreamUpdateDma + * to pump into guest memory. + */ +DECLCALLBACK(void) hdaR3StreamUpdateAsyncIoJob(PPDMDEVINS pDevIns, PAUDMIXSINK pSink, void *pvUser) +{ + PHDASTATE const pThis = PDMDEVINS_2_DATA(pDevIns, PHDASTATE); + PHDASTATER3 const pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PHDASTATER3); + PHDASTREAMR3 const pStreamR3 = (PHDASTREAMR3)pvUser; + PHDASTREAM const pStreamShared = &pThis->aStreams[pStreamR3 - &pThisCC->aStreams[0]]; + Assert(pStreamR3 - &pThisCC->aStreams[0] == pStreamR3->u8SD); + Assert(pStreamShared->u8SD == pStreamR3->u8SD); + RT_NOREF(pSink); + + /* + * Make sure we haven't change sink and that it's still active (it + * should be or we wouldn't have been called). + */ + AssertReturnVoid(pStreamR3->pMixSink && pSink == pStreamR3->pMixSink->pMixSink); + AssertReturnVoid(AudioMixerSinkIsActive(pSink)); + + /* + * Output streams (SDO). + */ + if (hdaGetDirFromSD(pStreamShared->u8SD) == PDMAUDIODIR_OUT) + hdaR3StreamPushToMixer(pStreamShared, pStreamR3, pSink, RTTimeNanoTS()); + /* + * Input stream (SDI). + */ + else + { + Assert(hdaGetDirFromSD(pStreamShared->u8SD) == PDMAUDIODIR_IN); + hdaR3StreamPullFromMixer(pStreamShared, pStreamR3, pSink); + } +} + +#endif /* IN_RING3 */ + diff --git a/src/VBox/Devices/Audio/DevHdaStream.h b/src/VBox/Devices/Audio/DevHdaStream.h new file mode 100644 index 00000000..0d1065e2 --- /dev/null +++ b/src/VBox/Devices/Audio/DevHdaStream.h @@ -0,0 +1,330 @@ +/* $Id: DevHdaStream.h $ */ +/** @file + * Intel HD Audio Controller Emulation - Streams. + */ + +/* + * Copyright (C) 2017-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 + */ + +#ifndef VBOX_INCLUDED_SRC_Audio_DevHdaStream_h +#define VBOX_INCLUDED_SRC_Audio_DevHdaStream_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#ifndef VBOX_INCLUDED_SRC_Audio_DevHda_h +# error "Only include DevHda.h!" +#endif + + +/** + * Structure containing HDA stream debug stuff, configurable at runtime. + */ +typedef struct HDASTREAMDEBUGRT +{ + /** Whether debugging is enabled or not. */ + bool fEnabled; + uint8_t Padding[7]; + /** File for dumping stream reads / writes. + * For input streams, this dumps data being written to the device FIFO, + * whereas for output streams this dumps data being read from the device FIFO. */ + R3PTRTYPE(PAUDIOHLPFILE) pFileStream; + /** File for dumping raw DMA reads / writes. + * For input streams, this dumps data being written to the device DMA, + * whereas for output streams this dumps data being read from the device DMA. */ + R3PTRTYPE(PAUDIOHLPFILE) pFileDMARaw; + /** File for dumping mapped (that is, extracted) DMA reads / writes. */ + R3PTRTYPE(PAUDIOHLPFILE) pFileDMAMapped; +} HDASTREAMDEBUGRT; + +/** + * Structure containing HDA stream debug information. + */ +typedef struct HDASTREAMDEBUG +{ + /** Runtime debug info. */ + HDASTREAMDEBUGRT Runtime; + uint64_t au64Alignment[2]; +} HDASTREAMDEBUG; + +/** + * Internal state of a HDA stream. + */ +typedef struct HDASTREAMSTATE +{ + /** Flag indicating whether this stream currently is + * in reset mode and therefore not acccessible by the guest. */ + volatile bool fInReset; + /** Flag indicating if the stream is in running state or not. */ + volatile bool fRunning; + /** How many interrupts are pending due to + * BDLE interrupt-on-completion (IOC) bits set. */ + uint8_t cTransferPendingInterrupts; + /** Input streams only: Set when we switch from feeding the guest silence and + * commits to proving actual audio input bytes. */ + bool fInputPreBuffered; + /** Input streams only: The number of bytes we need to prebuffer. */ + uint32_t cbInputPreBuffer; + /** Timestamp (absolute, in timer ticks) of the last DMA data transfer. + * @note This is used for wall clock (WALCLK) calculations. */ + uint64_t volatile tsTransferLast; + /** The stream's current configuration (matches SDnFMT). */ + PDMAUDIOSTREAMCFG Cfg; + /** Timestamp (real time, in ns) of last DMA transfer. */ + uint64_t tsLastTransferNs; + /** Timestamp (real time, in ns) of last stream read (to backends). + * When running in async I/O mode, this differs from \a tsLastTransferNs, + * because reading / processing will be done in a separate stream. */ + uint64_t tsLastReadNs; + + /** The start time for the playback (on the timer clock). */ + uint64_t tsStart; + + /** @name DMA engine + * @{ */ + /** Timestamp (absolute, in timer ticks) of the next DMA data transfer. + * Next for determining the next scheduling window. + * Can be 0 if no next transfer is scheduled. */ + uint64_t tsTransferNext; + /** The size of the current DMA transfer period. */ + uint32_t cbCurDmaPeriod; + /** The size of an average transfer. */ + uint32_t cbAvgTransfer; + + /** Current circular buffer read offset (for tracing & logging). */ + uint64_t offRead; + /** Current circular buffer write offset (for tracing & logging). */ + uint64_t offWrite; + + /** The offset into the current BDLE. */ + uint32_t offCurBdle; + /** LVI + 1 */ + uint16_t cBdles; + /** The index of the current BDLE. + * This is the entry which period is currently "running" on the DMA timer. */ + uint8_t idxCurBdle; + /** The number of prologue scheduling steps. + * This is used when the tail BDLEs doesn't have IOC set. */ + uint8_t cSchedulePrologue; + /** Number of scheduling steps. */ + uint16_t cSchedule; + /** Current scheduling step. */ + uint16_t idxSchedule; + /** Current loop number within the current scheduling step. */ + uint32_t idxScheduleLoop; + + /** Buffer descriptors and additional timer scheduling state. + * (Same as HDABDLEDESC, with more sensible naming.) */ + struct + { + /** The buffer address. */ + uint64_t GCPhys; + /** The buffer size (guest bytes). */ + uint32_t cb; + /** The flags (only bit 0 is defined). */ + uint32_t fFlags; + } aBdl[256]; + /** Scheduling steps. */ + struct + { + /** Number of timer ticks per period. + * ASSUMES that we don't need a full second and that the timer resolution + * isn't much higher than nanoseconds. */ + uint32_t cPeriodTicks; + /** The period length in host bytes. */ + uint32_t cbPeriod; + /** Number of times to repeat the period. */ + uint32_t cLoops; + /** The BDL index of the first entry. */ + uint8_t idxFirst; + /** The number of BDL entries. */ + uint8_t cEntries; + uint8_t abPadding[2]; + } aSchedule[512+8]; + +#ifdef VBOX_HDA_WITH_ON_REG_ACCESS_DMA + /** Number of valid bytes in abDma. + * @note Volatile to prevent the compiler from re-reading it after we've + * validated the value in ring-0. */ + uint32_t volatile cbDma; + /** Total number of bytes going via abDma this timer period. */ + uint32_t cbDmaTotal; + /** DMA bounce buffer for ring-0 register reads (LPIB). */ + uint8_t abDma[2048 - 8]; +#endif + /** @} */ +} HDASTREAMSTATE; +AssertCompileSizeAlignment(HDASTREAMSTATE, 16); +AssertCompileMemberAlignment(HDASTREAMSTATE, aBdl, 8); +AssertCompileMemberAlignment(HDASTREAMSTATE, aBdl, 16); +AssertCompileMemberAlignment(HDASTREAMSTATE, aSchedule, 16); + +/** + * An HDA stream (SDI / SDO) - shared. + * + * @note This HDA stream has nothing to do with a regular audio stream handled + * by the audio connector or the audio mixer. This HDA stream is a serial + * data in/out stream (SDI/SDO) defined in hardware and can contain + * multiple audio streams in one single SDI/SDO (interleaving streams). + * + * Contains only register values which do *not* change until a stream reset + * occurs. + */ +typedef struct HDASTREAM +{ + /** Internal state of this stream. */ + HDASTREAMSTATE State; + + /** Stream descriptor number (SDn). */ + uint8_t u8SD; + /** Current channel index. + * For a stereo stream, this is u8Channel + 1. */ + uint8_t u8Channel; + /** FIFO Watermark (checked + translated in bytes, FIFOW). + * This will be update from hdaRegWriteSDFIFOW() and also copied + * hdaR3StreamInit() for some reason. */ + uint8_t u8FIFOW; + + /** @name Register values at stream setup. + * These will all be copied in hdaR3StreamInit(). + * @{ */ + /** FIFO Size (checked + translated in bytes, FIFOS). + * This is supposedly the max number of bytes we'll be DMA'ing in one chunk + * and correspondingly the LPIB & wall clock update jumps. However, we're + * not at all being honest with the guest about this. */ + uint8_t u8FIFOS; + /** Cyclic Buffer Length (SDnCBL) - Represents the size of the ring buffer. */ + uint32_t u32CBL; + /** Last Valid Index (SDnLVI). */ + uint16_t u16LVI; + /** Format (SDnFMT). */ + uint16_t u16FMT; + uint8_t abPadding[4]; + /** DMA base address (SDnBDPU - SDnBDPL). */ + uint64_t u64BDLBase; + /** @} */ + + /** The timer for pumping data thru the attached LUN drivers. */ + TMTIMERHANDLE hTimer; + + /** Pad the structure size to a 64 byte alignment. */ + uint64_t au64Padding1[2]; +} HDASTREAM; +AssertCompileMemberAlignment(HDASTREAM, State.aBdl, 16); +AssertCompileMemberAlignment(HDASTREAM, State.aSchedule, 16); +AssertCompileSizeAlignment(HDASTREAM, 64); +/** Pointer to an HDA stream (SDI / SDO). */ +typedef HDASTREAM *PHDASTREAM; + + +/** + * An HDA stream (SDI / SDO) - ring-3 bits. + */ +typedef struct HDASTREAMR3 +{ + /** Stream descriptor number (SDn). */ + uint8_t u8SD; + uint8_t abPadding[7]; + /** The shared state for the parent HDA device. */ + R3PTRTYPE(PHDASTATE) pHDAStateShared; + /** The ring-3 state for the parent HDA device. */ + R3PTRTYPE(PHDASTATER3) pHDAStateR3; + /** Pointer to HDA sink this stream is attached to. */ + R3PTRTYPE(PHDAMIXERSINK) pMixSink; + /** Internal state of this stream. */ + struct + { + /** Circular buffer (FIFO) for holding DMA'ed data. */ + R3PTRTYPE(PRTCIRCBUF) pCircBuf; + /** The mixer sink this stream has registered AIO update callback with. + * This is NULL till we register it, typically in hdaR3StreamEnable. + * (The problem with following the pMixSink assignment is that hdaR3StreamReset + * sets it without updating the HDA sink structure, so things get out of + * wack in hdaR3MixerControl later in the initial device reset.) */ + PAUDMIXSINK pAioRegSink; + + /** Size of the DMA buffer (pCircBuf) in bytes. */ + uint32_t StatDmaBufSize; + /** Number of used bytes in the DMA buffer (pCircBuf). */ + uint32_t StatDmaBufUsed; + /** Counter for all under/overflows problems. */ + STAMCOUNTER StatDmaFlowProblems; + /** Counter for unresovled under/overflows problems. */ + STAMCOUNTER StatDmaFlowErrors; + /** Number of bytes involved in unresolved flow errors. */ + STAMCOUNTER StatDmaFlowErrorBytes; + /** DMA skipped because buffer interrupt pending. */ + STAMCOUNTER StatDmaSkippedPendingBcis; + + STAMPROFILE StatStart; + STAMPROFILE StatReset; + STAMPROFILE StatStop; + } State; + /** Debug bits. */ + HDASTREAMDEBUG Dbg; + uint64_t au64Alignment[3]; +} HDASTREAMR3; +AssertCompileSizeAlignment(HDASTREAMR3, 64); +/** Pointer to an HDA stream (SDI / SDO). */ +typedef HDASTREAMR3 *PHDASTREAMR3; + +/** @name Stream functions (all contexts). + * @{ + */ +VBOXSTRICTRC hdaStreamDoOnAccessDmaOutput(PPDMDEVINS pDevIns, PHDASTATE pThis, PHDASTREAM pStreamShared, + uint64_t tsNow, uint32_t cbToTransfer); +VBOXSTRICTRC hdaStreamMaybeDoOnAccessDmaOutput(PPDMDEVINS pDevIns, PHDASTATE pThis, + PHDASTREAM pStreamShared, uint64_t tsNow); +/** @} */ + +#ifdef IN_RING3 + +/** @name Stream functions (ring-3). + * @{ + */ +int hdaR3StreamConstruct(PHDASTREAM pStreamShared, PHDASTREAMR3 pStreamR3, PHDASTATE pThis, + PHDASTATER3 pThisCC, uint8_t uSD); +void hdaR3StreamDestroy(PHDASTREAMR3 pStreamR3); +int hdaR3StreamSetUp(PPDMDEVINS pDevIns, PHDASTATE pThis, PHDASTREAM pStreamShared, + PHDASTREAMR3 pStreamR3, uint8_t uSD); +void hdaR3StreamReset(PHDASTATE pThis, PHDASTATER3 pThisCC, + PHDASTREAM pStreamShared, PHDASTREAMR3 pStreamR3, uint8_t uSD); +int hdaR3StreamEnable(PHDASTATE pThis, PHDASTREAM pStreamShared, PHDASTREAMR3 pStreamR3, bool fEnable); +void hdaR3StreamMarkStarted(PPDMDEVINS pDevIns, PHDASTATE pThis, PHDASTREAM pStreamShared, uint64_t tsNow); +void hdaR3StreamMarkStopped(PHDASTREAM pStreamShared); + +uint64_t hdaR3StreamTimerMain(PPDMDEVINS pDevIns, PHDASTATE pThis, PHDASTATER3 pThisCC, + PHDASTREAM pStreamShared, PHDASTREAMR3 pStreamR3); +DECLCALLBACK(void) hdaR3StreamUpdateAsyncIoJob(PPDMDEVINS pDevIns, PAUDMIXSINK pSink, void *pvUser); +/** @} */ + +/** @name Helper functions associated with the stream code. + * @{ */ +int hdaR3SDFMTToPCMProps(uint16_t u16SDFMT, PPDMAUDIOPCMPROPS pProps); +# ifdef LOG_ENABLED +void hdaR3BDLEDumpAll(PPDMDEVINS pDevIns, PHDASTATE pThis, uint64_t u64BDLBase, uint16_t cBDLE); +# endif +/** @} */ + +#endif /* IN_RING3 */ +#endif /* !VBOX_INCLUDED_SRC_Audio_DevHdaStream_h */ + diff --git a/src/VBox/Devices/Audio/DevIchAc97.cpp b/src/VBox/Devices/Audio/DevIchAc97.cpp new file mode 100644 index 00000000..92ec2446 --- /dev/null +++ b/src/VBox/Devices/Audio/DevIchAc97.cpp @@ -0,0 +1,4800 @@ +/* $Id: DevIchAc97.cpp $ */ +/** @file + * DevIchAc97 - VBox ICH AC97 Audio Controller. + */ + +/* + * Copyright (C) 2006-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 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_DEV_AC97 +#include <VBox/log.h> +#include <VBox/vmm/pdmdev.h> +#include <VBox/vmm/pdmaudioifs.h> +#include <VBox/vmm/pdmaudioinline.h> +#include <VBox/AssertGuest.h> + +#include <iprt/assert.h> +#ifdef IN_RING3 +# ifdef DEBUG +# include <iprt/file.h> +# endif +# include <iprt/mem.h> +# include <iprt/semaphore.h> +# include <iprt/string.h> +# include <iprt/uuid.h> +# include <iprt/zero.h> +#endif + +#include "VBoxDD.h" + +#include "AudioMixBuffer.h" +#include "AudioMixer.h" +#include "AudioHlp.h" + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +/** Current saved state version. */ +#define AC97_SAVED_STATE_VERSION 1 + +/** Default timer frequency (in Hz). */ +#define AC97_TIMER_HZ_DEFAULT 100 + +/** Maximum number of streams we support. */ +#define AC97_MAX_STREAMS 3 + +/** Maximum FIFO size (in bytes) - unused. */ +#define AC97_FIFO_MAX 256 + +/** @name AC97_SR_XXX - Status Register Bits (AC97_NABM_OFF_SR, PI_SR, PO_SR, MC_SR). + * @{ */ +#define AC97_SR_FIFOE RT_BIT(4) /**< rwc, FIFO error. */ +#define AC97_SR_BCIS RT_BIT(3) /**< rwc, Buffer completion interrupt status. */ +#define AC97_SR_LVBCI RT_BIT(2) /**< rwc, Last valid buffer completion interrupt. */ +#define AC97_SR_CELV RT_BIT(1) /**< ro, Current equals last valid. */ +#define AC97_SR_DCH RT_BIT(0) /**< ro, Controller halted. */ +#define AC97_SR_VALID_MASK (RT_BIT(5) - 1) +#define AC97_SR_WCLEAR_MASK (AC97_SR_FIFOE | AC97_SR_BCIS | AC97_SR_LVBCI) +#define AC97_SR_RO_MASK (AC97_SR_DCH | AC97_SR_CELV) +#define AC97_SR_INT_MASK (AC97_SR_FIFOE | AC97_SR_BCIS | AC97_SR_LVBCI) +/** @} */ + +/** @name AC97_CR_XXX - Control Register Bits (AC97_NABM_OFF_CR, PI_CR, PO_CR, MC_CR). + * @{ */ +#define AC97_CR_IOCE RT_BIT(4) /**< rw, Interrupt On Completion Enable. */ +#define AC97_CR_FEIE RT_BIT(3) /**< rw FIFO Error Interrupt Enable. */ +#define AC97_CR_LVBIE RT_BIT(2) /**< rw Last Valid Buffer Interrupt Enable. */ +#define AC97_CR_RR RT_BIT(1) /**< rw Reset Registers. */ +#define AC97_CR_RPBM RT_BIT(0) /**< rw Run/Pause Bus Master. */ +#define AC97_CR_VALID_MASK (RT_BIT(5) - 1) +#define AC97_CR_DONT_CLEAR_MASK (AC97_CR_IOCE | AC97_CR_FEIE | AC97_CR_LVBIE) +/** @} */ + +/** @name AC97_GC_XXX - Global Control Bits (see AC97_GLOB_CNT). + * @{ */ +#define AC97_GC_WR 4 /**< rw Warm reset. */ +#define AC97_GC_CR 2 /**< rw Cold reset. */ +#define AC97_GC_VALID_MASK (RT_BIT(6) - 1) +/** @} */ + +/** @name AC97_GS_XXX - Global Status Bits (AC97_GLOB_STA). + * @{ */ +#define AC97_GS_MD3 RT_BIT(17) /**< rw */ +#define AC97_GS_AD3 RT_BIT(16) /**< rw */ +#define AC97_GS_RCS RT_BIT(15) /**< rwc */ +#define AC97_GS_B3S12 RT_BIT(14) /**< ro */ +#define AC97_GS_B2S12 RT_BIT(13) /**< ro */ +#define AC97_GS_B1S12 RT_BIT(12) /**< ro */ +#define AC97_GS_S1R1 RT_BIT(11) /**< rwc */ +#define AC97_GS_S0R1 RT_BIT(10) /**< rwc */ +#define AC97_GS_S1CR RT_BIT(9) /**< ro */ +#define AC97_GS_S0CR RT_BIT(8) /**< ro */ +#define AC97_GS_MINT RT_BIT(7) /**< ro */ +#define AC97_GS_POINT RT_BIT(6) /**< ro */ +#define AC97_GS_PIINT RT_BIT(5) /**< ro */ +#define AC97_GS_RSRVD (RT_BIT(4) | RT_BIT(3)) +#define AC97_GS_MOINT RT_BIT(2) /**< ro */ +#define AC97_GS_MIINT RT_BIT(1) /**< ro */ +#define AC97_GS_GSCI RT_BIT(0) /**< rwc */ +#define AC97_GS_RO_MASK ( AC97_GS_B3S12 \ + | AC97_GS_B2S12 \ + | AC97_GS_B1S12 \ + | AC97_GS_S1CR \ + | AC97_GS_S0CR \ + | AC97_GS_MINT \ + | AC97_GS_POINT \ + | AC97_GS_PIINT \ + | AC97_GS_RSRVD \ + | AC97_GS_MOINT \ + | AC97_GS_MIINT) +#define AC97_GS_VALID_MASK (RT_BIT(18) - 1) +#define AC97_GS_WCLEAR_MASK (AC97_GS_RCS | AC97_GS_S1R1 | AC97_GS_S0R1 | AC97_GS_GSCI) +/** @} */ + +/** @name Buffer Descriptor (BDLE, BDL). + * @{ */ +#define AC97_BD_IOC RT_BIT(31) /**< Interrupt on Completion. */ +#define AC97_BD_BUP RT_BIT(30) /**< Buffer Underrun Policy. */ + +#define AC97_BD_LEN_MASK 0xFFFF /**< Mask for the BDL buffer length. */ + +#define AC97_BD_LEN_CTL_MBZ UINT32_C(0x3fff0000) /**< Must-be-zero mask for AC97BDLE.ctl_len. */ + +#define AC97_MAX_BDLE 32 /**< Maximum number of BDLEs. */ +/** @} */ + +/** @name Extended Audio ID Register (EAID). + * @{ */ +#define AC97_EAID_VRA RT_BIT(0) /**< Variable Rate Audio. */ +#define AC97_EAID_VRM RT_BIT(3) /**< Variable Rate Mic Audio. */ +#define AC97_EAID_REV0 RT_BIT(10) /**< AC'97 revision compliance. */ +#define AC97_EAID_REV1 RT_BIT(11) /**< AC'97 revision compliance. */ +/** @} */ + +/** @name Extended Audio Control and Status Register (EACS). + * @{ */ +#define AC97_EACS_VRA RT_BIT(0) /**< Variable Rate Audio (4.2.1.1). */ +#define AC97_EACS_VRM RT_BIT(3) /**< Variable Rate Mic Audio (4.2.1.1). */ +/** @} */ + +/** @name Baseline Audio Register Set (BARS). + * @{ */ +#define AC97_BARS_VOL_MASK 0x1f /**< Volume mask for the Baseline Audio Register Set (5.7.2). */ +#define AC97_BARS_GAIN_MASK 0x0f /**< Gain mask for the Baseline Audio Register Set. */ +#define AC97_BARS_VOL_MUTE_SHIFT 15 /**< Mute bit shift for the Baseline Audio Register Set (5.7.2). */ +/** @} */ + +/** AC'97 uses 1.5dB steps, we use 0.375dB steps: 1 AC'97 step equals 4 PDM steps. */ +#define AC97_DB_FACTOR 4 + +/** @name Recording inputs? + * @{ */ +#define AC97_REC_MIC UINT8_C(0) +#define AC97_REC_CD UINT8_C(1) +#define AC97_REC_VIDEO UINT8_C(2) +#define AC97_REC_AUX UINT8_C(3) +#define AC97_REC_LINE_IN UINT8_C(4) +#define AC97_REC_STEREO_MIX UINT8_C(5) +#define AC97_REC_MONO_MIX UINT8_C(6) +#define AC97_REC_PHONE UINT8_C(7) +#define AC97_REC_MASK UINT8_C(7) +/** @} */ + +/** @name Mixer registers / NAM BAR registers? + * @{ */ +#define AC97_Reset 0x00 +#define AC97_Master_Volume_Mute 0x02 +#define AC97_Headphone_Volume_Mute 0x04 /**< Also known as AUX, see table 16, section 5.7. */ +#define AC97_Master_Volume_Mono_Mute 0x06 +#define AC97_Master_Tone_RL 0x08 +#define AC97_PC_BEEP_Volume_Mute 0x0a +#define AC97_Phone_Volume_Mute 0x0c +#define AC97_Mic_Volume_Mute 0x0e +#define AC97_Line_In_Volume_Mute 0x10 +#define AC97_CD_Volume_Mute 0x12 +#define AC97_Video_Volume_Mute 0x14 +#define AC97_Aux_Volume_Mute 0x16 +#define AC97_PCM_Out_Volume_Mute 0x18 +#define AC97_Record_Select 0x1a +#define AC97_Record_Gain_Mute 0x1c +#define AC97_Record_Gain_Mic_Mute 0x1e +#define AC97_General_Purpose 0x20 +#define AC97_3D_Control 0x22 +#define AC97_AC_97_RESERVED 0x24 +#define AC97_Powerdown_Ctrl_Stat 0x26 +#define AC97_Extended_Audio_ID 0x28 +#define AC97_Extended_Audio_Ctrl_Stat 0x2a +#define AC97_PCM_Front_DAC_Rate 0x2c +#define AC97_PCM_Surround_DAC_Rate 0x2e +#define AC97_PCM_LFE_DAC_Rate 0x30 +#define AC97_PCM_LR_ADC_Rate 0x32 +#define AC97_MIC_ADC_Rate 0x34 +#define AC97_6Ch_Vol_C_LFE_Mute 0x36 +#define AC97_6Ch_Vol_L_R_Surround_Mute 0x38 +#define AC97_Vendor_Reserved 0x58 +#define AC97_AD_Misc 0x76 +#define AC97_Vendor_ID1 0x7c +#define AC97_Vendor_ID2 0x7e +/** @} */ + +/** @name Analog Devices miscellaneous regiter bits used in AD1980. + * @{ */ +#define AC97_AD_MISC_LOSEL RT_BIT(5) /**< Surround (rear) goes to line out outputs. */ +#define AC97_AD_MISC_HPSEL RT_BIT(10) /**< PCM (front) goes to headphone outputs. */ +/** @} */ + + +/** @name BUP flag values. + * @{ */ +#define BUP_SET RT_BIT_32(0) +#define BUP_LAST RT_BIT_32(1) +/** @} */ + +/** @name AC'97 source indices. + * @note The order of these indices is fixed (also applies for saved states) for + * the moment. So make sure you know what you're done when altering this! + * @{ + */ +#define AC97SOUNDSOURCE_PI_INDEX 0 /**< PCM in */ +#define AC97SOUNDSOURCE_PO_INDEX 1 /**< PCM out */ +#define AC97SOUNDSOURCE_MC_INDEX 2 /**< Mic in */ +#define AC97SOUNDSOURCE_MAX 3 /**< Max sound sources. */ +/** @} */ + +/** Port number (offset into NABM BAR) to stream index. */ +#define AC97_PORT2IDX(a_idx) ( ((a_idx) >> 4) & 3 ) +/** Port number (offset into NABM BAR) to stream index, but no masking. */ +#define AC97_PORT2IDX_UNMASKED(a_idx) ( ((a_idx) >> 4) ) + +/** @name Stream offsets + * @{ */ +#define AC97_NABM_OFF_BDBAR 0x0 /**< Buffer Descriptor Base Address */ +#define AC97_NABM_OFF_CIV 0x4 /**< Current Index Value */ +#define AC97_NABM_OFF_LVI 0x5 /**< Last Valid Index */ +#define AC97_NABM_OFF_SR 0x6 /**< Status Register */ +#define AC97_NABM_OFF_PICB 0x8 /**< Position in Current Buffer */ +#define AC97_NABM_OFF_PIV 0xa /**< Prefetched Index Value */ +#define AC97_NABM_OFF_CR 0xb /**< Control Register */ +#define AC97_NABM_OFF_MASK 0xf /**< Mask for getting the the per-stream register. */ +/** @} */ + + +/** @name PCM in NABM BAR registers (0x00..0x0f). + * @{ */ +#define PI_BDBAR (AC97SOUNDSOURCE_PI_INDEX * 0x10 + 0x0) /**< PCM in: Buffer Descriptor Base Address */ +#define PI_CIV (AC97SOUNDSOURCE_PI_INDEX * 0x10 + 0x4) /**< PCM in: Current Index Value */ +#define PI_LVI (AC97SOUNDSOURCE_PI_INDEX * 0x10 + 0x5) /**< PCM in: Last Valid Index */ +#define PI_SR (AC97SOUNDSOURCE_PI_INDEX * 0x10 + 0x6) /**< PCM in: Status Register */ +#define PI_PICB (AC97SOUNDSOURCE_PI_INDEX * 0x10 + 0x8) /**< PCM in: Position in Current Buffer */ +#define PI_PIV (AC97SOUNDSOURCE_PI_INDEX * 0x10 + 0xa) /**< PCM in: Prefetched Index Value */ +#define PI_CR (AC97SOUNDSOURCE_PI_INDEX * 0x10 + 0xb) /**< PCM in: Control Register */ +/** @} */ + +/** @name PCM out NABM BAR registers (0x10..0x1f). + * @{ */ +#define PO_BDBAR (AC97SOUNDSOURCE_PO_INDEX * 0x10 + 0x0) /**< PCM out: Buffer Descriptor Base Address */ +#define PO_CIV (AC97SOUNDSOURCE_PO_INDEX * 0x10 + 0x4) /**< PCM out: Current Index Value */ +#define PO_LVI (AC97SOUNDSOURCE_PO_INDEX * 0x10 + 0x5) /**< PCM out: Last Valid Index */ +#define PO_SR (AC97SOUNDSOURCE_PO_INDEX * 0x10 + 0x6) /**< PCM out: Status Register */ +#define PO_PICB (AC97SOUNDSOURCE_PO_INDEX * 0x10 + 0x8) /**< PCM out: Position in Current Buffer */ +#define PO_PIV (AC97SOUNDSOURCE_PO_INDEX * 0x10 + 0xa) /**< PCM out: Prefetched Index Value */ +#define PO_CR (AC97SOUNDSOURCE_PO_INDEX * 0x10 + 0xb) /**< PCM out: Control Register */ +/** @} */ + +/** @name Mic in NABM BAR registers (0x20..0x2f). + * @{ */ +#define MC_BDBAR (AC97SOUNDSOURCE_MC_INDEX * 0x10 + 0x0) /**< PCM in: Buffer Descriptor Base Address */ +#define MC_CIV (AC97SOUNDSOURCE_MC_INDEX * 0x10 + 0x4) /**< PCM in: Current Index Value */ +#define MC_LVI (AC97SOUNDSOURCE_MC_INDEX * 0x10 + 0x5) /**< PCM in: Last Valid Index */ +#define MC_SR (AC97SOUNDSOURCE_MC_INDEX * 0x10 + 0x6) /**< PCM in: Status Register */ +#define MC_PICB (AC97SOUNDSOURCE_MC_INDEX * 0x10 + 0x8) /**< PCM in: Position in Current Buffer */ +#define MC_PIV (AC97SOUNDSOURCE_MC_INDEX * 0x10 + 0xa) /**< PCM in: Prefetched Index Value */ +#define MC_CR (AC97SOUNDSOURCE_MC_INDEX * 0x10 + 0xb) /**< PCM in: Control Register */ +/** @} */ + +/** @name Misc NABM BAR registers. + * @{ */ +/** NABMBAR: Global Control Register. + * @note This is kind of in the MIC IN area. */ +#define AC97_GLOB_CNT 0x2c +/** NABMBAR: Global Status. */ +#define AC97_GLOB_STA 0x30 +/** Codec Access Semaphore Register. */ +#define AC97_CAS 0x34 +/** @} */ + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +/** The ICH AC'97 (Intel) controller (shared). */ +typedef struct AC97STATE *PAC97STATE; +/** The ICH AC'97 (Intel) controller (ring-3). */ +typedef struct AC97STATER3 *PAC97STATER3; + +/** + * Buffer Descriptor List Entry (BDLE). + * + * (See section 3.2.1 in Intel document number 252751-001, or section 1.2.2.1 in + * Intel document number 302349-003.) + */ +typedef struct AC97BDLE +{ + /** Location of data buffer (bits 31:1). */ + uint32_t addr; + /** Flags (bits 31 + 30) and length (bits 15:0) of data buffer (in audio samples). + * @todo split up into two 16-bit fields. */ + uint32_t ctl_len; +} AC97BDLE; +AssertCompileSize(AC97BDLE, 8); +/** Pointer to BDLE. */ +typedef AC97BDLE *PAC97BDLE; + +/** + * Bus master register set for an audio stream. + * + * (See section 16.2 in Intel document 301473-002, or section 2.2 in Intel + * document 302349-003.) + */ +typedef struct AC97BMREGS +{ + uint32_t bdbar; /**< rw 0, Buffer Descriptor List: BAR (Base Address Register). */ + uint8_t civ; /**< ro 0, Current index value. */ + uint8_t lvi; /**< rw 0, Last valid index. */ + uint16_t sr; /**< rw 1, Status register. */ + uint16_t picb; /**< ro 0, Position in current buffer (samples left to process). */ + uint8_t piv; /**< ro 0, Prefetched index value. */ + uint8_t cr; /**< rw 0, Control register. */ + int32_t bd_valid; /**< Whether current BDLE is initialized or not. */ + AC97BDLE bd; /**< Current Buffer Descriptor List Entry (BDLE). */ +} AC97BMREGS; +AssertCompileSizeAlignment(AC97BMREGS, 8); +/** Pointer to the BM registers of an audio stream. */ +typedef AC97BMREGS *PAC97BMREGS; + +/** + * The internal state of an AC'97 stream. + */ +typedef struct AC97STREAMSTATE +{ + /** Critical section for this stream. */ + RTCRITSECT CritSect; + /** Circular buffer (FIFO) for holding DMA'ed data. */ + R3PTRTYPE(PRTCIRCBUF) pCircBuf; +#if HC_ARCH_BITS == 32 + uint32_t Padding; +#endif + /** Current circular buffer read offset (for tracing & logging). */ + uint64_t offRead; + /** Current circular buffer write offset (for tracing & logging). */ + uint64_t offWrite; + /** The stream's current configuration. */ + PDMAUDIOSTREAMCFG Cfg; //+108 + /** Timestamp of the last DMA data transfer. */ + uint64_t tsTransferLast; + /** Timestamp of the next DMA data transfer. + * Next for determining the next scheduling window. + * Can be 0 if no next transfer is scheduled. */ + uint64_t tsTransferNext; + /** The stream's timer Hz rate. + * This value can can be different from the device's default Hz rate, + * depending on the rate the stream expects (e.g. for 5.1 speaker setups). + * Set in R3StreamInit(). */ + uint16_t uTimerHz; + /** Set if we've registered the asynchronous update job. */ + bool fRegisteredAsyncUpdateJob; + /** Input streams only: Set when we switch from feeding the guest silence and + * commits to proving actual audio input bytes. */ + bool fInputPreBuffered; + /** This is ZERO if stream setup succeeded, otherwise it's the RTTimeNanoTS() at + * which to retry setting it up. The latter applies only to same + * parameters. */ + uint64_t nsRetrySetup; + /** Timestamp (in ns) of last stream update. */ + uint64_t tsLastUpdateNs; + + /** Size of the DMA buffer (pCircBuf) in bytes. */ + uint32_t StatDmaBufSize; + /** Number of used bytes in the DMA buffer (pCircBuf). */ + uint32_t StatDmaBufUsed; + /** Counter for all under/overflows problems. */ + STAMCOUNTER StatDmaFlowProblems; + /** Counter for unresovled under/overflows problems. */ + STAMCOUNTER StatDmaFlowErrors; + /** Number of bytes involved in unresolved flow errors. */ + STAMCOUNTER StatDmaFlowErrorBytes; + STAMCOUNTER StatDmaSkippedDch; + STAMCOUNTER StatDmaSkippedPendingBcis; + STAMPROFILE StatStart; + STAMPROFILE StatReset; + STAMPROFILE StatStop; + STAMPROFILE StatReSetUpChanged; + STAMPROFILE StatReSetUpSame; + STAMCOUNTER StatWriteLviRecover; + STAMCOUNTER StatWriteCr; +} AC97STREAMSTATE; +AssertCompileSizeAlignment(AC97STREAMSTATE, 8); +/** Pointer to internal state of an AC'97 stream. */ +typedef AC97STREAMSTATE *PAC97STREAMSTATE; + +/** + * Runtime configurable debug stuff for an AC'97 stream. + */ +typedef struct AC97STREAMDEBUGRT +{ + /** Whether debugging is enabled or not. */ + bool fEnabled; + uint8_t Padding[7]; + /** File for dumping stream reads / writes. + * For input streams, this dumps data being written to the device FIFO, + * whereas for output streams this dumps data being read from the device FIFO. */ + R3PTRTYPE(PAUDIOHLPFILE) pFileStream; + /** File for dumping DMA reads / writes. + * For input streams, this dumps data being written to the device DMA, + * whereas for output streams this dumps data being read from the device DMA. */ + R3PTRTYPE(PAUDIOHLPFILE) pFileDMA; +} AC97STREAMDEBUGRT; + +/** + * Debug stuff for an AC'97 stream. + */ +typedef struct AC97STREAMDEBUG +{ + /** Runtime debug stuff. */ + AC97STREAMDEBUGRT Runtime; +} AC97STREAMDEBUG; + +/** + * The shared AC'97 stream state. + */ +typedef struct AC97STREAM +{ + /** Bus master registers of this stream. */ + AC97BMREGS Regs; + /** Stream number (SDn). */ + uint8_t u8SD; + uint8_t abPadding0[7]; + + /** The timer for pumping data thru the attached LUN drivers. */ + TMTIMERHANDLE hTimer; + /** When the timer was armed (timer clock). */ + uint64_t uArmedTs; + /** (Virtual) clock ticks per transfer. */ + uint64_t cDmaPeriodTicks; + /** Transfer chunk size (in bytes) of a transfer period. */ + uint32_t cbDmaPeriod; + /** DMA period counter (for logging). */ + uint32_t uDmaPeriod; + + STAMCOUNTER StatWriteLvi; + STAMCOUNTER StatWriteSr1; + STAMCOUNTER StatWriteSr2; + STAMCOUNTER StatWriteBdBar; +} AC97STREAM; +AssertCompileSizeAlignment(AC97STREAM, 8); +/** Pointer to a shared AC'97 stream state. */ +typedef AC97STREAM *PAC97STREAM; + + +/** + * The ring-3 AC'97 stream state. + */ +typedef struct AC97STREAMR3 +{ + /** Stream number (SDn). */ + uint8_t u8SD; + uint8_t abPadding0[7]; + /** Internal state of this stream. */ + AC97STREAMSTATE State; + /** Debug stuff. */ + AC97STREAMDEBUG Dbg; +} AC97STREAMR3; +AssertCompileSizeAlignment(AC97STREAMR3, 8); +/** Pointer to an AC'97 stream state for ring-3. */ +typedef AC97STREAMR3 *PAC97STREAMR3; + + +/** + * A driver stream (host backend). + * + * Each driver has its own instances of audio mixer streams, which then + * can go into the same (or even different) audio mixer sinks. + */ +typedef struct AC97DRIVERSTREAM +{ + /** Associated mixer stream handle. */ + R3PTRTYPE(PAUDMIXSTREAM) pMixStrm; +} AC97DRIVERSTREAM; +/** Pointer to a driver stream. */ +typedef AC97DRIVERSTREAM *PAC97DRIVERSTREAM; + +/** + * A host backend driver (LUN). + */ +typedef struct AC97DRIVER +{ + /** Node for storing this driver in our device driver list of AC97STATE. */ + RTLISTNODER3 Node; + /** LUN # to which this driver has been assigned. */ + uint8_t uLUN; + /** Whether this driver is in an attached state or not. */ + bool fAttached; + uint8_t abPadding[6]; + /** Pointer to attached driver base interface. */ + R3PTRTYPE(PPDMIBASE) pDrvBase; + /** Audio connector interface to the underlying host backend. */ + R3PTRTYPE(PPDMIAUDIOCONNECTOR) pConnector; + /** Driver stream for line input. */ + AC97DRIVERSTREAM LineIn; + /** Driver stream for mic input. */ + AC97DRIVERSTREAM MicIn; + /** Driver stream for output. */ + AC97DRIVERSTREAM Out; + /** The LUN description. */ + char szDesc[48 - 2]; +} AC97DRIVER; +/** Pointer to a host backend driver (LUN). */ +typedef AC97DRIVER *PAC97DRIVER; + +/** + * Debug settings. + */ +typedef struct AC97STATEDEBUG +{ + /** Whether debugging is enabled or not. */ + bool fEnabled; + bool afAlignment[7]; + /** Path where to dump the debug output to. + * Can be NULL, in which the system's temporary directory will be used then. */ + R3PTRTYPE(char *) pszOutPath; +} AC97STATEDEBUG; + + +/* Codec models. */ +typedef enum AC97CODEC +{ + AC97CODEC_INVALID = 0, /**< Customary illegal zero value. */ + AC97CODEC_STAC9700, /**< SigmaTel STAC9700 */ + AC97CODEC_AD1980, /**< Analog Devices AD1980 */ + AC97CODEC_AD1981B, /**< Analog Devices AD1981B */ + AC97CODEC_32BIT_HACK = 0x7fffffff +} AC97CODEC; + + +/** + * The shared AC'97 device state. + */ +typedef struct AC97STATE +{ + /** Critical section protecting the AC'97 state. */ + PDMCRITSECT CritSect; + /** Global Control (Bus Master Control Register). */ + uint32_t glob_cnt; + /** Global Status (Bus Master Control Register). */ + uint32_t glob_sta; + /** Codec Access Semaphore Register (Bus Master Control Register). */ + uint32_t cas; + uint32_t last_samp; + uint8_t mixer_data[256]; + /** Array of AC'97 streams (parallel to AC97STATER3::aStreams). */ + AC97STREAM aStreams[AC97_MAX_STREAMS]; + /** The device timer Hz rate. Defaults to AC97_TIMER_HZ_DEFAULT_DEFAULT. */ + uint16_t uTimerHz; + /** Config: Internal input DMA buffer size override, specified in milliseconds. + * Zero means default size according to buffer and stream config. + * @sa BufSizeInMs config value. */ + uint16_t cMsCircBufIn; + /** Config: Internal output DMA buffer size override, specified in milliseconds. + * Zero means default size according to buffer and stream config. + * @sa BufSizeOutMs config value. */ + uint16_t cMsCircBufOut; + uint16_t au16Padding1[1]; + uint8_t silence[128]; + uint32_t bup_flag; + /** Codec model. */ + AC97CODEC enmCodecModel; + + /** PCI region \#0: NAM I/O ports. */ + IOMIOPORTHANDLE hIoPortsNam; + /** PCI region \#0: NANM I/O ports. */ + IOMIOPORTHANDLE hIoPortsNabm; + + STAMCOUNTER StatUnimplementedNabmReads; + STAMCOUNTER StatUnimplementedNabmWrites; + STAMCOUNTER StatUnimplementedNamReads; + STAMCOUNTER StatUnimplementedNamWrites; +#ifdef VBOX_WITH_STATISTICS + STAMPROFILE StatTimer; +#endif +} AC97STATE; +AssertCompileMemberAlignment(AC97STATE, aStreams, 8); +AssertCompileMemberAlignment(AC97STATE, StatUnimplementedNabmReads, 8); + + +/** + * The ring-3 AC'97 device state. + */ +typedef struct AC97STATER3 +{ + /** Array of AC'97 streams (parallel to AC97STATE:aStreams). */ + AC97STREAMR3 aStreams[AC97_MAX_STREAMS]; + /** R3 pointer to the device instance. */ + PPDMDEVINSR3 pDevIns; + /** List of associated LUN drivers (AC97DRIVER). */ + RTLISTANCHORR3 lstDrv; + /** The device's software mixer. */ + R3PTRTYPE(PAUDIOMIXER) pMixer; + /** Audio sink for PCM output. */ + R3PTRTYPE(PAUDMIXSINK) pSinkOut; + /** Audio sink for line input. */ + R3PTRTYPE(PAUDMIXSINK) pSinkLineIn; + /** Audio sink for microphone input. */ + R3PTRTYPE(PAUDMIXSINK) pSinkMicIn; + /** The base interface for LUN\#0. */ + PDMIBASE IBase; + /** Debug settings. */ + AC97STATEDEBUG Dbg; +} AC97STATER3; +AssertCompileMemberAlignment(AC97STATER3, aStreams, 8); +/** Pointer to the ring-3 AC'97 device state. */ +typedef AC97STATER3 *PAC97STATER3; + + +/** + * Acquires the AC'97 lock. + */ +#define DEVAC97_LOCK(a_pDevIns, a_pThis) \ + do { \ + int const rcLock = PDMDevHlpCritSectEnter((a_pDevIns), &(a_pThis)->CritSect, VERR_IGNORED); \ + PDM_CRITSECT_RELEASE_ASSERT_RC_DEV((a_pDevIns), &(a_pThis)->CritSect, rcLock); \ + } while (0) + +/** + * Acquires the AC'97 lock or returns. + */ +# define DEVAC97_LOCK_RETURN(a_pDevIns, a_pThis, a_rcBusy) \ + do { \ + int rcLock = PDMDevHlpCritSectEnter((a_pDevIns), &(a_pThis)->CritSect, a_rcBusy); \ + if (rcLock == VINF_SUCCESS) \ + { /* likely */ } \ + else \ + { \ + AssertRC(rcLock); \ + return rcLock; \ + } \ + } while (0) + +/** + * Releases the AC'97 lock. + */ +#define DEVAC97_UNLOCK(a_pDevIns, a_pThis) \ + do { PDMDevHlpCritSectLeave((a_pDevIns), &(a_pThis)->CritSect); } while (0) + + +#ifndef VBOX_DEVICE_STRUCT_TESTCASE + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ +static void ichac97StreamUpdateSR(PPDMDEVINS pDevIns, PAC97STATE pThis, PAC97STREAM pStream, uint32_t new_sr); +static uint16_t ichac97MixerGet(PAC97STATE pThis, uint32_t uMixerIdx); +#ifdef IN_RING3 +DECLINLINE(void) ichac97R3StreamLock(PAC97STREAMR3 pStreamCC); +DECLINLINE(void) ichac97R3StreamUnlock(PAC97STREAMR3 pStreamCC); +static void ichac97R3DbgPrintBdl(PPDMDEVINS pDevIns, PAC97STATE pThis, PAC97STREAM pStream, + PCDBGFINFOHLP pHlp, const char *pszPrefix); +static DECLCALLBACK(void) ichac97R3Reset(PPDMDEVINS pDevIns); +#endif + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +#ifdef IN_RING3 +/** NABM I/O port descriptions. */ +static const IOMIOPORTDESC g_aNabmPorts[] = +{ + { "PCM IN - BDBAR", "PCM IN - BDBAR", NULL, NULL }, + { "", NULL, NULL, NULL }, + { "", NULL, NULL, NULL }, + { "", NULL, NULL, NULL }, + { "PCM IN - CIV", "PCM IN - CIV", NULL, NULL }, + { "PCM IN - LVI", "PCM IN - LIV", NULL, NULL }, + { "PCM IN - SR", "PCM IN - SR", NULL, NULL }, + { "", NULL, NULL, NULL }, + { "PCM IN - PICB", "PCM IN - PICB", NULL, NULL }, + { "", NULL, NULL, NULL }, + { "PCM IN - PIV", "PCM IN - PIV", NULL, NULL }, + { "PCM IN - CR", "PCM IN - CR", NULL, NULL }, + { "", NULL, NULL, NULL }, + { "", NULL, NULL, NULL }, + { "", NULL, NULL, NULL }, + { "", NULL, NULL, NULL }, + + { "PCM OUT - BDBAR", "PCM OUT - BDBAR", NULL, NULL }, + { "", NULL, NULL, NULL }, + { "", NULL, NULL, NULL }, + { "", NULL, NULL, NULL }, + { "PCM OUT - CIV", "PCM OUT - CIV", NULL, NULL }, + { "PCM OUT - LVI", "PCM OUT - LIV", NULL, NULL }, + { "PCM OUT - SR", "PCM OUT - SR", NULL, NULL }, + { "", NULL, NULL, NULL }, + { "PCM OUT - PICB", "PCM OUT - PICB", NULL, NULL }, + { "", NULL, NULL, NULL }, + { "PCM OUT - PIV", "PCM OUT - PIV", NULL, NULL }, + { "PCM OUT - CR", "PCM IN - CR", NULL, NULL }, + { "", NULL, NULL, NULL }, + { "", NULL, NULL, NULL }, + { "", NULL, NULL, NULL }, + { "", NULL, NULL, NULL }, + + { "MIC IN - BDBAR", "MIC IN - BDBAR", NULL, NULL }, + { "", NULL, NULL, NULL }, + { "", NULL, NULL, NULL }, + { "", NULL, NULL, NULL }, + { "MIC IN - CIV", "MIC IN - CIV", NULL, NULL }, + { "MIC IN - LVI", "MIC IN - LIV", NULL, NULL }, + { "MIC IN - SR", "MIC IN - SR", NULL, NULL }, + { "", NULL, NULL, NULL }, + { "MIC IN - PICB", "MIC IN - PICB", NULL, NULL }, + { "", NULL, NULL, NULL }, + { "MIC IN - PIV", "MIC IN - PIV", NULL, NULL }, + { "MIC IN - CR", "MIC IN - CR", NULL, NULL }, + { "GLOB CNT", "GLOB CNT", NULL, NULL }, + { "", NULL, NULL, NULL }, + { "", NULL, NULL, NULL }, + { "", NULL, NULL, NULL }, + + { "GLOB STA", "GLOB STA", NULL, NULL }, + { "", NULL, NULL, NULL }, + { "", NULL, NULL, NULL }, + { "", NULL, NULL, NULL }, + { "CAS", "CAS", NULL, NULL }, + { NULL, NULL, NULL, NULL }, +}; + +/** @name Source indices + * @{ */ +# define AC97SOUNDSOURCE_PI_INDEX 0 /**< PCM in */ +# define AC97SOUNDSOURCE_PO_INDEX 1 /**< PCM out */ +# define AC97SOUNDSOURCE_MC_INDEX 2 /**< Mic in */ +# define AC97SOUNDSOURCE_MAX 3 /**< Max sound sources. */ +/** @} */ + +/** Port number (offset into NABM BAR) to stream index. */ +# define AC97_PORT2IDX(a_idx) ( ((a_idx) >> 4) & 3 ) +/** Port number (offset into NABM BAR) to stream index, but no masking. */ +# define AC97_PORT2IDX_UNMASKED(a_idx) ( ((a_idx) >> 4) ) + +/** @name Stream offsets + * @{ */ +# define AC97_NABM_OFF_BDBAR 0x0 /**< Buffer Descriptor Base Address */ +# define AC97_NABM_OFF_CIV 0x4 /**< Current Index Value */ +# define AC97_NABM_OFF_LVI 0x5 /**< Last Valid Index */ +# define AC97_NABM_OFF_SR 0x6 /**< Status Register */ +# define AC97_NABM_OFF_PICB 0x8 /**< Position in Current Buffer */ +# define AC97_NABM_OFF_PIV 0xa /**< Prefetched Index Value */ +# define AC97_NABM_OFF_CR 0xb /**< Control Register */ +# define AC97_NABM_OFF_MASK 0xf /**< Mask for getting the the per-stream register. */ +/** @} */ + +#endif /* IN_RING3 */ + + + +static void ichac97WarmReset(PAC97STATE pThis) +{ + NOREF(pThis); +} + +static void ichac97ColdReset(PAC97STATE pThis) +{ + NOREF(pThis); +} + + +#ifdef IN_RING3 + +/** + * Returns the audio direction of a specified stream descriptor. + * + * @return Audio direction. + */ +DECLINLINE(PDMAUDIODIR) ichac97R3GetDirFromSD(uint8_t uSD) +{ + switch (uSD) + { + case AC97SOUNDSOURCE_PI_INDEX: return PDMAUDIODIR_IN; + case AC97SOUNDSOURCE_PO_INDEX: return PDMAUDIODIR_OUT; + case AC97SOUNDSOURCE_MC_INDEX: return PDMAUDIODIR_IN; + } + + AssertFailed(); + return PDMAUDIODIR_UNKNOWN; +} + + +/** + * Retrieves the audio mixer sink of a corresponding AC'97 stream index. + * + * @returns Pointer to audio mixer sink if found, or NULL if not found / invalid. + * @param pThisCC The ring-3 AC'97 state. + * @param uIndex Stream index to get audio mixer sink for. + */ +DECLINLINE(PAUDMIXSINK) ichac97R3IndexToSink(PAC97STATER3 pThisCC, uint8_t uIndex) +{ + switch (uIndex) + { + case AC97SOUNDSOURCE_PI_INDEX: return pThisCC->pSinkLineIn; + case AC97SOUNDSOURCE_PO_INDEX: return pThisCC->pSinkOut; + case AC97SOUNDSOURCE_MC_INDEX: return pThisCC->pSinkMicIn; + default: + AssertMsgFailedReturn(("Wrong index %RU8\n", uIndex), NULL); + } +} + + +/********************************************************************************************************************************* +* Stream DMA * +*********************************************************************************************************************************/ + +/** + * Retrieves the available size of (buffered) audio data (in bytes) of a given AC'97 stream. + * + * @returns Available data (in bytes). + * @param pStreamCC The AC'97 stream to retrieve size for (ring-3). + */ +DECLINLINE(uint32_t) ichac97R3StreamGetUsed(PAC97STREAMR3 pStreamCC) +{ + PRTCIRCBUF const pCircBuf = pStreamCC->State.pCircBuf; + if (pCircBuf) + return (uint32_t)RTCircBufUsed(pCircBuf); + return 0; +} + + +/** + * Retrieves the free size of audio data (in bytes) of a given AC'97 stream. + * + * @returns Free data (in bytes). + * @param pStreamCC AC'97 stream to retrieve size for (ring-3). + */ +DECLINLINE(uint32_t) ichac97R3StreamGetFree(PAC97STREAMR3 pStreamCC) +{ + PRTCIRCBUF const pCircBuf = pStreamCC->State.pCircBuf; + if (pCircBuf) + return (uint32_t)RTCircBufFree(pCircBuf); + return 0; +} + + +# if 0 /* Unused */ +static void ichac97R3WriteBUP(PAC97STATE pThis, uint32_t cbElapsed) +{ + LogFlowFunc(("cbElapsed=%RU32\n", cbElapsed)); + + if (!(pThis->bup_flag & BUP_SET)) + { + if (pThis->bup_flag & BUP_LAST) + { + unsigned int i; + uint32_t *p = (uint32_t*)pThis->silence; + for (i = 0; i < sizeof(pThis->silence) / 4; i++) /** @todo r=andy Assumes 16-bit samples, stereo. */ + *p++ = pThis->last_samp; + } + else + RT_ZERO(pThis->silence); + + pThis->bup_flag |= BUP_SET; + } + + while (cbElapsed) + { + uint32_t cbToWrite = RT_MIN(cbElapsed, (uint32_t)sizeof(pThis->silence)); + uint32_t cbWrittenToStream; + + int rc2 = AudioMixerSinkWrite(pThisCC->pSinkOut, AUDMIXOP_COPY, + pThis->silence, cbToWrite, &cbWrittenToStream); + if (RT_SUCCESS(rc2)) + { + if (cbWrittenToStream < cbToWrite) /* Lagging behind? */ + LogFlowFunc(("Warning: Only written %RU32 / %RU32 bytes, expect lags\n", cbWrittenToStream, cbToWrite)); + } + + /* Always report all data as being written; + * backends who were not able to catch up have to deal with it themselves. */ + Assert(cbElapsed >= cbToWrite); + cbElapsed -= cbToWrite; + } +} +# endif /* Unused */ + + +/** + * Fetches the next buffer descriptor (BDLE) updating the stream registers. + * + * This will skip zero length descriptors. + * + * @returns Zero, or AC97_SR_BCIS if skipped zero length buffer with IOC set. + * @param pDevIns The device instance. + * @param pStream AC'97 stream to fetch BDLE for. + * @param pStreamCC The AC'97 stream, ring-3 state. + * + * @remarks Updates CIV, PIV, BD and PICB. + * + * @note Both PIV and CIV will be zero after a stream reset, so the first + * time we advance the buffer position afterwards, CIV will remain zero + * and PIV becomes 1. Thus we will start processing from BDLE00 and + * not BDLE01 as CIV=0 may lead you to think. + */ +static uint32_t ichac97R3StreamFetchNextBdle(PPDMDEVINS pDevIns, PAC97STREAM pStream, PAC97STREAMR3 pStreamCC) +{ + RT_NOREF(pStreamCC); + uint32_t fSrBcis = 0; + + /* + * Loop for skipping zero length entries. + */ + for (;;) + { + /* Advance the buffer. */ + pStream->Regs.civ = pStream->Regs.piv % AC97_MAX_BDLE /* (paranoia) */; + pStream->Regs.piv = (pStream->Regs.piv + 1) % AC97_MAX_BDLE; + + /* Load it. */ + AC97BDLE Bdle = { 0, 0 }; + PDMDevHlpPCIPhysRead(pDevIns, pStream->Regs.bdbar + pStream->Regs.civ * sizeof(AC97BDLE), &Bdle, sizeof(AC97BDLE)); + pStream->Regs.bd_valid = 1; + pStream->Regs.bd.addr = RT_H2LE_U32(Bdle.addr) & ~3; + pStream->Regs.bd.ctl_len = RT_H2LE_U32(Bdle.ctl_len); + pStream->Regs.picb = pStream->Regs.bd.ctl_len & AC97_BD_LEN_MASK; + + LogFlowFunc(("BDLE%02u: %#RX32 L %#x / LB %#x, ctl=%#06x%s%s\n", + pStream->Regs.civ, pStream->Regs.bd.addr, pStream->Regs.bd.ctl_len & AC97_BD_LEN_MASK, + (pStream->Regs.bd.ctl_len & AC97_BD_LEN_MASK) * PDMAudioPropsSampleSize(&pStreamCC->State.Cfg.Props), + pStream->Regs.bd.ctl_len >> 16, + pStream->Regs.bd.ctl_len & AC97_BD_IOC ? " ioc" : "", + pStream->Regs.bd.ctl_len & AC97_BD_BUP ? " bup" : "")); + + /* Complain about any reserved bits set in CTL and ADDR: */ + ASSERT_GUEST_MSG(!(pStream->Regs.bd.ctl_len & AC97_BD_LEN_CTL_MBZ), + ("Reserved bits set: %#RX32\n", pStream->Regs.bd.ctl_len)); + ASSERT_GUEST_MSG(!(RT_H2LE_U32(Bdle.addr) & 3), + ("Reserved addr bits set: %#RX32\n", RT_H2LE_U32(Bdle.addr) )); + + /* If the length is non-zero or if we've reached LVI, we're done regardless + of what's been loaded. Otherwise, we skip zero length buffers. */ + if (pStream->Regs.picb) + break; + if (pStream->Regs.civ == (pStream->Regs.lvi % AC97_MAX_BDLE /* (paranoia) */)) + { + LogFunc(("BDLE%02u is zero length! Can't skip (CIV=LVI). %#RX32 %#RX32\n", pStream->Regs.civ, Bdle.addr, Bdle.ctl_len)); + break; + } + LogFunc(("BDLE%02u is zero length! Skipping. %#RX32 %#RX32\n", pStream->Regs.civ, Bdle.addr, Bdle.ctl_len)); + + /* If the buffer has IOC set, make sure it's triggered by the caller. */ + if (pStream->Regs.bd.ctl_len & AC97_BD_IOC) + fSrBcis |= AC97_SR_BCIS; + } + + /* 1.2.4.2 PCM Buffer Restrictions (in 302349-003) - #1 */ + ASSERT_GUEST_MSG(!(pStream->Regs.picb & 1), + ("Odd lengths buffers are not allowed: %#x (%d) samples\n", pStream->Regs.picb, pStream->Regs.picb)); + + /* 1.2.4.2 PCM Buffer Restrictions (in 302349-003) - #2 */ + ASSERT_GUEST_MSG(pStream->Regs.picb > 0, ("Zero length buffers not allowed to terminate list (LVI=%u CIV=%u)\n", + pStream->Regs.lvi, pStream->Regs.civ)); + + return fSrBcis; +} + + +/** + * Transfers data of an AC'97 stream according to its usage (input / output). + * + * For an SDO (output) stream this means reading DMA data from the device to + * the AC'97 stream's internal FIFO buffer. + * + * For an SDI (input) stream this is reading audio data from the AC'97 stream's + * internal FIFO buffer and writing it as DMA data to the device. + * + * @returns VBox status code. + * @param pDevIns The device instance. + * @param pThis The shared AC'97 state. + * @param pStream The AC'97 stream to update (shared). + * @param pStreamCC The AC'97 stream to update (ring-3). + * @param cbToProcess The max amount of data to process (i.e. + * put into / remove from the circular buffer). + * Unless something is going seriously wrong, this + * will always be transfer size for the current + * period. The current period will never be + * larger than what can be stored in the current + * buffer (i.e. what PICB indicates). + * @param fWriteSilence Whether to write silence if this is an input + * stream (done while waiting for backend to get + * going). + * @param fInput Set if input, clear if output. + */ +static int ichac97R3StreamTransfer(PPDMDEVINS pDevIns, PAC97STATE pThis, PAC97STREAM pStream, + PAC97STREAMR3 pStreamCC, uint32_t cbToProcess, bool fWriteSilence, bool fInput) +{ + if (RT_LIKELY(cbToProcess > 0)) + Assert(PDMAudioPropsIsSizeAligned(&pStreamCC->State.Cfg.Props, cbToProcess)); + else + return VINF_SUCCESS; + + ichac97R3StreamLock(pStreamCC); + + /* + * Check that the controller is not halted (DCH) and that the buffer + * completion interrupt isn't pending. + */ + /** @todo r=bird: Why do we not just barge ahead even when BCIS is set? Can't + * find anything in spec indicating that we shouldn't. Linux shouldn't + * care if be bundle IOCs, as it checks how many steps we've taken using + * CIV. The Windows AC'97 sample driver doesn't care at all, since it + * just sets LIV to CIV-1 (thought that's probably not what the real + * windows driver does)... + * + * This is not going to sound good if it happens often enough, because + * each time we'll lose one DMA period (exact length depends on the + * buffer here). + * + * If we're going to keep this hack, there should be a + * PDMDevHlpTimerSetRelative call arm-ing the DMA timer to fire shortly + * after BCIS is cleared. Otherwise, we might lag behind even more + * before we get stuff going again. + * + * I just wish there was some clear reasoning in the source code for + * weird shit like this. This is just random voodoo. Sigh^3! */ + if (!(pStream->Regs.sr & (AC97_SR_DCH | AC97_SR_BCIS))) /* Controller halted? */ + { /* not halted nor does it have pending interrupt - likely */ } + else + { + /** @todo Stop DMA timer when DCH is set. */ + if (pStream->Regs.sr & AC97_SR_DCH) + { + STAM_REL_COUNTER_INC(&pStreamCC->State.StatDmaSkippedDch); + LogFunc(("[SD%RU8] DCH set\n", pStream->u8SD)); + } + if (pStream->Regs.sr & AC97_SR_BCIS) + { + STAM_REL_COUNTER_INC(&pStreamCC->State.StatDmaSkippedPendingBcis); + LogFunc(("[SD%RU8] BCIS set\n", pStream->u8SD)); + } + if ((pStream->Regs.cr & AC97_CR_RPBM) /* Bus master operation started. */ && !fInput) + { + /*ichac97R3WriteBUP(pThis, cbToProcess);*/ + } + + ichac97R3StreamUnlock(pStreamCC); + return VINF_SUCCESS; + } + + /* 0x1ba*2 = 0x374 (884) 0x3c0 + * Transfer loop. + */ +#ifdef LOG_ENABLED + uint32_t cbProcessedTotal = 0; +#endif + int rc = VINF_SUCCESS; + PRTCIRCBUF pCircBuf = pStreamCC->State.pCircBuf; + AssertReturnStmt(pCircBuf, ichac97R3StreamUnlock(pStreamCC), VINF_SUCCESS); + Assert((uint32_t)pStream->Regs.picb * PDMAudioPropsSampleSize(&pStreamCC->State.Cfg.Props) >= cbToProcess); + Log3Func(("[SD%RU8] cbToProcess=%#x PICB=%#x/%#x\n", pStream->u8SD, cbToProcess, + pStream->Regs.picb, pStream->Regs.picb * PDMAudioPropsSampleSize(&pStreamCC->State.Cfg.Props))); + + while (cbToProcess > 0) + { + uint32_t cbChunk = cbToProcess; + + /* + * Output. + */ + if (!fInput) + { + void *pvDst = NULL; + size_t cbDst = 0; + RTCircBufAcquireWriteBlock(pCircBuf, cbChunk, &pvDst, &cbDst); + + if (cbDst) + { + int rc2 = PDMDevHlpPCIPhysRead(pDevIns, pStream->Regs.bd.addr, pvDst, cbDst); + AssertRC(rc2); + + if (RT_LIKELY(!pStreamCC->Dbg.Runtime.pFileDMA)) + { /* likely */ } + else + AudioHlpFileWrite(pStreamCC->Dbg.Runtime.pFileDMA, pvDst, cbDst); + } + + RTCircBufReleaseWriteBlock(pCircBuf, cbDst); + + cbChunk = (uint32_t)cbDst; /* Update the current chunk size to what really has been written. */ + } + /* + * Input. + */ + else if (!fWriteSilence) + { + void *pvSrc = NULL; + size_t cbSrc = 0; + RTCircBufAcquireReadBlock(pCircBuf, cbChunk, &pvSrc, &cbSrc); + + if (cbSrc) + { + int rc2 = PDMDevHlpPCIPhysWrite(pDevIns, pStream->Regs.bd.addr, pvSrc, cbSrc); + AssertRC(rc2); + + if (RT_LIKELY(!pStreamCC->Dbg.Runtime.pFileDMA)) + { /* likely */ } + else + AudioHlpFileWrite(pStreamCC->Dbg.Runtime.pFileDMA, pvSrc, cbSrc); + } + + RTCircBufReleaseReadBlock(pCircBuf, cbSrc); + + cbChunk = (uint32_t)cbSrc; /* Update the current chunk size to what really has been read. */ + } + else + { + /* Since the format is signed 16-bit or 32-bit integer samples, we can + use g_abRTZero64K as source and avoid some unnecessary bzero() work. */ + cbChunk = RT_MIN(cbChunk, sizeof(g_abRTZero64K)); + cbChunk = PDMAudioPropsFloorBytesToFrame(&pStreamCC->State.Cfg.Props, cbChunk); + + int rc2 = PDMDevHlpPCIPhysWrite(pDevIns, pStream->Regs.bd.addr, g_abRTZero64K, cbChunk); + AssertRC(rc2); + } + + Assert(PDMAudioPropsIsSizeAligned(&pStreamCC->State.Cfg.Props, cbChunk)); + Assert(cbChunk <= cbToProcess); + + /* + * Advance. + */ + pStream->Regs.picb -= cbChunk / PDMAudioPropsSampleSize(&pStreamCC->State.Cfg.Props); + pStream->Regs.bd.addr += cbChunk; + cbToProcess -= cbChunk; +#ifdef LOG_ENABLED + cbProcessedTotal += cbChunk; +#endif + LogFlowFunc(("[SD%RU8] cbChunk=%#x, cbToProcess=%#x, cbTotal=%#x picb=%#x\n", + pStream->u8SD, cbChunk, cbToProcess, cbProcessedTotal, pStream->Regs.picb)); + } + + /* + * Fetch a new buffer descriptor if we've exhausted the current one. + */ + if (!pStream->Regs.picb) + { + uint32_t fNewSr = pStream->Regs.sr & ~AC97_SR_CELV; + + if (pStream->Regs.bd.ctl_len & AC97_BD_IOC) + fNewSr |= AC97_SR_BCIS; + + if (pStream->Regs.civ != pStream->Regs.lvi) + fNewSr |= ichac97R3StreamFetchNextBdle(pDevIns, pStream, pStreamCC); + else + { + LogFunc(("Underrun CIV (%RU8) == LVI (%RU8)\n", pStream->Regs.civ, pStream->Regs.lvi)); + fNewSr |= AC97_SR_LVBCI | AC97_SR_DCH | AC97_SR_CELV; + pThis->bup_flag = (pStream->Regs.bd.ctl_len & AC97_BD_BUP) ? BUP_LAST : 0; + /** @todo r=bird: The bup_flag isn't cleared anywhere else. We should probably + * do what the spec says, and keep writing zeros (silence). + * Alternatively, we could hope the guest will pause the DMA engine + * immediately after seeing this condition, in which case we should + * stop the DMA timer from being re-armed. */ + } + + ichac97StreamUpdateSR(pDevIns, pThis, pStream, fNewSr); + } + + ichac97R3StreamUnlock(pStreamCC); + LogFlowFuncLeaveRC(rc); + return rc; +} + + +/** + * Input streams: Pulls data from the mixer, putting it in the internal DMA + * buffer. + * + * @param pStreamR3 The AC'97 stream (ring-3 bits). + * @param pSink The mixer sink to pull from. + */ +static void ichac97R3StreamPullFromMixer(PAC97STREAMR3 pStreamR3, PAUDMIXSINK pSink) +{ +# ifdef LOG_ENABLED + uint64_t const offWriteOld = pStreamR3->State.offWrite; +# endif + pStreamR3->State.offWrite = AudioMixerSinkTransferToCircBuf(pSink, + pStreamR3->State.pCircBuf, + pStreamR3->State.offWrite, + pStreamR3->u8SD, + pStreamR3->Dbg.Runtime.fEnabled + ? pStreamR3->Dbg.Runtime.pFileStream : NULL); + + Log3Func(("[SD%RU8] transferred=%#RX64 bytes -> @%#RX64\n", pStreamR3->u8SD, + pStreamR3->State.offWrite - offWriteOld, pStreamR3->State.offWrite)); + + /* Update buffer stats. */ + pStreamR3->State.StatDmaBufUsed = (uint32_t)RTCircBufUsed(pStreamR3->State.pCircBuf); +} + + +/** + * Output streams: Pushes data to the mixer. + * + * @param pStreamR3 The AC'97 stream (ring-3 bits). + * @param pSink The mixer sink to push to. + */ +static void ichac97R3StreamPushToMixer(PAC97STREAMR3 pStreamR3, PAUDMIXSINK pSink) +{ +# ifdef LOG_ENABLED + uint64_t const offReadOld = pStreamR3->State.offRead; +# endif + pStreamR3->State.offRead = AudioMixerSinkTransferFromCircBuf(pSink, + pStreamR3->State.pCircBuf, + pStreamR3->State.offRead, + pStreamR3->u8SD, + pStreamR3->Dbg.Runtime.fEnabled + ? pStreamR3->Dbg.Runtime.pFileStream : NULL); + + Log3Func(("[SD%RU8] transferred=%#RX64 bytes -> @%#RX64\n", pStreamR3->u8SD, + pStreamR3->State.offRead - offReadOld, pStreamR3->State.offRead)); + + /* Update buffer stats. */ + pStreamR3->State.StatDmaBufUsed = (uint32_t)RTCircBufUsed(pStreamR3->State.pCircBuf); +} + + +/** + * Updates an AC'97 stream by doing its DMA transfers. + * + * The host sink(s) set the overall pace (bird: no it doesn't, the DMA timer + * does - we just hope like heck it matches the speed at which the *backend* + * host audio driver processes samples). + * + * @param pDevIns The device instance. + * @param pThis The shared AC'97 state. + * @param pThisCC The ring-3 AC'97 state. + * @param pStream The AC'97 stream to update (shared). + * @param pStreamCC The AC'97 stream to update (ring-3). + * @param pSink The sink being updated. + */ +static void ichac97R3StreamUpdateDma(PPDMDEVINS pDevIns, PAC97STATE pThis, PAC97STATER3 pThisCC, + PAC97STREAM pStream, PAC97STREAMR3 pStreamCC, PAUDMIXSINK pSink) +{ + RT_NOREF(pThisCC); + int rc2; + + /* The amount we're supposed to be transfering in this DMA period. */ + uint32_t cbPeriod = pStream->cbDmaPeriod; + + /* + * Output streams (SDO). + */ + if (pStreamCC->State.Cfg.enmDir == PDMAUDIODIR_OUT) + { + /* + * Check how much room we have in our DMA buffer. There should be at + * least one period worth of space there or we're in an overflow situation. + */ + uint32_t cbStreamFree = ichac97R3StreamGetFree(pStreamCC); + if (cbStreamFree >= cbPeriod) + { /* likely */ } + else + { + STAM_REL_COUNTER_INC(&pStreamCC->State.StatDmaFlowProblems); + LogFunc(("Warning! Stream #%u has insufficient space free: %u bytes, need %u. Will try move data out of the buffer...\n", + pStreamCC->u8SD, cbStreamFree, cbPeriod)); + int rc = AudioMixerSinkTryLock(pSink); + if (RT_SUCCESS(rc)) + { + ichac97R3StreamPushToMixer(pStreamCC, pSink); + AudioMixerSinkUpdate(pSink, 0, 0); + AudioMixerSinkUnlock(pSink); + } + else + RTThreadYield(); + LogFunc(("Gained %u bytes.\n", ichac97R3StreamGetFree(pStreamCC) - cbStreamFree)); + + cbStreamFree = ichac97R3StreamGetFree(pStreamCC); + if (cbStreamFree < cbPeriod) + { + /* Unable to make sufficient space. Drop the whole buffer content. + * This is needed in order to keep the device emulation running at a constant rate, + * at the cost of losing valid (but too much) data. */ + STAM_REL_COUNTER_INC(&pStreamCC->State.StatDmaFlowErrors); + LogRel2(("AC97: Warning: Hit stream #%RU8 overflow, dropping %u bytes of audio data\n", + pStreamCC->u8SD, ichac97R3StreamGetUsed(pStreamCC))); +# ifdef AC97_STRICT + AssertMsgFailed(("Hit stream #%RU8 overflow -- timing bug?\n", pStreamCC->u8SD)); +# endif + RTCircBufReset(pStreamCC->State.pCircBuf); + pStreamCC->State.offWrite = 0; + pStreamCC->State.offRead = 0; + cbStreamFree = ichac97R3StreamGetFree(pStreamCC); + Assert(cbStreamFree >= cbPeriod); + } + } + + /* + * Do the DMA transfer. + */ + Log3Func(("[SD%RU8] PICB=%#x samples / %RU64 ms, cbFree=%#x / %RU64 ms, cbTransferChunk=%#x / %RU64 ms\n", pStream->u8SD, + pStream->Regs.picb, PDMAudioPropsBytesToMilli(&pStreamCC->State.Cfg.Props, + PDMAudioPropsSampleSize(&pStreamCC->State.Cfg.Props) + * pStream->Regs.picb), + cbStreamFree, PDMAudioPropsBytesToMilli(&pStreamCC->State.Cfg.Props, cbStreamFree), + cbPeriod, PDMAudioPropsBytesToMilli(&pStreamCC->State.Cfg.Props, cbPeriod))); + + rc2 = ichac97R3StreamTransfer(pDevIns, pThis, pStream, pStreamCC, RT_MIN(cbStreamFree, cbPeriod), + false /*fWriteSilence*/, false /*fInput*/); + AssertRC(rc2); + + pStreamCC->State.tsLastUpdateNs = RTTimeNanoTS(); + + + /* + * Notify the AIO thread. + */ + rc2 = AudioMixerSinkSignalUpdateJob(pSink); + AssertRC(rc2); + } + /* + * Input stream (SDI). + */ + else + { + /* + * See how much data we've got buffered... + */ + bool fWriteSilence = false; + uint32_t cbStreamUsed = ichac97R3StreamGetUsed(pStreamCC); + if (pStreamCC->State.fInputPreBuffered && cbStreamUsed >= cbPeriod) + { /*likely*/ } + /* + * Because it may take a while for the input stream to get going (at least + * with pulseaudio), we feed the guest silence till we've pre-buffer a + * couple of timer Hz periods. (This avoid lots of bogus buffer underruns + * when starting an input stream and hogging the timer EMT.) + */ + else if (!pStreamCC->State.fInputPreBuffered) + { + uint32_t const cbPreBuffer = PDMAudioPropsNanoToBytes(&pStreamCC->State.Cfg.Props, + RT_NS_1SEC / pStreamCC->State.uTimerHz); + if (cbStreamUsed < cbPreBuffer) + { + Log3Func(("Pre-buffering (got %#x out of %#x bytes)...\n", cbStreamUsed, cbPreBuffer)); + fWriteSilence = true; + cbStreamUsed = cbPeriod; + } + else + { + Log3Func(("Completed pre-buffering (got %#x, needed %#x bytes).\n", cbStreamUsed, cbPreBuffer)); + pStreamCC->State.fInputPreBuffered = true; + fWriteSilence = ichac97R3StreamGetFree(pStreamCC) >= cbPreBuffer + cbPreBuffer / 2; + if (fWriteSilence) + cbStreamUsed = cbPeriod; + } + } + /* + * When we're low on data, we must really try fetch some ourselves + * as buffer underruns must not happen. + */ + else + { + STAM_REL_COUNTER_INC(&pStreamCC->State.StatDmaFlowProblems); + LogFunc(("Warning! Stream #%u has insufficient data available: %u bytes, need %u. Will try move pull more data into the buffer...\n", + pStreamCC->u8SD, cbStreamUsed, cbPeriod)); + int rc = AudioMixerSinkTryLock(pSink); + if (RT_SUCCESS(rc)) + { + AudioMixerSinkUpdate(pSink, cbStreamUsed, cbPeriod); + ichac97R3StreamPullFromMixer(pStreamCC, pSink); + AudioMixerSinkUnlock(pSink); + } + else + RTThreadYield(); + LogFunc(("Gained %u bytes.\n", ichac97R3StreamGetUsed(pStreamCC) - cbStreamUsed)); + cbStreamUsed = ichac97R3StreamGetUsed(pStreamCC); + if (cbStreamUsed < cbPeriod) + { + /* Unable to find sufficient input data by simple prodding. + In order to keep a constant byte stream following thru the DMA + engine into the guest, we will try again and then fall back on + filling the gap with silence. */ + uint32_t cbSilence = 0; + do + { + AudioMixerSinkLock(pSink); + + cbStreamUsed = ichac97R3StreamGetUsed(pStreamCC); + if (cbStreamUsed < cbPeriod) + { + ichac97R3StreamPullFromMixer(pStreamCC, pSink); + cbStreamUsed = ichac97R3StreamGetUsed(pStreamCC); + while (cbStreamUsed < cbPeriod) + { + void *pvDstBuf; + size_t cbDstBuf; + RTCircBufAcquireWriteBlock(pStreamCC->State.pCircBuf, cbPeriod - cbStreamUsed, + &pvDstBuf, &cbDstBuf); + RT_BZERO(pvDstBuf, cbDstBuf); + RTCircBufReleaseWriteBlock(pStreamCC->State.pCircBuf, cbDstBuf); + cbSilence += (uint32_t)cbDstBuf; + cbStreamUsed += (uint32_t)cbDstBuf; + } + } + + AudioMixerSinkUnlock(pSink); + } while (cbStreamUsed < cbPeriod); + if (cbSilence > 0) + { + STAM_REL_COUNTER_INC(&pStreamCC->State.StatDmaFlowErrors); + STAM_REL_COUNTER_ADD(&pStreamCC->State.StatDmaFlowErrorBytes, cbSilence); + LogRel2(("AC97: Warning: Stream #%RU8 underrun, added %u bytes of silence (%u us)\n", pStreamCC->u8SD, + cbSilence, PDMAudioPropsBytesToMicro(&pStreamCC->State.Cfg.Props, cbSilence))); + } + } + } + + /* + * Do the DMA'ing. + */ + if (cbStreamUsed) + { + rc2 = ichac97R3StreamTransfer(pDevIns, pThis, pStream, pStreamCC, RT_MIN(cbPeriod, cbStreamUsed), + fWriteSilence, true /*fInput*/); + AssertRC(rc2); + + pStreamCC->State.tsLastUpdateNs = RTTimeNanoTS(); + } + + /* + * We should always kick the AIO thread. + */ + /** @todo This isn't entirely ideal. If we get into an underrun situation, + * we ideally want the AIO thread to run right before the DMA timer + * rather than right after it ran. */ + Log5Func(("Notifying AIO thread\n")); + rc2 = AudioMixerSinkSignalUpdateJob(pSink); + AssertRC(rc2); + } +} + + +/** + * @callback_method_impl{FNAUDMIXSINKUPDATE} + * + * For output streams this moves data from the internal DMA buffer (in which + * ichac97R3StreamUpdateDma put it), thru the mixer and to the various backend + * audio devices. + * + * For input streams this pulls data from the backend audio device(s), thru the + * mixer and puts it in the internal DMA buffer ready for + * ichac97R3StreamUpdateDma to pump into guest memory. + */ +static DECLCALLBACK(void) ichac97R3StreamUpdateAsyncIoJob(PPDMDEVINS pDevIns, PAUDMIXSINK pSink, void *pvUser) +{ + PAC97STATER3 const pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PAC97STATER3); + PAC97STREAMR3 const pStreamCC = (PAC97STREAMR3)pvUser; + Assert(pStreamCC->u8SD == (uintptr_t)(pStreamCC - &pThisCC->aStreams[0])); + Assert(pSink == ichac97R3IndexToSink(pThisCC, pStreamCC->u8SD)); + RT_NOREF(pThisCC); + + /* + * Output (SDO). + */ + if (pStreamCC->State.Cfg.enmDir == PDMAUDIODIR_OUT) + ichac97R3StreamPushToMixer(pStreamCC, pSink); + /* + * Input (SDI). + */ + else + ichac97R3StreamPullFromMixer(pStreamCC, pSink); +} + + +/** + * Updates the next transfer based on a specific amount of bytes. + * + * @param pDevIns The device instance. + * @param pStream The AC'97 stream to update (shared). + * @param pStreamCC The AC'97 stream to update (ring-3). + */ +static void ichac97R3StreamTransferUpdate(PPDMDEVINS pDevIns, PAC97STREAM pStream, PAC97STREAMR3 pStreamCC) +{ + /* + * Get the number of bytes left in the current buffer. + * + * This isn't entirely optimal iff the current entry doesn't have IOC set, in + * that case we should use the number of bytes to the next IOC. Unfortuantely, + * it seems the spec doesn't allow us to prefetch more than one BDLE, so we + * probably cannot look ahead without violating that restriction. This is + * probably a purely theoretical problem at this point. + */ + uint32_t const cbLeftInBdle = pStream->Regs.picb * PDMAudioPropsSampleSize(&pStreamCC->State.Cfg.Props); + if (cbLeftInBdle > 0) /** @todo r=bird: see todo about this in ichac97R3StreamFetchBDLE. */ + { + /* + * Since the buffer can be up to 0xfffe samples long (frame aligning stereo + * prevents 0xffff), which translates to 743ms at a 44.1kHz rate, we must + * also take the nominal timer frequency into account here so we keep + * moving data at a steady rate. (In theory, I think the guest can even + * set up just one buffer and anticipate where we are in the buffer + * processing when it writes/reads from it. Linux seems to be doing such + * configs when not playing or something.) + */ + uint32_t const cbMaxPerHz = PDMAudioPropsNanoToBytes(&pStreamCC->State.Cfg.Props, RT_NS_1SEC / pStreamCC->State.uTimerHz); + + if (cbLeftInBdle <= cbMaxPerHz) + pStream->cbDmaPeriod = cbLeftInBdle; + /* Try avoid leaving a very short period at the end of a buffer. */ + else if (cbLeftInBdle >= cbMaxPerHz + cbMaxPerHz / 2) + pStream->cbDmaPeriod = cbMaxPerHz; + else + pStream->cbDmaPeriod = PDMAudioPropsFloorBytesToFrame(&pStreamCC->State.Cfg.Props, cbLeftInBdle / 2); + + /* + * Translate the chunk size to timer ticks. + */ + uint64_t const cNsXferChunk = PDMAudioPropsBytesToNano(&pStreamCC->State.Cfg.Props, pStream->cbDmaPeriod); + pStream->cDmaPeriodTicks = PDMDevHlpTimerFromNano(pDevIns, pStream->hTimer, cNsXferChunk); + Assert(pStream->cDmaPeriodTicks > 0); + + Log3Func(("[SD%RU8] cbLeftInBdle=%#RX32 cbMaxPerHz=%#RX32 (%RU16Hz) -> cbDmaPeriod=%#RX32 cDmaPeriodTicks=%RX64\n", + pStream->u8SD, cbLeftInBdle, cbMaxPerHz, pStreamCC->State.uTimerHz, pStream->cbDmaPeriod, pStream->cDmaPeriodTicks)); + } +} + + +/** + * Sets the virtual device timer to a new expiration time. + * + * @param pDevIns The device instance. + * @param pStream AC'97 stream to set timer for. + * @param cTicksToDeadline The number of ticks to the new deadline. + * + * @remarks This used to be more complicated a long time ago... + */ +DECLINLINE(void) ichac97R3TimerSet(PPDMDEVINS pDevIns, PAC97STREAM pStream, uint64_t cTicksToDeadline) +{ + int rc = PDMDevHlpTimerSetRelative(pDevIns, pStream->hTimer, cTicksToDeadline, &pStream->uArmedTs); + AssertRC(rc); +} + + +/** + * @callback_method_impl{FNTMTIMERDEV, + * Timer callback which handles the audio data transfers on a periodic basis.} + */ +static DECLCALLBACK(void) ichac97R3Timer(PPDMDEVINS pDevIns, TMTIMERHANDLE hTimer, void *pvUser) +{ + PAC97STATE pThis = PDMDEVINS_2_DATA(pDevIns, PAC97STATE); + STAM_PROFILE_START(&pThis->StatTimer, a); + PAC97STATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PAC97STATER3); + PAC97STREAM pStream = (PAC97STREAM)pvUser; + PAC97STREAMR3 pStreamCC = &RT_SAFE_SUBSCRIPT8(pThisCC->aStreams, pStream->u8SD); + Assert(hTimer == pStream->hTimer); RT_NOREF(hTimer); + + Assert(pStream - &pThis->aStreams[0] == pStream->u8SD); + Assert(PDMDevHlpCritSectIsOwner(pDevIns, &pThis->CritSect)); + Assert(PDMDevHlpTimerIsLockOwner(pDevIns, pStream->hTimer)); + + PAUDMIXSINK pSink = ichac97R3IndexToSink(pThisCC, pStream->u8SD); + if (pSink && AudioMixerSinkIsActive(pSink)) + { + ichac97R3StreamUpdateDma(pDevIns, pThis, pThisCC, pStream, pStreamCC, pSink); + + pStream->uDmaPeriod++; + ichac97R3StreamTransferUpdate(pDevIns, pStream, pStreamCC); + ichac97R3TimerSet(pDevIns, pStream, pStream->cDmaPeriodTicks); + } + + STAM_PROFILE_STOP(&pThis->StatTimer, a); +} + +#endif /* IN_RING3 */ + + +/********************************************************************************************************************************* +* AC'97 Stream Management * +*********************************************************************************************************************************/ +#ifdef IN_RING3 + +/** + * Locks an AC'97 stream for serialized access. + * + * @returns VBox status code. + * @param pStreamCC The AC'97 stream to lock (ring-3). + */ +DECLINLINE(void) ichac97R3StreamLock(PAC97STREAMR3 pStreamCC) +{ + int rc2 = RTCritSectEnter(&pStreamCC->State.CritSect); + AssertRC(rc2); +} + +/** + * Unlocks a formerly locked AC'97 stream. + * + * @returns VBox status code. + * @param pStreamCC The AC'97 stream to unlock (ring-3). + */ +DECLINLINE(void) ichac97R3StreamUnlock(PAC97STREAMR3 pStreamCC) +{ + int rc2 = RTCritSectLeave(&pStreamCC->State.CritSect); + AssertRC(rc2); +} + +#endif /* IN_RING3 */ + +/** + * Updates the status register (SR) of an AC'97 audio stream. + * + * @param pDevIns The device instance. + * @param pThis The shared AC'97 state. + * @param pStream AC'97 stream to update SR for. + * @param new_sr New value for status register (SR). + */ +static void ichac97StreamUpdateSR(PPDMDEVINS pDevIns, PAC97STATE pThis, PAC97STREAM pStream, uint32_t new_sr) +{ + bool fSignal = false; + int iIRQL = 0; + + uint32_t new_mask = new_sr & AC97_SR_INT_MASK; + uint32_t old_mask = pStream->Regs.sr & AC97_SR_INT_MASK; + + if (new_mask ^ old_mask) + { + /** @todo Is IRQ deasserted when only one of status bits is cleared? */ + if (!new_mask) + { + fSignal = true; + iIRQL = 0; + } + else if ((new_mask & AC97_SR_LVBCI) && (pStream->Regs.cr & AC97_CR_LVBIE)) + { + fSignal = true; + iIRQL = 1; + } + else if ((new_mask & AC97_SR_BCIS) && (pStream->Regs.cr & AC97_CR_IOCE)) + { + fSignal = true; + iIRQL = 1; + } + } + + pStream->Regs.sr = new_sr; + + LogFlowFunc(("IOC%d, LVB%d, sr=%#x, fSignal=%RTbool, IRQL=%d\n", + pStream->Regs.sr & AC97_SR_BCIS, pStream->Regs.sr & AC97_SR_LVBCI, pStream->Regs.sr, fSignal, iIRQL)); + + if (fSignal) + { + static uint32_t const s_aMasks[] = { AC97_GS_PIINT, AC97_GS_POINT, AC97_GS_MINT }; + Assert(pStream->u8SD < AC97_MAX_STREAMS); + if (iIRQL) + pThis->glob_sta |= s_aMasks[pStream->u8SD]; + else + pThis->glob_sta &= ~s_aMasks[pStream->u8SD]; + + LogFlowFunc(("Setting IRQ level=%d\n", iIRQL)); + PDMDevHlpPCISetIrq(pDevIns, 0, iIRQL); + } +} + +/** + * Writes a new value to a stream's status register (SR). + * + * @param pDevIns The device instance. + * @param pThis The shared AC'97 device state. + * @param pStream Stream to update SR for. + * @param u32Val New value to set the stream's SR to. + */ +static void ichac97StreamWriteSR(PPDMDEVINS pDevIns, PAC97STATE pThis, PAC97STREAM pStream, uint32_t u32Val) +{ + Log3Func(("[SD%RU8] SR <- %#x (sr %#x)\n", pStream->u8SD, u32Val, pStream->Regs.sr)); + + pStream->Regs.sr |= u32Val & ~(AC97_SR_RO_MASK | AC97_SR_WCLEAR_MASK); + ichac97StreamUpdateSR(pDevIns, pThis, pStream, pStream->Regs.sr & ~(u32Val & AC97_SR_WCLEAR_MASK)); +} + +#ifdef IN_RING3 + +/** + * Resets an AC'97 stream. + * + * @param pThis The shared AC'97 state. + * @param pStream The AC'97 stream to reset (shared). + * @param pStreamCC The AC'97 stream to reset (ring-3). + */ +static void ichac97R3StreamReset(PAC97STATE pThis, PAC97STREAM pStream, PAC97STREAMR3 pStreamCC) +{ + ichac97R3StreamLock(pStreamCC); + + LogFunc(("[SD%RU8]\n", pStream->u8SD)); + + if (pStreamCC->State.pCircBuf) + RTCircBufReset(pStreamCC->State.pCircBuf); + + pStream->Regs.bdbar = 0; + pStream->Regs.civ = 0; + pStream->Regs.lvi = 0; + + pStream->Regs.picb = 0; + pStream->Regs.piv = 0; /* Note! Because this is also zero, we will actually start transferring with BDLE00. */ + pStream->Regs.cr &= AC97_CR_DONT_CLEAR_MASK; + pStream->Regs.bd_valid = 0; + + RT_ZERO(pThis->silence); + + ichac97R3StreamUnlock(pStreamCC); +} + +/** + * Retrieves a specific driver stream of a AC'97 driver. + * + * @returns Pointer to driver stream if found, or NULL if not found. + * @param pDrv Driver to retrieve driver stream for. + * @param enmDir Stream direction to retrieve. + * @param enmPath Stream destination / source to retrieve. + */ +static PAC97DRIVERSTREAM ichac97R3MixerGetDrvStream(PAC97DRIVER pDrv, PDMAUDIODIR enmDir, PDMAUDIOPATH enmPath) +{ + if (enmDir == PDMAUDIODIR_IN) + { + LogFunc(("enmRecSource=%d\n", enmPath)); + switch (enmPath) + { + case PDMAUDIOPATH_IN_LINE: + return &pDrv->LineIn; + case PDMAUDIOPATH_IN_MIC: + return &pDrv->MicIn; + default: + AssertFailedBreak(); + } + } + else if (enmDir == PDMAUDIODIR_OUT) + { + LogFunc(("enmPlaybackDst=%d\n", enmPath)); + switch (enmPath) + { + case PDMAUDIOPATH_OUT_FRONT: + return &pDrv->Out; + default: + AssertFailedBreak(); + } + } + else + AssertFailed(); + + return NULL; +} + +/** + * Adds a driver stream to a specific mixer sink. + * + * Called by ichac97R3MixerAddDrvStreams() and ichac97R3MixerAddDrv(). + * + * @returns VBox status code. + * @param pDevIns The device instance. + * @param pMixSink Mixer sink to add driver stream to. + * @param pCfg Stream configuration to use. + * @param pDrv Driver stream to add. + */ +static int ichac97R3MixerAddDrvStream(PPDMDEVINS pDevIns, PAUDMIXSINK pMixSink, PCPDMAUDIOSTREAMCFG pCfg, PAC97DRIVER pDrv) +{ + AssertPtrReturn(pMixSink, VERR_INVALID_POINTER); + LogFunc(("[LUN#%RU8] %s\n", pDrv->uLUN, pCfg->szName)); + + int rc; + PAC97DRIVERSTREAM pDrvStream = ichac97R3MixerGetDrvStream(pDrv, pCfg->enmDir, pCfg->enmPath); + if (pDrvStream) + { + AssertMsg(pDrvStream->pMixStrm == NULL, ("[LUN#%RU8] Driver stream already present when it must not\n", pDrv->uLUN)); + + PAUDMIXSTREAM pMixStrm; + rc = AudioMixerSinkCreateStream(pMixSink, pDrv->pConnector, pCfg, pDevIns, &pMixStrm); + LogFlowFunc(("LUN#%RU8: Created stream \"%s\" for sink, rc=%Rrc\n", pDrv->uLUN, pCfg->szName, rc)); + if (RT_SUCCESS(rc)) + { + rc = AudioMixerSinkAddStream(pMixSink, pMixStrm); + LogFlowFunc(("LUN#%RU8: Added stream \"%s\" to sink, rc=%Rrc\n", pDrv->uLUN, pCfg->szName, rc)); + if (RT_SUCCESS(rc)) + pDrvStream->pMixStrm = pMixStrm; + else + AudioMixerStreamDestroy(pMixStrm, pDevIns, true /*fImmediate*/); + } + } + else + rc = VERR_INVALID_PARAMETER; + + LogFlowFuncLeaveRC(rc); + return rc; +} + + +/** + * Adds all current driver streams to a specific mixer sink. + * + * Called by ichac97R3StreamSetUp(). + * + * @returns VBox status code. + * @param pDevIns The device instance. + * @param pThisCC The ring-3 AC'97 state. + * @param pMixSink Mixer sink to add stream to. + * @param pCfg Stream configuration to use. + */ +static int ichac97R3MixerAddDrvStreams(PPDMDEVINS pDevIns, PAC97STATER3 pThisCC, PAUDMIXSINK pMixSink, PCPDMAUDIOSTREAMCFG pCfg) +{ + AssertPtrReturn(pMixSink, VERR_INVALID_POINTER); + + int rc; + if (AudioHlpStreamCfgIsValid(pCfg)) + { + rc = AudioMixerSinkSetFormat(pMixSink, &pCfg->Props, pCfg->Device.cMsSchedulingHint); + if (RT_SUCCESS(rc)) + { + PAC97DRIVER pDrv; + RTListForEach(&pThisCC->lstDrv, pDrv, AC97DRIVER, Node) + { + int rc2 = ichac97R3MixerAddDrvStream(pDevIns, pMixSink, pCfg, pDrv); + if (RT_FAILURE(rc2)) + LogFunc(("Attaching stream failed with %Rrc\n", rc2)); + + /* Do not pass failure to rc here, as there might be drivers which aren't + configured / ready yet. */ + } + } + } + else + rc = VERR_INVALID_PARAMETER; + + LogFlowFuncLeaveRC(rc); + return rc; +} + + +/** + * Removes a driver stream from a specific mixer sink. + * + * Worker for ichac97R3MixerRemoveDrvStreams. + * + * @param pDevIns The device instance. + * @param pMixSink Mixer sink to remove audio streams from. + * @param enmDir Stream direction to remove. + * @param enmPath Stream destination / source to remove. + * @param pDrv Driver stream to remove. + */ +static void ichac97R3MixerRemoveDrvStream(PPDMDEVINS pDevIns, PAUDMIXSINK pMixSink, PDMAUDIODIR enmDir, + PDMAUDIOPATH enmPath, PAC97DRIVER pDrv) +{ + PAC97DRIVERSTREAM pDrvStream = ichac97R3MixerGetDrvStream(pDrv, enmDir, enmPath); + if (pDrvStream) + { + if (pDrvStream->pMixStrm) + { + AudioMixerSinkRemoveStream(pMixSink, pDrvStream->pMixStrm); + + AudioMixerStreamDestroy(pDrvStream->pMixStrm, pDevIns, false /*fImmediate*/); + pDrvStream->pMixStrm = NULL; + } + } +} + +/** + * Removes all driver streams from a specific mixer sink. + * + * Called by ichac97R3StreamSetUp() and ichac97R3StreamsDestroy(). + * + * @param pDevIns The device instance. + * @param pThisCC The ring-3 AC'97 state. + * @param pMixSink Mixer sink to remove audio streams from. + * @param enmDir Stream direction to remove. + * @param enmPath Stream destination / source to remove. + */ +static void ichac97R3MixerRemoveDrvStreams(PPDMDEVINS pDevIns, PAC97STATER3 pThisCC, PAUDMIXSINK pMixSink, + PDMAUDIODIR enmDir, PDMAUDIOPATH enmPath) +{ + AssertPtrReturnVoid(pMixSink); + + PAC97DRIVER pDrv; + RTListForEach(&pThisCC->lstDrv, pDrv, AC97DRIVER, Node) + { + ichac97R3MixerRemoveDrvStream(pDevIns, pMixSink, enmDir, enmPath, pDrv); + } +} + + +/** + * Gets the frequency of a given stream. + * + * @returns The frequency. Zero if invalid stream index. + * @param pThis The shared AC'97 device state. + * @param idxStream The stream. + */ +DECLINLINE(uint32_t) ichach97R3CalcStreamHz(PAC97STATE pThis, uint8_t idxStream) +{ + switch (idxStream) + { + case AC97SOUNDSOURCE_PI_INDEX: + return ichac97MixerGet(pThis, AC97_PCM_LR_ADC_Rate); + + case AC97SOUNDSOURCE_MC_INDEX: + return ichac97MixerGet(pThis, AC97_MIC_ADC_Rate); + + case AC97SOUNDSOURCE_PO_INDEX: + return ichac97MixerGet(pThis, AC97_PCM_Front_DAC_Rate); + + default: + AssertMsgFailedReturn(("%d\n", idxStream), 0); + } +} + + +/** + * Gets the PCM properties for a given stream. + * + * @returns pProps. + * @param pThis The shared AC'97 device state. + * @param idxStream Which stream + * @param pProps Where to return the stream properties. + */ +DECLINLINE(PPDMAUDIOPCMPROPS) ichach97R3CalcStreamProps(PAC97STATE pThis, uint8_t idxStream, PPDMAUDIOPCMPROPS pProps) +{ + PDMAudioPropsInit(pProps, 2 /*16-bit*/, true /*signed*/, 2 /*stereo*/, ichach97R3CalcStreamHz(pThis, idxStream)); + return pProps; +} + + +/** + * Sets up an AC'97 stream with its current mixer settings. + * + * This will set up an AC'97 stream with 2 (stereo) channels, 16-bit samples and + * the last set sample rate in the AC'97 mixer for this stream. + * + * @returns VBox status code. + * @retval VINF_NO_CHANGE if the streams weren't re-created. + * + * @param pDevIns The device instance. + * @param pThis The shared AC'97 device state (shared). + * @param pThisCC The shared AC'97 device state (ring-3). + * @param pStream The AC'97 stream to open (shared). + * @param pStreamCC The AC'97 stream to open (ring-3). + * @param fForce Whether to force re-opening the stream or not. + * Otherwise re-opening only will happen if the PCM properties have changed. + * + * @remarks This is called holding: + * -# The AC'97 device lock. + * -# The AC'97 stream lock. + * -# The mixer sink lock (to prevent racing AIO thread). + */ +static int ichac97R3StreamSetUp(PPDMDEVINS pDevIns, PAC97STATE pThis, PAC97STATER3 pThisCC, PAC97STREAM pStream, + PAC97STREAMR3 pStreamCC, bool fForce) +{ + /* + * Assemble the stream config and get the associated mixer sink. + */ + PDMAUDIOPCMPROPS PropsTmp; + PDMAUDIOSTREAMCFG Cfg; + PDMAudioStrmCfgInitWithProps(&Cfg, ichach97R3CalcStreamProps(pThis, pStream->u8SD, &PropsTmp)); + Assert(Cfg.enmDir != PDMAUDIODIR_UNKNOWN); + + PAUDMIXSINK pMixSink; + switch (pStream->u8SD) + { + case AC97SOUNDSOURCE_PI_INDEX: + Cfg.enmDir = PDMAUDIODIR_IN; + Cfg.enmPath = PDMAUDIOPATH_IN_LINE; + RTStrCopy(Cfg.szName, sizeof(Cfg.szName), "Line-In"); + + pMixSink = pThisCC->pSinkLineIn; + break; + + case AC97SOUNDSOURCE_MC_INDEX: + Cfg.enmDir = PDMAUDIODIR_IN; + Cfg.enmPath = PDMAUDIOPATH_IN_MIC; + RTStrCopy(Cfg.szName, sizeof(Cfg.szName), "Mic-In"); + + pMixSink = pThisCC->pSinkMicIn; + break; + + case AC97SOUNDSOURCE_PO_INDEX: + Cfg.enmDir = PDMAUDIODIR_OUT; + Cfg.enmPath = PDMAUDIOPATH_OUT_FRONT; + RTStrCopy(Cfg.szName, sizeof(Cfg.szName), "Output"); + + pMixSink = pThisCC->pSinkOut; + break; + + default: + AssertMsgFailedReturn(("u8SD=%d\n", pStream->u8SD), VERR_INTERNAL_ERROR_3); + } + + /* + * Don't continue if the frequency is out of range (the rest of the + * properties should be okay). + * Note! Don't assert on this as we may easily end up here with Hz=0. + */ + char szTmp[PDMAUDIOSTRMCFGTOSTRING_MAX]; + if (AudioHlpStreamCfgIsValid(&Cfg)) + { } + else + { + LogFunc(("Invalid stream #%u rate: %s\n", pStreamCC->u8SD, PDMAudioStrmCfgToString(&Cfg, szTmp, sizeof(szTmp)) )); + return VERR_OUT_OF_RANGE; + } + + /* + * Read the buffer descriptors and check what the max distance between + * interrupts are, so we can more correctly size the internal DMA buffer. + * + * Note! The buffer list are not fixed once the stream starts running as + * with HDA, so this is just a general idea of what the guest is + * up to and we cannot really make much of a plan out of it. + */ + uint8_t const bLvi = pStream->Regs.lvi % AC97_MAX_BDLE /* paranoia */; + uint8_t const bCiv = pStream->Regs.civ % AC97_MAX_BDLE /* paranoia */; + uint32_t const uAddrBdl = pStream->Regs.bdbar; + + /* Linux does this a number of times while probing/whatever the device. The + IOMMU usually does allow us to read address zero, so let's skip and hope + for a better config before the guest actually wants to play/record. + (Note that bLvi and bCiv are also zero then, but I'm not entirely sure if + that can be taken to mean anything as such, as it still indicates that + BDLE00 is valid (LVI == last valid index).) */ + /** @todo Instead of refusing to read address zero, we should probably allow + * reading address zero if explicitly programmed. But, too much work now. */ + if (uAddrBdl != 0) + LogFlowFunc(("bdbar=%#x bLvi=%#x bCiv=%#x\n", uAddrBdl, bLvi, bCiv)); + else + { + LogFunc(("Invalid stream #%u: bdbar=%#x bLvi=%#x bCiv=%#x (%s)\n", pStreamCC->u8SD, uAddrBdl, bLvi, bCiv, + PDMAudioStrmCfgToString(&Cfg, szTmp, sizeof(szTmp)))); + return VERR_OUT_OF_RANGE; + } + + AC97BDLE aBdl[AC97_MAX_BDLE]; + RT_ZERO(aBdl); + PDMDevHlpPCIPhysRead(pDevIns, uAddrBdl, aBdl, sizeof(aBdl)); + + uint32_t cSamplesMax = 0; + uint32_t cSamplesMin = UINT32_MAX; + uint32_t cSamplesCur = 0; + uint32_t cSamplesTotal = 0; + uint32_t cBuffers = 1; + for (uintptr_t i = bCiv; ; cBuffers++) + { + Log2Func(("BDLE%02u: %#x LB %#x; %#x\n", i, aBdl[i].addr, aBdl[i].ctl_len & AC97_BD_LEN_MASK, aBdl[i].ctl_len >> 16)); + cSamplesTotal += aBdl[i].ctl_len & AC97_BD_LEN_MASK; + cSamplesCur += aBdl[i].ctl_len & AC97_BD_LEN_MASK; + if (aBdl[i].ctl_len & AC97_BD_IOC) + { + if (cSamplesCur > cSamplesMax) + cSamplesMax = cSamplesCur; + if (cSamplesCur < cSamplesMin) + cSamplesMin = cSamplesCur; + cSamplesCur = 0; + } + + /* Advance. */ + if (i != bLvi) + i = (i + 1) % RT_ELEMENTS(aBdl); + else + break; + } + if (!cSamplesCur) + { /* likely */ } + else if (!cSamplesMax) + { + LogFlowFunc(("%u buffers without IOC set, assuming %#x samples as the IOC period.\n", cBuffers, cSamplesMax)); + cSamplesMin = cSamplesMax = cSamplesCur; + } + else if (cSamplesCur > cSamplesMax) + { + LogFlowFunc(("final buffer is without IOC, using open period as max (%#x vs current max %#x).\n", cSamplesCur, cSamplesMax)); + cSamplesMax = cSamplesCur; + } + else + LogFlowFunc(("final buffer is without IOC, ignoring (%#x vs current max %#x).\n", cSamplesCur, cSamplesMax)); + + uint32_t const cbDmaMinBuf = cSamplesMax * PDMAudioPropsSampleSize(&Cfg.Props) * 3; /* see further down */ + uint32_t const cMsDmaMinBuf = PDMAudioPropsBytesToMilli(&Cfg.Props, cbDmaMinBuf); + LogRel3(("AC97: [SD%RU8] buffer length stats: total=%#x in %u buffers, min=%#x, max=%#x => min DMA buffer %u ms / %#x bytes\n", + pStream->u8SD, cSamplesTotal, cBuffers, cSamplesMin, cSamplesMax, cMsDmaMinBuf, cbDmaMinBuf)); + + /* + * Calculate the timer Hz / scheduling hint based on the stream frame rate. + */ + uint32_t uTimerHz; + if (pThis->uTimerHz == AC97_TIMER_HZ_DEFAULT) /* Make sure that we don't have any custom Hz rate set we want to enforce */ + { + if (Cfg.Props.uHz > 44100) /* E.g. 48000 Hz. */ + uTimerHz = 200; + else + uTimerHz = AC97_TIMER_HZ_DEFAULT; + } + else + uTimerHz = pThis->uTimerHz; + + if ( uTimerHz >= 10 + && uTimerHz <= 500) + { /* likely */ } + else + { + LogFunc(("[SD%RU8] Adjusting uTimerHz=%u to %u\n", pStream->u8SD, uTimerHz, + Cfg.Props.uHz > 44100 ? 200 : AC97_TIMER_HZ_DEFAULT)); + uTimerHz = Cfg.Props.uHz > 44100 ? 200 : AC97_TIMER_HZ_DEFAULT; + } + + /* Translate it to a scheduling hint. */ + uint32_t const cMsSchedulingHint = RT_MS_1SEC / uTimerHz; + + /* + * Calculate the circular buffer size so we can decide whether to recreate + * the stream or not. + * + * As mentioned in the HDA code, this should be at least able to hold the + * data transferred in three DMA periods and in three AIO period (whichever + * is higher). However, if we assume that the DMA code will engage the DMA + * timer thread (currently EMT) if the AIO thread isn't getting schduled to + * transfer data thru the stack, we don't need to go overboard and double + * the minimums here. The less buffer the less possible delay can build when + * TM is doing catch up. + */ + uint32_t cMsCircBuf = Cfg.enmDir == PDMAUDIODIR_IN ? pThis->cMsCircBufIn : pThis->cMsCircBufOut; + cMsCircBuf = RT_MAX(cMsCircBuf, cMsDmaMinBuf); + cMsCircBuf = RT_MAX(cMsCircBuf, cMsSchedulingHint * 3); + cMsCircBuf = RT_MIN(cMsCircBuf, RT_MS_1SEC * 2); + uint32_t const cbCircBuf = PDMAudioPropsMilliToBytes(&Cfg.Props, cMsCircBuf); + + LogFlowFunc(("Stream %u: uTimerHz: %u -> %u; cMsSchedulingHint: %u -> %u; cbCircBuf: %#zx -> %#x (%u ms, cMsDmaMinBuf=%u)%s\n", + pStreamCC->u8SD, pStreamCC->State.uTimerHz, uTimerHz, + pStreamCC->State.Cfg.Device.cMsSchedulingHint, cMsSchedulingHint, + pStreamCC->State.pCircBuf ? RTCircBufSize(pStreamCC->State.pCircBuf) : 0, cbCircBuf, cMsCircBuf, cMsDmaMinBuf, + !pStreamCC->State.pCircBuf || RTCircBufSize(pStreamCC->State.pCircBuf) != cbCircBuf ? " - re-creating DMA buffer" : "")); + + /* + * Update the stream's timer rate and scheduling hint, re-registering the AIO + * update job if necessary. + */ + if ( pStreamCC->State.Cfg.Device.cMsSchedulingHint != cMsSchedulingHint + || !pStreamCC->State.fRegisteredAsyncUpdateJob) + { + if (pStreamCC->State.fRegisteredAsyncUpdateJob) + AudioMixerSinkRemoveUpdateJob(pMixSink, ichac97R3StreamUpdateAsyncIoJob, pStreamCC); + int rc2 = AudioMixerSinkAddUpdateJob(pMixSink, ichac97R3StreamUpdateAsyncIoJob, pStreamCC, + pStreamCC->State.Cfg.Device.cMsSchedulingHint); + AssertRC(rc2); + pStreamCC->State.fRegisteredAsyncUpdateJob = RT_SUCCESS(rc2) || rc2 == VERR_ALREADY_EXISTS; + } + + pStreamCC->State.uTimerHz = uTimerHz; + Cfg.Device.cMsSchedulingHint = cMsSchedulingHint; + + /* + * Re-create the circular buffer if necessary, resetting if not. + */ + if ( pStreamCC->State.pCircBuf + && RTCircBufSize(pStreamCC->State.pCircBuf) == cbCircBuf) + RTCircBufReset(pStreamCC->State.pCircBuf); + else + { + if (pStreamCC->State.pCircBuf) + RTCircBufDestroy(pStreamCC->State.pCircBuf); + + int rc = RTCircBufCreate(&pStreamCC->State.pCircBuf, cbCircBuf); + AssertRCReturnStmt(rc, pStreamCC->State.pCircBuf = NULL, rc); + + pStreamCC->State.StatDmaBufSize = (uint32_t)RTCircBufSize(pStreamCC->State.pCircBuf); + } + Assert(pStreamCC->State.StatDmaBufSize == cbCircBuf); + + /* + * Only (re-)create the stream (and driver chain) if we really have to. + * Otherwise avoid this and just reuse it, as this costs performance. + */ + int rc = VINF_SUCCESS; + if ( fForce + || !PDMAudioStrmCfgMatchesProps(&Cfg, &pStreamCC->State.Cfg.Props) + || (pStreamCC->State.nsRetrySetup && RTTimeNanoTS() >= pStreamCC->State.nsRetrySetup)) + { + LogRel2(("AC97: Setting up stream #%u: %s\n", pStreamCC->u8SD, PDMAudioStrmCfgToString(&Cfg, szTmp, sizeof(szTmp)) )); + + ichac97R3MixerRemoveDrvStreams(pDevIns, pThisCC, pMixSink, Cfg.enmDir, Cfg.enmPath); + + rc = ichac97R3MixerAddDrvStreams(pDevIns, pThisCC, pMixSink, &Cfg); + if (RT_SUCCESS(rc)) + { + PDMAudioStrmCfgCopy(&pStreamCC->State.Cfg, &Cfg); + pStreamCC->State.nsRetrySetup = 0; + LogFlowFunc(("[SD%RU8] success (uHz=%u)\n", pStreamCC->u8SD, PDMAudioPropsHz(&Cfg.Props))); + } + else + { + LogFunc(("[SD%RU8] ichac97R3MixerAddDrvStreams failed: %Rrc (uHz=%u)\n", + pStreamCC->u8SD, rc, PDMAudioPropsHz(&Cfg.Props))); + pStreamCC->State.nsRetrySetup = RTTimeNanoTS() + 5*RT_NS_1SEC_64; /* retry in 5 seconds, unless config changes. */ + } + } + else + { + LogFlowFunc(("[SD%RU8] Skipping set-up (unchanged: %s)\n", + pStreamCC->u8SD, PDMAudioStrmCfgToString(&Cfg, szTmp, sizeof(szTmp)))); + rc = VINF_NO_CHANGE; + } + return rc; +} + + +/** + * Tears down an AC'97 stream (counter part to ichac97R3StreamSetUp). + * + * Empty stub at present, nothing to do here as we reuse streams and only really + * re-open them if parameters changed (seldom). + * + * @param pStream The AC'97 stream to close (shared). + */ +static void ichac97R3StreamTearDown(PAC97STREAM pStream) +{ + RT_NOREF(pStream); + LogFlowFunc(("[SD%RU8]\n", pStream->u8SD)); +} + + +/** + * Tears down and sets up an AC'97 stream on the backend side with the current + * AC'97 mixer settings for this stream. + * + * @returns VBox status code. + * @param pDevIns The device instance. + * @param pThis The shared AC'97 device state. + * @param pThisCC The ring-3 AC'97 device state. + * @param pStream The AC'97 stream to re-open (shared). + * @param pStreamCC The AC'97 stream to re-open (ring-3). + * @param fForce Whether to force re-opening the stream or not. + * Otherwise re-opening only will happen if the PCM properties have changed. + */ +static int ichac97R3StreamReSetUp(PPDMDEVINS pDevIns, PAC97STATE pThis, PAC97STATER3 pThisCC, + PAC97STREAM pStream, PAC97STREAMR3 pStreamCC, bool fForce) +{ + STAM_REL_PROFILE_START_NS(&pStreamCC->State.StatReSetUpChanged, r); + LogFlowFunc(("[SD%RU8]\n", pStream->u8SD)); + Assert(pStream->u8SD == pStreamCC->u8SD); + Assert(pStream - &pThis->aStreams[0] == pStream->u8SD); + Assert(pStreamCC - &pThisCC->aStreams[0] == pStream->u8SD); + + ichac97R3StreamTearDown(pStream); + int rc = ichac97R3StreamSetUp(pDevIns, pThis, pThisCC, pStream, pStreamCC, fForce); + if (rc == VINF_NO_CHANGE) + STAM_REL_PROFILE_STOP_NS(&pStreamCC->State.StatReSetUpSame, r); + else + STAM_REL_PROFILE_STOP_NS(&pStreamCC->State.StatReSetUpChanged, r); + return rc; +} + + +/** + * Enables or disables an AC'97 audio stream. + * + * @returns VBox status code. + * @param pDevIns The device instance. + * @param pThis The shared AC'97 state. + * @param pThisCC The ring-3 AC'97 state. + * @param pStream The AC'97 stream to enable or disable (shared state). + * @param pStreamCC The ring-3 stream state (matching to @a pStream). + * @param fEnable Whether to enable or disable the stream. + * + */ +static int ichac97R3StreamEnable(PPDMDEVINS pDevIns, PAC97STATE pThis, PAC97STATER3 pThisCC, + PAC97STREAM pStream, PAC97STREAMR3 pStreamCC, bool fEnable) +{ + ichac97R3StreamLock(pStreamCC); + PAUDMIXSINK const pSink = ichac97R3IndexToSink(pThisCC, pStream->u8SD); + AudioMixerSinkLock(pSink); + + int rc = VINF_SUCCESS; + /* + * Enable. + */ + if (fEnable) + { + /* Reset the input pre-buffering state and DMA period counter. */ + pStreamCC->State.fInputPreBuffered = false; + pStream->uDmaPeriod = 0; + + /* Set up (update) the AC'97 stream as needed. */ + rc = ichac97R3StreamSetUp(pDevIns, pThis, pThisCC, pStream, pStreamCC, false /* fForce */); + if (RT_SUCCESS(rc)) + { + /* Open debug files. */ + if (RT_LIKELY(!pStreamCC->Dbg.Runtime.fEnabled)) + { /* likely */ } + else + { + if (!AudioHlpFileIsOpen(pStreamCC->Dbg.Runtime.pFileStream)) + AudioHlpFileOpen(pStreamCC->Dbg.Runtime.pFileStream, AUDIOHLPFILE_DEFAULT_OPEN_FLAGS, + &pStreamCC->State.Cfg.Props); + if (!AudioHlpFileIsOpen(pStreamCC->Dbg.Runtime.pFileDMA)) + AudioHlpFileOpen(pStreamCC->Dbg.Runtime.pFileDMA, AUDIOHLPFILE_DEFAULT_OPEN_FLAGS, + &pStreamCC->State.Cfg.Props); + } + + /* Do the actual enabling (won't fail as long as pSink is valid). */ + rc = AudioMixerSinkStart(pSink); + } + } + /* + * Disable + */ + else + { + rc = AudioMixerSinkDrainAndStop(pSink, pStreamCC->State.pCircBuf ? (uint32_t)RTCircBufUsed(pStreamCC->State.pCircBuf) : 0); + ichac97R3StreamTearDown(pStream); + } + + /* Make sure to leave the lock before (eventually) starting the timer. */ + AudioMixerSinkUnlock(pSink); + ichac97R3StreamUnlock(pStreamCC); + LogFunc(("[SD%RU8] fEnable=%RTbool, rc=%Rrc\n", pStream->u8SD, fEnable, rc)); + return rc; +} + + +/** + * Returns whether an AC'97 stream is enabled or not. + * + * Only used by ichac97R3SaveExec(). + * + * @returns VBox status code. + * @param pThisCC The ring-3 AC'97 device state. + * @param pStream Stream to return status for. + */ +static bool ichac97R3StreamIsEnabled(PAC97STATER3 pThisCC, PAC97STREAM pStream) +{ + PAUDMIXSINK pSink = ichac97R3IndexToSink(pThisCC, pStream->u8SD); + bool fIsEnabled = pSink && (AudioMixerSinkGetStatus(pSink) & AUDMIXSINK_STS_RUNNING); + + LogFunc(("[SD%RU8] fIsEnabled=%RTbool\n", pStream->u8SD, fIsEnabled)); + return fIsEnabled; +} + + +/** + * Terminates an AC'97 audio stream (VM destroy). + * + * This is called by ichac97R3StreamsDestroy during VM poweroff & destruction. + * + * @returns VBox status code. + * @param pThisCC The ring-3 AC'97 state. + * @param pStream The AC'97 stream to destroy (shared). + * @param pStreamCC The AC'97 stream to destroy (ring-3). + * @sa ichac97R3StreamConstruct + */ +static void ichac97R3StreamDestroy(PAC97STATER3 pThisCC, PAC97STREAM pStream, PAC97STREAMR3 pStreamCC) +{ + LogFlowFunc(("[SD%RU8]\n", pStream->u8SD)); + + ichac97R3StreamTearDown(pStream); + + int rc2 = RTCritSectDelete(&pStreamCC->State.CritSect); + AssertRC(rc2); + + if (pStreamCC->State.fRegisteredAsyncUpdateJob) + { + PAUDMIXSINK pSink = ichac97R3IndexToSink(pThisCC, pStream->u8SD); + if (pSink) + AudioMixerSinkRemoveUpdateJob(pSink, ichac97R3StreamUpdateAsyncIoJob, pStreamCC); + pStreamCC->State.fRegisteredAsyncUpdateJob = false; + } + + if (RT_LIKELY(!pStreamCC->Dbg.Runtime.fEnabled)) + { /* likely */ } + else + { + AudioHlpFileDestroy(pStreamCC->Dbg.Runtime.pFileStream); + pStreamCC->Dbg.Runtime.pFileStream = NULL; + + AudioHlpFileDestroy(pStreamCC->Dbg.Runtime.pFileDMA); + pStreamCC->Dbg.Runtime.pFileDMA = NULL; + } + + if (pStreamCC->State.pCircBuf) + { + RTCircBufDestroy(pStreamCC->State.pCircBuf); + pStreamCC->State.pCircBuf = NULL; + } + + LogFlowFuncLeave(); +} + + +/** + * Initializes an AC'97 audio stream (VM construct). + * + * This is only called by ichac97R3Construct. + * + * @returns VBox status code. + * @param pThisCC The ring-3 AC'97 state. + * @param pStream The AC'97 stream to create (shared). + * @param pStreamCC The AC'97 stream to create (ring-3). + * @param u8SD Stream descriptor number to assign. + * @sa ichac97R3StreamDestroy + */ +static int ichac97R3StreamConstruct(PAC97STATER3 pThisCC, PAC97STREAM pStream, PAC97STREAMR3 pStreamCC, uint8_t u8SD) +{ + LogFunc(("[SD%RU8] pStream=%p\n", u8SD, pStream)); + + AssertReturn(u8SD < AC97_MAX_STREAMS, VERR_INVALID_PARAMETER); + pStream->u8SD = u8SD; + pStreamCC->u8SD = u8SD; + + int rc = RTCritSectInit(&pStreamCC->State.CritSect); + AssertRCReturn(rc, rc); + + pStreamCC->Dbg.Runtime.fEnabled = pThisCC->Dbg.fEnabled; + + if (RT_LIKELY(!pStreamCC->Dbg.Runtime.fEnabled)) + { /* likely */ } + else + { + int rc2 = AudioHlpFileCreateF(&pStreamCC->Dbg.Runtime.pFileStream, AUDIOHLPFILE_FLAGS_NONE, AUDIOHLPFILETYPE_WAV, + pThisCC->Dbg.pszOutPath, AUDIOHLPFILENAME_FLAGS_NONE, 0 /*uInstance*/, + ichac97R3GetDirFromSD(pStream->u8SD) == PDMAUDIODIR_IN + ? "ac97StreamWriteSD%RU8" : "ac97StreamReadSD%RU8", pStream->u8SD); + AssertRC(rc2); + + rc2 = AudioHlpFileCreateF(&pStreamCC->Dbg.Runtime.pFileDMA, AUDIOHLPFILE_FLAGS_NONE, AUDIOHLPFILETYPE_WAV, + pThisCC->Dbg.pszOutPath, AUDIOHLPFILENAME_FLAGS_NONE, 0 /*uInstance*/, + ichac97R3GetDirFromSD(pStream->u8SD) == PDMAUDIODIR_IN + ? "ac97DMAWriteSD%RU8" : "ac97DMAReadSD%RU8", pStream->u8SD); + AssertRC(rc2); + + /* Delete stale debugging files from a former run. */ + AudioHlpFileDelete(pStreamCC->Dbg.Runtime.pFileStream); + AudioHlpFileDelete(pStreamCC->Dbg.Runtime.pFileDMA); + } + + return rc; +} + +#endif /* IN_RING3 */ + + +/********************************************************************************************************************************* +* NABM I/O Port Handlers (Global + Stream) * +*********************************************************************************************************************************/ + +/** + * @callback_method_impl{FNIOMIOPORTNEWIN} + */ +static DECLCALLBACK(VBOXSTRICTRC) +ichac97IoPortNabmRead(PPDMDEVINS pDevIns, void *pvUser, RTIOPORT offPort, uint32_t *pu32, unsigned cb) +{ + PAC97STATE pThis = PDMDEVINS_2_DATA(pDevIns, PAC97STATE); + RT_NOREF(pvUser); + + DEVAC97_LOCK_RETURN(pDevIns, pThis, VINF_IOM_R3_IOPORT_READ); + + /* Get the index of the NABMBAR port. */ + if ( AC97_PORT2IDX_UNMASKED(offPort) < AC97_MAX_STREAMS + && offPort != AC97_GLOB_CNT) + { + PAC97STREAM pStream = &pThis->aStreams[AC97_PORT2IDX(offPort)]; + + switch (cb) + { + case 1: + switch (offPort & AC97_NABM_OFF_MASK) + { + case AC97_NABM_OFF_CIV: + /* Current Index Value Register */ + *pu32 = pStream->Regs.civ; + Log3Func(("CIV[%d] -> %#x\n", AC97_PORT2IDX(offPort), *pu32)); + break; + case AC97_NABM_OFF_LVI: + /* Last Valid Index Register */ + *pu32 = pStream->Regs.lvi; + Log3Func(("LVI[%d] -> %#x\n", AC97_PORT2IDX(offPort), *pu32)); + break; + case AC97_NABM_OFF_PIV: + /* Prefetched Index Value Register */ + *pu32 = pStream->Regs.piv; + Log3Func(("PIV[%d] -> %#x\n", AC97_PORT2IDX(offPort), *pu32)); + break; + case AC97_NABM_OFF_CR: + /* Control Register */ + *pu32 = pStream->Regs.cr; + Log3Func(("CR[%d] -> %#x\n", AC97_PORT2IDX(offPort), *pu32)); + break; + case AC97_NABM_OFF_SR: + /* Status Register (lower part) */ + *pu32 = RT_LO_U8(pStream->Regs.sr); + Log3Func(("SRb[%d] -> %#x\n", AC97_PORT2IDX(offPort), *pu32)); + break; + default: + *pu32 = UINT32_MAX; + LogRel2(("AC97: Warning: Unimplemented NAMB read offPort=%#x LB 1 (line " RT_XSTR(__LINE__) ")\n", offPort)); + STAM_REL_COUNTER_INC(&pThis->StatUnimplementedNabmReads); + break; + } + break; + + case 2: + switch (offPort & AC97_NABM_OFF_MASK) + { + case AC97_NABM_OFF_SR: + /* Status Register */ + *pu32 = pStream->Regs.sr; + Log3Func(("SR[%d] -> %#x\n", AC97_PORT2IDX(offPort), *pu32)); + break; + case AC97_NABM_OFF_PICB: + /* Position in Current Buffer + * --- + * We can do DMA work here if we want to give the guest a better impression of + * the DMA engine of a real device. For ring-0 we'd have to add some buffering + * to AC97STREAM (4K or so), only going to ring-3 if full. Ring-3 would commit + * that buffer and write directly to the internal DMA pCircBuf. + * + * Checking a Linux guest (knoppix 8.6.2), I see some PIC reads each DMA cycle, + * however most of these happen very very early, 1-10% into the buffer. So, I'm + * not sure if it's worth it, as it'll be a big complication... */ +#if 1 + *pu32 = pStream->Regs.picb; +# ifdef LOG_ENABLED + if (LogIs3Enabled()) + { + uint64_t offPeriod = PDMDevHlpTimerGet(pDevIns, pStream->hTimer) - pStream->uArmedTs; + Log3Func(("PICB[%d] -> %#x (%RU64 of %RU64 ticks / %RU64%% into DMA period #%RU32)\n", + AC97_PORT2IDX(offPort), *pu32, offPeriod, pStream->cDmaPeriodTicks, + pStream->cDmaPeriodTicks ? offPeriod * 100 / pStream->cDmaPeriodTicks : 0, + pStream->uDmaPeriod)); + } +# endif +#else /* For trying out sub-buffer PICB. Will cause distortions, but can be helpful to see if it help eliminate other issues. */ + if ( (pStream->Regs.cr & AC97_CR_RPBM) + && !(pStream->Regs.sr & AC97_SR_DCH) + && pStream->uArmedTs > 0 + && pStream->cDmaPeriodTicks > 0) + { + uint64_t const offPeriod = PDMDevHlpTimerGet(pDevIns, pStream->hTimer) - pStream->uArmedTs; + uint32_t cSamples; + if (offPeriod < pStream->cDmaPeriodTicks) + cSamples = pStream->Regs.picb * offPeriod / pStream->cDmaPeriodTicks; + else + cSamples = pStream->Regs.picb; + if (cSamples + 8 < pStream->Regs.picb) + { /* likely */ } + else if (pStream->Regs.picb > 8) + cSamples = pStream->Regs.picb - 8; + else + cSamples = 0; + *pu32 = pStream->Regs.picb - cSamples; + Log3Func(("PICB[%d] -> %#x (PICB=%#x cSamples=%#x offPeriod=%RU64 of %RU64 / %RU64%%)\n", + AC97_PORT2IDX(offPort), *pu32, pStream->Regs.picb, cSamples, offPeriod, + pStream->cDmaPeriodTicks, offPeriod * 100 / pStream->cDmaPeriodTicks)); + } + else + { + *pu32 = pStream->Regs.picb; + Log3Func(("PICB[%d] -> %#x\n", AC97_PORT2IDX(offPort), *pu32)); + } +#endif + break; + default: + *pu32 = UINT32_MAX; + LogRel2(("AC97: Warning: Unimplemented NAMB read offPort=%#x LB 2 (line " RT_XSTR(__LINE__) ")\n", offPort)); + STAM_REL_COUNTER_INC(&pThis->StatUnimplementedNabmReads); + break; + } + break; + + case 4: + switch (offPort & AC97_NABM_OFF_MASK) + { + case AC97_NABM_OFF_BDBAR: + /* Buffer Descriptor Base Address Register */ + *pu32 = pStream->Regs.bdbar; + Log3Func(("BMADDR[%d] -> %#x\n", AC97_PORT2IDX(offPort), *pu32)); + break; + case AC97_NABM_OFF_CIV: + /* 32-bit access: Current Index Value Register + + * Last Valid Index Register + + * Status Register */ + *pu32 = pStream->Regs.civ | ((uint32_t)pStream->Regs.lvi << 8) | ((uint32_t)pStream->Regs.sr << 16); + Log3Func(("CIV LVI SR[%d] -> %#x, %#x, %#x\n", + AC97_PORT2IDX(offPort), pStream->Regs.civ, pStream->Regs.lvi, pStream->Regs.sr)); + break; + case AC97_NABM_OFF_PICB: + /* 32-bit access: Position in Current Buffer Register + + * Prefetched Index Value Register + + * Control Register */ + *pu32 = pStream->Regs.picb | ((uint32_t)pStream->Regs.piv << 16) | ((uint32_t)pStream->Regs.cr << 24); + Log3Func(("PICB PIV CR[%d] -> %#x %#x %#x %#x\n", + AC97_PORT2IDX(offPort), *pu32, pStream->Regs.picb, pStream->Regs.piv, pStream->Regs.cr)); + break; + + default: + *pu32 = UINT32_MAX; + LogRel2(("AC97: Warning: Unimplemented NAMB read offPort=%#x LB 4 (line " RT_XSTR(__LINE__) ")\n", offPort)); + STAM_REL_COUNTER_INC(&pThis->StatUnimplementedNabmReads); + break; + } + break; + + default: + DEVAC97_UNLOCK(pDevIns, pThis); + AssertFailed(); + return VERR_IOM_IOPORT_UNUSED; + } + } + else + { + switch (cb) + { + case 1: + switch (offPort) + { + case AC97_CAS: + /* Codec Access Semaphore Register */ + Log3Func(("CAS %d\n", pThis->cas)); + *pu32 = pThis->cas; + pThis->cas = 1; + break; + default: + *pu32 = UINT32_MAX; + LogRel2(("AC97: Warning: Unimplemented NAMB read offPort=%#x LB 1 (line " RT_XSTR(__LINE__) ")\n", offPort)); + STAM_REL_COUNTER_INC(&pThis->StatUnimplementedNabmReads); + break; + } + break; + + case 2: + *pu32 = UINT32_MAX; + LogRel2(("AC97: Warning: Unimplemented NAMB read offPort=%#x LB 2 (line " RT_XSTR(__LINE__) ")\n", offPort)); + STAM_REL_COUNTER_INC(&pThis->StatUnimplementedNabmReads); + break; + + case 4: + switch (offPort) + { + case AC97_GLOB_CNT: + /* Global Control */ + *pu32 = pThis->glob_cnt; + Log3Func(("glob_cnt -> %#x\n", *pu32)); + break; + case AC97_GLOB_STA: + /* Global Status */ + *pu32 = pThis->glob_sta | AC97_GS_S0CR; + Log3Func(("glob_sta -> %#x\n", *pu32)); + break; + default: + *pu32 = UINT32_MAX; + LogRel2(("AC97: Warning: Unimplemented NAMB read offPort=%#x LB 4 (line " RT_XSTR(__LINE__) ")\n", offPort)); + STAM_REL_COUNTER_INC(&pThis->StatUnimplementedNabmReads); + break; + } + break; + + default: + DEVAC97_UNLOCK(pDevIns, pThis); + AssertFailed(); + return VERR_IOM_IOPORT_UNUSED; + } + } + + DEVAC97_UNLOCK(pDevIns, pThis); + return VINF_SUCCESS; +} + + +/** + * @callback_method_impl{FNIOMIOPORTNEWOUT} + */ +static DECLCALLBACK(VBOXSTRICTRC) +ichac97IoPortNabmWrite(PPDMDEVINS pDevIns, void *pvUser, RTIOPORT offPort, uint32_t u32, unsigned cb) +{ + PAC97STATE pThis = PDMDEVINS_2_DATA(pDevIns, PAC97STATE); +#ifdef IN_RING3 + PAC97STATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PAC97STATER3); +#endif + RT_NOREF(pvUser); + + VBOXSTRICTRC rc = VINF_SUCCESS; + if ( AC97_PORT2IDX_UNMASKED(offPort) < AC97_MAX_STREAMS + && offPort != AC97_GLOB_CNT) + { +#ifdef IN_RING3 + PAC97STREAMR3 pStreamCC = &pThisCC->aStreams[AC97_PORT2IDX(offPort)]; +#endif + PAC97STREAM pStream = &pThis->aStreams[AC97_PORT2IDX(offPort)]; + + switch (cb) + { + case 1: + switch (offPort & AC97_NABM_OFF_MASK) + { + /* + * Last Valid Index. + */ + case AC97_NABM_OFF_LVI: + DEVAC97_LOCK_RETURN(pDevIns, pThis, VINF_IOM_R3_IOPORT_WRITE); + + if ( !(pStream->Regs.sr & AC97_SR_DCH) + || !(pStream->Regs.cr & AC97_CR_RPBM)) + { + pStream->Regs.lvi = u32 % AC97_MAX_BDLE; + STAM_REL_COUNTER_INC(&pStream->StatWriteLvi); + DEVAC97_UNLOCK(pDevIns, pThis); + Log3Func(("[SD%RU8] LVI <- %#x\n", pStream->u8SD, u32)); + } + else + { +#ifdef IN_RING3 + /* Recover from underflow situation where CIV caught up with LVI + and the DMA processing stopped. We clear the status condition, + update LVI and then try to load the next BDLE. Unfortunately, + we cannot do this from ring-3 as much of the BDLE state is + ring-3 only. */ + pStream->Regs.sr &= ~(AC97_SR_DCH | AC97_SR_CELV); + pStream->Regs.lvi = u32 % AC97_MAX_BDLE; + if (ichac97R3StreamFetchNextBdle(pDevIns, pStream, pStreamCC)) + ichac97StreamUpdateSR(pDevIns, pThis, pStream, pStream->Regs.sr | AC97_SR_BCIS); + + /* We now have to re-arm the DMA timer according to the new BDLE length. + This means leaving the device lock to avoid virtual sync lock order issues. */ + ichac97R3StreamTransferUpdate(pDevIns, pStream, pStreamCC); + uint64_t const cTicksToDeadline = pStream->cDmaPeriodTicks; + + /** @todo Stop the DMA timer when we get into the AC97_SR_CELV situation to + * avoid potential race here. */ + STAM_REL_COUNTER_INC(&pStreamCC->State.StatWriteLviRecover); + DEVAC97_UNLOCK(pDevIns, pThis); + + LogFunc(("[SD%RU8] LVI <- %#x; CIV=%#x PIV=%#x SR=%#x cTicksToDeadline=%#RX64 [recovering]\n", + pStream->u8SD, u32, pStream->Regs.civ, pStream->Regs.piv, pStream->Regs.sr, cTicksToDeadline)); + + int rc2 = PDMDevHlpTimerSetRelative(pDevIns, pStream->hTimer, cTicksToDeadline, &pStream->uArmedTs); + AssertRC(rc2); +#else + rc = VINF_IOM_R3_IOPORT_WRITE; +#endif + } + break; + + /* + * Control Registers. + */ + case AC97_NABM_OFF_CR: + { +#ifdef IN_RING3 + DEVAC97_LOCK(pDevIns, pThis); + STAM_REL_COUNTER_INC(&pStreamCC->State.StatWriteCr); + + uint32_t const fCrChanged = pStream->Regs.cr ^ u32; + Log3Func(("[SD%RU8] CR <- %#x (was %#x; changed %#x)\n", pStream->u8SD, u32, pStream->Regs.cr, fCrChanged)); + + /* + * Busmaster reset. + */ + if (u32 & AC97_CR_RR) + { + STAM_REL_PROFILE_START_NS(&pStreamCC->State.StatReset, r); + LogFunc(("[SD%RU8] Reset\n", pStream->u8SD)); + + /* Make sure that Run/Pause Bus Master bit (RPBM) is cleared (0). + 3.2.7 in 302349-003 says RPBM be must be clear when resetting + and that behavior is undefined if it's set. */ + ASSERT_GUEST_STMT((pStream->Regs.cr & AC97_CR_RPBM) == 0, + ichac97R3StreamEnable(pDevIns, pThis, pThisCC, pStream, + pStreamCC, false /* fEnable */)); + + ichac97R3StreamReset(pThis, pStream, pStreamCC); + + ichac97StreamUpdateSR(pDevIns, pThis, pStream, AC97_SR_DCH); /** @todo Do we need to do that? */ + + DEVAC97_UNLOCK(pDevIns, pThis); + STAM_REL_PROFILE_STOP_NS(&pStreamCC->State.StatReset, r); + break; + } + + /* + * Write the new value to the register and if RPBM didn't change we're done. + */ + pStream->Regs.cr = u32 & AC97_CR_VALID_MASK; + + if (!(fCrChanged & AC97_CR_RPBM)) + DEVAC97_UNLOCK(pDevIns, pThis); /* Probably not so likely, but avoid one extra intentation level. */ + /* + * Pause busmaster. + */ + else if (!(pStream->Regs.cr & AC97_CR_RPBM)) + { + STAM_REL_PROFILE_START_NS(&pStreamCC->State.StatStop, p); + LogFunc(("[SD%RU8] Pause busmaster (disable stream) SR=%#x -> %#x\n", + pStream->u8SD, pStream->Regs.sr, pStream->Regs.sr | AC97_SR_DCH)); + ichac97R3StreamEnable(pDevIns, pThis, pThisCC, pStream, pStreamCC, false /* fEnable */); + pStream->Regs.sr |= AC97_SR_DCH; + + DEVAC97_UNLOCK(pDevIns, pThis); + STAM_REL_PROFILE_STOP_NS(&pStreamCC->State.StatStop, p); + } + /* + * Run busmaster. + */ + else + { + STAM_REL_PROFILE_START_NS(&pStreamCC->State.StatStart, r); + LogFunc(("[SD%RU8] Run busmaster (enable stream) SR=%#x -> %#x\n", + pStream->u8SD, pStream->Regs.sr, pStream->Regs.sr & ~AC97_SR_DCH)); + pStream->Regs.sr &= ~AC97_SR_DCH; + + if (ichac97R3StreamFetchNextBdle(pDevIns, pStream, pStreamCC)) + ichac97StreamUpdateSR(pDevIns, pThis, pStream, pStream->Regs.sr | AC97_SR_BCIS); +# ifdef LOG_ENABLED + if (LogIsFlowEnabled()) + ichac97R3DbgPrintBdl(pDevIns, pThis, pStream, PDMDevHlpDBGFInfoLogHlp(pDevIns), "ichac97IoPortNabmWrite: "); +# endif + ichac97R3StreamEnable(pDevIns, pThis, pThisCC, pStream, pStreamCC, true /* fEnable */); + + /* + * Arm the DMA timer. Must drop the AC'97 device lock first as it would + * create a lock order violation with the virtual sync time lock otherwise. + */ + ichac97R3StreamTransferUpdate(pDevIns, pStream, pStreamCC); + uint64_t const cTicksToDeadline = pStream->cDmaPeriodTicks; + + DEVAC97_UNLOCK(pDevIns, pThis); + + /** @todo for output streams we could probably service this a little bit + * earlier if we push it, just to reduce the lag... For HDA we do a + * DMA run immediately after the stream is enabled. */ + int rc2 = PDMDevHlpTimerSetRelative(pDevIns, pStream->hTimer, cTicksToDeadline, &pStream->uArmedTs); + AssertRC(rc2); + + STAM_REL_PROFILE_STOP_NS(&pStreamCC->State.StatStart, r); + } +#else /* !IN_RING3 */ + rc = VINF_IOM_R3_IOPORT_WRITE; +#endif + break; + } + + /* + * Status Registers. + */ + case AC97_NABM_OFF_SR: + DEVAC97_LOCK_RETURN(pDevIns, pThis, VINF_IOM_R3_IOPORT_WRITE); + ichac97StreamWriteSR(pDevIns, pThis, pStream, u32); + STAM_REL_COUNTER_INC(&pStream->StatWriteSr1); + DEVAC97_UNLOCK(pDevIns, pThis); + break; + + default: + /* Linux tries to write CIV. */ + LogRel2(("AC97: Warning: Unimplemented NAMB write offPort=%#x%s <- %#x LB 1 (line " RT_XSTR(__LINE__) ")\n", + offPort, (offPort & AC97_NABM_OFF_MASK) == AC97_NABM_OFF_CIV ? " (CIV)" : "" , u32)); + STAM_REL_COUNTER_INC(&pThis->StatUnimplementedNabmWrites); + break; + } + break; + + case 2: + switch (offPort & AC97_NABM_OFF_MASK) + { + case AC97_NABM_OFF_SR: + DEVAC97_LOCK_RETURN(pDevIns, pThis, VINF_IOM_R3_IOPORT_WRITE); + ichac97StreamWriteSR(pDevIns, pThis, pStream, u32); + STAM_REL_COUNTER_INC(&pStream->StatWriteSr2); + DEVAC97_UNLOCK(pDevIns, pThis); + break; + default: + LogRel2(("AC97: Warning: Unimplemented NAMB write offPort=%#x <- %#x LB 2 (line " RT_XSTR(__LINE__) ")\n", offPort, u32)); + STAM_REL_COUNTER_INC(&pThis->StatUnimplementedNabmWrites); + break; + } + break; + + case 4: + switch (offPort & AC97_NABM_OFF_MASK) + { + case AC97_NABM_OFF_BDBAR: + DEVAC97_LOCK_RETURN(pDevIns, pThis, VINF_IOM_R3_IOPORT_WRITE); + /* Buffer Descriptor list Base Address Register */ + pStream->Regs.bdbar = u32 & ~(uint32_t)3; + Log3Func(("[SD%RU8] BDBAR <- %#x (bdbar %#x)\n", AC97_PORT2IDX(offPort), u32, pStream->Regs.bdbar)); + STAM_REL_COUNTER_INC(&pStream->StatWriteBdBar); + DEVAC97_UNLOCK(pDevIns, pThis); + break; + default: + LogRel2(("AC97: Warning: Unimplemented NAMB write offPort=%#x <- %#x LB 4 (line " RT_XSTR(__LINE__) ")\n", offPort, u32)); + STAM_REL_COUNTER_INC(&pThis->StatUnimplementedNabmWrites); + break; + } + break; + + default: + AssertMsgFailed(("offPort=%#x <- %#x LB %u\n", offPort, u32, cb)); + break; + } + } + else + { + switch (cb) + { + case 1: + LogRel2(("AC97: Warning: Unimplemented NAMB write offPort=%#x <- %#x LB 1 (line " RT_XSTR(__LINE__) ")\n", offPort, u32)); + STAM_REL_COUNTER_INC(&pThis->StatUnimplementedNabmWrites); + break; + + case 2: + LogRel2(("AC97: Warning: Unimplemented NAMB write offPort=%#x <- %#x LB 2 (line " RT_XSTR(__LINE__) ")\n", offPort, u32)); + STAM_REL_COUNTER_INC(&pThis->StatUnimplementedNabmWrites); + break; + + case 4: + switch (offPort) + { + case AC97_GLOB_CNT: + /* Global Control */ + DEVAC97_LOCK_RETURN(pDevIns, pThis, VINF_IOM_R3_IOPORT_WRITE); + if (u32 & AC97_GC_WR) + ichac97WarmReset(pThis); + if (u32 & AC97_GC_CR) + ichac97ColdReset(pThis); + if (!(u32 & (AC97_GC_WR | AC97_GC_CR))) + pThis->glob_cnt = u32 & AC97_GC_VALID_MASK; + Log3Func(("glob_cnt <- %#x (glob_cnt %#x)\n", u32, pThis->glob_cnt)); + DEVAC97_UNLOCK(pDevIns, pThis); + break; + case AC97_GLOB_STA: + /* Global Status */ + DEVAC97_LOCK_RETURN(pDevIns, pThis, VINF_IOM_R3_IOPORT_WRITE); + pThis->glob_sta &= ~(u32 & AC97_GS_WCLEAR_MASK); + pThis->glob_sta |= (u32 & ~(AC97_GS_WCLEAR_MASK | AC97_GS_RO_MASK)) & AC97_GS_VALID_MASK; + Log3Func(("glob_sta <- %#x (glob_sta %#x)\n", u32, pThis->glob_sta)); + DEVAC97_UNLOCK(pDevIns, pThis); + break; + default: + LogRel2(("AC97: Warning: Unimplemented NAMB write offPort=%#x <- %#x LB 4 (line " RT_XSTR(__LINE__) ")\n", offPort, u32)); + STAM_REL_COUNTER_INC(&pThis->StatUnimplementedNabmWrites); + break; + } + break; + + default: + AssertMsgFailed(("offPort=%#x <- %#x LB %u\n", offPort, u32, cb)); + break; + } + } + + return rc; +} + + +/********************************************************************************************************************************* +* Mixer & NAM I/O handlers * +*********************************************************************************************************************************/ + +/** + * Sets a AC'97 mixer control to a specific value. + * + * @returns VBox status code. + * @param pThis The shared AC'97 state. + * @param uMixerIdx Mixer control to set value for. + * @param uVal Value to set. + */ +static void ichac97MixerSet(PAC97STATE pThis, uint8_t uMixerIdx, uint16_t uVal) +{ + AssertMsgReturnVoid(uMixerIdx + 2U <= sizeof(pThis->mixer_data), + ("Index %RU8 out of bounds (%zu)\n", uMixerIdx, sizeof(pThis->mixer_data))); + + LogRel2(("AC97: Setting mixer index #%RU8 to %RU16 (%RU8 %RU8)\n", uMixerIdx, uVal, RT_HI_U8(uVal), RT_LO_U8(uVal))); + + pThis->mixer_data[uMixerIdx + 0] = RT_LO_U8(uVal); + pThis->mixer_data[uMixerIdx + 1] = RT_HI_U8(uVal); +} + + +/** + * Gets a value from a specific AC'97 mixer control. + * + * @returns Retrieved mixer control value. + * @param pThis The shared AC'97 state. + * @param uMixerIdx Mixer control to get value for. + */ +static uint16_t ichac97MixerGet(PAC97STATE pThis, uint32_t uMixerIdx) +{ + AssertMsgReturn(uMixerIdx + 2U <= sizeof(pThis->mixer_data), + ("Index %RU8 out of bounds (%zu)\n", uMixerIdx, sizeof(pThis->mixer_data)), + UINT16_MAX); + return RT_MAKE_U16(pThis->mixer_data[uMixerIdx + 0], pThis->mixer_data[uMixerIdx + 1]); +} + +#ifdef IN_RING3 + +/** + * Sets the volume of a specific AC'97 mixer control. + * + * This currently only supports attenuation -- gain support is currently not implemented. + * + * @returns VBox status code. + * @param pThis The shared AC'97 state. + * @param pThisCC The ring-3 AC'97 state. + * @param index AC'97 mixer index to set volume for. + * @param enmMixerCtl Corresponding audio mixer sink. + * @param uVal Volume value to set. + */ +static int ichac97R3MixerSetVolume(PAC97STATE pThis, PAC97STATER3 pThisCC, int index, PDMAUDIOMIXERCTL enmMixerCtl, uint32_t uVal) +{ + /* + * From AC'97 SoundMax Codec AD1981A/AD1981B: + * "Because AC '97 defines 6-bit volume registers, to maintain compatibility whenever the + * D5 or D13 bits are set to 1, their respective lower five volume bits are automatically + * set to 1 by the Codec logic. On readback, all lower 5 bits will read ones whenever + * these bits are set to 1." + * + * Linux ALSA depends on this behavior to detect that only 5 bits are used for volume + * control and the optional 6th bit is not used. Note that this logic only applies to the + * master volume controls. + */ + if ( index == AC97_Master_Volume_Mute + || index == AC97_Headphone_Volume_Mute + || index == AC97_Master_Volume_Mono_Mute) + { + if (uVal & RT_BIT(5)) /* D5 bit set? */ + uVal |= RT_BIT(4) | RT_BIT(3) | RT_BIT(2) | RT_BIT(1) | RT_BIT(0); + if (uVal & RT_BIT(13)) /* D13 bit set? */ + uVal |= RT_BIT(12) | RT_BIT(11) | RT_BIT(10) | RT_BIT(9) | RT_BIT(8); + } + + const bool fCtlMuted = (uVal >> AC97_BARS_VOL_MUTE_SHIFT) & 1; + uint8_t uCtlAttLeft = (uVal >> 8) & AC97_BARS_VOL_MASK; + uint8_t uCtlAttRight = uVal & AC97_BARS_VOL_MASK; + + /* For the master and headphone volume, 0 corresponds to 0dB attenuation. For the other + * volume controls, 0 means 12dB gain and 8 means unity gain. + */ + if (index != AC97_Master_Volume_Mute && index != AC97_Headphone_Volume_Mute) + { +# ifndef VBOX_WITH_AC97_GAIN_SUPPORT + /* NB: Currently there is no gain support, only attenuation. */ + uCtlAttLeft = uCtlAttLeft < 8 ? 0 : uCtlAttLeft - 8; + uCtlAttRight = uCtlAttRight < 8 ? 0 : uCtlAttRight - 8; +# endif + } + Assert(uCtlAttLeft <= 255 / AC97_DB_FACTOR); + Assert(uCtlAttRight <= 255 / AC97_DB_FACTOR); + + LogFunc(("index=0x%x, uVal=%RU32, enmMixerCtl=%RU32\n", index, uVal, enmMixerCtl)); + LogFunc(("uCtlAttLeft=%RU8, uCtlAttRight=%RU8 ", uCtlAttLeft, uCtlAttRight)); + + /* + * For AC'97 volume controls, each additional step means -1.5dB attenuation with + * zero being maximum. In contrast, we're internally using 255 (PDMAUDIO_VOLUME_MAX) + * steps, each -0.375dB, where 0 corresponds to -96dB and 255 corresponds to 0dB. + */ + uint8_t lVol = PDMAUDIO_VOLUME_MAX - uCtlAttLeft * AC97_DB_FACTOR; + uint8_t rVol = PDMAUDIO_VOLUME_MAX - uCtlAttRight * AC97_DB_FACTOR; + + Log(("-> fMuted=%RTbool, lVol=%RU8, rVol=%RU8\n", fCtlMuted, lVol, rVol)); + + int rc = VINF_SUCCESS; + + if (pThisCC->pMixer) /* Device can be in reset state, so no mixer available. */ + { + PDMAUDIOVOLUME Vol; + PDMAudioVolumeInitFromStereo(&Vol, fCtlMuted, lVol, rVol); + + PAUDMIXSINK pSink = NULL; + switch (enmMixerCtl) + { + case PDMAUDIOMIXERCTL_VOLUME_MASTER: + rc = AudioMixerSetMasterVolume(pThisCC->pMixer, &Vol); + break; + + case PDMAUDIOMIXERCTL_FRONT: + pSink = pThisCC->pSinkOut; + break; + + case PDMAUDIOMIXERCTL_MIC_IN: + case PDMAUDIOMIXERCTL_LINE_IN: + /* These are recognized but do nothing. */ + break; + + default: + AssertFailed(); + rc = VERR_NOT_SUPPORTED; + break; + } + + if (pSink) + rc = AudioMixerSinkSetVolume(pSink, &Vol); + } + + ichac97MixerSet(pThis, index, uVal); + + if (RT_FAILURE(rc)) + LogFlowFunc(("Failed with %Rrc\n", rc)); + + return rc; +} + +/** + * Sets the gain of a specific AC'97 recording control. + * + * @note Gain support is currently not implemented in PDM audio. + * + * @returns VBox status code. + * @param pThis The shared AC'97 state. + * @param pThisCC The ring-3 AC'97 state. + * @param index AC'97 mixer index to set volume for. + * @param enmMixerCtl Corresponding audio mixer sink. + * @param uVal Volume value to set. + */ +static int ichac97R3MixerSetGain(PAC97STATE pThis, PAC97STATER3 pThisCC, int index, PDMAUDIOMIXERCTL enmMixerCtl, uint32_t uVal) +{ + /* + * For AC'97 recording controls, each additional step means +1.5dB gain with + * zero being 0dB gain and 15 being +22.5dB gain. + */ + bool const fCtlMuted = (uVal >> AC97_BARS_VOL_MUTE_SHIFT) & 1; + uint8_t uCtlGainLeft = (uVal >> 8) & AC97_BARS_GAIN_MASK; + uint8_t uCtlGainRight = uVal & AC97_BARS_GAIN_MASK; + + Assert(uCtlGainLeft <= 255 / AC97_DB_FACTOR); + Assert(uCtlGainRight <= 255 / AC97_DB_FACTOR); + + LogFunc(("index=0x%x, uVal=%RU32, enmMixerCtl=%RU32\n", index, uVal, enmMixerCtl)); + LogFunc(("uCtlGainLeft=%RU8, uCtlGainRight=%RU8 ", uCtlGainLeft, uCtlGainRight)); + + uint8_t lVol = PDMAUDIO_VOLUME_MAX + uCtlGainLeft * AC97_DB_FACTOR; + uint8_t rVol = PDMAUDIO_VOLUME_MAX + uCtlGainRight * AC97_DB_FACTOR; + + /* We do not currently support gain. Since AC'97 does not support attenuation + * for the recording input, the best we can do is set the maximum volume. + */ +# ifndef VBOX_WITH_AC97_GAIN_SUPPORT + /* NB: Currently there is no gain support, only attenuation. Since AC'97 does not + * support attenuation for the recording inputs, the best we can do is set the + * maximum volume. + */ + lVol = rVol = PDMAUDIO_VOLUME_MAX; +# endif + + Log(("-> fMuted=%RTbool, lVol=%RU8, rVol=%RU8\n", fCtlMuted, lVol, rVol)); + + int rc = VINF_SUCCESS; + + if (pThisCC->pMixer) /* Device can be in reset state, so no mixer available. */ + { + PDMAUDIOVOLUME Vol; + PDMAudioVolumeInitFromStereo(&Vol, fCtlMuted, lVol, rVol); + + PAUDMIXSINK pSink = NULL; + switch (enmMixerCtl) + { + case PDMAUDIOMIXERCTL_MIC_IN: + pSink = pThisCC->pSinkMicIn; + break; + + case PDMAUDIOMIXERCTL_LINE_IN: + pSink = pThisCC->pSinkLineIn; + break; + + default: + AssertFailed(); + rc = VERR_NOT_SUPPORTED; + break; + } + + if (pSink) + { + rc = AudioMixerSinkSetVolume(pSink, &Vol); + /* There is only one AC'97 recording gain control. If line in + * is changed, also update the microphone. If the optional dedicated + * microphone is changed, only change that. + * NB: The codecs we support do not have the dedicated microphone control. + */ + if (pSink == pThisCC->pSinkLineIn && pThisCC->pSinkMicIn) + rc = AudioMixerSinkSetVolume(pSink, &Vol); + } + } + + ichac97MixerSet(pThis, index, uVal); + + if (RT_FAILURE(rc)) + LogFlowFunc(("Failed with %Rrc\n", rc)); + + return rc; +} + + +/** + * Converts an AC'97 recording source index to a PDM audio recording source. + * + * @returns PDM audio recording source. + * @param uIdx AC'97 index to convert. + */ +static PDMAUDIOPATH ichac97R3IdxToRecSource(uint8_t uIdx) +{ + switch (uIdx) + { + case AC97_REC_MIC: return PDMAUDIOPATH_IN_MIC; + case AC97_REC_CD: return PDMAUDIOPATH_IN_CD; + case AC97_REC_VIDEO: return PDMAUDIOPATH_IN_VIDEO; + case AC97_REC_AUX: return PDMAUDIOPATH_IN_AUX; + case AC97_REC_LINE_IN: return PDMAUDIOPATH_IN_LINE; + case AC97_REC_PHONE: return PDMAUDIOPATH_IN_PHONE; + default: + break; + } + + LogFlowFunc(("Unknown record source %d, using MIC\n", uIdx)); + return PDMAUDIOPATH_IN_MIC; +} + + +/** + * Converts a PDM audio recording source to an AC'97 recording source index. + * + * @returns AC'97 recording source index. + * @param enmRecSrc PDM audio recording source to convert. + */ +static uint8_t ichac97R3RecSourceToIdx(PDMAUDIOPATH enmRecSrc) +{ + switch (enmRecSrc) + { + case PDMAUDIOPATH_IN_MIC: return AC97_REC_MIC; + case PDMAUDIOPATH_IN_CD: return AC97_REC_CD; + case PDMAUDIOPATH_IN_VIDEO: return AC97_REC_VIDEO; + case PDMAUDIOPATH_IN_AUX: return AC97_REC_AUX; + case PDMAUDIOPATH_IN_LINE: return AC97_REC_LINE_IN; + case PDMAUDIOPATH_IN_PHONE: return AC97_REC_PHONE; + default: + AssertMsgFailedBreak(("%d\n", enmRecSrc)); + } + + LogFlowFunc(("Unknown audio recording source %d using MIC\n", enmRecSrc)); + return AC97_REC_MIC; +} + + +/** + * Performs an AC'97 mixer record select to switch to a different recording + * source. + * + * @param pThis The shared AC'97 state. + * @param val AC'97 recording source index to set. + */ +static void ichac97R3MixerRecordSelect(PAC97STATE pThis, uint32_t val) +{ + uint8_t rs = val & AC97_REC_MASK; + uint8_t ls = (val >> 8) & AC97_REC_MASK; + + PDMAUDIOPATH const ars = ichac97R3IdxToRecSource(rs); + PDMAUDIOPATH const als = ichac97R3IdxToRecSource(ls); + + rs = ichac97R3RecSourceToIdx(ars); + ls = ichac97R3RecSourceToIdx(als); + + LogRel(("AC97: Record select to left=%s, right=%s\n", PDMAudioPathGetName(ars), PDMAudioPathGetName(als))); + + ichac97MixerSet(pThis, AC97_Record_Select, rs | (ls << 8)); +} + +/** + * Resets the AC'97 mixer. + * + * @returns VBox status code. + * @param pThis The shared AC'97 state. + * @param pThisCC The ring-3 AC'97 state. + */ +static int ichac97R3MixerReset(PAC97STATE pThis, PAC97STATER3 pThisCC) +{ + LogFlowFuncEnter(); + + RT_ZERO(pThis->mixer_data); + + /* Note: Make sure to reset all registers first before bailing out on error. */ + + ichac97MixerSet(pThis, AC97_Reset , 0x0000); /* 6940 */ + ichac97MixerSet(pThis, AC97_Master_Volume_Mono_Mute , 0x8000); + ichac97MixerSet(pThis, AC97_PC_BEEP_Volume_Mute , 0x0000); + + ichac97MixerSet(pThis, AC97_Phone_Volume_Mute , 0x8008); + ichac97MixerSet(pThis, AC97_Mic_Volume_Mute , 0x8008); + ichac97MixerSet(pThis, AC97_CD_Volume_Mute , 0x8808); + ichac97MixerSet(pThis, AC97_Aux_Volume_Mute , 0x8808); + ichac97MixerSet(pThis, AC97_Record_Gain_Mic_Mute , 0x8000); + ichac97MixerSet(pThis, AC97_General_Purpose , 0x0000); + ichac97MixerSet(pThis, AC97_3D_Control , 0x0000); + ichac97MixerSet(pThis, AC97_Powerdown_Ctrl_Stat , 0x000f); + + /* Configure Extended Audio ID (EAID) + Control & Status (EACS) registers. */ + const uint16_t fEAID = AC97_EAID_REV1 | AC97_EACS_VRA | AC97_EACS_VRM; /* Our hardware is AC'97 rev2.3 compliant. */ + const uint16_t fEACS = AC97_EACS_VRA | AC97_EACS_VRM; /* Variable Rate PCM Audio (VRA) + Mic-In (VRM) capable. */ + + LogRel(("AC97: Mixer reset (EAID=0x%x, EACS=0x%x)\n", fEAID, fEACS)); + + ichac97MixerSet(pThis, AC97_Extended_Audio_ID, fEAID); + ichac97MixerSet(pThis, AC97_Extended_Audio_Ctrl_Stat, fEACS); + ichac97MixerSet(pThis, AC97_PCM_Front_DAC_Rate , 0xbb80 /* 48000 Hz by default */); + ichac97MixerSet(pThis, AC97_PCM_Surround_DAC_Rate , 0xbb80 /* 48000 Hz by default */); + ichac97MixerSet(pThis, AC97_PCM_LFE_DAC_Rate , 0xbb80 /* 48000 Hz by default */); + ichac97MixerSet(pThis, AC97_PCM_LR_ADC_Rate , 0xbb80 /* 48000 Hz by default */); + ichac97MixerSet(pThis, AC97_MIC_ADC_Rate , 0xbb80 /* 48000 Hz by default */); + + if (pThis->enmCodecModel == AC97CODEC_AD1980) + { + /* Analog Devices 1980 (AD1980) */ + ichac97MixerSet(pThis, AC97_Reset , 0x0010); /* Headphones. */ + ichac97MixerSet(pThis, AC97_Vendor_ID1 , 0x4144); + ichac97MixerSet(pThis, AC97_Vendor_ID2 , 0x5370); + ichac97MixerSet(pThis, AC97_Headphone_Volume_Mute , 0x8000); + } + else if (pThis->enmCodecModel == AC97CODEC_AD1981B) + { + /* Analog Devices 1981B (AD1981B) */ + ichac97MixerSet(pThis, AC97_Vendor_ID1 , 0x4144); + ichac97MixerSet(pThis, AC97_Vendor_ID2 , 0x5374); + } + else + { + /* Sigmatel 9700 (STAC9700) */ + ichac97MixerSet(pThis, AC97_Vendor_ID1 , 0x8384); + ichac97MixerSet(pThis, AC97_Vendor_ID2 , 0x7600); /* 7608 */ + } + ichac97R3MixerRecordSelect(pThis, 0); + + /* The default value is 8000h, which corresponds to 0 dB attenuation with mute on. */ + ichac97R3MixerSetVolume(pThis, pThisCC, AC97_Master_Volume_Mute, PDMAUDIOMIXERCTL_VOLUME_MASTER, 0x8000); + + /* The default value for stereo registers is 8808h, which corresponds to 0 dB gain with mute on.*/ + ichac97R3MixerSetVolume(pThis, pThisCC, AC97_PCM_Out_Volume_Mute, PDMAUDIOMIXERCTL_FRONT, 0x8808); + ichac97R3MixerSetVolume(pThis, pThisCC, AC97_Line_In_Volume_Mute, PDMAUDIOMIXERCTL_LINE_IN, 0x8808); + ichac97R3MixerSetVolume(pThis, pThisCC, AC97_Mic_Volume_Mute, PDMAUDIOMIXERCTL_MIC_IN, 0x8008); + + /* The default for record controls is 0 dB gain with mute on. */ + ichac97R3MixerSetGain(pThis, pThisCC, AC97_Record_Gain_Mute, PDMAUDIOMIXERCTL_LINE_IN, 0x8000); + ichac97R3MixerSetGain(pThis, pThisCC, AC97_Record_Gain_Mic_Mute, PDMAUDIOMIXERCTL_MIC_IN, 0x8000); + + return VINF_SUCCESS; +} + +#endif /* IN_RING3 */ + +/** + * @callback_method_impl{FNIOMIOPORTNEWIN} + */ +static DECLCALLBACK(VBOXSTRICTRC) +ichac97IoPortNamRead(PPDMDEVINS pDevIns, void *pvUser, RTIOPORT offPort, uint32_t *pu32, unsigned cb) +{ + PAC97STATE pThis = PDMDEVINS_2_DATA(pDevIns, PAC97STATE); + RT_NOREF(pvUser); + Assert(offPort < 256); + + DEVAC97_LOCK_RETURN(pDevIns, pThis, VINF_IOM_R3_IOPORT_READ); + + VBOXSTRICTRC rc = VINF_SUCCESS; + switch (cb) + { + case 1: + LogRel2(("AC97: Warning: Unimplemented NAM read offPort=%#x LB 1 (line " RT_XSTR(__LINE__) ")\n", offPort)); + STAM_REL_COUNTER_INC(&pThis->StatUnimplementedNamReads); + pThis->cas = 0; + *pu32 = UINT32_MAX; + break; + + case 2: + pThis->cas = 0; + *pu32 = ichac97MixerGet(pThis, offPort); + break; + + case 4: + LogRel2(("AC97: Warning: Unimplemented NAM read offPort=%#x LB 4 (line " RT_XSTR(__LINE__) ")\n", offPort)); + STAM_REL_COUNTER_INC(&pThis->StatUnimplementedNamReads); + pThis->cas = 0; + *pu32 = UINT32_MAX; + break; + + default: + AssertFailed(); + rc = VERR_IOM_IOPORT_UNUSED; + break; + } + + DEVAC97_UNLOCK(pDevIns, pThis); + return rc; +} + +/** + * @callback_method_impl{FNIOMIOPORTNEWOUT} + */ +static DECLCALLBACK(VBOXSTRICTRC) +ichac97IoPortNamWrite(PPDMDEVINS pDevIns, void *pvUser, RTIOPORT offPort, uint32_t u32, unsigned cb) +{ + PAC97STATE pThis = PDMDEVINS_2_DATA(pDevIns, PAC97STATE); +#ifdef IN_RING3 + PAC97STATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PAC97STATER3); +#endif + RT_NOREF(pvUser); + + DEVAC97_LOCK_RETURN(pDevIns, pThis, VINF_IOM_R3_IOPORT_WRITE); + + VBOXSTRICTRC rc = VINF_SUCCESS; + switch (cb) + { + case 1: + LogRel2(("AC97: Warning: Unimplemented NAM write offPort=%#x <- %#x LB 1 (line " RT_XSTR(__LINE__) ")\n", offPort, u32)); + STAM_REL_COUNTER_INC(&pThis->StatUnimplementedNamWrites); + pThis->cas = 0; + break; + + case 2: + { + pThis->cas = 0; + switch (offPort) + { + case AC97_Reset: +#ifdef IN_RING3 + ichac97R3Reset(pDevIns); +#else + rc = VINF_IOM_R3_IOPORT_WRITE; +#endif + break; + case AC97_Powerdown_Ctrl_Stat: + u32 &= ~0xf; + u32 |= ichac97MixerGet(pThis, offPort) & 0xf; + ichac97MixerSet(pThis, offPort, u32); + break; + case AC97_Master_Volume_Mute: + if (pThis->enmCodecModel == AC97CODEC_AD1980) + { + if (ichac97MixerGet(pThis, AC97_AD_Misc) & AC97_AD_MISC_LOSEL) + break; /* Register controls surround (rear), do nothing. */ + } +#ifdef IN_RING3 + ichac97R3MixerSetVolume(pThis, pThisCC, offPort, PDMAUDIOMIXERCTL_VOLUME_MASTER, u32); +#else + rc = VINF_IOM_R3_IOPORT_WRITE; +#endif + break; + case AC97_Headphone_Volume_Mute: + if (pThis->enmCodecModel == AC97CODEC_AD1980) + { + if (ichac97MixerGet(pThis, AC97_AD_Misc) & AC97_AD_MISC_HPSEL) + { + /* Register controls PCM (front) outputs. */ +#ifdef IN_RING3 + ichac97R3MixerSetVolume(pThis, pThisCC, offPort, PDMAUDIOMIXERCTL_VOLUME_MASTER, u32); +#else + rc = VINF_IOM_R3_IOPORT_WRITE; +#endif + } + } + break; + case AC97_PCM_Out_Volume_Mute: +#ifdef IN_RING3 + ichac97R3MixerSetVolume(pThis, pThisCC, offPort, PDMAUDIOMIXERCTL_FRONT, u32); +#else + rc = VINF_IOM_R3_IOPORT_WRITE; +#endif + break; + case AC97_Line_In_Volume_Mute: +#ifdef IN_RING3 + ichac97R3MixerSetVolume(pThis, pThisCC, offPort, PDMAUDIOMIXERCTL_LINE_IN, u32); +#else + rc = VINF_IOM_R3_IOPORT_WRITE; +#endif + break; + case AC97_Record_Select: +#ifdef IN_RING3 + ichac97R3MixerRecordSelect(pThis, u32); +#else + rc = VINF_IOM_R3_IOPORT_WRITE; +#endif + break; + case AC97_Record_Gain_Mute: +#ifdef IN_RING3 + /* Newer Ubuntu guests rely on that when controlling gain and muting + * the recording (capturing) levels. */ + ichac97R3MixerSetGain(pThis, pThisCC, offPort, PDMAUDIOMIXERCTL_LINE_IN, u32); +#else + rc = VINF_IOM_R3_IOPORT_WRITE; +#endif + break; + case AC97_Record_Gain_Mic_Mute: +#ifdef IN_RING3 + /* Ditto; see note above. */ + ichac97R3MixerSetGain(pThis, pThisCC, offPort, PDMAUDIOMIXERCTL_MIC_IN, u32); +#else + rc = VINF_IOM_R3_IOPORT_WRITE; +#endif + break; + case AC97_Vendor_ID1: + case AC97_Vendor_ID2: + LogFunc(("Attempt to write vendor ID to %#x\n", u32)); + break; + case AC97_Extended_Audio_ID: + LogFunc(("Attempt to write extended audio ID to %#x\n", u32)); + break; + case AC97_Extended_Audio_Ctrl_Stat: +#ifdef IN_RING3 + /* + * Handle VRA bits. + */ + if (!(u32 & AC97_EACS_VRA)) /* Check if VRA bit is not set. */ + { + ichac97MixerSet(pThis, AC97_PCM_Front_DAC_Rate, 0xbb80); /* Set default (48000 Hz). */ + /** @todo r=bird: Why reopen it now? Can't we put that off till it's + * actually used? */ + ichac97R3StreamReSetUp(pDevIns, pThis, pThisCC, &pThis->aStreams[AC97SOUNDSOURCE_PO_INDEX], + &pThisCC->aStreams[AC97SOUNDSOURCE_PO_INDEX], true /* fForce */); + + ichac97MixerSet(pThis, AC97_PCM_LR_ADC_Rate, 0xbb80); /* Set default (48000 Hz). */ + /** @todo r=bird: Why reopen it now? Can't we put that off till it's + * actually used? */ + ichac97R3StreamReSetUp(pDevIns, pThis, pThisCC, &pThis->aStreams[AC97SOUNDSOURCE_PI_INDEX], + &pThisCC->aStreams[AC97SOUNDSOURCE_PI_INDEX], true /* fForce */); + } + else + LogRel2(("AC97: Variable rate audio (VRA) is not supported\n")); + + /* + * Handle VRM bits. + */ + if (!(u32 & AC97_EACS_VRM)) /* Check if VRM bit is not set. */ + { + ichac97MixerSet(pThis, AC97_MIC_ADC_Rate, 0xbb80); /* Set default (48000 Hz). */ + /** @todo r=bird: Why reopen it now? Can't we put that off till it's + * actually used? */ + ichac97R3StreamReSetUp(pDevIns, pThis, pThisCC, &pThis->aStreams[AC97SOUNDSOURCE_MC_INDEX], + &pThisCC->aStreams[AC97SOUNDSOURCE_MC_INDEX], true /* fForce */); + } + else + LogRel2(("AC97: Variable rate microphone audio (VRM) is not supported\n")); + + LogRel2(("AC97: Setting extended audio control to %#x\n", u32)); + ichac97MixerSet(pThis, AC97_Extended_Audio_Ctrl_Stat, u32); +#else /* !IN_RING3 */ + rc = VINF_IOM_R3_IOPORT_WRITE; +#endif + break; + case AC97_PCM_Front_DAC_Rate: /* Output slots 3, 4, 6. */ +#ifdef IN_RING3 + if (ichac97MixerGet(pThis, AC97_Extended_Audio_Ctrl_Stat) & AC97_EACS_VRA) + { + LogRel2(("AC97: Setting front DAC rate to 0x%x\n", u32)); + ichac97MixerSet(pThis, offPort, u32); + /** @todo r=bird: Why reopen it now? Can't we put that off till it's + * actually used? */ + ichac97R3StreamReSetUp(pDevIns, pThis, pThisCC, &pThis->aStreams[AC97SOUNDSOURCE_PO_INDEX], + &pThisCC->aStreams[AC97SOUNDSOURCE_PO_INDEX], true /* fForce */); + } + else + LogRel2(("AC97: Setting front DAC rate (0x%x) when VRA is not set is forbidden, ignoring\n", u32)); +#else + rc = VINF_IOM_R3_IOPORT_WRITE; +#endif + break; + case AC97_MIC_ADC_Rate: /* Input slot 6. */ +#ifdef IN_RING3 + if (ichac97MixerGet(pThis, AC97_Extended_Audio_Ctrl_Stat) & AC97_EACS_VRM) + { + LogRel2(("AC97: Setting microphone ADC rate to 0x%x\n", u32)); + ichac97MixerSet(pThis, offPort, u32); + /** @todo r=bird: Why reopen it now? Can't we put that off till it's + * actually used? */ + ichac97R3StreamReSetUp(pDevIns, pThis, pThisCC, &pThis->aStreams[AC97SOUNDSOURCE_MC_INDEX], + &pThisCC->aStreams[AC97SOUNDSOURCE_MC_INDEX], true /* fForce */); + } + else + LogRel2(("AC97: Setting microphone ADC rate (0x%x) when VRM is not set is forbidden, ignoring\n", u32)); +#else + rc = VINF_IOM_R3_IOPORT_WRITE; +#endif + break; + case AC97_PCM_LR_ADC_Rate: /* Input slots 3, 4. */ +#ifdef IN_RING3 + if (ichac97MixerGet(pThis, AC97_Extended_Audio_Ctrl_Stat) & AC97_EACS_VRA) + { + LogRel2(("AC97: Setting line-in ADC rate to 0x%x\n", u32)); + ichac97MixerSet(pThis, offPort, u32); + /** @todo r=bird: Why reopen it now? Can't we put that off till it's + * actually used? */ + ichac97R3StreamReSetUp(pDevIns, pThis, pThisCC, &pThis->aStreams[AC97SOUNDSOURCE_PI_INDEX], + &pThisCC->aStreams[AC97SOUNDSOURCE_PI_INDEX], true /* fForce */); + } + else + LogRel2(("AC97: Setting line-in ADC rate (0x%x) when VRA is not set is forbidden, ignoring\n", u32)); +#else + rc = VINF_IOM_R3_IOPORT_WRITE; +#endif + break; + default: + /* Most of these are to register we don't care about like AC97_CD_Volume_Mute + and AC97_Master_Volume_Mono_Mute or things we don't need to handle specially. + Thus this is not a 'warning' but an 'info log message. */ + LogRel2(("AC97: Info: Unimplemented NAM write offPort=%#x <- %#x LB 2 (line " RT_XSTR(__LINE__) ")\n", offPort, u32)); + STAM_REL_COUNTER_INC(&pThis->StatUnimplementedNamWrites); + ichac97MixerSet(pThis, offPort, u32); + break; + } + break; + } + + case 4: + LogRel2(("AC97: Warning: Unimplemented NAM write offPort=%#x <- %#x LB 4 (line " RT_XSTR(__LINE__) ")\n", offPort, u32)); + STAM_REL_COUNTER_INC(&pThis->StatUnimplementedNamWrites); + pThis->cas = 0; + break; + + default: + AssertMsgFailed(("Unhandled NAM write offPort=%#x, cb=%u u32=%#x\n", offPort, cb, u32)); + break; + } + + DEVAC97_UNLOCK(pDevIns, pThis); + return rc; +} + +#ifdef IN_RING3 + + +/********************************************************************************************************************************* +* State Saving & Loading * +*********************************************************************************************************************************/ + +/** + * Saves (serializes) an AC'97 stream using SSM. + * + * @param pDevIns Device instance. + * @param pSSM Saved state manager (SSM) handle to use. + * @param pStream AC'97 stream to save. + */ +static void ichac97R3SaveStream(PPDMDEVINS pDevIns, PSSMHANDLE pSSM, PAC97STREAM pStream) +{ + PCPDMDEVHLPR3 pHlp = pDevIns->pHlpR3; + + pHlp->pfnSSMPutU32(pSSM, pStream->Regs.bdbar); + pHlp->pfnSSMPutU8( pSSM, pStream->Regs.civ); + pHlp->pfnSSMPutU8( pSSM, pStream->Regs.lvi); + pHlp->pfnSSMPutU16(pSSM, pStream->Regs.sr); + pHlp->pfnSSMPutU16(pSSM, pStream->Regs.picb); + pHlp->pfnSSMPutU8( pSSM, pStream->Regs.piv); + pHlp->pfnSSMPutU8( pSSM, pStream->Regs.cr); + pHlp->pfnSSMPutS32(pSSM, pStream->Regs.bd_valid); + pHlp->pfnSSMPutU32(pSSM, pStream->Regs.bd.addr); + pHlp->pfnSSMPutU32(pSSM, pStream->Regs.bd.ctl_len); +} + + +/** + * @callback_method_impl{FNSSMDEVSAVEEXEC} + */ +static DECLCALLBACK(int) ichac97R3SaveExec(PPDMDEVINS pDevIns, PSSMHANDLE pSSM) +{ + PAC97STATE pThis = PDMDEVINS_2_DATA(pDevIns, PAC97STATE); + PAC97STATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PAC97STATER3); + PCPDMDEVHLPR3 pHlp = pDevIns->pHlpR3; + LogFlowFuncEnter(); + + pHlp->pfnSSMPutU32(pSSM, pThis->glob_cnt); + pHlp->pfnSSMPutU32(pSSM, pThis->glob_sta); + pHlp->pfnSSMPutU32(pSSM, pThis->cas); + + /* + * The order that the streams are saved here is fixed, so don't change. + */ + /** @todo r=andy For the next saved state version, add unique stream identifiers and a stream count. */ + for (unsigned i = 0; i < AC97_MAX_STREAMS; i++) + ichac97R3SaveStream(pDevIns, pSSM, &pThis->aStreams[i]); + + pHlp->pfnSSMPutMem(pSSM, pThis->mixer_data, sizeof(pThis->mixer_data)); + + /* The stream order is against fixed and set in stone. */ + uint8_t afActiveStrms[AC97SOUNDSOURCE_MAX]; + afActiveStrms[AC97SOUNDSOURCE_PI_INDEX] = ichac97R3StreamIsEnabled(pThisCC, &pThis->aStreams[AC97SOUNDSOURCE_PI_INDEX]); + afActiveStrms[AC97SOUNDSOURCE_PO_INDEX] = ichac97R3StreamIsEnabled(pThisCC, &pThis->aStreams[AC97SOUNDSOURCE_PO_INDEX]); + afActiveStrms[AC97SOUNDSOURCE_MC_INDEX] = ichac97R3StreamIsEnabled(pThisCC, &pThis->aStreams[AC97SOUNDSOURCE_MC_INDEX]); + AssertCompile(RT_ELEMENTS(afActiveStrms) == 3); + pHlp->pfnSSMPutMem(pSSM, afActiveStrms, sizeof(afActiveStrms)); + + LogFlowFuncLeaveRC(VINF_SUCCESS); + return VINF_SUCCESS; +} + + +/** + * Loads an AC'97 stream from SSM. + * + * @returns VBox status code. + * @param pDevIns The device instance. + * @param pSSM Saved state manager (SSM) handle to use. + * @param pStream AC'97 stream to load. + */ +static int ichac97R3LoadStream(PPDMDEVINS pDevIns, PSSMHANDLE pSSM, PAC97STREAM pStream) +{ + PCPDMDEVHLPR3 pHlp = pDevIns->pHlpR3; + + pHlp->pfnSSMGetU32(pSSM, &pStream->Regs.bdbar); + pHlp->pfnSSMGetU8( pSSM, &pStream->Regs.civ); + pHlp->pfnSSMGetU8( pSSM, &pStream->Regs.lvi); + pHlp->pfnSSMGetU16(pSSM, &pStream->Regs.sr); + pHlp->pfnSSMGetU16(pSSM, &pStream->Regs.picb); + pHlp->pfnSSMGetU8( pSSM, &pStream->Regs.piv); + pHlp->pfnSSMGetU8( pSSM, &pStream->Regs.cr); + pHlp->pfnSSMGetS32(pSSM, &pStream->Regs.bd_valid); + pHlp->pfnSSMGetU32(pSSM, &pStream->Regs.bd.addr); + return pHlp->pfnSSMGetU32(pSSM, &pStream->Regs.bd.ctl_len); +} + + +/** + * @callback_method_impl{FNSSMDEVLOADEXEC} + */ +static DECLCALLBACK(int) ichac97R3LoadExec(PPDMDEVINS pDevIns, PSSMHANDLE pSSM, uint32_t uVersion, uint32_t uPass) +{ + PAC97STATE pThis = PDMDEVINS_2_DATA(pDevIns, PAC97STATE); + PAC97STATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PAC97STATER3); + PCPDMDEVHLPR3 pHlp = pDevIns->pHlpR3; + + LogRel2(("ichac97LoadExec: uVersion=%RU32, uPass=0x%x\n", uVersion, uPass)); + + AssertMsgReturn (uVersion == AC97_SAVED_STATE_VERSION, ("%RU32\n", uVersion), VERR_SSM_UNSUPPORTED_DATA_UNIT_VERSION); + Assert(uPass == SSM_PASS_FINAL); NOREF(uPass); + + pHlp->pfnSSMGetU32(pSSM, &pThis->glob_cnt); + pHlp->pfnSSMGetU32(pSSM, &pThis->glob_sta); + pHlp->pfnSSMGetU32(pSSM, &pThis->cas); + + /* + * The order the streams are loaded here is critical (defined by + * AC97SOUNDSOURCE_XX_INDEX), so don't touch! + */ + for (unsigned i = 0; i < AC97_MAX_STREAMS; i++) + { + int rc = ichac97R3LoadStream(pDevIns, pSSM, &pThis->aStreams[i]); + AssertRCReturn(rc, rc); + } + + pHlp->pfnSSMGetMem(pSSM, pThis->mixer_data, sizeof(pThis->mixer_data)); + + ichac97R3MixerRecordSelect(pThis, ichac97MixerGet(pThis, AC97_Record_Select)); + ichac97R3MixerSetVolume(pThis, pThisCC, AC97_Master_Volume_Mute, PDMAUDIOMIXERCTL_VOLUME_MASTER, + ichac97MixerGet(pThis, AC97_Master_Volume_Mute)); + ichac97R3MixerSetVolume(pThis, pThisCC, AC97_PCM_Out_Volume_Mute, PDMAUDIOMIXERCTL_FRONT, + ichac97MixerGet(pThis, AC97_PCM_Out_Volume_Mute)); + ichac97R3MixerSetVolume(pThis, pThisCC, AC97_Line_In_Volume_Mute, PDMAUDIOMIXERCTL_LINE_IN, + ichac97MixerGet(pThis, AC97_Line_In_Volume_Mute)); + ichac97R3MixerSetVolume(pThis, pThisCC, AC97_Mic_Volume_Mute, PDMAUDIOMIXERCTL_MIC_IN, + ichac97MixerGet(pThis, AC97_Mic_Volume_Mute)); + ichac97R3MixerSetGain(pThis, pThisCC, AC97_Record_Gain_Mic_Mute, PDMAUDIOMIXERCTL_MIC_IN, + ichac97MixerGet(pThis, AC97_Record_Gain_Mic_Mute)); + ichac97R3MixerSetGain(pThis, pThisCC, AC97_Record_Gain_Mute, PDMAUDIOMIXERCTL_LINE_IN, + ichac97MixerGet(pThis, AC97_Record_Gain_Mute)); + if (pThis->enmCodecModel == AC97CODEC_AD1980) + if (ichac97MixerGet(pThis, AC97_AD_Misc) & AC97_AD_MISC_HPSEL) + ichac97R3MixerSetVolume(pThis, pThisCC, AC97_Headphone_Volume_Mute, PDMAUDIOMIXERCTL_VOLUME_MASTER, + ichac97MixerGet(pThis, AC97_Headphone_Volume_Mute)); + + /* + * Again the stream order is set is stone. + */ + uint8_t afActiveStrms[AC97SOUNDSOURCE_MAX]; + int rc = pHlp->pfnSSMGetMem(pSSM, afActiveStrms, sizeof(afActiveStrms)); + AssertRCReturn(rc, rc); + + for (unsigned i = 0; i < AC97_MAX_STREAMS; i++) + { + const bool fEnable = RT_BOOL(afActiveStrms[i]); + const PAC97STREAM pStream = &pThis->aStreams[i]; + const PAC97STREAMR3 pStreamCC = &pThisCC->aStreams[i]; + + rc = ichac97R3StreamEnable(pDevIns, pThis, pThisCC, pStream, pStreamCC, fEnable); + AssertRC(rc); + if ( fEnable + && RT_SUCCESS(rc)) + { + /* + * We need to make sure to update the stream's next transfer (if any) when + * restoring from a saved state. + * + * Otherwise pStream->cDmaPeriodTicks always will be 0 and thus streams won't + * resume when running while the saved state has been taken. + * + * Also see oem2ticketref:52. + */ + ichac97R3StreamTransferUpdate(pDevIns, pStream, pStreamCC); + + /* Re-arm the timer for this stream. */ + /** @todo r=aeichner This causes a VM hang upon saved state resume when NetBSD is used as a guest + * Stopping the timer if cDmaPeriodTicks is 0 is a workaround but needs further investigation, + * see @bugref{9759} for more information. */ + if (pStream->cDmaPeriodTicks) + ichac97R3TimerSet(pDevIns, pStream, pStream->cDmaPeriodTicks); + else + PDMDevHlpTimerStop(pDevIns, pStream->hTimer); + } + + /* Keep going. */ + } + + pThis->bup_flag = 0; + pThis->last_samp = 0; + + return VINF_SUCCESS; +} + + +/********************************************************************************************************************************* +* Debug Info Items * +*********************************************************************************************************************************/ + +/** Used by ichac97R3DbgInfoStream and ichac97R3DbgInfoBDL. */ +static int ichac97R3DbgLookupStrmIdx(PCDBGFINFOHLP pHlp, const char *pszArgs) +{ + if (pszArgs && *pszArgs) + { + int32_t idxStream; + int rc = RTStrToInt32Full(pszArgs, 0, &idxStream); + if (RT_SUCCESS(rc) && idxStream >= -1 && idxStream < AC97_MAX_STREAMS) + return idxStream; + pHlp->pfnPrintf(pHlp, "Argument '%s' is not a valid stream number!\n", pszArgs); + } + return -1; +} + + +/** + * Generic buffer descriptor list dumper. + */ +static void ichac97R3DbgPrintBdl(PPDMDEVINS pDevIns, PAC97STATE pThis, PAC97STREAM pStream, + PCDBGFINFOHLP pHlp, const char *pszPrefix) +{ + uint8_t const bLvi = pStream->Regs.lvi; + uint8_t const bCiv = pStream->Regs.civ; + pHlp->pfnPrintf(pHlp, "%sBDL for stream #%u: @ %#RX32 LB 0x100; CIV=%#04x LVI=%#04x:\n", + pszPrefix, pStream->u8SD, pStream->Regs.bdbar, bCiv, bLvi); + if (pStream->Regs.bdbar != 0) + { + /* Read all in one go. */ + AC97BDLE aBdl[AC97_MAX_BDLE]; + RT_ZERO(aBdl); + PDMDevHlpPCIPhysRead(pDevIns, pStream->Regs.bdbar, aBdl, sizeof(aBdl)); + + /* Get the audio props for the stream so we can translate the sizes correctly. */ + PDMAUDIOPCMPROPS Props; + ichach97R3CalcStreamProps(pThis, pStream->u8SD, &Props); + + /* Dump them. */ + uint64_t cbTotal = 0; + uint64_t cbValid = 0; + for (unsigned i = 0; i < RT_ELEMENTS(aBdl); i++) + { + aBdl[i].addr = RT_LE2H_U32(aBdl[i].addr); + aBdl[i].ctl_len = RT_LE2H_U32(aBdl[i].ctl_len); + + bool const fValid = bCiv <= bLvi + ? i >= bCiv && i <= bLvi + : i >= bCiv || i <= bLvi; + + uint32_t const cb = (aBdl[i].ctl_len & AC97_BD_LEN_MASK) * PDMAudioPropsSampleSize(&Props); /** @todo or frame size? OSDev says frame... */ + cbTotal += cb; + if (fValid) + cbValid += cb; + + char szFlags[64]; + szFlags[0] = '\0'; + if (aBdl[i].ctl_len & ~(AC97_BD_LEN_MASK | AC97_BD_IOC | AC97_BD_BUP)) + RTStrPrintf(szFlags, sizeof(szFlags), " !!fFlags=%#x!!\n", aBdl[i].ctl_len & ~AC97_BD_LEN_MASK); + + pHlp->pfnPrintf(pHlp, "%s %cBDLE%02u: %#010RX32 L %#06x / LB %#RX32 / %RU64ms%s%s%s%s\n", + pszPrefix, fValid ? ' ' : '?', i, aBdl[i].addr, + aBdl[i].ctl_len & AC97_BD_LEN_MASK, cb, PDMAudioPropsBytesToMilli(&Props, cb), + aBdl[i].ctl_len & AC97_BD_IOC ? " ioc" : "", + aBdl[i].ctl_len & AC97_BD_BUP ? " bup" : "", + szFlags, !(aBdl[i].addr & 3) ? "" : " !!Addr!!"); + } + + pHlp->pfnPrintf(pHlp, "%sTotal: %#RX64 bytes (%RU64), %RU64 ms; Valid: %#RX64 bytes (%RU64), %RU64 ms\n", pszPrefix, + cbTotal, cbTotal, PDMAudioPropsBytesToMilli(&Props, cbTotal), + cbValid, cbValid, PDMAudioPropsBytesToMilli(&Props, cbValid) ); + } +} + + +/** + * @callback_method_impl{FNDBGFHANDLERDEV, ac97bdl} + */ +static DECLCALLBACK(void) ichac97R3DbgInfoBDL(PPDMDEVINS pDevIns, PCDBGFINFOHLP pHlp, const char *pszArgs) +{ + PAC97STATE pThis = PDMDEVINS_2_DATA(pDevIns, PAC97STATE); + int idxStream = ichac97R3DbgLookupStrmIdx(pHlp, pszArgs); + if (idxStream != -1) + ichac97R3DbgPrintBdl(pDevIns, pThis, &pThis->aStreams[idxStream], pHlp, ""); + else + for (idxStream = 0; idxStream < AC97_MAX_STREAMS; ++idxStream) + ichac97R3DbgPrintBdl(pDevIns, pThis, &pThis->aStreams[idxStream], pHlp, ""); +} + + +/** Worker for ichac97R3DbgInfoStream. */ +static void ichac97R3DbgPrintStream(PCDBGFINFOHLP pHlp, PAC97STREAM pStream, PAC97STREAMR3 pStreamR3) +{ + char szTmp[PDMAUDIOSTRMCFGTOSTRING_MAX]; + pHlp->pfnPrintf(pHlp, "Stream #%d: %s\n", pStream->u8SD, + PDMAudioStrmCfgToString(&pStreamR3->State.Cfg, szTmp, sizeof(szTmp))); + pHlp->pfnPrintf(pHlp, " BDBAR %#010RX32\n", pStream->Regs.bdbar); + pHlp->pfnPrintf(pHlp, " CIV %#04RX8\n", pStream->Regs.civ); + pHlp->pfnPrintf(pHlp, " LVI %#04RX8\n", pStream->Regs.lvi); + pHlp->pfnPrintf(pHlp, " SR %#06RX16\n", pStream->Regs.sr); + pHlp->pfnPrintf(pHlp, " PICB %#06RX16\n", pStream->Regs.picb); + pHlp->pfnPrintf(pHlp, " PIV %#04RX8\n", pStream->Regs.piv); + pHlp->pfnPrintf(pHlp, " CR %#04RX8\n", pStream->Regs.cr); + if (pStream->Regs.bd_valid) + { + pHlp->pfnPrintf(pHlp, " BD.ADDR %#010RX32\n", pStream->Regs.bd.addr); + pHlp->pfnPrintf(pHlp, " BD.LEN %#04RX16\n", (uint16_t)pStream->Regs.bd.ctl_len); + pHlp->pfnPrintf(pHlp, " BD.CTL %#04RX16\n", (uint16_t)(pStream->Regs.bd.ctl_len >> 16)); + } + + pHlp->pfnPrintf(pHlp, " offRead %#RX64\n", pStreamR3->State.offRead); + pHlp->pfnPrintf(pHlp, " offWrite %#RX64\n", pStreamR3->State.offWrite); + pHlp->pfnPrintf(pHlp, " uTimerHz %RU16\n", pStreamR3->State.uTimerHz); + pHlp->pfnPrintf(pHlp, " cDmaPeriodTicks %RU64\n", pStream->cDmaPeriodTicks); + pHlp->pfnPrintf(pHlp, " cbDmaPeriod %#RX32\n", pStream->cbDmaPeriod); +} + + +/** + * @callback_method_impl{FNDBGFHANDLERDEV, ac97stream} + */ +static DECLCALLBACK(void) ichac97R3DbgInfoStream(PPDMDEVINS pDevIns, PCDBGFINFOHLP pHlp, const char *pszArgs) +{ + PAC97STATE pThis = PDMDEVINS_2_DATA(pDevIns, PAC97STATE); + PAC97STATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PAC97STATER3); + int idxStream = ichac97R3DbgLookupStrmIdx(pHlp, pszArgs); + if (idxStream != -1) + ichac97R3DbgPrintStream(pHlp, &pThis->aStreams[idxStream], &pThisCC->aStreams[idxStream]); + else + for (idxStream = 0; idxStream < AC97_MAX_STREAMS; ++idxStream) + ichac97R3DbgPrintStream(pHlp, &pThis->aStreams[idxStream], &pThisCC->aStreams[idxStream]); +} + + +/** + * @callback_method_impl{FNDBGFHANDLERDEV, ac97mixer} + */ +static DECLCALLBACK(void) ichac97R3DbgInfoMixer(PPDMDEVINS pDevIns, PCDBGFINFOHLP pHlp, const char *pszArgs) +{ + PAC97STATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PAC97STATER3); + if (pThisCC->pMixer) + AudioMixerDebug(pThisCC->pMixer, pHlp, pszArgs); + else + pHlp->pfnPrintf(pHlp, "Mixer not available\n"); +} + + +/********************************************************************************************************************************* +* PDMIBASE * +*********************************************************************************************************************************/ + +/** + * @interface_method_impl{PDMIBASE,pfnQueryInterface} + */ +static DECLCALLBACK(void *) ichac97R3QueryInterface(struct PDMIBASE *pInterface, const char *pszIID) +{ + PAC97STATER3 pThisCC = RT_FROM_MEMBER(pInterface, AC97STATER3, IBase); + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIBASE, &pThisCC->IBase); + return NULL; +} + + +/********************************************************************************************************************************* +* PDMDEVREG * +*********************************************************************************************************************************/ + +/** + * Destroys all AC'97 audio streams of the device. + * + * @param pDevIns The device AC'97 instance. + * @param pThis The shared AC'97 state. + * @param pThisCC The ring-3 AC'97 state. + */ +static void ichac97R3StreamsDestroy(PPDMDEVINS pDevIns, PAC97STATE pThis, PAC97STATER3 pThisCC) +{ + LogFlowFuncEnter(); + + /* + * Destroy all AC'97 streams. + */ + for (unsigned i = 0; i < AC97_MAX_STREAMS; i++) + ichac97R3StreamDestroy(pThisCC, &pThis->aStreams[i], &pThisCC->aStreams[i]); + + /* + * Destroy all sinks. + */ + if (pThisCC->pSinkLineIn) + { + ichac97R3MixerRemoveDrvStreams(pDevIns, pThisCC, pThisCC->pSinkLineIn, PDMAUDIODIR_IN, PDMAUDIOPATH_IN_LINE); + + AudioMixerSinkDestroy(pThisCC->pSinkLineIn, pDevIns); + pThisCC->pSinkLineIn = NULL; + } + + if (pThisCC->pSinkMicIn) + { + ichac97R3MixerRemoveDrvStreams(pDevIns, pThisCC, pThisCC->pSinkMicIn, PDMAUDIODIR_IN, PDMAUDIOPATH_IN_MIC); + + AudioMixerSinkDestroy(pThisCC->pSinkMicIn, pDevIns); + pThisCC->pSinkMicIn = NULL; + } + + if (pThisCC->pSinkOut) + { + ichac97R3MixerRemoveDrvStreams(pDevIns, pThisCC, pThisCC->pSinkOut, PDMAUDIODIR_OUT, PDMAUDIOPATH_OUT_FRONT); + + AudioMixerSinkDestroy(pThisCC->pSinkOut, pDevIns); + pThisCC->pSinkOut = NULL; + } +} + + +/** + * Powers off the device. + * + * @param pDevIns Device instance to power off. + */ +static DECLCALLBACK(void) ichac97R3PowerOff(PPDMDEVINS pDevIns) +{ + PAC97STATE pThis = PDMDEVINS_2_DATA(pDevIns, PAC97STATE); + PAC97STATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PAC97STATER3); + + LogRel2(("AC97: Powering off ...\n")); + + /* Note: Involves mixer stream / sink destruction, so also do this here + * instead of in ichac97R3Destruct(). */ + ichac97R3StreamsDestroy(pDevIns, pThis, pThisCC); + + /* + * Note: Destroy the mixer while powering off and *not* in ichac97R3Destruct, + * giving the mixer the chance to release any references held to + * PDM audio streams it maintains. + */ + if (pThisCC->pMixer) + { + AudioMixerDestroy(pThisCC->pMixer, pDevIns); + pThisCC->pMixer = NULL; + } +} + + +/** + * @interface_method_impl{PDMDEVREG,pfnReset} + * + * @remarks The original sources didn't install a reset handler, but it seems to + * make sense to me so we'll do it. + */ +static DECLCALLBACK(void) ichac97R3Reset(PPDMDEVINS pDevIns) +{ + PAC97STATE pThis = PDMDEVINS_2_DATA(pDevIns, PAC97STATE); + PAC97STATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PAC97STATER3); + + LogRel(("AC97: Reset\n")); + + /* + * Reset the mixer too. The Windows XP driver seems to rely on + * this. At least it wants to read the vendor id before it resets + * the codec manually. + */ + ichac97R3MixerReset(pThis, pThisCC); + + /* + * Reset all streams. + */ + for (unsigned i = 0; i < AC97_MAX_STREAMS; i++) + { + ichac97R3StreamEnable(pDevIns, pThis, pThisCC, &pThis->aStreams[i], &pThisCC->aStreams[i], false /* fEnable */); + ichac97R3StreamReset(pThis, &pThis->aStreams[i], &pThisCC->aStreams[i]); + } + + /* + * Reset mixer sinks. + * + * Do the reset here instead of in ichac97R3StreamReset(); + * the mixer sink(s) might still have data to be processed when an audio stream gets reset. + */ + AudioMixerSinkReset(pThisCC->pSinkLineIn); + AudioMixerSinkReset(pThisCC->pSinkMicIn); + AudioMixerSinkReset(pThisCC->pSinkOut); +} + + +/** + * Adds a specific AC'97 driver to the driver chain. + * + * Only called from ichac97R3Attach(). + * + * @returns VBox status code. + * @param pDevIns The device instance. + * @param pThisCC The ring-3 AC'97 device state. + * @param pDrv The AC'97 driver to add. + */ +static int ichac97R3MixerAddDrv(PPDMDEVINS pDevIns, PAC97STATER3 pThisCC, PAC97DRIVER pDrv) +{ + int rc = VINF_SUCCESS; + + if (AudioHlpStreamCfgIsValid(&pThisCC->aStreams[AC97SOUNDSOURCE_PI_INDEX].State.Cfg)) + rc = ichac97R3MixerAddDrvStream(pDevIns, pThisCC->pSinkLineIn, + &pThisCC->aStreams[AC97SOUNDSOURCE_PI_INDEX].State.Cfg, pDrv); + + if (AudioHlpStreamCfgIsValid(&pThisCC->aStreams[AC97SOUNDSOURCE_PO_INDEX].State.Cfg)) + { + int rc2 = ichac97R3MixerAddDrvStream(pDevIns, pThisCC->pSinkOut, + &pThisCC->aStreams[AC97SOUNDSOURCE_PO_INDEX].State.Cfg, pDrv); + if (RT_SUCCESS(rc)) + rc = rc2; + } + + if (AudioHlpStreamCfgIsValid(&pThisCC->aStreams[AC97SOUNDSOURCE_MC_INDEX].State.Cfg)) + { + int rc2 = ichac97R3MixerAddDrvStream(pDevIns, pThisCC->pSinkMicIn, + &pThisCC->aStreams[AC97SOUNDSOURCE_MC_INDEX].State.Cfg, pDrv); + if (RT_SUCCESS(rc)) + rc = rc2; + } + + return rc; +} + + +/** + * Worker for ichac97R3Construct() and ichac97R3Attach(). + * + * @returns VBox status code. + * @param pDevIns The device instance. + * @param pThisCC The ring-3 AC'97 device state. + * @param uLUN The logical unit which is being attached. + * @param ppDrv Attached driver instance on success. Optional. + */ +static int ichac97R3AttachInternal(PPDMDEVINS pDevIns, PAC97STATER3 pThisCC, unsigned uLUN, PAC97DRIVER *ppDrv) +{ + /* + * Allocate a new driver structure and try attach the driver. + */ + PAC97DRIVER pDrv = (PAC97DRIVER)RTMemAllocZ(sizeof(AC97DRIVER)); + AssertPtrReturn(pDrv, VERR_NO_MEMORY); + RTStrPrintf(pDrv->szDesc, sizeof(pDrv->szDesc), "Audio driver port (AC'97) for LUN #%u", uLUN); + + PPDMIBASE pDrvBase; + int rc = PDMDevHlpDriverAttach(pDevIns, uLUN, &pThisCC->IBase, &pDrvBase, pDrv->szDesc); + if (RT_SUCCESS(rc)) + { + pDrv->pConnector = PDMIBASE_QUERY_INTERFACE(pDrvBase, PDMIAUDIOCONNECTOR); + AssertPtr(pDrv->pConnector); + if (RT_VALID_PTR(pDrv->pConnector)) + { + pDrv->pDrvBase = pDrvBase; + pDrv->uLUN = uLUN; + + /* Attach to driver list if not attached yet. */ + if (!pDrv->fAttached) + { + RTListAppend(&pThisCC->lstDrv, &pDrv->Node); + pDrv->fAttached = true; + } + + if (ppDrv) + *ppDrv = pDrv; + + /* + * While we're here, give the windows backends a hint about our typical playback + * configuration. + */ + if ( pDrv->pConnector + && pDrv->pConnector->pfnStreamConfigHint) + { + /* 48kHz */ + PDMAUDIOSTREAMCFG Cfg; + RT_ZERO(Cfg); + Cfg.enmDir = PDMAUDIODIR_OUT; + Cfg.enmPath = PDMAUDIOPATH_OUT_FRONT; + Cfg.Device.cMsSchedulingHint = 5; + Cfg.Backend.cFramesPreBuffering = UINT32_MAX; + PDMAudioPropsInit(&Cfg.Props, 2, true /*fSigned*/, 2, 48000); + RTStrPrintf(Cfg.szName, sizeof(Cfg.szName), "output 48kHz 2ch S16 (HDA config hint)"); + + pDrv->pConnector->pfnStreamConfigHint(pDrv->pConnector, &Cfg); /* (may trash CfgReq) */ +# if 0 + /* 44.1kHz */ + RT_ZERO(Cfg); + Cfg.enmDir = PDMAUDIODIR_OUT; + Cfg.enmPath = PDMAUDIOPATH_OUT_FRONT; + Cfg.Device.cMsSchedulingHint = 10; + Cfg.Backend.cFramesPreBuffering = UINT32_MAX; + PDMAudioPropsInit(&Cfg.Props, 2, true /*fSigned*/, 2, 44100); + RTStrPrintf(Cfg.szName, sizeof(Cfg.szName), "output 44.1kHz 2ch S16 (HDA config hint)"); + + pDrv->pConnector->pfnStreamConfigHint(pDrv->pConnector, &Cfg); /* (may trash CfgReq) */ +# endif + } + + LogFunc(("LUN#%u: returns VINF_SUCCESS (pCon=%p)\n", uLUN, pDrv->pConnector)); + return VINF_SUCCESS; + } + RTMemFree(pDrv); + rc = VERR_PDM_MISSING_INTERFACE_BELOW; + } + else if (rc == VERR_PDM_NO_ATTACHED_DRIVER) + LogFunc(("No attached driver for LUN #%u\n", uLUN)); + else + LogFunc(("Attached driver for LUN #%u failed: %Rrc\n", uLUN, rc)); + RTMemFree(pDrv); + + LogFunc(("LUN#%u: rc=%Rrc\n", uLUN, rc)); + return rc; +} + + +/** + * @interface_method_impl{PDMDEVREGR3,pfnAttach} + */ +static DECLCALLBACK(int) ichac97R3Attach(PPDMDEVINS pDevIns, unsigned iLUN, uint32_t fFlags) +{ + PAC97STATE pThis = PDMDEVINS_2_DATA(pDevIns, PAC97STATE); + PAC97STATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PAC97STATER3); + RT_NOREF(fFlags); + LogFunc(("iLUN=%u, fFlags=%#x\n", iLUN, fFlags)); + + DEVAC97_LOCK(pDevIns, pThis); + + PAC97DRIVER pDrv; + int rc = ichac97R3AttachInternal(pDevIns, pThisCC, iLUN, &pDrv); + if (RT_SUCCESS(rc)) + { + int rc2 = ichac97R3MixerAddDrv(pDevIns, pThisCC, pDrv); + if (RT_FAILURE(rc2)) + LogFunc(("ichac97R3MixerAddDrv failed with %Rrc (ignored)\n", rc2)); + } + + DEVAC97_UNLOCK(pDevIns, pThis); + + return rc; +} + + +/** + * Removes a specific AC'97 driver from the driver chain and destroys its + * associated streams. + * + * Only called from ichac97R3Detach(). + * + * @param pDevIns The device instance. + * @param pThisCC The ring-3 AC'97 device state. + * @param pDrv AC'97 driver to remove. + */ +static void ichac97R3MixerRemoveDrv(PPDMDEVINS pDevIns, PAC97STATER3 pThisCC, PAC97DRIVER pDrv) +{ + if (pDrv->MicIn.pMixStrm) + { + AudioMixerSinkRemoveStream(pThisCC->pSinkMicIn, pDrv->MicIn.pMixStrm); + AudioMixerStreamDestroy(pDrv->MicIn.pMixStrm, pDevIns, true /*fImmediate*/); + pDrv->MicIn.pMixStrm = NULL; + } + + if (pDrv->LineIn.pMixStrm) + { + AudioMixerSinkRemoveStream(pThisCC->pSinkLineIn, pDrv->LineIn.pMixStrm); + AudioMixerStreamDestroy(pDrv->LineIn.pMixStrm, pDevIns, true /*fImmediate*/); + pDrv->LineIn.pMixStrm = NULL; + } + + if (pDrv->Out.pMixStrm) + { + AudioMixerSinkRemoveStream(pThisCC->pSinkOut, pDrv->Out.pMixStrm); + AudioMixerStreamDestroy(pDrv->Out.pMixStrm, pDevIns, true /*fImmediate*/); + pDrv->Out.pMixStrm = NULL; + } + + RTListNodeRemove(&pDrv->Node); +} + + +/** + * @interface_method_impl{PDMDEVREG,pfnDetach} + */ +static DECLCALLBACK(void) ichac97R3Detach(PPDMDEVINS pDevIns, unsigned iLUN, uint32_t fFlags) +{ + PAC97STATE pThis = PDMDEVINS_2_DATA(pDevIns, PAC97STATE); + PAC97STATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PAC97STATER3); + RT_NOREF(fFlags); + + LogFunc(("iLUN=%u, fFlags=0x%x\n", iLUN, fFlags)); + + DEVAC97_LOCK(pDevIns, pThis); + + PAC97DRIVER pDrv; + RTListForEach(&pThisCC->lstDrv, pDrv, AC97DRIVER, Node) + { + if (pDrv->uLUN == iLUN) + { + /* Remove the driver from our list and destory it's associated streams. + This also will un-set the driver as a recording source (if associated). */ + ichac97R3MixerRemoveDrv(pDevIns, pThisCC, pDrv); + LogFunc(("Detached LUN#%u\n", pDrv->uLUN)); + + DEVAC97_UNLOCK(pDevIns, pThis); + + RTMemFree(pDrv); + return; + } + } + + DEVAC97_UNLOCK(pDevIns, pThis); + LogFunc(("LUN#%u was not found\n", iLUN)); +} + + +/** + * @interface_method_impl{PDMDEVREG,pfnDestruct} + */ +static DECLCALLBACK(int) ichac97R3Destruct(PPDMDEVINS pDevIns) +{ + PDMDEV_CHECK_VERSIONS_RETURN_QUIET(pDevIns); /* this shall come first */ + PAC97STATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PAC97STATER3); + + LogFlowFuncEnter(); + + PAC97DRIVER pDrv, pDrvNext; + RTListForEachSafe(&pThisCC->lstDrv, pDrv, pDrvNext, AC97DRIVER, Node) + { + RTListNodeRemove(&pDrv->Node); + RTMemFree(pDrv); + } + + /* Sanity. */ + Assert(RTListIsEmpty(&pThisCC->lstDrv)); + + /* We don't always go via PowerOff, so make sure the mixer is destroyed. */ + if (pThisCC->pMixer) + { + AudioMixerDestroy(pThisCC->pMixer, pDevIns); + pThisCC->pMixer = NULL; + } + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMDEVREG,pfnConstruct} + */ +static DECLCALLBACK(int) ichac97R3Construct(PPDMDEVINS pDevIns, int iInstance, PCFGMNODE pCfg) +{ + PDMDEV_CHECK_VERSIONS_RETURN(pDevIns); /* this shall come first */ + PAC97STATE pThis = PDMDEVINS_2_DATA(pDevIns, PAC97STATE); + PAC97STATER3 pThisCC = PDMDEVINS_2_DATA_CC(pDevIns, PAC97STATER3); + PCPDMDEVHLPR3 pHlp = pDevIns->pHlpR3; + Assert(iInstance == 0); RT_NOREF(iInstance); + + /* + * Initialize data so we can run the destructor without scewing up. + */ + pThisCC->pDevIns = pDevIns; + pThisCC->IBase.pfnQueryInterface = ichac97R3QueryInterface; + RTListInit(&pThisCC->lstDrv); + + /* + * Validate and read configuration. + */ + PDMDEV_VALIDATE_CONFIG_RETURN(pDevIns, "BufSizeInMs|BufSizeOutMs|Codec|TimerHz|DebugEnabled|DebugPathOut", ""); + + /** @devcfgm{ac97,BufSizeInMs,uint16_t,0,2000,0,ms} + * The size of the DMA buffer for input streams expressed in milliseconds. */ + int rc = pHlp->pfnCFGMQueryU16Def(pCfg, "BufSizeInMs", &pThis->cMsCircBufIn, 0); + if (RT_FAILURE(rc)) + return PDMDEV_SET_ERROR(pDevIns, rc, + N_("AC97 configuration error: failed to read 'BufSizeInMs' as 16-bit unsigned integer")); + if (pThis->cMsCircBufIn > 2000) + return PDMDEV_SET_ERROR(pDevIns, VERR_OUT_OF_RANGE, + N_("AC97 configuration error: 'BufSizeInMs' is out of bound, max 2000 ms")); + + /** @devcfgm{ac97,BufSizeOutMs,uint16_t,0,2000,0,ms} + * The size of the DMA buffer for output streams expressed in milliseconds. */ + rc = pHlp->pfnCFGMQueryU16Def(pCfg, "BufSizeOutMs", &pThis->cMsCircBufOut, 0); + if (RT_FAILURE(rc)) + return PDMDEV_SET_ERROR(pDevIns, rc, + N_("AC97 configuration error: failed to read 'BufSizeOutMs' as 16-bit unsigned integer")); + if (pThis->cMsCircBufOut > 2000) + return PDMDEV_SET_ERROR(pDevIns, VERR_OUT_OF_RANGE, + N_("AC97 configuration error: 'BufSizeOutMs' is out of bound, max 2000 ms")); + + /** @devcfgm{ac97,TimerHz,uint16_t,10,1000,100,ms} + * Currently the approximate rate at which the asynchronous I/O threads move + * data from/to the DMA buffer, thru the mixer and drivers stack, and + * to/from the host device/whatever. (It does NOT govern any DMA timer rate any + * more as might be hinted at by the name.) */ + rc = pHlp->pfnCFGMQueryU16Def(pCfg, "TimerHz", &pThis->uTimerHz, AC97_TIMER_HZ_DEFAULT); + if (RT_FAILURE(rc)) + return PDMDEV_SET_ERROR(pDevIns, rc, + N_("AC'97 configuration error: failed to read 'TimerHz' as a 16-bit unsigned integer")); + if (pThis->uTimerHz < 10 || pThis->uTimerHz > 1000) + return PDMDEV_SET_ERROR(pDevIns, VERR_OUT_OF_RANGE, + N_("AC'97 configuration error: 'TimerHz' is out of range (10-1000 Hz)")); + + if (pThis->uTimerHz != AC97_TIMER_HZ_DEFAULT) + LogRel(("AC97: Using custom device timer rate: %RU16 Hz\n", pThis->uTimerHz)); + + rc = pHlp->pfnCFGMQueryBoolDef(pCfg, "DebugEnabled", &pThisCC->Dbg.fEnabled, false); + if (RT_FAILURE(rc)) + return PDMDEV_SET_ERROR(pDevIns, rc, + N_("AC97 configuration error: failed to read debugging enabled flag as boolean")); + + rc = pHlp->pfnCFGMQueryStringAllocDef(pCfg, "DebugPathOut", &pThisCC->Dbg.pszOutPath, NULL); + if (RT_FAILURE(rc)) + return PDMDEV_SET_ERROR(pDevIns, rc, + N_("AC97 configuration error: failed to read debugging output path flag as string")); + + if (pThisCC->Dbg.fEnabled) + LogRel2(("AC97: Debug output will be saved to '%s'\n", pThisCC->Dbg.pszOutPath)); + + /* + * The AD1980 codec (with corresponding PCI subsystem vendor ID) is whitelisted + * in the Linux kernel; Linux makes no attempt to measure the data rate and assumes + * 48 kHz rate, which is exactly what we need. Same goes for AD1981B. + */ + char szCodec[20]; + rc = pHlp->pfnCFGMQueryStringDef(pCfg, "Codec", &szCodec[0], sizeof(szCodec), "STAC9700"); + if (RT_FAILURE(rc)) + return PDMDEV_SET_ERROR(pDevIns, VERR_PDM_DEVINS_UNKNOWN_CFG_VALUES, + N_("AC'97 configuration error: Querying \"Codec\" as string failed")); + if (!strcmp(szCodec, "STAC9700")) + pThis->enmCodecModel = AC97CODEC_STAC9700; + else if (!strcmp(szCodec, "AD1980")) + pThis->enmCodecModel = AC97CODEC_AD1980; + else if (!strcmp(szCodec, "AD1981B")) + pThis->enmCodecModel = AC97CODEC_AD1981B; + else + return PDMDevHlpVMSetError(pDevIns, VERR_PDM_DEVINS_UNKNOWN_CFG_VALUES, RT_SRC_POS, + N_("AC'97 configuration error: The \"Codec\" value \"%s\" is unsupported"), szCodec); + + LogRel(("AC97: Using codec '%s'\n", szCodec)); + + /* + * Use an own critical section for the device instead of the default + * one provided by PDM. This allows fine-grained locking in combination + * with TM when timer-specific stuff is being called in e.g. the MMIO handlers. + */ + rc = PDMDevHlpCritSectInit(pDevIns, &pThis->CritSect, RT_SRC_POS, "AC'97"); + AssertRCReturn(rc, rc); + + rc = PDMDevHlpSetDeviceCritSect(pDevIns, PDMDevHlpCritSectGetNop(pDevIns)); + AssertRCReturn(rc, rc); + + /* + * Initialize data (most of it anyway). + */ + /* PCI Device */ + PPDMPCIDEV pPciDev = pDevIns->apPciDevs[0]; + PCIDevSetVendorId(pPciDev, 0x8086); /* 00 ro - intel. */ Assert(pPciDev->abConfig[0x00] == 0x86); Assert(pPciDev->abConfig[0x01] == 0x80); + PCIDevSetDeviceId(pPciDev, 0x2415); /* 02 ro - 82801 / 82801aa(?). */ Assert(pPciDev->abConfig[0x02] == 0x15); Assert(pPciDev->abConfig[0x03] == 0x24); + PCIDevSetCommand(pPciDev, 0x0000); /* 04 rw,ro - pcicmd. */ Assert(pPciDev->abConfig[0x04] == 0x00); Assert(pPciDev->abConfig[0x05] == 0x00); + PCIDevSetStatus(pPciDev, VBOX_PCI_STATUS_DEVSEL_MEDIUM | VBOX_PCI_STATUS_FAST_BACK); /* 06 rwc?,ro? - pcists. */ Assert(pPciDev->abConfig[0x06] == 0x80); Assert(pPciDev->abConfig[0x07] == 0x02); + PCIDevSetRevisionId(pPciDev, 0x01); /* 08 ro - rid. */ Assert(pPciDev->abConfig[0x08] == 0x01); + PCIDevSetClassProg(pPciDev, 0x00); /* 09 ro - pi. */ Assert(pPciDev->abConfig[0x09] == 0x00); + PCIDevSetClassSub(pPciDev, 0x01); /* 0a ro - scc; 01 == Audio. */ Assert(pPciDev->abConfig[0x0a] == 0x01); + PCIDevSetClassBase(pPciDev, 0x04); /* 0b ro - bcc; 04 == multimedia.*/Assert(pPciDev->abConfig[0x0b] == 0x04); + PCIDevSetHeaderType(pPciDev, 0x00); /* 0e ro - headtyp. */ Assert(pPciDev->abConfig[0x0e] == 0x00); + PCIDevSetBaseAddress(pPciDev, 0, /* 10 rw - nambar - native audio mixer base. */ + true /* fIoSpace */, false /* fPrefetchable */, false /* f64Bit */, 0x00000000); Assert(pPciDev->abConfig[0x10] == 0x01); Assert(pPciDev->abConfig[0x11] == 0x00); Assert(pPciDev->abConfig[0x12] == 0x00); Assert(pPciDev->abConfig[0x13] == 0x00); + PCIDevSetBaseAddress(pPciDev, 1, /* 14 rw - nabmbar - native audio bus mastering. */ + true /* fIoSpace */, false /* fPrefetchable */, false /* f64Bit */, 0x00000000); Assert(pPciDev->abConfig[0x14] == 0x01); Assert(pPciDev->abConfig[0x15] == 0x00); Assert(pPciDev->abConfig[0x16] == 0x00); Assert(pPciDev->abConfig[0x17] == 0x00); + PCIDevSetInterruptLine(pPciDev, 0x00); /* 3c rw. */ Assert(pPciDev->abConfig[0x3c] == 0x00); + PCIDevSetInterruptPin(pPciDev, 0x01); /* 3d ro - INTA#. */ Assert(pPciDev->abConfig[0x3d] == 0x01); + + if (pThis->enmCodecModel == AC97CODEC_AD1980) + { + PCIDevSetSubSystemVendorId(pPciDev, 0x1028); /* 2c ro - Dell.) */ + PCIDevSetSubSystemId(pPciDev, 0x0177); /* 2e ro. */ + } + else if (pThis->enmCodecModel == AC97CODEC_AD1981B) + { + PCIDevSetSubSystemVendorId(pPciDev, 0x1028); /* 2c ro - Dell.) */ + PCIDevSetSubSystemId(pPciDev, 0x01ad); /* 2e ro. */ + } + else + { + PCIDevSetSubSystemVendorId(pPciDev, 0x8086); /* 2c ro - Intel.) */ + PCIDevSetSubSystemId(pPciDev, 0x0000); /* 2e ro. */ + } + + /* + * Register the PCI device and associated I/O regions. + */ + rc = PDMDevHlpPCIRegister(pDevIns, pPciDev); + if (RT_FAILURE(rc)) + return rc; + + rc = PDMDevHlpPCIIORegionCreateIo(pDevIns, 0 /*iPciRegion*/, 256 /*cPorts*/, + ichac97IoPortNamWrite, ichac97IoPortNamRead, NULL /*pvUser*/, + "ICHAC97 NAM", NULL /*paExtDescs*/, &pThis->hIoPortsNam); + AssertRCReturn(rc, rc); + + rc = PDMDevHlpPCIIORegionCreateIo(pDevIns, 1 /*iPciRegion*/, 64 /*cPorts*/, + ichac97IoPortNabmWrite, ichac97IoPortNabmRead, NULL /*pvUser*/, + "ICHAC97 NABM", g_aNabmPorts, &pThis->hIoPortsNabm); + AssertRCReturn(rc, rc); + + /* + * Saved state. + */ + rc = PDMDevHlpSSMRegister(pDevIns, AC97_SAVED_STATE_VERSION, sizeof(*pThis), ichac97R3SaveExec, ichac97R3LoadExec); + if (RT_FAILURE(rc)) + return rc; + + /* + * Attach drivers. We ASSUME they are configured consecutively without any + * gaps, so we stop when we hit the first LUN w/o a driver configured. + */ + for (unsigned iLun = 0; ; iLun++) + { + AssertBreak(iLun < UINT8_MAX); + LogFunc(("Trying to attach driver for LUN#%u ...\n", iLun)); + rc = ichac97R3AttachInternal(pDevIns, pThisCC, iLun, NULL /* ppDrv */); + if (rc == VERR_PDM_NO_ATTACHED_DRIVER) + { + LogFunc(("cLUNs=%u\n", iLun)); + break; + } + AssertLogRelMsgReturn(RT_SUCCESS(rc), ("LUN#%u: rc=%Rrc\n", iLun, rc), rc); + } + + uint32_t fMixer = AUDMIXER_FLAGS_NONE; + if (pThisCC->Dbg.fEnabled) + fMixer |= AUDMIXER_FLAGS_DEBUG; + + rc = AudioMixerCreate("AC'97 Mixer", 0 /* uFlags */, &pThisCC->pMixer); + AssertRCReturn(rc, rc); + + rc = AudioMixerCreateSink(pThisCC->pMixer, "Line In", + PDMAUDIODIR_IN, pDevIns, &pThisCC->pSinkLineIn); + AssertRCReturn(rc, rc); + rc = AudioMixerCreateSink(pThisCC->pMixer, "Microphone In", + PDMAUDIODIR_IN, pDevIns, &pThisCC->pSinkMicIn); + AssertRCReturn(rc, rc); + rc = AudioMixerCreateSink(pThisCC->pMixer, "PCM Output", + PDMAUDIODIR_OUT, pDevIns, &pThisCC->pSinkOut); + AssertRCReturn(rc, rc); + + /* + * Create all hardware streams. + */ + AssertCompile(RT_ELEMENTS(pThis->aStreams) == AC97_MAX_STREAMS); + for (unsigned i = 0; i < AC97_MAX_STREAMS; i++) + { + rc = ichac97R3StreamConstruct(pThisCC, &pThis->aStreams[i], &pThisCC->aStreams[i], i /* SD# */); + AssertRCReturn(rc, rc); + } + + /* + * Create the emulation timers (one per stream). + * + * We must the critical section for the timers as the device has a + * noop section associated with it. + * + * Note: Use TMCLOCK_VIRTUAL_SYNC here, as the guest's AC'97 driver + * relies on exact (virtual) DMA timing and uses DMA Position Buffers + * instead of the LPIB registers. + */ + /** @todo r=bird: The need to use virtual sync is perhaps because TM + * doesn't schedule regular TMCLOCK_VIRTUAL timers as accurately as it + * should (VT-x preemption timer, etc). Hope to address that before + * long. @bugref{9943}. */ + static const char * const s_apszNames[] = { "AC97 PI", "AC97 PO", "AC97 MC" }; + AssertCompile(RT_ELEMENTS(s_apszNames) == AC97_MAX_STREAMS); + for (unsigned i = 0; i < AC97_MAX_STREAMS; i++) + { + rc = PDMDevHlpTimerCreate(pDevIns, TMCLOCK_VIRTUAL_SYNC, ichac97R3Timer, &pThis->aStreams[i], + TMTIMER_FLAGS_NO_CRIT_SECT | TMTIMER_FLAGS_RING0, s_apszNames[i], &pThis->aStreams[i].hTimer); + AssertRCReturn(rc, rc); + + rc = PDMDevHlpTimerSetCritSect(pDevIns, pThis->aStreams[i].hTimer, &pThis->CritSect); + AssertRCReturn(rc, rc); + } + + ichac97R3Reset(pDevIns); + + /* + * Info items. + */ + //PDMDevHlpDBGFInfoRegister(pDevIns, "ac97", "AC'97 registers. (ac97 [register case-insensitive])", ichac97R3DbgInfo); + PDMDevHlpDBGFInfoRegister(pDevIns, "ac97bdl", "AC'97 buffer descriptor list (BDL). (ac97bdl [stream number])", + ichac97R3DbgInfoBDL); + PDMDevHlpDBGFInfoRegister(pDevIns, "ac97stream", "AC'97 stream info. (ac97stream [stream number])", ichac97R3DbgInfoStream); + PDMDevHlpDBGFInfoRegister(pDevIns, "ac97mixer", "AC'97 mixer state.", ichac97R3DbgInfoMixer); + + /* + * Register statistics. + */ + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatUnimplementedNabmReads, STAMTYPE_COUNTER, "UnimplementedNabmReads", STAMUNIT_OCCURENCES, "Unimplemented NABM register reads."); + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatUnimplementedNabmWrites, STAMTYPE_COUNTER, "UnimplementedNabmWrites", STAMUNIT_OCCURENCES, "Unimplemented NABM register writes."); + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatUnimplementedNamReads, STAMTYPE_COUNTER, "UnimplementedNamReads", STAMUNIT_OCCURENCES, "Unimplemented NAM register reads."); + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatUnimplementedNamWrites, STAMTYPE_COUNTER, "UnimplementedNamWrites", STAMUNIT_OCCURENCES, "Unimplemented NAM register writes."); +# ifdef VBOX_WITH_STATISTICS + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatTimer, STAMTYPE_PROFILE, "Timer", STAMUNIT_TICKS_PER_CALL, "Profiling ichac97Timer."); +# endif + for (unsigned idxStream = 0; idxStream < RT_ELEMENTS(pThis->aStreams); idxStream++) + { + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStreams[idxStream].cbDmaPeriod, STAMTYPE_U32, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "Bytes to transfer in the current DMA period.", "Stream%u/cbTransferChunk", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStreams[idxStream].Regs.cr, STAMTYPE_X8, STAMVISIBILITY_ALWAYS, STAMUNIT_NONE, + "Control register (CR), bit 0 is the run bit.", "Stream%u/reg-CR", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStreams[idxStream].Regs.sr, STAMTYPE_X16, STAMVISIBILITY_ALWAYS, STAMUNIT_NONE, + "Status register (SR).", "Stream%u/reg-SR", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.Cfg.Props.uHz, STAMTYPE_U32, STAMVISIBILITY_USED, STAMUNIT_HZ, + "The stream frequency.", "Stream%u/Hz", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.Cfg.Props.cbFrame, STAMTYPE_U8, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "The frame size.", "Stream%u/FrameSize", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.offRead, STAMTYPE_U64, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "Virtual internal buffer read position.", "Stream%u/offRead", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.offWrite, STAMTYPE_U64, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "Virtual internal buffer write position.", "Stream%u/offWrite", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatDmaBufSize, STAMTYPE_U32, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "Size of the internal DMA buffer.", "Stream%u/DMABufSize", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatDmaBufUsed, STAMTYPE_U32, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "Number of bytes used in the internal DMA buffer.", "Stream%u/DMABufUsed", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatDmaFlowProblems, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + "Number of internal DMA buffer problems.", "Stream%u/DMABufferProblems", idxStream); + if (ichac97R3GetDirFromSD(idxStream) == PDMAUDIODIR_OUT) + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatDmaFlowErrors, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + "Number of internal DMA buffer overflows.", "Stream%u/DMABufferOverflows", idxStream); + else + { + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatDmaFlowErrors, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + "Number of internal DMA buffer underuns.", "Stream%u/DMABufferUnderruns", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatDmaFlowErrorBytes, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "Number of bytes of silence added to cope with underruns.", "Stream%u/DMABufferSilence", idxStream); + } + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatDmaSkippedDch, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + "DMA transfer period skipped, controller halted (DCH).", "Stream%u/DMASkippedDch", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatDmaSkippedPendingBcis, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + "DMA transfer period skipped because of BCIS pending.", "Stream%u/DMASkippedPendingBCIS", idxStream); + + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatStart, STAMTYPE_PROFILE, STAMVISIBILITY_USED, STAMUNIT_NS_PER_CALL, + "Starting the stream.", "Stream%u/Start", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatStop, STAMTYPE_PROFILE, STAMVISIBILITY_USED, STAMUNIT_NS_PER_CALL, + "Stopping the stream.", "Stream%u/Stop", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatReset, STAMTYPE_PROFILE, STAMVISIBILITY_USED, STAMUNIT_NS_PER_CALL, + "Resetting the stream.", "Stream%u/Reset", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatReSetUpChanged, STAMTYPE_PROFILE, STAMVISIBILITY_USED, STAMUNIT_NS_PER_CALL, + "ichac97R3StreamReSetUp when recreating the streams.", "Stream%u/ReSetUp-Change", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatReSetUpSame, STAMTYPE_PROFILE, STAMVISIBILITY_USED, STAMUNIT_NS_PER_CALL, + "ichac97R3StreamReSetUp when no change.", "Stream%u/ReSetUp-NoChange", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatWriteCr, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + "CR register writes.", "Stream%u/WriteCr", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThisCC->aStreams[idxStream].State.StatWriteLviRecover, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + "LVI register writes recovering from underflow.", "Stream%u/WriteLviRecover", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStreams[idxStream].StatWriteLvi, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + "LVI register writes (non-recoving).", "Stream%u/WriteLvi", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStreams[idxStream].StatWriteSr1, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + "SR register 1-byte writes.", "Stream%u/WriteSr-1byte", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStreams[idxStream].StatWriteSr2, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + "SR register 2-byte writes.", "Stream%u/WriteSr-2byte", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStreams[idxStream].StatWriteBdBar, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + "BDBAR register writes.", "Stream%u/WriteBdBar", idxStream); + } + + LogFlowFuncLeaveRC(VINF_SUCCESS); + return VINF_SUCCESS; +} + +#else /* !IN_RING3 */ + +/** + * @callback_method_impl{PDMDEVREGR0,pfnConstruct} + */ +static DECLCALLBACK(int) ichac97RZConstruct(PPDMDEVINS pDevIns) +{ + PDMDEV_CHECK_VERSIONS_RETURN(pDevIns); + PAC97STATE pThis = PDMDEVINS_2_DATA(pDevIns, PAC97STATE); + + int rc = PDMDevHlpSetDeviceCritSect(pDevIns, PDMDevHlpCritSectGetNop(pDevIns)); + AssertRCReturn(rc, rc); + + rc = PDMDevHlpIoPortSetUpContext(pDevIns, pThis->hIoPortsNam, ichac97IoPortNamWrite, ichac97IoPortNamRead, NULL /*pvUser*/); + AssertRCReturn(rc, rc); + rc = PDMDevHlpIoPortSetUpContext(pDevIns, pThis->hIoPortsNabm, ichac97IoPortNabmWrite, ichac97IoPortNabmRead, NULL /*pvUser*/); + AssertRCReturn(rc, rc); + + return VINF_SUCCESS; +} + +#endif /* !IN_RING3 */ + +/** + * The device registration structure. + */ +const PDMDEVREG g_DeviceICHAC97 = +{ + /* .u32Version = */ PDM_DEVREG_VERSION, + /* .uReserved0 = */ 0, + /* .szName = */ "ichac97", + /* .fFlags = */ PDM_DEVREG_FLAGS_DEFAULT_BITS | PDM_DEVREG_FLAGS_RZ | PDM_DEVREG_FLAGS_NEW_STYLE + | PDM_DEVREG_FLAGS_FIRST_POWEROFF_NOTIFICATION /* stream clearnup with working drivers */, + /* .fClass = */ PDM_DEVREG_CLASS_AUDIO, + /* .cMaxInstances = */ 1, + /* .uSharedVersion = */ 42, + /* .cbInstanceShared = */ sizeof(AC97STATE), + /* .cbInstanceCC = */ CTX_EXPR(sizeof(AC97STATER3), 0, 0), + /* .cbInstanceRC = */ 0, + /* .cMaxPciDevices = */ 1, + /* .cMaxMsixVectors = */ 0, + /* .pszDescription = */ "ICH AC'97 Audio Controller", +#if defined(IN_RING3) + /* .pszRCMod = */ "VBoxDDRC.rc", + /* .pszR0Mod = */ "VBoxDDR0.r0", + /* .pfnConstruct = */ ichac97R3Construct, + /* .pfnDestruct = */ ichac97R3Destruct, + /* .pfnRelocate = */ NULL, + /* .pfnMemSetup = */ NULL, + /* .pfnPowerOn = */ NULL, + /* .pfnReset = */ ichac97R3Reset, + /* .pfnSuspend = */ NULL, + /* .pfnResume = */ NULL, + /* .pfnAttach = */ ichac97R3Attach, + /* .pfnDetach = */ ichac97R3Detach, + /* .pfnQueryInterface = */ NULL, + /* .pfnInitComplete = */ NULL, + /* .pfnPowerOff = */ ichac97R3PowerOff, + /* .pfnSoftReset = */ NULL, + /* .pfnReserved0 = */ NULL, + /* .pfnReserved1 = */ NULL, + /* .pfnReserved2 = */ NULL, + /* .pfnReserved3 = */ NULL, + /* .pfnReserved4 = */ NULL, + /* .pfnReserved5 = */ NULL, + /* .pfnReserved6 = */ NULL, + /* .pfnReserved7 = */ NULL, +#elif defined(IN_RING0) + /* .pfnEarlyConstruct = */ NULL, + /* .pfnConstruct = */ ichac97RZConstruct, + /* .pfnDestruct = */ NULL, + /* .pfnFinalDestruct = */ NULL, + /* .pfnRequest = */ NULL, + /* .pfnReserved0 = */ NULL, + /* .pfnReserved1 = */ NULL, + /* .pfnReserved2 = */ NULL, + /* .pfnReserved3 = */ NULL, + /* .pfnReserved4 = */ NULL, + /* .pfnReserved5 = */ NULL, + /* .pfnReserved6 = */ NULL, + /* .pfnReserved7 = */ NULL, +#elif defined(IN_RC) + /* .pfnConstruct = */ ichac97RZConstruct, + /* .pfnReserved0 = */ NULL, + /* .pfnReserved1 = */ NULL, + /* .pfnReserved2 = */ NULL, + /* .pfnReserved3 = */ NULL, + /* .pfnReserved4 = */ NULL, + /* .pfnReserved5 = */ NULL, + /* .pfnReserved6 = */ NULL, + /* .pfnReserved7 = */ NULL, +#else +# error "Not in IN_RING3, IN_RING0 or IN_RC!" +#endif + /* .u32VersionEnd = */ PDM_DEVREG_VERSION +}; + +#endif /* !VBOX_DEVICE_STRUCT_TESTCASE */ + diff --git a/src/VBox/Devices/Audio/DevSB16.cpp b/src/VBox/Devices/Audio/DevSB16.cpp new file mode 100644 index 00000000..985776a0 --- /dev/null +++ b/src/VBox/Devices/Audio/DevSB16.cpp @@ -0,0 +1,3159 @@ +/* $Id: DevSB16.cpp $ */ +/** @file + * DevSB16 - VBox SB16 Audio Controller. + */ + +/* + * Copyright (C) 2015-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 + * -------------------------------------------------------------------- + * + * This code is based on: sb16.c from QEMU AUDIO subsystem (r3917). + * QEMU Soundblaster 16 emulation + * + * Copyright (c) 2003-2005 Vassili Karpov (malc) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_DEV_SB16 +#include <VBox/log.h> +#include <iprt/assert.h> +#include <iprt/file.h> +#ifdef IN_RING3 +# include <iprt/mem.h> +# include <iprt/string.h> +# include <iprt/uuid.h> +#endif + +#include <VBox/vmm/pdmdev.h> +#include <VBox/vmm/pdmaudioifs.h> +#include <VBox/vmm/pdmaudioinline.h> +#include <VBox/AssertGuest.h> + +#include "VBoxDD.h" + +#include "AudioMixBuffer.h" +#include "AudioMixer.h" +#include "AudioHlp.h" + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +/** Default timer frequency (in Hz). */ +#define SB16_TIMER_HZ_DEFAULT 100 +/** The maximum number of separate streams we currently implement. + * Currently we only support one stream only, namely the output stream. */ +#define SB16_MAX_STREAMS 1 +/** The (zero-based) index of the output stream in \a aStreams. */ +#define SB16_IDX_OUT 0 + +/** Current saved state version. */ +#define SB16_SAVE_STATE_VERSION 2 +/** The version used in VirtualBox version 3.0 and earlier. This didn't include the config dump. */ +#define SB16_SAVE_STATE_VERSION_VBOX_30 1 + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +static const char e3[] = "COPYRIGHT (C) CREATIVE TECHNOLOGY LTD, 1992."; + + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +/** Pointer to the SB16 state. */ +typedef struct SB16STATE *PSB16STATE; + +/** + * The internal state of a SB16 stream. + */ +typedef struct SB16STREAMSTATE +{ + /** Flag indicating whether this stream is in enabled state or not. */ + bool fEnabled; + /** Set if we've registered the asynchronous update job. */ + bool fRegisteredAsyncUpdateJob; + /** DMA cache to read data from / write data to. */ + PRTCIRCBUF pCircBuf; + /** Current circular buffer read offset (for tracing & logging). */ + uint64_t offRead; + /** Current circular buffer write offset (for tracing & logging). */ + uint64_t offWrite; + + /** Size of the DMA buffer (pCircBuf) in bytes. */ + uint32_t StatDmaBufSize; + /** Number of used bytes in the DMA buffer (pCircBuf). */ + uint32_t StatDmaBufUsed; +} SB16STREAMSTATE; +/** Pointer to internal state of an SB16 stream. */ +typedef SB16STREAMSTATE *PSB16STREAMSTATE; + +/** + * Structure defining a (host backend) driver stream. + * Each driver has its own instances of audio mixer streams, which then + * can go into the same (or even different) audio mixer sinks. + */ +typedef struct SB16DRIVERSTREAM +{ + /** Associated mixer stream handle. */ + R3PTRTYPE(PAUDMIXSTREAM) pMixStrm; + /** The stream's current configuration. */ +} SB16DRIVERSTREAM, *PSB16DRIVERSTREAM; + +/** + * Struct for tracking a host backend driver, i.e. our per-LUN data. + */ +typedef struct SB16DRIVER +{ + /** Node for storing this driver in our device driver list of SB16STATE. */ + RTLISTNODER3 Node; + /** Pointer to SB16 controller (state). */ + R3PTRTYPE(PSB16STATE) pSB16State; + /** Pointer to attached driver base interface. */ + R3PTRTYPE(PPDMIBASE) pDrvBase; + /** Audio connector interface to the underlying host backend. */ + R3PTRTYPE(PPDMIAUDIOCONNECTOR) pConnector; + /** Stream for output. */ + SB16DRIVERSTREAM Out; + /** LUN # to which this driver has been assigned. */ + uint8_t uLUN; + /** Whether this driver is in an attached state or not. */ + bool fAttached; + /** The LUN description. */ + char szDesc[48 - 2]; +} SB16DRIVER; +/** Pointer to the per-LUN data. */ +typedef SB16DRIVER *PSB16DRIVER; + +/** + * Runtime configurable debug stuff for a SB16 stream. + */ +typedef struct SB16STREAMDEBUGRT +{ + /** Whether debugging is enabled or not. */ + bool fEnabled; + uint8_t Padding[7]; + /** File for dumping DMA reads / writes. + * For input streams, this dumps data being written to the device DMA, + * whereas for output streams this dumps data being read from the device DMA. */ + R3PTRTYPE(PAUDIOHLPFILE) pFileDMA; +} SB16STREAMDEBUGRT; + +/** + * Debug stuff for a SB16 stream. + */ +typedef struct SB16STREAMDEBUG +{ + /** Runtime debug stuff. */ + SB16STREAMDEBUGRT Runtime; +} SB16STREAMDEBUG; + +/** + * Structure for keeping a SB16 hardware stream configuration. + */ +typedef struct SB16STREAMHWCFG +{ + /** IRQ # to use. */ + uint8_t uIrq; + /** Low DMA channel to use. */ + uint8_t uDmaChanLow; + /** High DMA channel to use. */ + uint8_t uDmaChanHigh; + /** IO port to use. */ + RTIOPORT uPort; + /** DSP version to expose. */ + uint16_t uVer; +} SB16STREAMHWCFG; + +/** + * Structure for a SB16 stream. + */ +typedef struct SB16STREAM +{ + /** The stream's own index in \a aStreams of SB16STATE. + * Set to UINT8_MAX if not set (yet). */ + uint8_t uIdx; + uint16_t uTimerHz; + /** The timer for pumping data thru the attached LUN drivers. */ + TMTIMERHANDLE hTimerIO; + /** The timer interval for pumping data thru the LUN drivers in timer ticks. */ + uint64_t cTicksTimerIOInterval; + /** Timestamp of the last timer callback (sb16TimerIO). + * Used to calculate thetime actually elapsed between two timer callbacks. + * This currently ASSMUMES that we only have one single (output) stream. */ + uint64_t tsTimerIO; /** @todo Make this a per-stream value. */ + /** The stream's currentconfiguration. */ + PDMAUDIOSTREAMCFG Cfg; + /** The stream's defaulthardware configuration, mostly done by jumper settings back then. */ + SB16STREAMHWCFG HwCfgDefault; + /** The stream's hardware configuration set at runtime. + * Might differ from the default configuration above and is needed for live migration. */ + SB16STREAMHWCFG HwCfgRuntime; + + int fifo; + int dma_auto; + /** Whether to use the high (\c true) or the low (\c false) DMA channel. */ + int fDmaUseHigh; + int can_write; /** @todo r=andy BUGBUG Value never gets set to 0! */ + int time_const; + /** The DMA transfer (block)size in bytes. */ + int32_t cbDmaBlockSize; + int32_t cbDmaLeft; /** Note: Can be < 0. Needs to 32-bit for backwards compatibility. */ + /** Internal state of this stream. */ + SB16STREAMSTATE State; + /** Debug stuff. */ + SB16STREAMDEBUG Dbg; +} SB16STREAM; +/** Pointer to a SB16 stream */ +typedef SB16STREAM *PSB16STREAM; + +/** + * SB16 debug settings. + */ +typedef struct SB16STATEDEBUG +{ + /** Whether debugging is enabled or not. */ + bool fEnabled; + bool afAlignment[7]; + /** Path where to dump the debug output to. + * Can be NULL, in which the system's temporary directory will be used then. */ + R3PTRTYPE(char *) pszOutPath; +} SB16STATEDEBUG; + +/** + * The SB16 state. + */ +typedef struct SB16STATE +{ + /** Pointer to the device instance. */ + PPDMDEVINSR3 pDevInsR3; + /** Pointer to the connector of the attached audio driver. */ + PPDMIAUDIOCONNECTOR pDrv; + + int dsp_in_idx; + int dsp_out_data_len; + int dsp_in_needed_bytes; + int cmd; + int highspeed; + + int v2x6; + + uint8_t csp_param; + uint8_t csp_value; + uint8_t csp_mode; + uint8_t csp_index; + uint8_t csp_regs[256]; + uint8_t csp_reg83[4]; + int csp_reg83r; + int csp_reg83w; + + uint8_t dsp_in_data[10]; + uint8_t dsp_out_data[50]; + uint8_t test_reg; + uint8_t last_read_byte; + int nzero; + + RTLISTANCHOR lstDrv; + /** IRQ timer */ + TMTIMERHANDLE hTimerIRQ; + /** The base interface for LUN\#0. */ + PDMIBASE IBase; + + /** Array of all SB16 hardware audio stream. */ + SB16STREAM aStreams[SB16_MAX_STREAMS]; + /** The device's software mixer. */ + R3PTRTYPE(PAUDIOMIXER) pMixer; + /** Audio sink for PCM output. */ + R3PTRTYPE(PAUDMIXSINK) pSinkOut; + + /** The two mixer I/O ports (port + 4). */ + IOMIOPORTHANDLE hIoPortsMixer; + /** The 10 DSP I/O ports (port + 6). */ + IOMIOPORTHANDLE hIoPortsDsp; + + /** Debug settings. */ + SB16STATEDEBUG Dbg; + + /* mixer state */ + uint8_t mixer_nreg; + uint8_t mixer_regs[256]; + +#ifdef VBOX_WITH_STATISTICS + STAMPROFILE StatTimerIO; + STAMCOUNTER StatBytesRead; +#endif +} SB16STATE; + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ +DECLINLINE(PDMAUDIODIR) sb16GetDirFromIndex(uint8_t uIdx); + +static int sb16StreamEnable(PSB16STATE pThis, PSB16STREAM pStream, bool fEnable, bool fForce); +static void sb16StreamReset(PSB16STATE pThis, PSB16STREAM pStream); +static int sb16StreamOpen(PPDMDEVINS pDevIns, PSB16STATE pThis, PSB16STREAM pStream); +static void sb16StreamClose(PPDMDEVINS pDevIns, PSB16STATE pThis, PSB16STREAM pStream); +DECLINLINE(PAUDMIXSINK) sb16StreamIndexToSink(PSB16STATE pThis, uint8_t uIdx); +static void sb16StreamTransferScheduleNext(PSB16STATE pThis, PSB16STREAM pStream, uint32_t cSamples); +static int sb16StreamDoDmaOutput(PSB16STATE pThis, PSB16STREAM pStream, int uDmaChan, uint32_t offDma, uint32_t cbDma, uint32_t cbToRead, uint32_t *pcbRead); +static DECLCALLBACK(void) sb16StreamUpdateAsyncIoJob(PPDMDEVINS pDevIns, PAUDMIXSINK pSink, void *pvUser); + +static DECLCALLBACK(void) sb16TimerIO(PPDMDEVINS pDevIns, TMTIMERHANDLE hTimer, void *pvUser); +static DECLCALLBACK(void) sb16TimerIRQ(PPDMDEVINS pDevIns, TMTIMERHANDLE hTimer, void *pvUser); +DECLINLINE(void) sb16TimerSet(PPDMDEVINS pDevIns, PSB16STREAM pStream, uint64_t cTicksToDeadline); + +static void sb16SpeakerControl(PSB16STATE pThis, bool fOn); +static void sb16UpdateVolume(PSB16STATE pThis); + + + +static void sb16SpeakerControl(PSB16STATE pThis, bool fOn) +{ + RT_NOREF(pThis, fOn); + + /** @todo This currently does nothing. */ +} + +static void sb16StreamControl(PPDMDEVINS pDevIns, PSB16STATE pThis, PSB16STREAM pStream, bool fRun) +{ + unsigned uDmaChan = pStream->fDmaUseHigh ? pStream->HwCfgRuntime.uDmaChanHigh : pStream->HwCfgRuntime.uDmaChanLow; + + LogFunc(("fRun=%RTbool, fDmaUseHigh=%RTbool, uDmaChan=%u\n", fRun, pStream->fDmaUseHigh, uDmaChan)); + + PDMDevHlpDMASetDREQ(pThis->pDevInsR3, uDmaChan, fRun ? 1 : 0); + + if (fRun != pStream->State.fEnabled) + { + if (fRun) + { + int rc = VINF_SUCCESS; + + if (pStream->Cfg.Props.uHz > 0) + { + rc = sb16StreamOpen(pDevIns, pThis, pStream); + if (RT_SUCCESS(rc)) + sb16UpdateVolume(pThis); + } + else + AssertFailed(); /** @todo Buggy code? */ + + if (RT_SUCCESS(rc)) + { + rc = sb16StreamEnable(pThis, pStream, true /* fEnable */, false /* fForce */); + if (RT_SUCCESS(rc)) + { + sb16TimerSet(pDevIns, pStream, pStream->cTicksTimerIOInterval); + + PDMDevHlpDMASchedule(pThis->pDevInsR3); + } + } + } + else + { + sb16StreamEnable(pThis, pStream, false /* fEnable */, false /* fForce */); + } + } +} + +#define DMA8_AUTO 1 +#define DMA8_HIGH 2 + +static void sb16DmaCmdContinue8(PPDMDEVINS pDevIns, PSB16STATE pThis, PSB16STREAM pStream) +{ + sb16StreamControl(pDevIns, pThis, pStream, true /* fRun */); +} + +static void sb16DmaCmd8(PPDMDEVINS pDevIns, PSB16STATE pThis, PSB16STREAM pStream, + int mask, int dma_len) +{ + pStream->fDmaUseHigh = 0; + + if (-1 == pStream->time_const) + { + if (pStream->Cfg.Props.uHz == 0) + pStream->Cfg.Props.uHz = 11025; + } + else + { + int tmp = (256 - pStream->time_const); + pStream->Cfg.Props.uHz = (1000000 + (tmp / 2)) / tmp; + } + + /** @todo r=bird: Use '(pThis->mixer_regs[0x0e] & 2) == 0 ? 1 : 2' like below? */ + unsigned cShiftChannels = PDMAudioPropsChannels(&pStream->Cfg.Props) >= 2 ? 1 : 0; + + if (dma_len != -1) + { + pStream->cbDmaBlockSize = dma_len << cShiftChannels; + } + else + { + /* This is apparently the only way to make both Act1/PL + and SecondReality/FC work + + r=andy Wow, actually someone who remembers Future Crew :-) + + Act1 sets block size via command 0x48 and it's an odd number + SR does the same with even number + Both use stereo, and Creatives own documentation states that + 0x48 sets block size in bytes less one.. go figure */ + pStream->cbDmaBlockSize &= ~cShiftChannels; + } + + pStream->Cfg.Props.uHz >>= cShiftChannels; + pStream->cbDmaLeft = pStream->cbDmaBlockSize; + /* pThis->highspeed = (mask & DMA8_HIGH) != 0; */ + pStream->dma_auto = (mask & DMA8_AUTO) != 0; + + PDMAudioPropsInit(&pStream->Cfg.Props, 1 /* 8-bit */, + false /* fSigned */, + (pThis->mixer_regs[0x0e] & 2) == 0 ? 1 : 2 /* Mono/Stereo */, + pStream->Cfg.Props.uHz); + + /** @todo Check if stream's DMA block size is properly aligned to the set PCM props. */ + + sb16DmaCmdContinue8(pDevIns, pThis, pStream); + sb16SpeakerControl(pThis, true /* fOn */); +} + +static void sb16DmaCmd(PPDMDEVINS pDevIns, PSB16STATE pThis, PSB16STREAM pStream, + uint8_t cmd, uint8_t d0, int dma_len) +{ + pStream->fDmaUseHigh = cmd < 0xc0; + pStream->fifo = (cmd >> 1) & 1; + pStream->dma_auto = (cmd >> 2) & 1; + + pStream->Cfg.Props.fSigned = RT_BOOL(d0 & RT_BIT_32(4)); + PDMAudioPropsSetChannels(&pStream->Cfg.Props, 1 + ((d0 >> 5) & 1)); + + switch (cmd >> 4) + { + case 11: + PDMAudioPropsSetSampleSize(&pStream->Cfg.Props, 2 /*16-bit*/); + break; + + case 12: + PDMAudioPropsSetSampleSize(&pStream->Cfg.Props, 1 /*8-bit*/); + break; + + default: + AssertFailed(); + break; + } + + if (-1 != pStream->time_const) + { +#if 1 + int tmp = 256 - pStream->time_const; + pStream->Cfg.Props.uHz = (1000000 + (tmp / 2)) / tmp; +#else + /* pThis->freq = 1000000 / ((255 - pStream->time_const) << pThis->fmt_stereo); */ + pThis->freq = 1000000 / ((255 - pStream->time_const)); +#endif + pStream->time_const = -1; + } + + pStream->cbDmaBlockSize = dma_len + 1; + pStream->cbDmaBlockSize <<= PDMAudioPropsSampleSize(&pStream->Cfg.Props) == 2 ? 1 : 0; + if (!pStream->dma_auto) + { + /* + * It is clear that for DOOM and auto-init this value + * shouldn't take stereo into account, while Miles Sound Systems + * setsound.exe with single transfer mode wouldn't work without it + * wonders of SB16 yet again. + */ + pStream->cbDmaBlockSize <<= PDMAudioPropsSampleSize(&pStream->Cfg.Props) == 2 ? 1 : 0; + } + + pStream->cbDmaLeft = pStream->cbDmaBlockSize; + + pThis->highspeed = 0; + + /** @todo Check if stream's DMA block size is properly aligned to the set PCM props. */ + + sb16StreamControl(pDevIns, pThis, pStream, true /* fRun */); + sb16SpeakerControl(pThis, true /* fOn */); +} + +static inline void sb16DspSeData(PSB16STATE pThis, uint8_t val) +{ + LogFlowFunc(("%#x\n", val)); + if ((size_t) pThis->dsp_out_data_len < sizeof (pThis->dsp_out_data)) + pThis->dsp_out_data[pThis->dsp_out_data_len++] = val; +} + +static inline uint8_t sb16DspGetData(PSB16STATE pThis) +{ + if (pThis->dsp_in_idx) + return pThis->dsp_in_data[--pThis->dsp_in_idx]; + AssertMsgFailed(("DSP input buffer underflow\n")); + return 0; +} + +static void sb16DspCmdLookup(PPDMDEVINS pDevIns, PSB16STATE pThis, PSB16STREAM pStream, uint8_t cmd) +{ + LogFlowFunc(("command %#x\n", cmd)); + + if (cmd > 0xaf && cmd < 0xd0) + { + if (cmd & 8) /** @todo Handle recording. */ + LogFlowFunc(("ADC not yet supported (command %#x)\n", cmd)); + + switch (cmd >> 4) + { + case 11: + case 12: + break; + default: + LogFlowFunc(("%#x wrong bits\n", cmd)); + } + + pThis->dsp_in_needed_bytes = 3; + } + else + { + pThis->dsp_in_needed_bytes = 0; + + /** @todo Use a mapping table with + * - a command verb (binary search) + * - required bytes + * - function callback handler + */ + + switch (cmd) + { + case 0x03: /* ASP Status */ + sb16DspSeData(pThis, 0x10); /* pThis->csp_param); */ + goto warn; + + case 0x04: /* DSP Status (Obsolete) / ASP ??? */ + pThis->dsp_in_needed_bytes = 1; + goto warn; + + case 0x05: /* ASP ??? */ + pThis->dsp_in_needed_bytes = 2; + goto warn; + + case 0x08: /* ??? */ + /* __asm__ ("int3"); */ + goto warn; + + case 0x09: /* ??? */ + sb16DspSeData(pThis, 0xf8); + goto warn; + + case 0x0e: /* ??? */ + pThis->dsp_in_needed_bytes = 2; + goto warn; + + case 0x0f: /* ??? */ + pThis->dsp_in_needed_bytes = 1; + goto warn; + + case 0x10: /* Direct mode DAC */ + pThis->dsp_in_needed_bytes = 1; + goto warn; + + case 0x14: /* DAC DMA, 8-bit, uncompressed */ + pThis->dsp_in_needed_bytes = 2; + pStream->cbDmaBlockSize = 0; + break; + + case 0x1c: /* Auto-Initialize DMA DAC, 8-bit */ + sb16DmaCmd8(pDevIns, pThis, pStream, DMA8_AUTO, -1); + break; + + case 0x20: /* Direct ADC, Juice/PL */ + sb16DspSeData(pThis, 0xff); + goto warn; + + case 0x35: /* MIDI Read Interrupt + Write Poll (UART) */ + LogRelMax2(32, ("SB16: MIDI support not implemented yet\n")); + break; + + case 0x40: /* Set Time Constant */ + pStream->time_const = -1; + pThis->dsp_in_needed_bytes = 1; + break; + + case 0x41: /* Set sample rate for input */ + pStream->Cfg.Props.uHz = 0; /** @todo r=andy Why do we reset output stuff here? */ + pStream->time_const = -1; + pThis->dsp_in_needed_bytes = 2; + break; + + case 0x42: /* Set sample rate for output */ + pStream->Cfg.Props.uHz = 0; + pStream->time_const = -1; + pThis->dsp_in_needed_bytes = 2; + goto warn; + + case 0x45: /* Continue Auto-Initialize DMA, 8-bit */ + sb16DspSeData(pThis, 0xaa); + goto warn; + + case 0x47: /* Continue Auto-Initialize DMA, 16-bit */ + break; + + case 0x48: /* Set DMA Block Size */ + pThis->dsp_in_needed_bytes = 2; + break; + + case 0x74: /* DMA DAC, 4-bit ADPCM */ + pThis->dsp_in_needed_bytes = 2; + LogFlowFunc(("4-bit ADPCM not implemented yet\n")); + break; + + case 0x75: /* DMA DAC, 4-bit ADPCM Reference */ + pThis->dsp_in_needed_bytes = 2; + LogFlowFunc(("DMA DAC, 4-bit ADPCM Reference not implemented\n")); + break; + + case 0x76: /* DMA DAC, 2.6-bit ADPCM */ + pThis->dsp_in_needed_bytes = 2; + LogFlowFunc(("DMA DAC, 2.6-bit ADPCM not implemented yet\n")); + break; + + case 0x77: /* DMA DAC, 2.6-bit ADPCM Reference */ + pThis->dsp_in_needed_bytes = 2; + LogFlowFunc(("ADPCM reference not implemented yet\n")); + break; + + case 0x7d: /* Auto-Initialize DMA DAC, 4-bit ADPCM Reference */ + LogFlowFunc(("Autio-Initialize DMA DAC, 4-bit ADPCM reference not implemented yet\n")); + break; + + case 0x7f: /* Auto-Initialize DMA DAC, 16-bit ADPCM Reference */ + LogFlowFunc(("Autio-Initialize DMA DAC, 2.6-bit ADPCM Reference not implemented yet\n")); + break; + + case 0x80: /* Silence DAC */ + pThis->dsp_in_needed_bytes = 2; + break; + + case 0x90: /* Auto-Initialize DMA DAC, 8-bit (High Speed) */ + RT_FALL_THROUGH(); + case 0x91: /* Normal DMA DAC, 8-bit (High Speed) */ + sb16DmaCmd8(pDevIns, pThis, pStream, (((cmd & 1) == 0) ? 1 : 0) | DMA8_HIGH, -1); + break; + + case 0xd0: /* Halt DMA operation. 8bit */ + sb16StreamControl(pDevIns, pThis, pStream, false /* fRun */); + break; + + case 0xd1: /* Speaker on */ + sb16SpeakerControl(pThis, true /* fOn */); + break; + + case 0xd3: /* Speaker off */ + sb16SpeakerControl(pThis, false /* fOn */); + break; + + case 0xd4: /* Continue DMA operation, 8-bit */ + /* KQ6 (or maybe Sierras audblst.drv in general) resets + the frequency between halt/continue */ + sb16DmaCmdContinue8(pDevIns, pThis, pStream); + break; + + case 0xd5: /* Halt DMA operation, 16-bit */ + sb16StreamControl(pDevIns, pThis, pStream, false /* fRun */); + break; + + case 0xd6: /* Continue DMA operation, 16-bit */ + sb16StreamControl(pDevIns, pThis, pStream, true /* fRun */); + break; + + case 0xd9: /* Exit auto-init DMA after this block, 16-bit */ + pStream->dma_auto = 0; + break; + + case 0xda: /* Exit auto-init DMA after this block, 8-bit */ + pStream->dma_auto = 0; + break; + + case 0xe0: /* DSP identification */ + pThis->dsp_in_needed_bytes = 1; + break; + + case 0xe1: /* DSP version */ + sb16DspSeData(pThis, RT_LO_U8(pStream->HwCfgRuntime.uVer)); + sb16DspSeData(pThis, RT_HI_U8(pStream->HwCfgRuntime.uVer)); + break; + + case 0xe2: /* ??? */ + pThis->dsp_in_needed_bytes = 1; + goto warn; + + case 0xe3: /* DSP copyright */ + { + for (int i = sizeof(e3) - 1; i >= 0; --i) + sb16DspSeData(pThis, e3[i]); + break; + } + + case 0xe4: /* Write test register */ + pThis->dsp_in_needed_bytes = 1; + break; + + case 0xe7: /* ??? */ + LogFlowFunc(("Attempt to probe for ESS (0xe7)?\n")); + break; + + case 0xe8: /* Read test register */ + sb16DspSeData(pThis, pThis->test_reg); + break; + + case 0xf2: /* IRQ Request, 8-bit */ + RT_FALL_THROUGH(); + case 0xf3: /* IRQ Request, 16-bit */ + { + sb16DspSeData(pThis, 0xaa); + pThis->mixer_regs[0x82] |= (cmd == 0xf2) ? 1 : 2; + PDMDevHlpISASetIrq(pThis->pDevInsR3, pStream->HwCfgRuntime.uIrq, 1); + break; + } + + case 0xf8: /* Undocumented, used by old Creative diagnostic programs */ + sb16DspSeData(pThis, 0); + goto warn; + + case 0xf9: /* ??? */ + pThis->dsp_in_needed_bytes = 1; + goto warn; + + case 0xfa: /* ??? */ + sb16DspSeData(pThis, 0); + goto warn; + + case 0xfc: /* ??? */ + sb16DspSeData(pThis, 0); + goto warn; + + default: + LogFunc(("Unrecognized DSP command %#x, ignored\n", cmd)); + break; + } + } + +exit: + + if (!pThis->dsp_in_needed_bytes) + pThis->cmd = -1; + else + pThis->cmd = cmd; + + return; + +warn: + LogFunc(("warning: command %#x,%d is not truly understood yet\n", cmd, pThis->dsp_in_needed_bytes)); + goto exit; +} + +DECLINLINE(uint16_t) sb16DspGetLoHi(PSB16STATE pThis) +{ + const uint8_t hi = sb16DspGetData(pThis); + const uint8_t lo = sb16DspGetData(pThis); + return RT_MAKE_U16(lo, hi); +} + +DECLINLINE(uint16_t) sb16DspGetHiLo(PSB16STATE pThis) +{ + const uint8_t lo = sb16DspGetData(pThis); + const uint8_t hi = sb16DspGetData(pThis); + return RT_MAKE_U16(lo, hi); +} + +static void sb16DspCmdComplete(PPDMDEVINS pDevIns, PSB16STATE pThis) +{ + LogFlowFunc(("Command %#x, in_index %d, needed_bytes %d\n", pThis->cmd, pThis->dsp_in_idx, pThis->dsp_in_needed_bytes)); + + int v0, v1, v2; + + PSB16STREAM pStream = &pThis->aStreams[SB16_IDX_OUT]; /** @ŧodo Improve this. */ + + if (pThis->cmd > 0xaf && pThis->cmd < 0xd0) + { + v2 = sb16DspGetData(pThis); + v1 = sb16DspGetData(pThis); + v0 = sb16DspGetData(pThis); + + if (pThis->cmd & 8) + LogFlowFunc(("ADC params cmd = %#x d0 = %d, d1 = %d, d2 = %d\n", pThis->cmd, v0, v1, v2)); + else + { + LogFlowFunc(("cmd = %#x d0 = %d, d1 = %d, d2 = %d\n", pThis->cmd, v0, v1, v2)); + sb16DmaCmd(pDevIns, pThis, pStream, pThis->cmd, v0, v1 + (v2 << 8)); + } + } + else + { + switch (pThis->cmd) + { + case 0x04: + pThis->csp_mode = sb16DspGetData(pThis); + pThis->csp_reg83r = 0; + pThis->csp_reg83w = 0; + LogFlowFunc(("CSP command 0x04: mode=%#x\n", pThis->csp_mode)); + break; + + case 0x05: + pThis->csp_param = sb16DspGetData(pThis); + pThis->csp_value = sb16DspGetData(pThis); + LogFlowFunc(("CSP command 0x05: param=%#x value=%#x\n", pThis->csp_param, pThis->csp_value)); + break; + + case 0x0e: + v0 = sb16DspGetData(pThis); + v1 = sb16DspGetData(pThis); + LogFlowFunc(("write CSP register %d <- %#x\n", v1, v0)); + if (v1 == 0x83) + { + LogFlowFunc(("0x83[%d] <- %#x\n", pThis->csp_reg83r, v0)); + pThis->csp_reg83[pThis->csp_reg83r % 4] = v0; + pThis->csp_reg83r += 1; + } + else + pThis->csp_regs[v1] = v0; + break; + + case 0x0f: + v0 = sb16DspGetData(pThis); + LogFlowFunc(("read CSP register %#x -> %#x, mode=%#x\n", v0, pThis->csp_regs[v0], pThis->csp_mode)); + if (v0 == 0x83) + { + LogFlowFunc(("0x83[%d] -> %#x\n", pThis->csp_reg83w, pThis->csp_reg83[pThis->csp_reg83w % 4])); + sb16DspSeData(pThis, pThis->csp_reg83[pThis->csp_reg83w % 4]); + pThis->csp_reg83w += 1; + } + else + sb16DspSeData(pThis, pThis->csp_regs[v0]); + break; + + case 0x10: + v0 = sb16DspGetData(pThis); + LogFlowFunc(("cmd 0x10 d0=%#x\n", v0)); + break; + + case 0x14: + sb16DmaCmd8(pDevIns, pThis, pStream, 0, sb16DspGetLoHi(pThis) + 1); + break; + + case 0x22: /* Sets the master volume. */ + /** @todo Setting the master volume is not implemented yet. */ + break; + + case 0x40: /* Sets the timer constant; SB16 is able to use sample rates via 0x41 instead. */ + pStream->time_const = sb16DspGetData(pThis); + LogFlowFunc(("set time const %d\n", pStream->time_const)); + break; + + case 0x42: /* Sets the input rate (in Hz). */ +#if 0 + LogFlowFunc(("cmd 0x42 might not do what it think it should\n")); +#endif + RT_FALL_THROUGH(); /** @todo BUGBUG FT2 sets output freq with this, go figure. */ + + case 0x41: /* Sets the output rate (in Hz). */ + pStream->Cfg.Props.uHz = sb16DspGetHiLo(pThis); + LogFlowFunc(("set freq to %RU16Hz\n", pStream->Cfg.Props.uHz)); + break; + + case 0x48: + pStream->cbDmaBlockSize = sb16DspGetLoHi(pThis) + 1; + LogFlowFunc(("set dma block len %d\n", pStream->cbDmaBlockSize)); + break; + + case 0x74: + case 0x75: + case 0x76: + case 0x77: + /* ADPCM stuff, ignore. */ + break; + + case 0x80: /* Sets the IRQ. */ + sb16StreamTransferScheduleNext(pThis, pStream, sb16DspGetLoHi(pThis) + 1); + break; + + case 0xe0: + v0 = sb16DspGetData(pThis); + pThis->dsp_out_data_len = 0; + LogFlowFunc(("E0=%#x\n", v0)); + sb16DspSeData(pThis, ~v0); + break; + + case 0xe2: + v0 = sb16DspGetData(pThis); + LogFlowFunc(("E2=%#x\n", v0)); + break; + + case 0xe4: + pThis->test_reg = sb16DspGetData(pThis); + break; + + case 0xf9: + v0 = sb16DspGetData(pThis); + switch (v0) + { + case 0x0e: + sb16DspSeData(pThis, 0xff); + break; + + case 0x0f: + sb16DspSeData(pThis, 0x07); + break; + + case 0x37: + sb16DspSeData(pThis, 0x38); + break; + + default: + sb16DspSeData(pThis, 0x00); + break; + } + break; + + default: + LogRel2(("SB16: Unrecognized command %#x, skipping\n", pThis->cmd)); + return; + } + } + + pThis->cmd = -1; + return; +} + +static void sb16DspCmdResetLegacy(PSB16STATE pThis) +{ + LogFlowFuncEnter(); + + /* Disable speaker(s). */ + sb16SpeakerControl(pThis, false /* fOn */); + + /* + * Reset all streams. + */ + for (unsigned i = 0; i < SB16_MAX_STREAMS; i++) + sb16StreamReset(pThis, &pThis->aStreams[i]); +} + +static void sb16DspCmdReset(PSB16STATE pThis) +{ + pThis->mixer_regs[0x82] = 0; + pThis->dsp_in_idx = 0; + pThis->dsp_out_data_len = 0; + pThis->dsp_in_needed_bytes = 0; + pThis->nzero = 0; + pThis->highspeed = 0; + pThis->v2x6 = 0; + pThis->cmd = -1; + + sb16DspSeData(pThis, 0xaa); + + sb16DspCmdResetLegacy(pThis); +} + +/** + * @callback_method_impl{PFNIOMIOPORTNEWOUT} + */ +static DECLCALLBACK(VBOXSTRICTRC) sb16IoPortDspWrite(PPDMDEVINS pDevIns, void *pvUser, RTIOPORT offPort, uint32_t u32, unsigned cb) +{ + PSB16STATE pThis = PDMDEVINS_2_DATA(pDevIns, PSB16STATE); + RT_NOREF(pvUser, cb); + + /** @todo Figure out how we can distinguish between streams. DSP port #, e.g. 0x220? */ + PSB16STREAM pStream = &pThis->aStreams[SB16_IDX_OUT]; + + LogFlowFunc(("write %#x <- %#x\n", offPort, u32)); + switch (offPort) + { + case 0: + switch (u32) + { + case 0x00: + { + if (pThis->v2x6 == 1) + { + if (0 && pThis->highspeed) + { + pThis->highspeed = 0; + PDMDevHlpISASetIrq(pThis->pDevInsR3, pStream->HwCfgRuntime.uIrq, 0); + sb16StreamControl(pDevIns, pThis, pStream, false /* fRun */); + } + else + sb16DspCmdReset(pThis); + } + pThis->v2x6 = 0; + break; + } + + case 0x01: + case 0x03: /* FreeBSD kludge */ + pThis->v2x6 = 1; + break; + + case 0xc6: + pThis->v2x6 = 0; /* Prince of Persia, csp.sys, diagnose.exe */ + break; + + case 0xb8: /* Panic */ + sb16DspCmdReset(pThis); + break; + + case 0x39: + sb16DspSeData(pThis, 0x38); + sb16DspCmdReset(pThis); + pThis->v2x6 = 0x39; + break; + + default: + pThis->v2x6 = u32; + break; + } + break; + + case 6: /* Write data or command | write status */ +#if 0 + if (pThis->highspeed) + break; +#endif + if (0 == pThis->dsp_in_needed_bytes) + { + sb16DspCmdLookup(pDevIns, pThis, pStream, u32); + } + else + { + if (pThis->dsp_in_idx == sizeof (pThis->dsp_in_data)) + { + AssertMsgFailed(("DSP input data overrun\n")); + } + else + { + pThis->dsp_in_data[pThis->dsp_in_idx++] = u32; + if (pThis->dsp_in_idx == pThis->dsp_in_needed_bytes) + { + pThis->dsp_in_needed_bytes = 0; + sb16DspCmdComplete(pDevIns, pThis); + } + } + } + break; + + default: + LogFlowFunc(("offPort=%#x, u32=%#x)\n", offPort, u32)); + break; + } + + return VINF_SUCCESS; +} + + +/** + * @callback_method_impl{PFNIOMIOPORTNEWIN} + */ +static DECLCALLBACK(VBOXSTRICTRC) sb16IoPortDspRead(PPDMDEVINS pDevIns, void *pvUser, RTIOPORT offPort, uint32_t *pu32, unsigned cb) +{ + RT_NOREF(pvUser, cb); + + PSB16STATE pThis = PDMDEVINS_2_DATA(pDevIns, PSB16STATE); + + uint32_t retval; + int ack = 0; + + /** @todo Figure out how we can distinguish between streams. */ + PSB16STREAM pStream = &pThis->aStreams[SB16_IDX_OUT]; + + /** @todo reject non-byte access? + * The spec does not mention a non-byte access so we should check how real hardware behaves. */ + + switch (offPort) + { + case 0: /* reset */ + retval = 0xff; + break; + + case 4: /* read data */ + if (pThis->dsp_out_data_len) + { + retval = pThis->dsp_out_data[--pThis->dsp_out_data_len]; + pThis->last_read_byte = retval; + } + else + { + if (pThis->cmd != -1) + LogFlowFunc(("empty output buffer for command %#x\n", pThis->cmd)); + retval = pThis->last_read_byte; + /* goto error; */ + } + break; + + case 6: /* 0 can write */ + retval = pStream->can_write ? 0 : 0x80; + break; + + case 7: /* timer interrupt clear */ + /* LogFlowFunc(("timer interrupt clear\n")); */ + retval = 0; + break; + + case 8: /* data available status | irq 8 ack */ + retval = (!pThis->dsp_out_data_len || pThis->highspeed) ? 0 : 0x80; + if (pThis->mixer_regs[0x82] & 1) + { + ack = 1; + pThis->mixer_regs[0x82] &= ~1; + PDMDevHlpISASetIrq(pThis->pDevInsR3, pStream->HwCfgRuntime.uIrq, 0); + } + break; + + case 9: /* irq 16 ack */ + retval = 0xff; + if (pThis->mixer_regs[0x82] & 2) + { + ack = 1; + pThis->mixer_regs[0x82] &= ~2; + PDMDevHlpISASetIrq(pThis->pDevInsR3, pStream->HwCfgRuntime.uIrq, 0); + } + break; + + default: + LogFlowFunc(("warning: sb16IoPortDspRead %#x error\n", offPort)); + return VERR_IOM_IOPORT_UNUSED; + } + + if (!ack) + LogFlowFunc(("read %#x -> %#x\n", offPort, retval)); + + *pu32 = retval; + return VINF_SUCCESS; +} + + +/********************************************************************************************************************************* +* Mixer functions * +*********************************************************************************************************************************/ + +static uint8_t sb16MixRegToVol(PSB16STATE pThis, int reg) +{ + /* The SB16 mixer has a 0 to -62dB range in 32 levels (2dB each step). + * We use a 0 to -96dB range in 256 levels (0.375dB each step). + * Only the top 5 bits of a mixer register are used. + */ + uint8_t steps = 31 - (pThis->mixer_regs[reg] >> 3); + uint8_t vol = 255 - steps * 16 / 3; /* (2dB*8) / (0.375dB*8) */ + return vol; +} + +/** + * Returns the device's current master volume. + * + * @param pThis SB16 state. + * @param pVol Where to store the master volume information. + */ +DECLINLINE(void) sb16GetMasterVolume(PSB16STATE pThis, PPDMAUDIOVOLUME pVol) +{ + /* There's no mute switch, only volume controls. */ + PDMAudioVolumeInitFromStereo(pVol, false /*fMuted*/, sb16MixRegToVol(pThis, 0x30), sb16MixRegToVol(pThis, 0x31)); +} + +/** + * Returns the device's current output stream volume. + * + * @param pThis SB16 state. + * @param pVol Where to store the output stream volume information. + */ +DECLINLINE(void) sb16GetPcmOutVolume(PSB16STATE pThis, PPDMAUDIOVOLUME pVol) +{ + /* There's no mute switch, only volume controls. */ + PDMAudioVolumeInitFromStereo(pVol, false /*fMuted*/, sb16MixRegToVol(pThis, 0x32), sb16MixRegToVol(pThis, 0x33)); +} + +static void sb16UpdateVolume(PSB16STATE pThis) +{ + PDMAUDIOVOLUME VolMaster; + sb16GetMasterVolume(pThis, &VolMaster); + + PDMAUDIOVOLUME VolOut; + sb16GetPcmOutVolume(pThis, &VolOut); + + /* Combine the master + output stream volume. */ + PDMAUDIOVOLUME VolCombined; + PDMAudioVolumeCombine(&VolCombined, &VolMaster, &VolOut); + + int rc2 = AudioMixerSinkSetVolume(pThis->pSinkOut, &VolCombined); + AssertRC(rc2); +} + +static void sb16MixerReset(PSB16STATE pThis) +{ + memset(pThis->mixer_regs, 0xff, 0x7f); + memset(pThis->mixer_regs + 0x83, 0xff, sizeof (pThis->mixer_regs) - 0x83); + + pThis->mixer_regs[0x02] = 4; /* master volume 3bits */ + pThis->mixer_regs[0x06] = 4; /* MIDI volume 3bits */ + pThis->mixer_regs[0x08] = 0; /* CD volume 3bits */ + pThis->mixer_regs[0x0a] = 0; /* voice volume 2bits */ + + /* d5=input filt, d3=lowpass filt, d1,d2=input source */ + pThis->mixer_regs[0x0c] = 0; + + /* d5=output filt, d1=stereo switch */ + pThis->mixer_regs[0x0e] = 0; + + /* voice volume L d5,d7, R d1,d3 */ + pThis->mixer_regs[0x04] = (12 << 4) | 12; + /* master ... */ + pThis->mixer_regs[0x22] = (12 << 4) | 12; + /* MIDI ... */ + pThis->mixer_regs[0x26] = (12 << 4) | 12; + + /* master/voice/MIDI L/R volume */ + for (int i = 0x30; i < 0x36; i++) + pThis->mixer_regs[i] = 24 << 3; /* -14 dB */ + + /* treble/bass */ + for (int i = 0x44; i < 0x48; i++) + pThis->mixer_regs[i] = 0x80; + + /* Update the master (mixer) and PCM out volumes. */ + sb16UpdateVolume(pThis); + + /* + * Reset mixer sinks. + * + * Do the reset here instead of in sb16StreamReset(); + * the mixer sink(s) might still have data to be processed when an audio stream gets reset. + */ + if (pThis->pSinkOut) + AudioMixerSinkReset(pThis->pSinkOut); +} + +static int magic_of_irq(int irq) +{ + switch (irq) + { + case 5: + return 2; + case 7: + return 4; + case 9: + return 1; + case 10: + return 8; + default: + break; + } + + LogFlowFunc(("bad irq %d\n", irq)); + return 2; +} + +static int irq_of_magic(int magic) +{ + switch (magic) + { + case 1: + return 9; + case 2: + return 5; + case 4: + return 7; + case 8: + return 10; + default: + break; + } + + LogFlowFunc(("bad irq magic %d\n", magic)); + return -1; +} + +static int sb16MixerWriteIndex(PSB16STATE pThis, PSB16STREAM pStream, uint8_t val) +{ + RT_NOREF(pStream); + pThis->mixer_nreg = val; + return VINF_SUCCESS; +} + +#ifndef VBOX +static uint32_t popcount(uint32_t u) +{ + u = ((u&0x55555555) + ((u>>1)&0x55555555)); + u = ((u&0x33333333) + ((u>>2)&0x33333333)); + u = ((u&0x0f0f0f0f) + ((u>>4)&0x0f0f0f0f)); + u = ((u&0x00ff00ff) + ((u>>8)&0x00ff00ff)); + u = ( u&0x0000ffff) + (u>>16); + return u; +} +#endif + +static uint32_t lsbindex(uint32_t u) +{ +#ifdef VBOX + return u ? ASMBitFirstSetU32(u) - 1 : 32; +#else + return popcount((u & -(int32_t)u) - 1); +#endif +} + +/* Convert SB16 to SB Pro mixer volume (left). */ +static inline void sb16ConvVolumeL(PSB16STATE pThis, unsigned reg, uint8_t val) +{ + /* High nibble in SBP mixer. */ + pThis->mixer_regs[reg] = (pThis->mixer_regs[reg] & 0x0f) | (val & 0xf0); +} + +/* Convert SB16 to SB Pro mixer volume (right). */ +static inline void sb16ConvVolumeR(PSB16STATE pThis, unsigned reg, uint8_t val) +{ + /* Low nibble in SBP mixer. */ + pThis->mixer_regs[reg] = (pThis->mixer_regs[reg] & 0xf0) | (val >> 4); +} + +/* Convert SB Pro to SB16 mixer volume (left + right). */ +static inline void sb16ConvVolumeOldToNew(PSB16STATE pThis, unsigned reg, uint8_t val) +{ + /* Left channel. */ + pThis->mixer_regs[reg + 0] = (val & 0xf0) | RT_BIT(3); + /* Right channel (the register immediately following). */ + pThis->mixer_regs[reg + 1] = (val << 4) | RT_BIT(3); +} + + +static int sb16MixerWriteData(PSB16STATE pThis, PSB16STREAM pStream, uint8_t val) +{ + bool fUpdateMaster = false; + bool fUpdateStream = false; + + LogFlowFunc(("[%#x] <- %#x\n", pThis->mixer_nreg, val)); + + switch (pThis->mixer_nreg) + { + case 0x00: + sb16MixerReset(pThis); + /* And update the actual volume, too. */ + fUpdateMaster = true; + fUpdateStream = true; + break; + + case 0x04: /* Translate from old style voice volume (L/R). */ + sb16ConvVolumeOldToNew(pThis, 0x32, val); + fUpdateStream = true; + break; + + case 0x22: /* Translate from old style master volume (L/R). */ + sb16ConvVolumeOldToNew(pThis, 0x30, val); + fUpdateMaster = true; + break; + + case 0x26: /* Translate from old style MIDI volume (L/R). */ + sb16ConvVolumeOldToNew(pThis, 0x34, val); + break; + + case 0x28: /* Translate from old style CD volume (L/R). */ + sb16ConvVolumeOldToNew(pThis, 0x36, val); + break; + + case 0x2E: /* Translate from old style line volume (L/R). */ + sb16ConvVolumeOldToNew(pThis, 0x38, val); + break; + + case 0x30: /* Translate to old style master volume (L). */ + sb16ConvVolumeL(pThis, 0x22, val); + fUpdateMaster = true; + break; + + case 0x31: /* Translate to old style master volume (R). */ + sb16ConvVolumeR(pThis, 0x22, val); + fUpdateMaster = true; + break; + + case 0x32: /* Translate to old style voice volume (L). */ + sb16ConvVolumeL(pThis, 0x04, val); + fUpdateStream = true; + break; + + case 0x33: /* Translate to old style voice volume (R). */ + sb16ConvVolumeR(pThis, 0x04, val); + fUpdateStream = true; + break; + + case 0x34: /* Translate to old style MIDI volume (L). */ + sb16ConvVolumeL(pThis, 0x26, val); + break; + + case 0x35: /* Translate to old style MIDI volume (R). */ + sb16ConvVolumeR(pThis, 0x26, val); + break; + + case 0x36: /* Translate to old style CD volume (L). */ + sb16ConvVolumeL(pThis, 0x28, val); + break; + + case 0x37: /* Translate to old style CD volume (R). */ + sb16ConvVolumeR(pThis, 0x28, val); + break; + + case 0x38: /* Translate to old style line volume (L). */ + sb16ConvVolumeL(pThis, 0x2E, val); + break; + + case 0x39: /* Translate to old style line volume (R). */ + sb16ConvVolumeR(pThis, 0x2E, val); + break; + + case 0x80: + { + int irq = irq_of_magic(val); + LogRelMax2(64, ("SB16: Setting IRQ to %d\n", irq)); + if (irq > 0) + pStream->HwCfgRuntime.uIrq = irq; + break; + } + + case 0x81: + { + int dma = lsbindex(val & 0xf); + int hdma = lsbindex(val & 0xf0); + if ( dma != pStream->HwCfgRuntime.uDmaChanLow + || hdma != pStream->HwCfgRuntime.uDmaChanHigh) + { + LogRelMax2(64, ("SB16: Attempt to change DMA 8bit %d(%d), 16bit %d(%d)\n", + dma, pStream->HwCfgRuntime.uDmaChanLow, hdma, pStream->HwCfgRuntime.uDmaChanHigh)); + } +#if 0 + pStream->dma = dma; + pStream->hdma = hdma; +#endif + break; + } + + case 0x82: + LogRelMax2(64, ("SB16: Attempt to write into IRQ status register to %#x\n", val)); + return VINF_SUCCESS; + + default: + if (pThis->mixer_nreg >= 0x80) + LogFlowFunc(("attempt to write mixer[%#x] <- %#x\n", pThis->mixer_nreg, val)); + break; + } + + pThis->mixer_regs[pThis->mixer_nreg] = val; + + /* Update the master (mixer) volume. */ + if ( fUpdateMaster + || fUpdateStream) + { + sb16UpdateVolume(pThis); + } + + return VINF_SUCCESS; +} + +/** + * @callback_method_impl{PFNIOMIOPORTNEWOUT} + */ +static DECLCALLBACK(VBOXSTRICTRC) sb16IoPortMixerWrite(PPDMDEVINS pDevIns, void *pvUser, RTIOPORT offPort, uint32_t u32, unsigned cb) +{ + PSB16STATE pThis = PDMDEVINS_2_DATA(pDevIns, PSB16STATE); + RT_NOREF(pvUser); + + /** @todo Figure out how we can distinguish between streams. */ + PSB16STREAM pStream = &pThis->aStreams[SB16_IDX_OUT]; + + switch (cb) + { + case 1: + switch (offPort) + { + case 0: + sb16MixerWriteIndex(pThis, pStream, u32); + break; + case 1: + sb16MixerWriteData(pThis, pStream, u32); + break; + default: + AssertFailed(); + } + break; + case 2: + sb16MixerWriteIndex(pThis, pStream, u32 & 0xff); + sb16MixerWriteData(pThis, pStream, (u32 >> 8) & 0xff); + break; + default: + ASSERT_GUEST_MSG_FAILED(("offPort=%#x cb=%d u32=%#x\n", offPort, cb, u32)); + break; + } + return VINF_SUCCESS; +} + +/** + * @callback_method_impl{PFNIOMIOPORTNEWIN} + */ +static DECLCALLBACK(VBOXSTRICTRC) sb16IoPortMixerRead(PPDMDEVINS pDevIns, void *pvUser, RTIOPORT offPort, uint32_t *pu32, unsigned cb) +{ + PSB16STATE pThis = PDMDEVINS_2_DATA(pDevIns, PSB16STATE); + RT_NOREF(pvUser, cb, offPort); + +#ifndef DEBUG_SB16_MOST + if (pThis->mixer_nreg != 0x82) + LogFlowFunc(("sb16IoPortMixerRead[%#x] -> %#x\n", pThis->mixer_nreg, pThis->mixer_regs[pThis->mixer_nreg])); +#else + LogFlowFunc(("sb16IoPortMixerRead[%#x] -> %#x\n", pThis->mixer_nreg, pThis->mixer_regs[pThis->mixer_nreg])); +#endif + *pu32 = pThis->mixer_regs[pThis->mixer_nreg]; + return VINF_SUCCESS; +} + + +/********************************************************************************************************************************* +* DMA handling * +*********************************************************************************************************************************/ + +/** + * Worker for sb16DMARead. + */ + +/** + * @callback_method_impl{FNDMATRANSFERHANDLER, + * Worker callback for both DMA channels.} + */ +static DECLCALLBACK(uint32_t) sb16DMARead(PPDMDEVINS pDevIns, void *pvUser, unsigned uChannel, uint32_t off, uint32_t cb) + +{ + PSB16STATE pThis = PDMDEVINS_2_DATA(pDevIns, PSB16STATE); + AssertPtr(pThis); + PSB16STREAM pStream = (PSB16STREAM)pvUser; + AssertPtr(pStream); + + int till, copy, free; + + if (pStream->cbDmaBlockSize <= 0) + { + LogFlowFunc(("invalid block size=%d uChannel=%d off=%d cb=%d\n", pStream->cbDmaBlockSize, uChannel, off, cb)); + return off; + } + + if (pStream->cbDmaLeft < 0) + pStream->cbDmaLeft = pStream->cbDmaBlockSize; + + free = cb; + + copy = free; + till = pStream->cbDmaLeft; + + Log4Func(("pos=%d %d, till=%d, len=%d\n", off, free, till, cb)); + + if (copy >= till) + { + if (0 == pStream->dma_auto) + { + copy = till; + } + else + { + if (copy >= till + pStream->cbDmaBlockSize) + copy = till; /* Make sure we won't skip IRQs. */ + } + } + + STAM_COUNTER_ADD(&pThis->StatBytesRead, copy); + + uint32_t written = 0; /* Shut up GCC. */ + int rc = sb16StreamDoDmaOutput(pThis, pStream, uChannel, off, cb, copy, &written); + AssertRC(rc); + + /** @todo Convert the rest to uin32_t / size_t. */ + off = (off + (int)written) % cb; + pStream->cbDmaLeft -= (int)written; /** @todo r=andy left_till_irq can be < 0. Correct? Revisit this. */ + + Log3Func(("pos %d/%d, free=%d, till=%d, copy=%d, written=%RU32, block_size=%d\n", + off, cb, free, pStream->cbDmaLeft, copy, copy, pStream->cbDmaBlockSize)); + + if (pStream->cbDmaLeft <= 0) + { + pThis->mixer_regs[0x82] |= (uChannel & 4) ? 2 : 1; + + PDMDevHlpISASetIrq(pThis->pDevInsR3, pStream->HwCfgRuntime.uIrq, 1); + + if (0 == pStream->dma_auto) /** @todo r=andy BUGBUG Why do we first assert the IRQ if dma_auto is 0? Revisit this. */ + { + sb16StreamControl(pDevIns, pThis, pStream, false /* fRun */); + sb16SpeakerControl(pThis, false /* fOn */); + } + } + + while (pStream->cbDmaLeft <= 0) + pStream->cbDmaLeft += pStream->cbDmaBlockSize; + + return off; +} + + +/********************************************************************************************************************************* +* Timer-related code * +*********************************************************************************************************************************/ + +/** + * @callback_method_impl{PFNTMTIMERDEV} + */ +static DECLCALLBACK(void) sb16TimerIRQ(PPDMDEVINS pDevIns, TMTIMERHANDLE hTimer, void *pvUser) +{ + PSB16STATE pThis = PDMDEVINS_2_DATA(pDevIns, PSB16STATE); + RT_NOREF(hTimer, pThis); + + PSB16STREAM pStream = (PSB16STREAM)pvUser; + AssertPtrReturnVoid(pStream); + + LogFlowFuncEnter(); + + pStream->can_write = 1; + PDMDevHlpISASetIrq(pDevIns, pStream->HwCfgRuntime.uIrq, 1); +} + +/** + * Sets the stream's I/O timer to a new expiration time. + * + * @param pDevIns The device instance. + * @param pStream SB16 stream to set timer for. + * @param cTicksToDeadline The number of ticks to the new deadline. + */ +DECLINLINE(void) sb16TimerSet(PPDMDEVINS pDevIns, PSB16STREAM pStream, uint64_t cTicksToDeadline) +{ + int rc = PDMDevHlpTimerSetRelative(pDevIns, pStream->hTimerIO, cTicksToDeadline, NULL /*pu64Now*/); + AssertRC(rc); +} + +/** + * @callback_method_impl{FNTMTIMERDEV} + */ +static DECLCALLBACK(void) sb16TimerIO(PPDMDEVINS pDevIns, TMTIMERHANDLE hTimer, void *pvUser) +{ + PSB16STATE pThis = PDMDEVINS_2_DATA(pDevIns, PSB16STATE); + STAM_PROFILE_START(&pThis->StatTimerIO, a); + + PSB16STREAM pStream = (PSB16STREAM)pvUser; + AssertPtrReturnVoid(pStream); + AssertReturnVoid(hTimer == pStream->hTimerIO); + + const uint64_t cTicksNow = PDMDevHlpTimerGet(pDevIns, pStream->hTimerIO); + + pStream->tsTimerIO = cTicksNow; + + PAUDMIXSINK pSink = sb16StreamIndexToSink(pThis, pStream->uIdx); + AssertPtrReturnVoid(pSink); + + const bool fSinkActive = AudioMixerSinkIsActive(pSink); + + LogFlowFunc(("fSinkActive=%RTbool\n", fSinkActive)); + + /* Schedule the next transfer. */ + PDMDevHlpDMASchedule(pDevIns); + + if (fSinkActive) + { + /** @todo adjust cTicks down by now much cbOutMin represents. */ + sb16TimerSet(pDevIns, pStream, pStream->cTicksTimerIOInterval); + } + + AudioMixerSinkSignalUpdateJob(pSink); + + STAM_PROFILE_STOP(&pThis->StatTimerIO, a); +} + + +/********************************************************************************************************************************* +* LUN (driver) management * +*********************************************************************************************************************************/ + +/** + * Retrieves a specific driver stream of a SB16 driver. + * + * @returns Pointer to driver stream if found, or NULL if not found. + * @param pDrv Driver to retrieve driver stream for. + * @param enmDir Stream direction to retrieve. + * @param enmPath Stream destination / source to retrieve. + */ +static PSB16DRIVERSTREAM sb16GetDrvStream(PSB16DRIVER pDrv, PDMAUDIODIR enmDir, PDMAUDIOPATH enmPath) +{ + PSB16DRIVERSTREAM pDrvStream = NULL; + + if (enmDir == PDMAUDIODIR_OUT) + { + LogFunc(("enmPath=%d\n", enmPath)); + + switch (enmPath) + { + case PDMAUDIOPATH_OUT_FRONT: + pDrvStream = &pDrv->Out; + break; + default: + AssertFailed(); + break; + } + } + else + Assert(enmDir == PDMAUDIODIR_IN /** @todo Recording not implemented yet. */); + + return pDrvStream; +} + +/** + * Adds a driver stream to a specific mixer sink. + * + * @returns VBox status code. + * @param pDevIns The device instance. + * @param pMixSink Mixer sink to add driver stream to. + * @param pCfg Stream configuration to use. + * @param pDrv Driver stream to add. + */ +static int sb16AddDrvStream(PPDMDEVINS pDevIns, PAUDMIXSINK pMixSink, PCPDMAUDIOSTREAMCFG pCfg, PSB16DRIVER pDrv) +{ + AssertReturn(pCfg->enmDir == PDMAUDIODIR_OUT, VERR_NOT_IMPLEMENTED); /* We don't support recording for SB16 so far. */ + LogFunc(("[LUN#%RU8] %s\n", pDrv->uLUN, pCfg->szName)); + + int rc; + PSB16DRIVERSTREAM pDrvStream = sb16GetDrvStream(pDrv, pCfg->enmDir, pCfg->enmPath); + if (pDrvStream) + { + AssertMsg(pDrvStream->pMixStrm == NULL, ("[LUN#%RU8] Driver stream already present when it must not\n", pDrv->uLUN)); + + PAUDMIXSTREAM pMixStrm; + rc = AudioMixerSinkCreateStream(pMixSink, pDrv->pConnector, pCfg, pDevIns, &pMixStrm); + LogFlowFunc(("LUN#%RU8: Created stream \"%s\" for sink, rc=%Rrc\n", pDrv->uLUN, pCfg->szName, rc)); + if (RT_SUCCESS(rc)) + { + rc = AudioMixerSinkAddStream(pMixSink, pMixStrm); + LogFlowFunc(("LUN#%RU8: Added stream \"%s\" to sink, rc=%Rrc\n", pDrv->uLUN, pCfg->szName, rc)); + if (RT_SUCCESS(rc)) + pDrvStream->pMixStrm = pMixStrm; + else + AudioMixerStreamDestroy(pMixStrm, pDevIns, true /*fImmediate*/); + } + } + else + rc = VERR_INVALID_PARAMETER; + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Adds all current driver streams to a specific mixer sink. + * + * @returns VBox status code. + * @param pDevIns The device instance. + * @param pThis The SB16 state. + * @param pMixSink Mixer sink to add stream to. + * @param pCfg Stream configuration to use. + */ +static int sb16AddDrvStreams(PPDMDEVINS pDevIns, PSB16STATE pThis, PAUDMIXSINK pMixSink, PCPDMAUDIOSTREAMCFG pCfg) +{ + AssertPtrReturn(pMixSink, VERR_INVALID_POINTER); + + int rc; + if (AudioHlpStreamCfgIsValid(pCfg)) + { + rc = AudioMixerSinkSetFormat(pMixSink, &pCfg->Props, pCfg->Device.cMsSchedulingHint); + if (RT_SUCCESS(rc)) + { + PSB16DRIVER pDrv; + RTListForEach(&pThis->lstDrv, pDrv, SB16DRIVER, Node) + { + int rc2 = sb16AddDrvStream(pDevIns, pMixSink, pCfg, pDrv); + if (RT_FAILURE(rc2)) + LogFunc(("Attaching stream failed with %Rrc\n", rc2)); + + /* Do not pass failure to rc here, as there might be drivers which aren't + * configured / ready yet. */ + } + } + } + else + rc = VERR_INVALID_PARAMETER; + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Removes a driver stream from a specific mixer sink. + * + * @param pDevIns The device instance. + * @param pMixSink Mixer sink to remove audio streams from. + * @param enmDir Stream direction to remove. + * @param enmPath Stream destination / source to remove. + * @param pDrv Driver stream to remove. + */ +static void sb16RemoveDrvStream(PPDMDEVINS pDevIns, PAUDMIXSINK pMixSink, PDMAUDIODIR enmDir, + PDMAUDIOPATH enmPath, PSB16DRIVER pDrv) +{ + PSB16DRIVERSTREAM pDrvStream = sb16GetDrvStream(pDrv, enmDir, enmPath); + if (pDrvStream) + { + if (pDrvStream->pMixStrm) + { + LogFlowFunc(("[LUN#%RU8]\n", pDrv->uLUN)); + + AudioMixerSinkRemoveStream(pMixSink, pDrvStream->pMixStrm); + + AudioMixerStreamDestroy(pDrvStream->pMixStrm, pDevIns, false /*fImmediate*/); + pDrvStream->pMixStrm = NULL; + } + } +} + +/** + * Removes all driver streams from a specific mixer sink. + * + * @param pDevIns The device instance. + * @param pThis The SB16 state. + * @param pMixSink Mixer sink to remove audio streams from. + * @param enmDir Stream direction to remove. + * @param enmPath Stream destination / source to remove. + */ +static void sb16RemoveDrvStreams(PPDMDEVINS pDevIns, PSB16STATE pThis, PAUDMIXSINK pMixSink, + PDMAUDIODIR enmDir, PDMAUDIOPATH enmPath) +{ + AssertPtrReturnVoid(pMixSink); + + PSB16DRIVER pDrv; + RTListForEach(&pThis->lstDrv, pDrv, SB16DRIVER, Node) + { + sb16RemoveDrvStream(pDevIns, pMixSink, enmDir, enmPath, pDrv); + } +} + +/** + * Adds a specific SB16 driver to the driver chain. + * + * @returns VBox status code. + * @param pDevIns The device instance. + * @param pThis The SB16 device state. + * @param pDrv The SB16 driver to add. + */ +static int sb16AddDrv(PPDMDEVINS pDevIns, PSB16STATE pThis, PSB16DRIVER pDrv) +{ + int rc = VINF_SUCCESS; + + for (unsigned i = 0; i < SB16_MAX_STREAMS; i++) + { + if (AudioHlpStreamCfgIsValid(&pThis->aStreams[i].Cfg)) + { + int rc2 = sb16AddDrvStream(pDevIns, sb16StreamIndexToSink(pThis, pThis->aStreams[i].uIdx), + &pThis->aStreams[i].Cfg, pDrv); + if (RT_SUCCESS(rc)) + rc = rc2; + } + } + + return rc; +} + +/** + * Removes a specific SB16 driver from the driver chain and destroys its + * associated streams. + * + * This is only used by sb16Detach. + * + * @param pDevIns The device instance. + * @param pThis The SB16 device state. + * @param pDrv SB16 driver to remove. + */ +static void sb16RemoveDrv(PPDMDEVINS pDevIns, PSB16STATE pThis, PSB16DRIVER pDrv) +{ + RT_NOREF(pDevIns); + + /** @todo We only implement one single output (playback) stream at the moment. */ + + if (pDrv->Out.pMixStrm) + { + AudioMixerSinkRemoveStream(pThis->pSinkOut, pDrv->Out.pMixStrm); + AudioMixerStreamDestroy(pDrv->Out.pMixStrm, pDevIns, true /*fImmediate*/); + pDrv->Out.pMixStrm = NULL; + } + + RTListNodeRemove(&pDrv->Node); +} + + +/********************************************************************************************************************************* +* Stream handling * +*********************************************************************************************************************************/ + +static int sb16StreamDoDmaOutput(PSB16STATE pThis, PSB16STREAM pStream, int uDmaChan, uint32_t offDma, uint32_t cbDma, + uint32_t cbToRead, uint32_t *pcbRead) +{ + uint32_t cbFree = (uint32_t)RTCircBufFree(pStream->State.pCircBuf); + //Assert(cbToRead <= cbFree); /** @todo Add statistics for overflows. */ + cbToRead = RT_MIN(cbToRead, cbFree); + + uint32_t cbReadTotal = 0; + while (cbToRead) + { + void *pv = NULL; + size_t cb = 0; + RTCircBufAcquireWriteBlock(pStream->State.pCircBuf, RT_MIN(cbDma - offDma, cbToRead), &pv, &cb); + + uint32_t cbRead = 0; + int rc = PDMDevHlpDMAReadMemory(pThis->pDevInsR3, uDmaChan, pv, offDma, (uint32_t)cb, &cbRead); + if (RT_SUCCESS(rc)) + Assert(cbRead == cb); + else + { + AssertMsgFailed(("Reading from DMA failed: %Rrc (cbReadTotal=%#x)\n", rc, cbReadTotal)); + RTCircBufReleaseWriteBlock(pStream->State.pCircBuf, 0); + if (cbReadTotal > 0) + break; + *pcbRead = 0; + return rc; + } + + if (RT_LIKELY(!pStream->Dbg.Runtime.pFileDMA)) + { /* likely */ } + else + AudioHlpFileWrite(pStream->Dbg.Runtime.pFileDMA, pv, cbRead); + + RTCircBufReleaseWriteBlock(pStream->State.pCircBuf, cbRead); + + Assert(cbToRead >= cbRead); + pStream->State.offWrite += cbRead; + offDma = (offDma + cbRead) % cbDma; + cbReadTotal += cbRead; + cbToRead -= cbRead; + } + + *pcbRead = cbReadTotal; + + /* Update buffer stats. */ + pStream->State.StatDmaBufUsed = (uint32_t)RTCircBufUsed(pStream->State.pCircBuf); + + return VINF_SUCCESS; +} + +/** + * Enables or disables a SB16 audio stream. + * + * @returns VBox status code. + * @param pThis The SB16 state. + * @param pStream The SB16 stream to enable or disable. + * @param fEnable Whether to enable or disable the stream. + * @param fForce Whether to force re-opening the stream or not. + * Otherwise re-opening only will happen if the PCM properties have changed. + */ +static int sb16StreamEnable(PSB16STATE pThis, PSB16STREAM pStream, bool fEnable, bool fForce) +{ + if ( !fForce + && fEnable == pStream->State.fEnabled) + return VINF_SUCCESS; + + LogFlowFunc(("fEnable=%RTbool, fForce=%RTbool, fStreamEnabled=%RTbool\n", fEnable, fForce, pStream->State.fEnabled)); + + PAUDMIXSINK pSink = sb16StreamIndexToSink(pThis, pStream->uIdx); + AssertPtrReturn(pSink, VERR_INTERNAL_ERROR_2); + + /* We only need to register the AIO update job the first time around as the requence doesn't change. */ + int rc; + if (fEnable && !pStream->State.fRegisteredAsyncUpdateJob) + { + rc = AudioMixerSinkAddUpdateJob(pSink, sb16StreamUpdateAsyncIoJob, pStream, RT_MS_1SEC / pStream->uTimerHz); + AssertRC(rc); + pStream->State.fRegisteredAsyncUpdateJob = RT_SUCCESS(rc) || rc == VERR_ALREADY_EXISTS; + } + + /* Tell the mixer. */ + if (fEnable) + { + rc = AudioMixerSinkStart(pSink); + AssertRCReturn(rc, rc); + } + else + { + rc = AudioMixerSinkDrainAndStop(pSink, pStream->State.pCircBuf ? (uint32_t)RTCircBufUsed(pStream->State.pCircBuf) : 0); + AssertRCReturn(rc, rc); + } + + pStream->State.fEnabled = fEnable; + + return rc; +} + +/** + * Retrieves the audio mixer sink of a corresponding SB16 stream. + * + * @returns Pointer to audio mixer sink if found, or NULL if not found / invalid. + * @param pThis The SB16 state. + * @param uIdx Stream index to get audio mixer sink for. + */ +DECLINLINE(PAUDMIXSINK) sb16StreamIndexToSink(PSB16STATE pThis, uint8_t uIdx) +{ + AssertReturn(uIdx <= SB16_MAX_STREAMS, NULL); + + /* Dead simple for now; make this more sophisticated if we have more stuff to cover. */ + if (uIdx == SB16_IDX_OUT) + return pThis->pSinkOut; /* Can be NULL if not configured / set up yet. */ + + AssertMsgFailed(("No sink attached (yet) for index %RU8\n", uIdx)); + return NULL; +} + +/** + * Returns the audio direction of a specified stream descriptor. + * + * @returns Audio direction. + * @param uIdx Stream index to get audio direction for. + */ +DECLINLINE(PDMAUDIODIR) sb16GetDirFromIndex(uint8_t uIdx) +{ + AssertReturn(uIdx <= SB16_MAX_STREAMS, PDMAUDIODIR_INVALID); + + /* Dead simple for now; make this more sophisticated if we have more stuff to cover. */ + if (uIdx == SB16_IDX_OUT) + return PDMAUDIODIR_OUT; + + return PDMAUDIODIR_INVALID; +} + +/** + * Creates a SB16 audio stream. + * + * @returns VBox status code. + * @param pThis The SB16 state. + * @param pStream The SB16 stream to create. + * @param uIdx Stream index to assign. + */ +static int sb16StreamCreate(PSB16STATE pThis, PSB16STREAM pStream, uint8_t uIdx) +{ + LogFlowFuncEnter(); + + pStream->Dbg.Runtime.fEnabled = pThis->Dbg.fEnabled; + + if (RT_LIKELY(!pStream->Dbg.Runtime.fEnabled)) + { /* likely */ } + else + { + int rc2 = AudioHlpFileCreateF(&pStream->Dbg.Runtime.pFileDMA, AUDIOHLPFILE_FLAGS_NONE, AUDIOHLPFILETYPE_WAV, + pThis->Dbg.pszOutPath, AUDIOHLPFILENAME_FLAGS_NONE, 0 /*uInstance*/, + sb16GetDirFromIndex(pStream->uIdx) == PDMAUDIODIR_IN + ? "sb16StreamWriteSD%RU8" : "sb16StreamReadSD%RU8", pStream->uIdx); + AssertRC(rc2); + + /* Delete stale debugging files from a former run. */ + AudioHlpFileDelete(pStream->Dbg.Runtime.pFileDMA); + } + + pStream->uIdx = uIdx; + + return VINF_SUCCESS; +} + +/** + * Destroys a SB16 audio stream. + * + * @returns VBox status code. + * @param pDevIns The device instance. + * @param pThis The SB16 state. + * @param pStream The SB16 stream to destroy. + */ +static int sb16StreamDestroy(PPDMDEVINS pDevIns, PSB16STATE pThis, PSB16STREAM pStream) +{ + LogFlowFuncEnter(); + + sb16StreamClose(pDevIns, pThis, pStream); + + if (pStream->State.fRegisteredAsyncUpdateJob) + { + PAUDMIXSINK pSink = sb16StreamIndexToSink(pThis, pStream->uIdx); + if (pSink) + AudioMixerSinkRemoveUpdateJob(pSink, sb16StreamUpdateAsyncIoJob, pStream); + pStream->State.fRegisteredAsyncUpdateJob = false; + } + + if (pStream->State.pCircBuf) + { + RTCircBufDestroy(pStream->State.pCircBuf); + pStream->State.pCircBuf = NULL; + } + + if (RT_LIKELY(!pStream->Dbg.Runtime.fEnabled)) + { /* likely */ } + else + { + AudioHlpFileDestroy(pStream->Dbg.Runtime.pFileDMA); + pStream->Dbg.Runtime.pFileDMA = NULL; + } + + pStream->uIdx = UINT8_MAX; + + return VINF_SUCCESS; +} + +/** + * Resets a SB16 stream. + * + * @param pThis The SB16 state. + * @param pStream The SB16 stream to reset. + */ +static void sb16StreamReset(PSB16STATE pThis, PSB16STREAM pStream) +{ + LogFlowFuncEnter(); + + PDMDevHlpISASetIrq(pThis->pDevInsR3, pStream->HwCfgRuntime.uIrq, 0); + if (pStream->dma_auto) + { + PDMDevHlpISASetIrq(pThis->pDevInsR3, pStream->HwCfgRuntime.uIrq, 1); + PDMDevHlpISASetIrq(pThis->pDevInsR3, pStream->HwCfgRuntime.uIrq, 0); + + pStream->dma_auto = 0; + } + + sb16StreamControl(pThis->pDevInsR3, pThis, pStream, false /* fRun */); + sb16StreamEnable(pThis, pStream, false /* fEnable */, false /* fForce */); + + switch (pStream->uIdx) + { + case SB16_IDX_OUT: + { + pStream->Cfg.enmDir = PDMAUDIODIR_OUT; + pStream->Cfg.enmPath = PDMAUDIOPATH_OUT_FRONT; + + PDMAudioPropsInit(&pStream->Cfg.Props, 1 /* 8-bit */, false /* fSigned */, 1 /* Mono */, 11025 /* uHz */); + RTStrCopy(pStream->Cfg.szName, sizeof(pStream->Cfg.szName), "Output"); + break; + } + + default: + AssertFailed(); + break; + } + + pStream->cbDmaLeft = 0; + pStream->cbDmaBlockSize = 0; + pStream->can_write = 1; /** @ŧodo r=andy BUGBUG Figure out why we (still) need this. */ + + /** @todo Also reset corresponding DSP values here? */ +} + +/** + * Opens a SB16 stream with its current mixer settings. + * + * @returns VBox status code. + * @param pDevIns The device instance. + * @param pThis The SB16 device state. + * @param pStream The SB16 stream to open. + * + * @note This currently only supports the one and only output stream. + */ +static int sb16StreamOpen(PPDMDEVINS pDevIns, PSB16STATE pThis, PSB16STREAM pStream) +{ + LogFlowFuncEnter(); + AssertLogRelReturn(PDMAudioPropsAreValid(&pStream->Cfg.Props), VERR_INTERNAL_ERROR_5); + + switch (pStream->uIdx) + { + case SB16_IDX_OUT: + pStream->Cfg.enmDir = PDMAUDIODIR_OUT; + pStream->Cfg.enmPath = PDMAUDIOPATH_OUT_FRONT; + + RTStrCopy(pStream->Cfg.szName, sizeof(pStream->Cfg.szName), "Output"); + break; + + default: + AssertFailed(); + break; + } + + LogRel2(("SB16: (Re-)Opening stream '%s' (%RU32Hz, %RU8 channels, %s%RU8)\n", pStream->Cfg.szName, pStream->Cfg.Props.uHz, + PDMAudioPropsChannels(&pStream->Cfg.Props), pStream->Cfg.Props.fSigned ? "S" : "U", + PDMAudioPropsSampleBits(&pStream->Cfg.Props))); + + /* (Re-)create the stream's internal ring buffer. */ + if (pStream->State.pCircBuf) + { + RTCircBufDestroy(pStream->State.pCircBuf); + pStream->State.pCircBuf = NULL; + } + + /** @todo r=bird: two DMA periods is probably too little. */ + const uint32_t cbCircBuf = PDMAudioPropsMilliToBytes(&pStream->Cfg.Props, + (RT_MS_1SEC / pStream->uTimerHz) * 2 /* Use double buffering here */); + + int rc = RTCircBufCreate(&pStream->State.pCircBuf, cbCircBuf); + AssertRCReturn(rc, rc); + pStream->State.StatDmaBufSize = (uint32_t)RTCircBufSize(pStream->State.pCircBuf); + + /* Set scheduling hint. */ + pStream->Cfg.Device.cMsSchedulingHint = RT_MS_1SEC / RT_MIN(pStream->uTimerHz, 1); + + PAUDMIXSINK pMixerSink = sb16StreamIndexToSink(pThis, pStream->uIdx); + AssertPtrReturn(pMixerSink, VERR_INVALID_POINTER); + + sb16RemoveDrvStreams(pDevIns, pThis, + sb16StreamIndexToSink(pThis, pStream->uIdx), pStream->Cfg.enmDir, pStream->Cfg.enmPath); + + rc = sb16AddDrvStreams(pDevIns, pThis, pMixerSink, &pStream->Cfg); + if (RT_SUCCESS(rc)) + { + if (RT_LIKELY(!pStream->Dbg.Runtime.fEnabled)) + { /* likely */ } + else + { + /* Make sure to close + delete a former debug file, as the PCM format has changed (e.g. U8 -> S16). */ + if (AudioHlpFileIsOpen(pStream->Dbg.Runtime.pFileDMA)) + { + AudioHlpFileClose(pStream->Dbg.Runtime.pFileDMA); + AudioHlpFileDelete(pStream->Dbg.Runtime.pFileDMA); + } + + int rc2 = AudioHlpFileOpen(pStream->Dbg.Runtime.pFileDMA, AUDIOHLPFILE_DEFAULT_OPEN_FLAGS, + &pStream->Cfg.Props); + AssertRC(rc2); + } + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Closes a SB16 stream. + * + * @param pDevIns The device instance. + * @param pThis SB16 state. + * @param pStream The SB16 stream to close. + */ +static void sb16StreamClose(PPDMDEVINS pDevIns, PSB16STATE pThis, PSB16STREAM pStream) +{ + RT_NOREF(pDevIns, pThis, pStream); + + LogFlowFuncEnter(); + + /* Nothing to do in here right now. */ +} + +static void sb16StreamTransferScheduleNext(PSB16STATE pThis, PSB16STREAM pStream, uint32_t cbBytes) +{ + RT_NOREF(pStream); + + uint64_t const uTimerHz = PDMDevHlpTimerGetFreq(pThis->pDevInsR3, pThis->hTimerIRQ); + + const uint64_t usBytes = PDMAudioPropsBytesToMicro(&pStream->Cfg.Props, cbBytes); + const uint64_t cTransferTicks = PDMDevHlpTimerFromMicro(pThis->pDevInsR3, pThis->hTimerIRQ, usBytes); + + LogFlowFunc(("%RU32 bytes -> %RU64 ticks\n", cbBytes, cTransferTicks)); + + if (cTransferTicks < uTimerHz / 1024) /** @todo Explain this. */ + { + LogFlowFunc(("IRQ\n")); + PDMDevHlpISASetIrq(pThis->pDevInsR3, pStream->HwCfgRuntime.uIrq, 1); + } + else + { + LogFlowFunc(("Scheduled\n")); + PDMDevHlpTimerSetRelative(pThis->pDevInsR3, pThis->hTimerIRQ, cTransferTicks, NULL); + } +} + + +/** + * Output streams: Pushes data to the mixer. + * + * @param pStream The SB16 stream. + * @param pSink The mixer sink to push to. + */ +static void sb16StreamPushToMixer(PSB16STREAM pStream, PAUDMIXSINK pSink) +{ +#ifdef LOG_ENABLED + uint64_t const offReadOld = pStream->State.offRead; +#endif + pStream->State.offRead = AudioMixerSinkTransferFromCircBuf(pSink, + pStream->State.pCircBuf, + pStream->State.offRead, + pStream->uIdx, + /** @todo pStream->Dbg.Runtime.fEnabled + ? pStream->Dbg.Runtime.pFileStream :*/ NULL); + + Log3Func(("[SD%RU8] transferred=%#RX64 bytes -> @%#RX64\n", pStream->uIdx, + pStream->State.offRead - offReadOld, pStream->State.offRead)); + + /* Update buffer stats. */ + pStream->State.StatDmaBufUsed = (uint32_t)RTCircBufUsed(pStream->State.pCircBuf); +} + + +/** + * @callback_method_impl{FNAUDMIXSINKUPDATE} + * + * For output streams this moves data from the internal DMA buffer (in which + * ichac97R3StreamUpdateDma put it), thru the mixer and to the various backend + * audio devices. + */ +static DECLCALLBACK(void) sb16StreamUpdateAsyncIoJob(PPDMDEVINS pDevIns, PAUDMIXSINK pSink, void *pvUser) +{ + PSB16STATE const pThis = PDMDEVINS_2_DATA(pDevIns, PSB16STATE); + PSB16STREAM const pStream = (PSB16STREAM)pvUser; + Assert(pStream->uIdx == (uintptr_t)(pStream - &pThis->aStreams[0])); + Assert(pSink == sb16StreamIndexToSink(pThis, pStream->uIdx)); + RT_NOREF(pThis); + + /* + * Output. + */ + if (sb16GetDirFromIndex(pStream->uIdx) == PDMAUDIODIR_OUT) + sb16StreamPushToMixer(pStream, pSink); + /* + * No input streams at present. + */ + else + AssertFailed(); +} + + +/********************************************************************************************************************************* +* Saved state handling * +*********************************************************************************************************************************/ + +/** + * @callback_method_impl{FNSSMDEVLIVEEXEC} + */ +static DECLCALLBACK(int) sb16LiveExec(PPDMDEVINS pDevIns, PSSMHANDLE pSSM, uint32_t uPass) +{ + RT_NOREF(uPass); + + PSB16STATE pThis = PDMDEVINS_2_DATA(pDevIns, PSB16STATE); + PCPDMDEVHLPR3 pHlp = pDevIns->pHlpR3; + + /** Currently the saved state only contains the one-and-only output stream. */ + PSB16STREAM pStream = &pThis->aStreams[SB16_IDX_OUT]; + + pHlp->pfnSSMPutS32(pSSM, pStream->HwCfgDefault.uIrq); + pHlp->pfnSSMPutS32(pSSM, pStream->HwCfgDefault.uDmaChanLow); + pHlp->pfnSSMPutS32(pSSM, pStream->HwCfgDefault.uDmaChanHigh); + pHlp->pfnSSMPutS32(pSSM, pStream->HwCfgDefault.uPort); + pHlp->pfnSSMPutS32(pSSM, pStream->HwCfgDefault.uVer); + return VINF_SSM_DONT_CALL_AGAIN; +} + +/** + * Worker for sb16SaveExec. + */ +static int sb16Save(PCPDMDEVHLPR3 pHlp, PSSMHANDLE pSSM, PSB16STATE pThis) +{ + /* The saved state only contains the one-and-only output stream. */ + PSB16STREAM pStream = &pThis->aStreams[SB16_IDX_OUT]; + + pHlp->pfnSSMPutS32(pSSM, pStream->HwCfgRuntime.uIrq); + pHlp->pfnSSMPutS32(pSSM, pStream->HwCfgRuntime.uDmaChanLow); + pHlp->pfnSSMPutS32(pSSM, pStream->HwCfgRuntime.uDmaChanHigh); + pHlp->pfnSSMPutS32(pSSM, pStream->HwCfgRuntime.uPort); + pHlp->pfnSSMPutS32(pSSM, pStream->HwCfgRuntime.uVer); + pHlp->pfnSSMPutS32(pSSM, pThis->dsp_in_idx); + pHlp->pfnSSMPutS32(pSSM, pThis->dsp_out_data_len); + + pHlp->pfnSSMPutS32(pSSM, PDMAudioPropsChannels(&pStream->Cfg.Props) >= 2 ? 1 : 0); + pHlp->pfnSSMPutS32(pSSM, PDMAudioPropsIsSigned(&pStream->Cfg.Props) ? 1 : 0); + pHlp->pfnSSMPutS32(pSSM, PDMAudioPropsSampleBits(&pStream->Cfg.Props)); + pHlp->pfnSSMPutU32(pSSM, 0); /* Legacy; was PDMAUDIOFMT, unused now. */ + + pHlp->pfnSSMPutS32(pSSM, pStream->dma_auto); + pHlp->pfnSSMPutS32(pSSM, pStream->cbDmaBlockSize); + pHlp->pfnSSMPutS32(pSSM, pStream->fifo); + pHlp->pfnSSMPutS32(pSSM, PDMAudioPropsHz(&pStream->Cfg.Props)); + pHlp->pfnSSMPutS32(pSSM, pStream->time_const); + pHlp->pfnSSMPutS32(pSSM, 0); /* Legacy; was speaker control (on/off) for output stream. */ + pHlp->pfnSSMPutS32(pSSM, pThis->dsp_in_needed_bytes); + pHlp->pfnSSMPutS32(pSSM, pThis->cmd); + pHlp->pfnSSMPutS32(pSSM, pStream->fDmaUseHigh); + pHlp->pfnSSMPutS32(pSSM, pThis->highspeed); + pHlp->pfnSSMPutS32(pSSM, pStream->can_write); + pHlp->pfnSSMPutS32(pSSM, pThis->v2x6); + + pHlp->pfnSSMPutU8 (pSSM, pThis->csp_param); + pHlp->pfnSSMPutU8 (pSSM, pThis->csp_value); + pHlp->pfnSSMPutU8 (pSSM, pThis->csp_mode); + pHlp->pfnSSMPutU8 (pSSM, pThis->csp_param); /* Bug compatible! */ + pHlp->pfnSSMPutMem(pSSM, pThis->csp_regs, 256); + pHlp->pfnSSMPutU8 (pSSM, pThis->csp_index); + pHlp->pfnSSMPutMem(pSSM, pThis->csp_reg83, 4); + pHlp->pfnSSMPutS32(pSSM, pThis->csp_reg83r); + pHlp->pfnSSMPutS32(pSSM, pThis->csp_reg83w); + + pHlp->pfnSSMPutMem(pSSM, pThis->dsp_in_data, sizeof(pThis->dsp_in_data)); + pHlp->pfnSSMPutMem(pSSM, pThis->dsp_out_data, sizeof(pThis->dsp_out_data)); + pHlp->pfnSSMPutU8 (pSSM, pThis->test_reg); + pHlp->pfnSSMPutU8 (pSSM, pThis->last_read_byte); + + pHlp->pfnSSMPutS32(pSSM, pThis->nzero); + pHlp->pfnSSMPutS32(pSSM, pStream->cbDmaLeft); + pHlp->pfnSSMPutS32(pSSM, pStream->State.fEnabled ? 1 : 0); + /* The stream's bitrate. Needed for backwards (legacy) compatibility. */ + pHlp->pfnSSMPutS32(pSSM, AudioHlpCalcBitrate(PDMAudioPropsSampleBits(&pThis->aStreams[SB16_IDX_OUT].Cfg.Props), + PDMAudioPropsHz(&pThis->aStreams[SB16_IDX_OUT].Cfg.Props), + PDMAudioPropsChannels(&pThis->aStreams[SB16_IDX_OUT].Cfg.Props))); + /* Block size alignment, superfluous and thus not saved anymore. Needed for backwards (legacy) compatibility. */ + pHlp->pfnSSMPutS32(pSSM, 0); + + pHlp->pfnSSMPutS32(pSSM, pThis->mixer_nreg); + return pHlp->pfnSSMPutMem(pSSM, pThis->mixer_regs, 256); +} + +/** + * @callback_method_impl{FNSSMDEVSAVEEXEC} + */ +static DECLCALLBACK(int) sb16SaveExec(PPDMDEVINS pDevIns, PSSMHANDLE pSSM) +{ + PSB16STATE pThis = PDMDEVINS_2_DATA(pDevIns, PSB16STATE); + PCPDMDEVHLPR3 pHlp = pDevIns->pHlpR3; + + sb16LiveExec(pDevIns, pSSM, 0); + return sb16Save(pHlp, pSSM, pThis); +} + +/** + * Worker for sb16LoadExec. + */ +static int sb16Load(PPDMDEVINS pDevIns, PSSMHANDLE pSSM, PSB16STATE pThis) +{ + PCPDMDEVHLPR3 pHlp = pDevIns->pHlpR3; + PSB16STREAM pStream = &pThis->aStreams[SB16_IDX_OUT]; /* The saved state only contains the one-and-only output stream. */ + int rc; + + int32_t i32Tmp; + pHlp->pfnSSMGetS32(pSSM, &i32Tmp); + pStream->HwCfgRuntime.uIrq = i32Tmp; /* IRQ. */ + pHlp->pfnSSMGetS32(pSSM, &i32Tmp); + pStream->HwCfgRuntime.uDmaChanLow = i32Tmp; /* Low (8-bit) DMA channel. */ + pHlp->pfnSSMGetS32(pSSM, &i32Tmp); + pStream->HwCfgRuntime.uDmaChanHigh = i32Tmp; /* High (16-bit) DMA channel. */ + pHlp->pfnSSMGetS32(pSSM, &i32Tmp); /* Used I/O port. */ + pStream->HwCfgRuntime.uPort = i32Tmp; + pHlp->pfnSSMGetS32(pSSM, &i32Tmp); /* DSP version running. */ + pStream->HwCfgRuntime.uVer = i32Tmp; + pHlp->pfnSSMGetS32(pSSM, &pThis->dsp_in_idx); + pHlp->pfnSSMGetS32(pSSM, &pThis->dsp_out_data_len); + + rc = pHlp->pfnSSMGetS32(pSSM, &i32Tmp); /* Output stream: Number of channels. */ + AssertRCReturn(rc, rc); + AssertReturn((uint32_t)i32Tmp <= 1, VERR_INVALID_PARAMETER); /* Paranoia. */ + if (i32Tmp) /* PDMAudioPropsSetChannels() will assert if channels are 0 (will be re-set on DMA run command). */ + PDMAudioPropsSetChannels(&pStream->Cfg.Props, (uint8_t)i32Tmp); + pHlp->pfnSSMGetS32(pSSM, &i32Tmp); /* Output stream: Signed format bit. */ + pStream->Cfg.Props.fSigned = i32Tmp != 0; + rc = pHlp->pfnSSMGetS32(pSSM, &i32Tmp); /* Output stream: Sample size in bits. */ + AssertRCReturn(rc, rc); + if (i32Tmp) /* PDMAudioPropsSetSampleSize() will assert if sample size is 0 (will be re-set on DMA run command). */ + PDMAudioPropsSetSampleSize(&pStream->Cfg.Props, (uint8_t)(i32Tmp / 8)); + + pHlp->pfnSSMSkip (pSSM, sizeof(int32_t)); /* Legacy; was PDMAUDIOFMT, unused now. */ + pHlp->pfnSSMGetS32(pSSM, &pStream->dma_auto); + pHlp->pfnSSMGetS32(pSSM, &pStream->cbDmaBlockSize); + pHlp->pfnSSMGetS32(pSSM, &pStream->fifo); + pHlp->pfnSSMGetS32(pSSM, &i32Tmp); pStream->Cfg.Props.uHz = i32Tmp; + pHlp->pfnSSMGetS32(pSSM, &pStream->time_const); + pHlp->pfnSSMSkip (pSSM, sizeof(int32_t)); /* Legacy; was speaker (on / off) for output stream. */ + pHlp->pfnSSMGetS32(pSSM, &pThis->dsp_in_needed_bytes); + pHlp->pfnSSMGetS32(pSSM, &pThis->cmd); + pHlp->pfnSSMGetS32(pSSM, &pStream->fDmaUseHigh); /* Output stream: Whether to use the high or low DMA channel. */ + pHlp->pfnSSMGetS32(pSSM, &pThis->highspeed); + pHlp->pfnSSMGetS32(pSSM, &pStream->can_write); + pHlp->pfnSSMGetS32(pSSM, &pThis->v2x6); + + pHlp->pfnSSMGetU8 (pSSM, &pThis->csp_param); + pHlp->pfnSSMGetU8 (pSSM, &pThis->csp_value); + pHlp->pfnSSMGetU8 (pSSM, &pThis->csp_mode); + pHlp->pfnSSMGetU8 (pSSM, &pThis->csp_param); /* Bug compatible! */ + pHlp->pfnSSMGetMem(pSSM, pThis->csp_regs, 256); + pHlp->pfnSSMGetU8 (pSSM, &pThis->csp_index); + pHlp->pfnSSMGetMem(pSSM, pThis->csp_reg83, 4); + pHlp->pfnSSMGetS32(pSSM, &pThis->csp_reg83r); + pHlp->pfnSSMGetS32(pSSM, &pThis->csp_reg83w); + + pHlp->pfnSSMGetMem(pSSM, pThis->dsp_in_data, sizeof(pThis->dsp_in_data)); + pHlp->pfnSSMGetMem(pSSM, pThis->dsp_out_data, sizeof(pThis->dsp_out_data)); + pHlp->pfnSSMGetU8 (pSSM, &pThis->test_reg); + pHlp->pfnSSMGetU8 (pSSM, &pThis->last_read_byte); + + pHlp->pfnSSMGetS32(pSSM, &pThis->nzero); + pHlp->pfnSSMGetS32(pSSM, &pStream->cbDmaLeft); + pHlp->pfnSSMGetS32(pSSM, &i32Tmp); /* Output stream: DMA currently running bit. */ + const bool fStreamEnabled = i32Tmp != 0; + pHlp->pfnSSMSkip (pSSM, sizeof(int32_t)); /* Legacy; was the output stream's current bitrate (in bytes). */ + pHlp->pfnSSMSkip (pSSM, sizeof(int32_t)); /* Legacy; was the output stream's DMA block alignment. */ + + int32_t mixer_nreg = 0; + rc = pHlp->pfnSSMGetS32(pSSM, &mixer_nreg); + AssertRCReturn(rc, rc); + pThis->mixer_nreg = (uint8_t)mixer_nreg; + rc = pHlp->pfnSSMGetMem(pSSM, pThis->mixer_regs, 256); + AssertRCReturn(rc, rc); + + if (fStreamEnabled) + { + /* Sanity: If stream is going be enabled, PCM props must be valid. Otherwise the saved state is borked somehow. */ + AssertMsgReturn(AudioHlpPcmPropsAreValidAndSupported(&pStream->Cfg.Props), + ("PCM properties for stream #%RU8 are invalid\n", pStream->uIdx), VERR_SSM_DATA_UNIT_FORMAT_CHANGED); + sb16StreamControl(pDevIns, pThis, pStream, true /* fRun */); + } + + /* Update the master (mixer) and PCM out volumes. */ + sb16UpdateVolume(pThis); + + return VINF_SUCCESS; +} + +/** + * @callback_method_impl{FNSSMDEVLOADEXEC} + */ +static DECLCALLBACK(int) sb16LoadExec(PPDMDEVINS pDevIns, PSSMHANDLE pSSM, uint32_t uVersion, uint32_t uPass) +{ + PSB16STATE pThis = PDMDEVINS_2_DATA(pDevIns, PSB16STATE); + PCPDMDEVHLPR3 pHlp = pDevIns->pHlpR3; + + AssertMsgReturn( uVersion == SB16_SAVE_STATE_VERSION + || uVersion == SB16_SAVE_STATE_VERSION_VBOX_30, + ("%u\n", uVersion), + VERR_SSM_UNSUPPORTED_DATA_UNIT_VERSION); + if (uVersion > SB16_SAVE_STATE_VERSION_VBOX_30) + { + /** Currently the saved state only contains the one-and-only output stream. */ + PSB16STREAM pStream = &pThis->aStreams[SB16_IDX_OUT]; + + int32_t irq; + pHlp->pfnSSMGetS32(pSSM, &irq); + int32_t dma; + pHlp->pfnSSMGetS32(pSSM, &dma); + int32_t hdma; + pHlp->pfnSSMGetS32(pSSM, &hdma); + int32_t port; + pHlp->pfnSSMGetS32(pSSM, &port); + int32_t ver; + int rc = pHlp->pfnSSMGetS32(pSSM, &ver); + AssertRCReturn (rc, rc); + + if ( irq != pStream->HwCfgDefault.uIrq + || dma != pStream->HwCfgDefault.uDmaChanLow + || hdma != pStream->HwCfgDefault.uDmaChanHigh + || port != pStream->HwCfgDefault.uPort + || ver != pStream->HwCfgDefault.uVer) + { + return pHlp->pfnSSMSetCfgError(pSSM, RT_SRC_POS, + N_("config changed: irq=%x/%x dma=%x/%x hdma=%x/%x port=%x/%x ver=%x/%x (saved/config)"), + irq, pStream->HwCfgDefault.uIrq, + dma, pStream->HwCfgDefault.uDmaChanLow, + hdma, pStream->HwCfgDefault.uDmaChanHigh, + port, pStream->HwCfgDefault.uPort, + ver, pStream->HwCfgDefault.uVer); + } + } + + if (uPass != SSM_PASS_FINAL) + return VINF_SUCCESS; + + return sb16Load(pDevIns, pSSM, pThis); +} + + +/********************************************************************************************************************************* +* Debug Info Items * +*********************************************************************************************************************************/ + +/** + * @callback_method_impl{FNDBGFHANDLERDEV, sb16mixer} + */ +static DECLCALLBACK(void) sb16DbgInfoMixer(PPDMDEVINS pDevIns, PCDBGFINFOHLP pHlp, const char *pszArgs) +{ + PSB16STATE pThis = PDMDEVINS_2_DATA(pDevIns, PSB16STATE); + if (pThis->pMixer) + AudioMixerDebug(pThis->pMixer, pHlp, pszArgs); + else + pHlp->pfnPrintf(pHlp, "Mixer not available\n"); +} + + +/********************************************************************************************************************************* +* IBase implementation * +*********************************************************************************************************************************/ + +/** + * @interface_method_impl{PDMIBASE,pfnQueryInterface} + */ +static DECLCALLBACK(void *) sb16QueryInterface(struct PDMIBASE *pInterface, const char *pszIID) +{ + PSB16STATE pThis = RT_FROM_MEMBER(pInterface, SB16STATE, IBase); + Assert(&pThis->IBase == pInterface); + + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIBASE, &pThis->IBase); + return NULL; +} + + +/********************************************************************************************************************************* +* Device (PDM) handling * +*********************************************************************************************************************************/ + +/** + * Worker for sb16Construct() and sb16Attach(). + * + * @returns VBox status code. + * @param pThis SB16 state. + * @param uLUN The logical unit which is being detached. + * @param ppDrv Attached driver instance on success. Optional. + */ +static int sb16AttachInternal(PSB16STATE pThis, unsigned uLUN, PSB16DRIVER *ppDrv) +{ + /* + * Allocate a new driver structure and try attach the driver. + */ + PSB16DRIVER pDrv = (PSB16DRIVER)RTMemAllocZ(sizeof(SB16DRIVER)); + AssertPtrReturn(pDrv, VERR_NO_MEMORY); + RTStrPrintf(pDrv->szDesc, sizeof(pDrv->szDesc), "Audio driver port (SB16) for LUN #%u", uLUN); + + PPDMIBASE pDrvBase; + int rc = PDMDevHlpDriverAttach(pThis->pDevInsR3, uLUN, &pThis->IBase, &pDrvBase, pDrv->szDesc); + if (RT_SUCCESS(rc)) + { + pDrv->pConnector = PDMIBASE_QUERY_INTERFACE(pDrvBase, PDMIAUDIOCONNECTOR); + AssertPtr(pDrv->pConnector); + if (RT_VALID_PTR(pDrv->pConnector)) + { + pDrv->pDrvBase = pDrvBase; + pDrv->pSB16State = pThis; + pDrv->uLUN = uLUN; + + /* Attach to driver list if not attached yet. */ + if (!pDrv->fAttached) + { + RTListAppend(&pThis->lstDrv, &pDrv->Node); + pDrv->fAttached = true; + } + + if (ppDrv) + *ppDrv = pDrv; + LogFunc(("LUN#%u: returns VINF_SUCCESS (pCon=%p)\n", uLUN, pDrv->pConnector)); + return VINF_SUCCESS; + } + rc = VERR_PDM_MISSING_INTERFACE_BELOW; + } + else if (rc == VERR_PDM_NO_ATTACHED_DRIVER) + LogFunc(("No attached driver for LUN #%u\n", uLUN)); + else + LogFunc(("Failed to attached driver for LUN #%u: %Rrc\n", uLUN, rc)); + RTMemFree(pDrv); + + LogFunc(("LUN#%u: rc=%Rrc\n", uLUN, rc)); + return rc; +} + +/** + * @interface_method_impl{PDMDEVREG,pfnAttach} + */ +static DECLCALLBACK(int) sb16Attach(PPDMDEVINS pDevIns, unsigned iLUN, uint32_t fFlags) +{ + PSB16STATE pThis = PDMDEVINS_2_DATA(pDevIns, PSB16STATE); + RT_NOREF(fFlags); + LogFunc(("iLUN=%u, fFlags=%#x\n", iLUN, fFlags)); + + /** @todo r=andy Any locking required here? */ + + PSB16DRIVER pDrv; + int rc = sb16AttachInternal(pThis, iLUN, &pDrv); + if (RT_SUCCESS(rc)) + { + int rc2 = sb16AddDrv(pDevIns, pThis, pDrv); + if (RT_FAILURE(rc2)) + LogFunc(("sb16AddDrv failed with %Rrc (ignored)\n", rc2)); + } + + return rc; +} + +/** + * @interface_method_impl{PDMDEVREG,pfnDetach} + */ +static DECLCALLBACK(void) sb16Detach(PPDMDEVINS pDevIns, unsigned iLUN, uint32_t fFlags) +{ + PSB16STATE pThis = PDMDEVINS_2_DATA(pDevIns, PSB16STATE); + RT_NOREF(fFlags); + + LogFunc(("iLUN=%u, fFlags=0x%x\n", iLUN, fFlags)); + + PSB16DRIVER pDrv; + RTListForEach(&pThis->lstDrv, pDrv, SB16DRIVER, Node) + { + if (pDrv->uLUN == iLUN) + { + sb16RemoveDrv(pDevIns, pThis, pDrv); + RTMemFree(pDrv); + return; + } + } + LogFunc(("LUN#%u was not found\n", iLUN)); +} + + +/** + * @interface_method_impl{PDMDEVREG,pfnReset} + */ +static DECLCALLBACK(void) sb16DevReset(PPDMDEVINS pDevIns) +{ + PSB16STATE pThis = PDMDEVINS_2_DATA(pDevIns, PSB16STATE); + + LogRel2(("SB16: Reset\n")); + + pThis->mixer_regs[0x82] = 0; + pThis->csp_regs[5] = 1; + pThis->csp_regs[9] = 0xf8; + + pThis->dsp_in_idx = 0; + pThis->dsp_out_data_len = 0; + pThis->dsp_in_needed_bytes = 0; + pThis->nzero = 0; + pThis->highspeed = 0; + pThis->v2x6 = 0; + pThis->cmd = -1; + + sb16MixerReset(pThis); + sb16SpeakerControl(pThis, false /* fOn */); + sb16DspCmdResetLegacy(pThis); +} + +/** + * Powers off the device. + * + * @param pDevIns Device instance to power off. + */ +static DECLCALLBACK(void) sb16PowerOff(PPDMDEVINS pDevIns) +{ + PSB16STATE pThis = PDMDEVINS_2_DATA(pDevIns, PSB16STATE); + + LogRel2(("SB16: Powering off ...\n")); + + /* + * Destroy all streams. + */ + for (unsigned i = 0; i < SB16_MAX_STREAMS; i++) + sb16StreamDestroy(pDevIns, pThis, &pThis->aStreams[i]); + + /* + * Destroy all sinks. + */ + if (pThis->pSinkOut) + { + AudioMixerSinkDestroy(pThis->pSinkOut, pDevIns); + pThis->pSinkOut = NULL; + } + /** @todo Ditto for sinks. */ + + /* + * Note: Destroy the mixer while powering off and *not* in sb16Destruct, + * giving the mixer the chance to release any references held to + * PDM audio streams it maintains. + */ + if (pThis->pMixer) + { + AudioMixerDestroy(pThis->pMixer, pDevIns); + pThis->pMixer = NULL; + } +} + +/** + * @interface_method_impl{PDMDEVREG,pfnDestruct} + */ +static DECLCALLBACK(int) sb16Destruct(PPDMDEVINS pDevIns) +{ + PDMDEV_CHECK_VERSIONS_RETURN_QUIET(pDevIns); /* this shall come first */ + PSB16STATE pThis = PDMDEVINS_2_DATA(pDevIns, PSB16STATE); + + LogFlowFuncEnter(); + + PSB16DRIVER pDrv; + while (!RTListIsEmpty(&pThis->lstDrv)) + { + pDrv = RTListGetFirst(&pThis->lstDrv, SB16DRIVER, Node); + + RTListNodeRemove(&pDrv->Node); + RTMemFree(pDrv); + } + + /* We don't always go via PowerOff, so make sure the mixer is destroyed. */ + if (pThis->pMixer) + { + AudioMixerDestroy(pThis->pMixer, pDevIns); + pThis->pMixer = NULL; + } + + return VINF_SUCCESS; +} + +/** + * @interface_method_impl{PDMDEVREGR3,pfnConstruct} + */ +static DECLCALLBACK(int) sb16Construct(PPDMDEVINS pDevIns, int iInstance, PCFGMNODE pCfg) +{ + PDMDEV_CHECK_VERSIONS_RETURN(pDevIns); /* this shall come first */ + PSB16STATE pThis = PDMDEVINS_2_DATA(pDevIns, PSB16STATE); + PCPDMDEVHLPR3 pHlp = pDevIns->pHlpR3; + RT_NOREF(iInstance); + + Assert(iInstance == 0); + + /* + * Initialize the data so sb16Destruct runs without a hitch if we return early. + */ + pThis->pDevInsR3 = pDevIns; + pThis->IBase.pfnQueryInterface = sb16QueryInterface; + pThis->cmd = -1; + + pThis->csp_regs[5] = 1; + pThis->csp_regs[9] = 0xf8; + + RTListInit(&pThis->lstDrv); + + /* + * Validate and read config data. + */ + /* Note: For now we only support the one-and-only output stream. */ + PSB16STREAM pStream = &pThis->aStreams[SB16_IDX_OUT]; + + PDMDEV_VALIDATE_CONFIG_RETURN(pDevIns, "IRQ|DMA|DMA16|Port|Version|TimerHz|DebugEnabled|DebugPathOut", ""); + int rc = pHlp->pfnCFGMQueryU8Def(pCfg, "IRQ", &pStream->HwCfgDefault.uIrq, 5); + if (RT_FAILURE(rc)) + return PDMDEV_SET_ERROR(pDevIns, rc, N_("SB16 configuration error: Failed to get the \"IRQ\" value")); + /* Sanity-check supported SB16 IRQs. */ + if ( 2 != pStream->HwCfgDefault.uIrq + && 5 != pStream->HwCfgDefault.uIrq + && 7 != pStream->HwCfgDefault.uIrq + && 10 != pStream->HwCfgDefault.uIrq) + return PDMDEV_SET_ERROR(pDevIns, rc, N_("SB16 configuration error: Invalid \"IRQ\" value.")); + pStream->HwCfgRuntime.uIrq = pStream->HwCfgDefault.uIrq; + + rc = pHlp->pfnCFGMQueryU8Def(pCfg, "DMA", &pStream->HwCfgDefault.uDmaChanLow, 1); + if (RT_FAILURE(rc)) + return PDMDEV_SET_ERROR(pDevIns, rc, N_("SB16 configuration error: Failed to get the \"DMA\" value")); + if ( 0 != pStream->HwCfgDefault.uDmaChanLow + && 1 != pStream->HwCfgDefault.uDmaChanLow + && 3 != pStream->HwCfgDefault.uDmaChanLow) + return PDMDEV_SET_ERROR(pDevIns, rc, N_("SB16 configuration error: Invalid \"DMA\" value.")); + pStream->HwCfgRuntime.uDmaChanLow = pStream->HwCfgDefault.uDmaChanLow; + + rc = pHlp->pfnCFGMQueryU8Def(pCfg, "DMA16", &pStream->HwCfgDefault.uDmaChanHigh, 5); + if (RT_FAILURE(rc)) + return PDMDEV_SET_ERROR(pDevIns, rc, N_("SB16 configuration error: Failed to get the \"DMA16\" value")); + if ( 5 != pStream->HwCfgDefault.uDmaChanHigh + && 6 != pStream->HwCfgDefault.uDmaChanHigh + && 7 != pStream->HwCfgDefault.uDmaChanHigh) + return PDMDEV_SET_ERROR(pDevIns, rc, N_("SB16 configuration error: Invalid \"DMA16\" value.")); + pStream->HwCfgRuntime.uDmaChanHigh = pStream->HwCfgDefault.uDmaChanHigh; + + rc = pHlp->pfnCFGMQueryPortDef(pCfg, "Port", &pStream->HwCfgDefault.uPort, 0x220); + if (RT_FAILURE(rc)) + return PDMDEV_SET_ERROR(pDevIns, rc, N_("SB16 configuration error: Failed to get the \"Port\" value")); + /* Sanity-check supported SB16 ports. */ + if ( 0x220 != pStream->HwCfgDefault.uPort + && 0x240 != pStream->HwCfgDefault.uPort + && 0x260 != pStream->HwCfgDefault.uPort + && 0x280 != pStream->HwCfgDefault.uPort) + return PDMDEV_SET_ERROR(pDevIns, rc, N_("SB16 configuration error: Invalid \"Port\" value. Did you specify it as a hex value (e.g. 0x220)?")); + pStream->HwCfgRuntime.uPort = pStream->HwCfgDefault.uPort; + + rc = pHlp->pfnCFGMQueryU16Def(pCfg, "Version", &pStream->HwCfgDefault.uVer, 0x0405); + if (RT_FAILURE(rc)) + return PDMDEV_SET_ERROR(pDevIns, rc, N_("SB16 configuration error: Failed to get the \"Version\" value")); + pStream->HwCfgRuntime.uVer = pStream->HwCfgDefault.uVer; + + rc = pHlp->pfnCFGMQueryU16Def(pCfg, "TimerHz", &pStream->uTimerHz, SB16_TIMER_HZ_DEFAULT); + if (RT_FAILURE(rc)) + return PDMDEV_SET_ERROR(pDevIns, rc, N_("SB16 configuration error: failed to read Hertz rate as unsigned integer")); + if (pStream->uTimerHz == 0) + return PDMDEV_SET_ERROR(pDevIns, rc, N_("SB16 configuration error: Hertz rate is invalid")); + if (pStream->uTimerHz > 2048) + return PDMDEV_SET_ERROR(pDevIns, rc, N_("SB16 configuration error: Maximum Hertz rate is 2048")); + + rc = pHlp->pfnCFGMQueryBoolDef(pCfg, "DebugEnabled", &pThis->Dbg.fEnabled, false); + if (RT_FAILURE(rc)) + return PDMDEV_SET_ERROR(pDevIns, rc, + N_("SB16 configuration error: failed to read debugging enabled flag as boolean")); + + rc = pHlp->pfnCFGMQueryStringAllocDef(pCfg, "DebugPathOut", &pThis->Dbg.pszOutPath, NULL); + if (RT_FAILURE(rc)) + return PDMDEV_SET_ERROR(pDevIns, rc, + N_("SB16 configuration error: failed to read debugging output path flag as string")); + + if (pThis->Dbg.fEnabled) + LogRel2(("SB16: Debug output will be saved to '%s'\n", pThis->Dbg.pszOutPath)); + + /* + * Create internal software mixer. + * Must come before we do the device's mixer reset. + */ + rc = AudioMixerCreate("SB16 Mixer", 0 /* uFlags */, &pThis->pMixer); + AssertRCReturn(rc, rc); + + AssertRCReturn(rc, rc); + rc = AudioMixerCreateSink(pThis->pMixer, "PCM Output", + PDMAUDIODIR_OUT, pDevIns, &pThis->pSinkOut); + AssertRCReturn(rc, rc); + + /* + * Create all hardware streams. + * For now we have one stream only, namely the output (playback) stream. + */ + AssertCompile(RT_ELEMENTS(pThis->aStreams) == SB16_MAX_STREAMS); + for (unsigned i = 0; i < SB16_MAX_STREAMS; i++) + { + rc = sb16StreamCreate(pThis, &pThis->aStreams[i], i /* uIdx */); + AssertRCReturn(rc, rc); + } + + /* + * Setup the mixer now that we've got the irq and dma channel numbers. + */ + pThis->mixer_regs[0x80] = magic_of_irq(pStream->HwCfgRuntime.uIrq); + pThis->mixer_regs[0x81] = (1 << pStream->HwCfgRuntime.uDmaChanLow) | (1 << pStream->HwCfgRuntime.uDmaChanHigh); + pThis->mixer_regs[0x82] = 2 << 5; + + /* + * Perform a device reset before we set up the mixer below, + * to have a defined state. This includes the mixer reset + legacy reset. + */ + sb16DevReset(pThis->pDevInsR3); + + /* + * Make sure that the mixer sink(s) have a valid format set. + * + * This is needed in order to make the driver attaching logic working done by Main + * for machine construction. Must come after sb16DevReset(). + */ + PSB16STREAM const pStreamOut = &pThis->aStreams[SB16_IDX_OUT]; + AudioMixerSinkSetFormat(pThis->pSinkOut, &pStreamOut->Cfg.Props, pStreamOut->Cfg.Device.cMsSchedulingHint); + + /* + * Create timers. + */ + rc = PDMDevHlpTimerCreate(pDevIns, TMCLOCK_VIRTUAL, sb16TimerIRQ, pThis, + TMTIMER_FLAGS_DEFAULT_CRIT_SECT | TMTIMER_FLAGS_NO_RING0, "SB16 IRQ", &pThis->hTimerIRQ); + AssertRCReturn(rc, rc); + + static const char * const s_apszNames[] = { "SB16 OUT" }; + AssertCompile(RT_ELEMENTS(s_apszNames) == SB16_MAX_STREAMS); + for (unsigned i = 0; i < SB16_MAX_STREAMS; i++) + { + rc = PDMDevHlpTimerCreate(pDevIns, TMCLOCK_VIRTUAL, sb16TimerIO, &pThis->aStreams[i], + TMTIMER_FLAGS_DEFAULT_CRIT_SECT | TMTIMER_FLAGS_NO_RING0, s_apszNames[i], &pThis->aStreams[i].hTimerIO); + AssertRCReturn(rc, rc); + + pThis->aStreams[i].cTicksTimerIOInterval = PDMDevHlpTimerGetFreq(pDevIns, pThis->aStreams[i].hTimerIO) + / pThis->aStreams[i].uTimerHz; + pThis->aStreams[i].tsTimerIO = PDMDevHlpTimerGet(pDevIns, pThis->aStreams[i].hTimerIO); + } + + /* + * Register I/O and DMA. + */ + static const IOMIOPORTDESC s_aAllDescs[] = + { + { "FM Music Status Port", "FM Music Register Address Port", NULL, NULL }, // 00h + { NULL, "FM Music Data Port", NULL, NULL }, // 01h + { "Advanced FM Music Status Port", "Advanced FM Music Register Address Port", NULL, NULL }, // 02h + { NULL, "Advanced FM Music Data Port", NULL, NULL }, // 03h + { NULL, "Mixer chip Register Address Port", NULL, NULL }, // 04h + { "Mixer chip Data Port", NULL, NULL, NULL }, // 05h + { NULL, "DSP Reset", NULL, NULL }, // 06h + { "Unused7", "Unused7", NULL, NULL }, // 07h + { "FM Music Status Port", "FM Music Register Port", NULL, NULL }, // 08h + { NULL, "FM Music Data Port", NULL, NULL }, // 09h + { "DSP Read Data Port", NULL, NULL, NULL }, // 0Ah + { "UnusedB", "UnusedB", NULL, NULL }, // 0Bh + { "DSP Write-Buffer Status", "DSP Write Command/Data", NULL, NULL }, // 0Ch + { "UnusedD", "UnusedD", NULL, NULL }, // 0Dh + { "DSP Read-Buffer Status", NULL, NULL, NULL }, // 0Eh + { "IRQ16ACK", NULL, NULL, NULL }, // 0Fh + { "CD-ROM Data Register", "CD-ROM Command Register", NULL, NULL }, // 10h + { "CD-ROM Status Register", NULL, NULL, NULL }, // 11h + { NULL, "CD-ROM Reset Register", NULL, NULL }, // 12h + { NULL, "CD-ROM Enable Register", NULL, NULL }, // 13h + { NULL, NULL, NULL, NULL }, + }; + + rc = PDMDevHlpIoPortCreateAndMap(pDevIns, pStream->HwCfgRuntime.uPort + 0x04 /*uPort*/, 2 /*cPorts*/, + sb16IoPortMixerWrite, sb16IoPortMixerRead, + "SB16 - Mixer", &s_aAllDescs[4], &pThis->hIoPortsMixer); + AssertRCReturn(rc, rc); + rc = PDMDevHlpIoPortCreateAndMap(pDevIns, pStream->HwCfgRuntime.uPort + 0x06 /*uPort*/, 10 /*cPorts*/, + sb16IoPortDspWrite, sb16IoPortDspRead, + "SB16 - DSP", &s_aAllDescs[6], &pThis->hIoPortsDsp); + AssertRCReturn(rc, rc); + + rc = PDMDevHlpDMARegister(pDevIns, pStream->HwCfgRuntime.uDmaChanHigh, sb16DMARead, &pThis->aStreams[SB16_IDX_OUT] /* pvUser */); + AssertRCReturn(rc, rc); + rc = PDMDevHlpDMARegister(pDevIns, pStream->HwCfgRuntime.uDmaChanLow, sb16DMARead, &pThis->aStreams[SB16_IDX_OUT] /* pvUser */); + AssertRCReturn(rc, rc); + + /* + * Register Saved state. + */ + rc = PDMDevHlpSSMRegister3(pDevIns, SB16_SAVE_STATE_VERSION, sizeof(SB16STATE), sb16LiveExec, sb16SaveExec, sb16LoadExec); + AssertRCReturn(rc, rc); + + LogRel2(("SB16: Using port %#x, DMA%RU8, IRQ%RU8\n", + pStream->HwCfgRuntime.uPort, pStream->HwCfgRuntime.uDmaChanLow, pStream->HwCfgRuntime.uIrq)); + + /* + * Attach drivers. We ASSUME they are configured consecutively without any + * gaps, so we stop when we hit the first LUN w/o a driver configured. + */ + for (unsigned iLun = 0; ; iLun++) + { + AssertBreak(iLun < UINT8_MAX); + LogFunc(("Trying to attach driver for LUN#%u ...\n", iLun)); + rc = sb16AttachInternal(pThis, iLun, NULL /* ppDrv */); + if (rc == VERR_PDM_NO_ATTACHED_DRIVER) + { + LogFunc(("cLUNs=%u\n", iLun)); + break; + } + AssertLogRelMsgReturn(RT_SUCCESS(rc), ("LUN#%u: rc=%Rrc\n", iLun, rc), rc); + } + + /* + * Register statistics. + */ +# ifdef VBOX_WITH_STATISTICS + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatTimerIO, STAMTYPE_PROFILE, "Timer", STAMUNIT_TICKS_PER_CALL, "Profiling sb16TimerIO."); + PDMDevHlpSTAMRegister(pDevIns, &pThis->StatBytesRead, STAMTYPE_COUNTER, "BytesRead", STAMUNIT_BYTES, "Bytes read from SB16 emulation."); +# endif + for (unsigned idxStream = 0; idxStream < RT_ELEMENTS(pThis->aStreams); idxStream++) + { + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStreams[idxStream].State.offRead, STAMTYPE_U64, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "Virtual internal buffer read position.", "Stream%u/offRead", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStreams[idxStream].State.offWrite, STAMTYPE_U64, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "Virtual internal buffer write position.", "Stream%u/offWrite", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStreams[idxStream].State.StatDmaBufSize, STAMTYPE_U32, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "Size of the internal DMA buffer.", "Stream%u/DMABufSize", idxStream); + PDMDevHlpSTAMRegisterF(pDevIns, &pThis->aStreams[idxStream].State.StatDmaBufUsed, STAMTYPE_U32, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "Number of bytes used in the internal DMA buffer.", "Stream%u/DMABufUsed", idxStream); + } + + /* + * Debug info items. + */ + //PDMDevHlpDBGFInfoRegister(pDevIns, "sb16", "SB16 registers. (sb16 [register case-insensitive])", sb16DbgInfo); + //PDMDevHlpDBGFInfoRegister(pDevIns, "sb16stream", "SB16 stream info. (sb16stream [stream number])", sb16DbgInfoStream); + PDMDevHlpDBGFInfoRegister(pDevIns, "sb16mixer", "SB16 mixer state.", sb16DbgInfoMixer); + + return VINF_SUCCESS; +} + +const PDMDEVREG g_DeviceSB16 = +{ + /* .u32Version = */ PDM_DEVREG_VERSION, + /* .uReserved0 = */ 0, + /* .szName = */ "sb16", + /* .fFlags = */ PDM_DEVREG_FLAGS_DEFAULT_BITS | PDM_DEVREG_FLAGS_NEW_STYLE + | PDM_DEVREG_FLAGS_FIRST_POWEROFF_NOTIFICATION /* stream clearnup with working drivers */, + /* .fClass = */ PDM_DEVREG_CLASS_AUDIO, + /* .cMaxInstances = */ 1, + /* .uSharedVersion = */ 42, + /* .cbInstanceShared = */ sizeof(SB16STATE), + /* .cbInstanceCC = */ 0, + /* .cbInstanceRC = */ 0, + /* .cMaxPciDevices = */ 0, + /* .cMaxMsixVectors = */ 0, + /* .pszDescription = */ "Sound Blaster 16 Controller", +#if defined(IN_RING3) + /* .pszRCMod = */ "", + /* .pszR0Mod = */ "", + /* .pfnConstruct = */ sb16Construct, + /* .pfnDestruct = */ sb16Destruct, + /* .pfnRelocate = */ NULL, + /* .pfnMemSetup = */ NULL, + /* .pfnPowerOn = */ NULL, + /* .pfnReset = */ sb16DevReset, + /* .pfnSuspend = */ NULL, + /* .pfnResume = */ NULL, + /* .pfnAttach = */ sb16Attach, + /* .pfnDetach = */ sb16Detach, + /* .pfnQueryInterface = */ NULL, + /* .pfnInitComplete = */ NULL, + /* .pfnPowerOff = */ sb16PowerOff, + /* .pfnSoftReset = */ NULL, + /* .pfnReserved0 = */ NULL, + /* .pfnReserved1 = */ NULL, + /* .pfnReserved2 = */ NULL, + /* .pfnReserved3 = */ NULL, + /* .pfnReserved4 = */ NULL, + /* .pfnReserved5 = */ NULL, + /* .pfnReserved6 = */ NULL, + /* .pfnReserved7 = */ NULL, +#elif defined(IN_RING0) + /* .pfnEarlyConstruct = */ NULL, + /* .pfnConstruct = */ NULL, + /* .pfnDestruct = */ NULL, + /* .pfnFinalDestruct = */ NULL, + /* .pfnRequest = */ NULL, + /* .pfnReserved0 = */ NULL, + /* .pfnReserved1 = */ NULL, + /* .pfnReserved2 = */ NULL, + /* .pfnReserved3 = */ NULL, + /* .pfnReserved4 = */ NULL, + /* .pfnReserved5 = */ NULL, + /* .pfnReserved6 = */ NULL, + /* .pfnReserved7 = */ NULL, +#elif defined(IN_RC) + /* .pfnConstruct = */ NULL, + /* .pfnReserved0 = */ NULL, + /* .pfnReserved1 = */ NULL, + /* .pfnReserved2 = */ NULL, + /* .pfnReserved3 = */ NULL, + /* .pfnReserved4 = */ NULL, + /* .pfnReserved5 = */ NULL, + /* .pfnReserved6 = */ NULL, + /* .pfnReserved7 = */ NULL, +#else +# error "Not in IN_RING3, IN_RING0 or IN_RC!" +#endif + /* .u32VersionEnd = */ PDM_DEVREG_VERSION +}; + diff --git a/src/VBox/Devices/Audio/DrvAudio.cpp b/src/VBox/Devices/Audio/DrvAudio.cpp new file mode 100644 index 00000000..c1ad8087 --- /dev/null +++ b/src/VBox/Devices/Audio/DrvAudio.cpp @@ -0,0 +1,4930 @@ +/* $Id: DrvAudio.cpp $ */ +/** @file + * Intermediate audio driver - Connects the audio device emulation with the host backend. + */ + +/* + * Copyright (C) 2006-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 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_DRV_AUDIO +#include <VBox/log.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 <VBox/vmm/pdmaudiohostenuminline.h> + +#include <iprt/alloc.h> +#include <iprt/asm-math.h> +#include <iprt/assert.h> +#include <iprt/circbuf.h> +#include <iprt/req.h> +#include <iprt/string.h> +#include <iprt/thread.h> +#include <iprt/uuid.h> + +#include "VBoxDD.h" + +#include <ctype.h> +#include <stdlib.h> + +#include "AudioHlp.h" +#include "AudioMixBuffer.h" + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +/** @name PDMAUDIOSTREAM_STS_XXX - Used internally by DRVAUDIOSTREAM::fStatus. + * @{ */ +/** No flags being set. */ +#define PDMAUDIOSTREAM_STS_NONE UINT32_C(0) +/** Set if the stream is enabled, clear if disabled. */ +#define PDMAUDIOSTREAM_STS_ENABLED RT_BIT_32(0) +/** Set if the stream is paused. + * Requires the ENABLED status to be set when used. */ +#define PDMAUDIOSTREAM_STS_PAUSED RT_BIT_32(1) +/** Output only: Set when the stream is draining. + * Requires the ENABLED status to be set when used. */ +#define PDMAUDIOSTREAM_STS_PENDING_DISABLE RT_BIT_32(2) + +/** Set if the backend for the stream has been created. + * + * This is generally always set after stream creation, but + * can be cleared if the re-initialization of the stream fails later on. + * Asynchronous init may still be incomplete, see + * PDMAUDIOSTREAM_STS_BACKEND_READY. */ +#define PDMAUDIOSTREAM_STS_BACKEND_CREATED RT_BIT_32(3) +/** The backend is ready (PDMIHOSTAUDIO::pfnStreamInitAsync is done). + * Requires the BACKEND_CREATED status to be set. */ +#define PDMAUDIOSTREAM_STS_BACKEND_READY RT_BIT_32(4) +/** Set if the stream needs to be re-initialized by the device (i.e. call + * PDMIAUDIOCONNECTOR::pfnStreamReInit). (The other status bits are preserved + * and are worked as normal while in this state, so that the stream can + * resume operation where it left off.) */ +#define PDMAUDIOSTREAM_STS_NEED_REINIT RT_BIT_32(5) +/** Validation mask for PDMIAUDIOCONNECTOR. */ +#define PDMAUDIOSTREAM_STS_VALID_MASK UINT32_C(0x0000003f) +/** Asserts the validity of the given stream status mask for PDMIAUDIOCONNECTOR. */ +#define PDMAUDIOSTREAM_STS_ASSERT_VALID(a_fStreamStatus) do { \ + AssertMsg(!((a_fStreamStatus) & ~PDMAUDIOSTREAM_STS_VALID_MASK), ("%#x\n", (a_fStreamStatus))); \ + Assert(!((a_fStreamStatus) & PDMAUDIOSTREAM_STS_PAUSED) || ((a_fStreamStatus) & PDMAUDIOSTREAM_STS_ENABLED)); \ + Assert(!((a_fStreamStatus) & PDMAUDIOSTREAM_STS_PENDING_DISABLE) || ((a_fStreamStatus) & PDMAUDIOSTREAM_STS_ENABLED)); \ + Assert(!((a_fStreamStatus) & PDMAUDIOSTREAM_STS_BACKEND_READY) || ((a_fStreamStatus) & PDMAUDIOSTREAM_STS_BACKEND_CREATED)); \ + } while (0) + +/** @} */ + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +/** + * Audio stream context. + * + * Needed for separating data from the guest and host side (per stream). + */ +typedef struct DRVAUDIOSTREAMCTX +{ + /** The stream's audio configuration. */ + PDMAUDIOSTREAMCFG Cfg; +} DRVAUDIOSTREAMCTX; + +/** + * Capture state of a stream wrt backend. + */ +typedef enum DRVAUDIOCAPTURESTATE +{ + /** Invalid zero value. */ + DRVAUDIOCAPTURESTATE_INVALID = 0, + /** No capturing or pre-buffering. */ + DRVAUDIOCAPTURESTATE_NO_CAPTURE, + /** Regular capturing. */ + DRVAUDIOCAPTURESTATE_CAPTURING, + /** Returning silence till the backend buffer has reched the configured + * pre-buffering level. */ + DRVAUDIOCAPTURESTATE_PREBUF, + /** End of valid values. */ + DRVAUDIOCAPTURESTATE_END +} DRVAUDIOCAPTURESTATE; + +/** + * Play state of a stream wrt backend. + */ +typedef enum DRVAUDIOPLAYSTATE +{ + /** Invalid zero value. */ + DRVAUDIOPLAYSTATE_INVALID = 0, + /** No playback or pre-buffering. */ + DRVAUDIOPLAYSTATE_NOPLAY, + /** Playing w/o any prebuffering. */ + DRVAUDIOPLAYSTATE_PLAY, + /** Parallel pre-buffering prior to a device switch (i.e. we're outputting to + * the old device and pre-buffering the same data in parallel). */ + DRVAUDIOPLAYSTATE_PLAY_PREBUF, + /** Initial pre-buffering or the pre-buffering for a device switch (if it + * the device setup took less time than filling up the pre-buffer). */ + DRVAUDIOPLAYSTATE_PREBUF, + /** The device initialization is taking too long, pre-buffering wraps around + * and drops samples. */ + DRVAUDIOPLAYSTATE_PREBUF_OVERDUE, + /** Same as play-prebuf, but we don't have a working output device any more. */ + DRVAUDIOPLAYSTATE_PREBUF_SWITCHING, + /** Working on committing the pre-buffered data. + * We'll typically leave this state immediately and go to PLAY, however if + * the backend cannot handle all the pre-buffered data at once, we'll stay + * here till it does. */ + DRVAUDIOPLAYSTATE_PREBUF_COMMITTING, + /** End of valid values. */ + DRVAUDIOPLAYSTATE_END +} DRVAUDIOPLAYSTATE; + + +/** + * Extended stream structure. + */ +typedef struct DRVAUDIOSTREAM +{ + /** The publicly visible bit. */ + PDMAUDIOSTREAM Core; + + /** Just an extra magic to verify that we allocated the stream rather than some + * faked up stuff from the device (DRVAUDIOSTREAM_MAGIC). */ + uintptr_t uMagic; + + /** List entry in DRVAUDIO::LstStreams. */ + RTLISTNODE ListEntry; + + /** Number of references to this stream. + * Only can be destroyed when the reference count reaches 0. */ + uint32_t volatile cRefs; + /** Stream status - PDMAUDIOSTREAM_STS_XXX. */ + uint32_t fStatus; + + /** Data to backend-specific stream data. + * This data block will be casted by the backend to access its backend-dependent data. + * + * That way the backends do not have access to the audio connector's data. */ + PPDMAUDIOBACKENDSTREAM pBackend; + + /** Set if pfnStreamCreate returned VINF_AUDIO_STREAM_ASYNC_INIT_NEEDED. */ + bool fNeedAsyncInit; + /** The fImmediate parameter value for pfnStreamDestroy. */ + bool fDestroyImmediate; + bool afPadding[2]; + + /** Number of (re-)tries while re-initializing the stream. */ + uint32_t cTriesReInit; + + /** The last backend state we saw. + * This is used to detect state changes (for what that is worth). */ + PDMHOSTAUDIOSTREAMSTATE enmLastBackendState; + + /** The pre-buffering threshold expressed in bytes. */ + uint32_t cbPreBufThreshold; + + /** The pfnStreamInitAsync request handle. */ + PRTREQ hReqInitAsync; + + /** The nanosecond timestamp when the stream was started. */ + uint64_t nsStarted; + /** Internal stream position (as per pfnStreamPlay/pfnStreamCapture). */ + uint64_t offInternal; + + /** Timestamp (in ns) since last trying to re-initialize. + * Might be 0 if has not been tried yet. */ + uint64_t nsLastReInit; + /** Timestamp (in ns) since last iteration. */ + uint64_t nsLastIterated; + /** Timestamp (in ns) since last playback / capture. */ + uint64_t nsLastPlayedCaptured; + /** Timestamp (in ns) since last read (input streams) or + * write (output streams). */ + uint64_t nsLastReadWritten; + + + /** Union for input/output specifics depending on enmDir. */ + union + { + /** + * The specifics for an audio input stream. + */ + struct + { + /** The capture state. */ + DRVAUDIOCAPTURESTATE enmCaptureState; + + struct + { + /** File for writing non-interleaved captures. */ + PAUDIOHLPFILE pFileCapture; + } Dbg; + struct + { + uint32_t cbBackendReadableBefore; + uint32_t cbBackendReadableAfter; +#ifdef VBOX_WITH_STATISTICS + STAMPROFILE ProfCapture; + STAMPROFILE ProfGetReadable; + STAMPROFILE ProfGetReadableBytes; +#endif + } Stats; + } In; + + /** + * The specifics for an audio output stream. + */ + struct + { + /** Space for pre-buffering. */ + uint8_t *pbPreBuf; + /** The size of the pre-buffer allocation (in bytes). */ + uint32_t cbPreBufAlloc; + /** The current pre-buffering read offset. */ + uint32_t offPreBuf; + /** Number of bytes we've pre-buffered. */ + uint32_t cbPreBuffered; + /** The play state. */ + DRVAUDIOPLAYSTATE enmPlayState; + + struct + { + /** File for writing stream playback. */ + PAUDIOHLPFILE pFilePlay; + } Dbg; + struct + { + uint32_t cbBackendWritableBefore; + uint32_t cbBackendWritableAfter; +#ifdef VBOX_WITH_STATISTICS + STAMPROFILE ProfPlay; + STAMPROFILE ProfGetWritable; + STAMPROFILE ProfGetWritableBytes; +#endif + } Stats; + } Out; + } RT_UNION_NM(u); +#ifdef VBOX_WITH_STATISTICS + STAMPROFILE StatProfGetState; + STAMPROFILE StatXfer; +#endif +} DRVAUDIOSTREAM; +/** Pointer to an extended stream structure. */ +typedef DRVAUDIOSTREAM *PDRVAUDIOSTREAM; + +/** Value for DRVAUDIOSTREAM::uMagic (Johann Sebastian Bach). */ +#define DRVAUDIOSTREAM_MAGIC UINT32_C(0x16850331) +/** Value for DRVAUDIOSTREAM::uMagic after destruction */ +#define DRVAUDIOSTREAM_MAGIC_DEAD UINT32_C(0x17500728) + + +/** + * Audio driver configuration data, tweakable via CFGM. + */ +typedef struct DRVAUDIOCFG +{ + /** PCM properties to use. */ + PDMAUDIOPCMPROPS Props; + /** Whether using signed sample data or not. + * Needed in order to know whether there is a custom value set in CFGM or not. + * By default set to UINT8_MAX if not set to a custom value. */ + uint8_t uSigned; + /** Whether swapping endianess of sample data or not. + * Needed in order to know whether there is a custom value set in CFGM or not. + * By default set to UINT8_MAX if not set to a custom value. */ + uint8_t uSwapEndian; + /** Configures the period size (in ms). + * This value reflects the time in between each hardware interrupt on the + * backend (host) side. */ + uint32_t uPeriodSizeMs; + /** Configures the (ring) buffer size (in ms). Often is a multiple of uPeriodMs. */ + uint32_t uBufferSizeMs; + /** Configures the pre-buffering size (in ms). + * Time needed in buffer before the stream becomes active (pre buffering). + * The bigger this value is, the more latency for the stream will occur. + * Set to 0 to disable pre-buffering completely. + * By default set to UINT32_MAX if not set to a custom value. */ + uint32_t uPreBufSizeMs; + /** The driver's debugging configuration. */ + struct + { + /** Whether audio debugging is enabled or not. */ + bool fEnabled; + /** Where to store the debugging files. */ + char szPathOut[RTPATH_MAX]; + } Dbg; +} DRVAUDIOCFG; +/** Pointer to tweakable audio configuration. */ +typedef DRVAUDIOCFG *PDRVAUDIOCFG; +/** Pointer to const tweakable audio configuration. */ +typedef DRVAUDIOCFG const *PCDRVAUDIOCFG; + + +/** + * Audio driver instance data. + * + * @implements PDMIAUDIOCONNECTOR + */ +typedef struct DRVAUDIO +{ + /** Read/Write critical section for guarding changes to pHostDrvAudio and + * BackendCfg during deteach/attach. Mostly taken in shared mode. + * @note Locking order: Must be entered after CritSectGlobals. + * @note Locking order: Must be entered after PDMAUDIOSTREAM::CritSect. */ + RTCRITSECTRW CritSectHotPlug; + /** Critical section for protecting: + * - LstStreams + * - cStreams + * - In.fEnabled + * - In.cStreamsFree + * - Out.fEnabled + * - Out.cStreamsFree + * @note Locking order: Must be entered before PDMAUDIOSTREAM::CritSect. + * @note Locking order: Must be entered before CritSectHotPlug. */ + RTCRITSECTRW CritSectGlobals; + /** List of audio streams (DRVAUDIOSTREAM). */ + RTLISTANCHOR LstStreams; + /** Number of streams in the list. */ + size_t cStreams; + struct + { + /** Whether this driver's input streams are enabled or not. + * This flag overrides all the attached stream statuses. */ + bool fEnabled; + /** Max. number of free input streams. + * UINT32_MAX for unlimited streams. */ + uint32_t cStreamsFree; + } In; + struct + { + /** Whether this driver's output streams are enabled or not. + * This flag overrides all the attached stream statuses. */ + bool fEnabled; + /** Max. number of free output streams. + * UINT32_MAX for unlimited streams. */ + uint32_t cStreamsFree; + } Out; + + /** Audio configuration settings retrieved from the backend. + * The szName field is used for the DriverName config value till we get the + * authoritative name from the backend (only for logging). */ + PDMAUDIOBACKENDCFG BackendCfg; + /** Our audio connector interface. */ + PDMIAUDIOCONNECTOR IAudioConnector; + /** Interface used by the host backend. */ + PDMIHOSTAUDIOPORT IHostAudioPort; + /** Pointer to the driver instance. */ + PPDMDRVINS pDrvIns; + /** Pointer to audio driver below us. */ + PPDMIHOSTAUDIO pHostDrvAudio; + + /** Request pool if the backend needs it for async stream creation. */ + RTREQPOOL hReqPool; + +#ifdef VBOX_WITH_AUDIO_ENUM + /** Handle to the timer for delayed re-enumeration of backend devices. */ + TMTIMERHANDLE hEnumTimer; + /** Unique name for the the disable-iteration timer. */ + char szEnumTimerName[24]; +#endif + + /** Input audio configuration values (static). */ + DRVAUDIOCFG CfgIn; + /** Output audio configuration values (static). */ + DRVAUDIOCFG CfgOut; + + STAMCOUNTER StatTotalStreamsCreated; +} DRVAUDIO; +/** Pointer to the instance data of an audio driver. */ +typedef DRVAUDIO *PDRVAUDIO; +/** Pointer to const instance data of an audio driver. */ +typedef DRVAUDIO const *PCDRVAUDIO; + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ +static int drvAudioStreamControlInternalBackend(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx, PDMAUDIOSTREAMCMD enmStreamCmd); +static int drvAudioStreamControlInternal(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx, PDMAUDIOSTREAMCMD enmStreamCmd); +static int drvAudioStreamUninitInternal(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx); +static uint32_t drvAudioStreamRetainInternal(PDRVAUDIOSTREAM pStreamEx); +static uint32_t drvAudioStreamReleaseInternal(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx, bool fMayDestroy); +static void drvAudioStreamResetInternal(PDRVAUDIOSTREAM pStreamEx); + + +/** Buffer size for drvAudioStreamStatusToStr. */ +# define DRVAUDIO_STATUS_STR_MAX sizeof("BACKEND_CREATED BACKEND_READY ENABLED PAUSED PENDING_DISABLED NEED_REINIT 0x12345678") + +/** + * Converts an audio stream status to a string. + * + * @returns pszDst + * @param pszDst Buffer to convert into, at least minimum size is + * DRVAUDIO_STATUS_STR_MAX. + * @param fStatus Stream status flags to convert. + */ +static const char *drvAudioStreamStatusToStr(char pszDst[DRVAUDIO_STATUS_STR_MAX], uint32_t fStatus) +{ + static const struct + { + const char *pszMnemonic; + uint32_t cchMnemnonic; + uint32_t fFlag; + } s_aFlags[] = + { + { RT_STR_TUPLE("BACKEND_CREATED "), PDMAUDIOSTREAM_STS_BACKEND_CREATED }, + { RT_STR_TUPLE("BACKEND_READY "), PDMAUDIOSTREAM_STS_BACKEND_READY }, + { RT_STR_TUPLE("ENABLED "), PDMAUDIOSTREAM_STS_ENABLED }, + { RT_STR_TUPLE("PAUSED "), PDMAUDIOSTREAM_STS_PAUSED }, + { RT_STR_TUPLE("PENDING_DISABLE "), PDMAUDIOSTREAM_STS_PENDING_DISABLE }, + { RT_STR_TUPLE("NEED_REINIT "), PDMAUDIOSTREAM_STS_NEED_REINIT }, + }; + if (!fStatus) + strcpy(pszDst, "NONE"); + else + { + char *psz = pszDst; + for (size_t i = 0; i < RT_ELEMENTS(s_aFlags); i++) + if (fStatus & s_aFlags[i].fFlag) + { + memcpy(psz, s_aFlags[i].pszMnemonic, s_aFlags[i].cchMnemnonic); + psz += s_aFlags[i].cchMnemnonic; + fStatus &= ~s_aFlags[i].fFlag; + if (!fStatus) + break; + } + if (fStatus == 0) + psz[-1] = '\0'; + else + psz += RTStrPrintf(psz, DRVAUDIO_STATUS_STR_MAX - (psz - pszDst), "%#x", fStatus); + Assert((uintptr_t)(psz - pszDst) <= DRVAUDIO_STATUS_STR_MAX); + } + return pszDst; +} + + +/** + * Get play state name string. + */ +static const char *drvAudioPlayStateName(DRVAUDIOPLAYSTATE enmState) +{ + switch (enmState) + { + case DRVAUDIOPLAYSTATE_INVALID: return "INVALID"; + case DRVAUDIOPLAYSTATE_NOPLAY: return "NOPLAY"; + case DRVAUDIOPLAYSTATE_PLAY: return "PLAY"; + case DRVAUDIOPLAYSTATE_PLAY_PREBUF: return "PLAY_PREBUF"; + case DRVAUDIOPLAYSTATE_PREBUF: return "PREBUF"; + case DRVAUDIOPLAYSTATE_PREBUF_OVERDUE: return "PREBUF_OVERDUE"; + case DRVAUDIOPLAYSTATE_PREBUF_SWITCHING: return "PREBUF_SWITCHING"; + case DRVAUDIOPLAYSTATE_PREBUF_COMMITTING: return "PREBUF_COMMITTING"; + case DRVAUDIOPLAYSTATE_END: + break; + } + return "BAD"; +} + +#ifdef LOG_ENABLED +/** + * Get capture state name string. + */ +static const char *drvAudioCaptureStateName(DRVAUDIOCAPTURESTATE enmState) +{ + switch (enmState) + { + case DRVAUDIOCAPTURESTATE_INVALID: return "INVALID"; + case DRVAUDIOCAPTURESTATE_NO_CAPTURE: return "NO_CAPTURE"; + case DRVAUDIOCAPTURESTATE_CAPTURING: return "CAPTURING"; + case DRVAUDIOCAPTURESTATE_PREBUF: return "PREBUF"; + case DRVAUDIOCAPTURESTATE_END: + break; + } + return "BAD"; +} +#endif + +/** + * Checks if the stream status is one that can be read from. + * + * @returns @c true if ready to be read from, @c false if not. + * @param fStatus Stream status to evaluate, PDMAUDIOSTREAM_STS_XXX. + * @note Not for backend statuses (use PDMAudioStrmStatusBackendCanRead)! + */ +DECLINLINE(bool) PDMAudioStrmStatusCanRead(uint32_t fStatus) +{ + PDMAUDIOSTREAM_STS_ASSERT_VALID(fStatus); + AssertReturn(!(fStatus & ~PDMAUDIOSTREAM_STS_VALID_MASK), false); + return (fStatus & ( PDMAUDIOSTREAM_STS_BACKEND_CREATED + | PDMAUDIOSTREAM_STS_ENABLED + | PDMAUDIOSTREAM_STS_PAUSED + | PDMAUDIOSTREAM_STS_NEED_REINIT)) + == ( PDMAUDIOSTREAM_STS_BACKEND_CREATED + | PDMAUDIOSTREAM_STS_ENABLED); +} + +/** + * Checks if the stream status is one that can be written to. + * + * @returns @c true if ready to be written to, @c false if not. + * @param fStatus Stream status to evaluate, PDMAUDIOSTREAM_STS_XXX. + * @note Not for backend statuses (use PDMAudioStrmStatusBackendCanWrite)! + */ +DECLINLINE(bool) PDMAudioStrmStatusCanWrite(uint32_t fStatus) +{ + PDMAUDIOSTREAM_STS_ASSERT_VALID(fStatus); + AssertReturn(!(fStatus & ~PDMAUDIOSTREAM_STS_VALID_MASK), false); + return (fStatus & ( PDMAUDIOSTREAM_STS_BACKEND_CREATED + | PDMAUDIOSTREAM_STS_ENABLED + | PDMAUDIOSTREAM_STS_PAUSED + | PDMAUDIOSTREAM_STS_PENDING_DISABLE + | PDMAUDIOSTREAM_STS_NEED_REINIT)) + == ( PDMAUDIOSTREAM_STS_BACKEND_CREATED + | PDMAUDIOSTREAM_STS_ENABLED); +} + +/** + * Checks if the stream status is a ready-to-operate one. + * + * @returns @c true if ready to operate, @c false if not. + * @param fStatus Stream status to evaluate, PDMAUDIOSTREAM_STS_XXX. + * @note Not for backend statuses! + */ +DECLINLINE(bool) PDMAudioStrmStatusIsReady(uint32_t fStatus) +{ + PDMAUDIOSTREAM_STS_ASSERT_VALID(fStatus); + AssertReturn(!(fStatus & ~PDMAUDIOSTREAM_STS_VALID_MASK), false); + return (fStatus & ( PDMAUDIOSTREAM_STS_BACKEND_CREATED + | PDMAUDIOSTREAM_STS_ENABLED + | PDMAUDIOSTREAM_STS_NEED_REINIT)) + == ( PDMAUDIOSTREAM_STS_BACKEND_CREATED + | PDMAUDIOSTREAM_STS_ENABLED); +} + + +/** + * Wrapper around PDMIHOSTAUDIO::pfnStreamGetStatus and checks the result. + * + * @returns A PDMHOSTAUDIOSTREAMSTATE value. + * @param pThis Pointer to the DrvAudio instance data. + * @param pStreamEx The stream to get the backend status for. + */ +DECLINLINE(PDMHOSTAUDIOSTREAMSTATE) drvAudioStreamGetBackendState(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx) +{ + if (pThis->pHostDrvAudio) + { + PDMHOSTAUDIOSTREAMSTATE enmState = pThis->pHostDrvAudio->pfnStreamGetState(pThis->pHostDrvAudio, pStreamEx->pBackend); + Log9Func(("%s: %s\n", pStreamEx->Core.Cfg.szName, PDMHostAudioStreamStateGetName(enmState) )); + Assert( enmState > PDMHOSTAUDIOSTREAMSTATE_INVALID + && enmState < PDMHOSTAUDIOSTREAMSTATE_END + && (enmState != PDMHOSTAUDIOSTREAMSTATE_DRAINING || pStreamEx->Core.Cfg.enmDir == PDMAUDIODIR_OUT)); + return enmState; + } + Log9Func(("%s: not-working\n", pStreamEx->Core.Cfg.szName)); + return PDMHOSTAUDIOSTREAMSTATE_NOT_WORKING; +} + + +/** + * Worker for drvAudioStreamProcessBackendStateChange that completes draining. + */ +DECLINLINE(void) drvAudioStreamProcessBackendStateChangeWasDraining(PDRVAUDIOSTREAM pStreamEx) +{ + Log(("drvAudioStreamProcessBackendStateChange: Stream '%s': Done draining - disabling stream.\n", pStreamEx->Core.Cfg.szName)); + pStreamEx->fStatus &= ~(PDMAUDIOSTREAM_STS_ENABLED | PDMAUDIOSTREAM_STS_PENDING_DISABLE); + drvAudioStreamResetInternal(pStreamEx); +} + + +/** + * Processes backend state change. + * + * @returns the new state value. + */ +static PDMHOSTAUDIOSTREAMSTATE drvAudioStreamProcessBackendStateChange(PDRVAUDIOSTREAM pStreamEx, + PDMHOSTAUDIOSTREAMSTATE enmNewState, + PDMHOSTAUDIOSTREAMSTATE enmOldState) +{ + PDMAUDIODIR const enmDir = pStreamEx->Core.Cfg.enmDir; +#ifdef LOG_ENABLED + DRVAUDIOPLAYSTATE const enmPlayState = enmDir == PDMAUDIODIR_OUT + ? pStreamEx->Out.enmPlayState : DRVAUDIOPLAYSTATE_INVALID; + DRVAUDIOCAPTURESTATE const enmCaptureState = enmDir == PDMAUDIODIR_IN + ? pStreamEx->In.enmCaptureState : DRVAUDIOCAPTURESTATE_INVALID; +#endif + Assert(enmNewState != enmOldState); + Assert(enmOldState > PDMHOSTAUDIOSTREAMSTATE_INVALID && enmOldState < PDMHOSTAUDIOSTREAMSTATE_END); + AssertReturn(enmNewState > PDMHOSTAUDIOSTREAMSTATE_INVALID && enmNewState < PDMHOSTAUDIOSTREAMSTATE_END, enmOldState); + + /* + * Figure out what happend and how that reflects on the playback state and stuff. + */ + switch (enmNewState) + { + case PDMHOSTAUDIOSTREAMSTATE_INITIALIZING: + /* Guess we're switching device. Nothing to do because the backend will tell us, right? */ + break; + + case PDMHOSTAUDIOSTREAMSTATE_NOT_WORKING: + case PDMHOSTAUDIOSTREAMSTATE_INACTIVE: + /* The stream has stopped working or is inactive. Switch stop any draining & to noplay mode. */ + if (pStreamEx->fStatus & PDMAUDIOSTREAM_STS_PENDING_DISABLE) + drvAudioStreamProcessBackendStateChangeWasDraining(pStreamEx); + if (enmDir == PDMAUDIODIR_OUT) + pStreamEx->Out.enmPlayState = DRVAUDIOPLAYSTATE_NOPLAY; + else + pStreamEx->In.enmCaptureState = DRVAUDIOCAPTURESTATE_NO_CAPTURE; + break; + + case PDMHOSTAUDIOSTREAMSTATE_OKAY: + switch (enmOldState) + { + case PDMHOSTAUDIOSTREAMSTATE_INITIALIZING: + /* Should be taken care of elsewhere, so do nothing. */ + break; + + case PDMHOSTAUDIOSTREAMSTATE_NOT_WORKING: + case PDMHOSTAUDIOSTREAMSTATE_INACTIVE: + /* Go back to pre-buffering/playing depending on whether it is enabled + or not, resetting the stream state. */ + drvAudioStreamResetInternal(pStreamEx); + break; + + case PDMHOSTAUDIOSTREAMSTATE_DRAINING: + /* Complete the draining. May race the iterate code. */ + if (pStreamEx->fStatus & PDMAUDIOSTREAM_STS_PENDING_DISABLE) + drvAudioStreamProcessBackendStateChangeWasDraining(pStreamEx); + break; + + /* no default: */ + case PDMHOSTAUDIOSTREAMSTATE_OKAY: /* impossible */ + case PDMHOSTAUDIOSTREAMSTATE_INVALID: + case PDMHOSTAUDIOSTREAMSTATE_END: + case PDMHOSTAUDIOSTREAMSTATE_32BIT_HACK: + break; + } + break; + + case PDMHOSTAUDIOSTREAMSTATE_DRAINING: + /* We do all we need to do when issuing the DRAIN command. */ + Assert(pStreamEx->fStatus & PDMAUDIOSTREAM_STS_PENDING_DISABLE); + break; + + /* no default: */ + case PDMHOSTAUDIOSTREAMSTATE_INVALID: + case PDMHOSTAUDIOSTREAMSTATE_END: + case PDMHOSTAUDIOSTREAMSTATE_32BIT_HACK: + break; + } + + if (enmDir == PDMAUDIODIR_OUT) + LogFunc(("Output stream '%s': %s/%s -> %s/%s\n", pStreamEx->Core.Cfg.szName, + PDMHostAudioStreamStateGetName(enmOldState), drvAudioPlayStateName(enmPlayState), + PDMHostAudioStreamStateGetName(enmNewState), drvAudioPlayStateName(pStreamEx->Out.enmPlayState) )); + else + LogFunc(("Input stream '%s': %s/%s -> %s/%s\n", pStreamEx->Core.Cfg.szName, + PDMHostAudioStreamStateGetName(enmOldState), drvAudioCaptureStateName(enmCaptureState), + PDMHostAudioStreamStateGetName(enmNewState), drvAudioCaptureStateName(pStreamEx->In.enmCaptureState) )); + + pStreamEx->enmLastBackendState = enmNewState; + return enmNewState; +} + + +/** + * This gets the backend state and handles changes compared to + * DRVAUDIOSTREAM::enmLastBackendState (updated). + * + * @returns A PDMHOSTAUDIOSTREAMSTATE value. + * @param pThis Pointer to the DrvAudio instance data. + * @param pStreamEx The stream to get the backend status for. + */ +DECLINLINE(PDMHOSTAUDIOSTREAMSTATE) drvAudioStreamGetBackendStateAndProcessChanges(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx) +{ + PDMHOSTAUDIOSTREAMSTATE const enmBackendState = drvAudioStreamGetBackendState(pThis, pStreamEx); + if (pStreamEx->enmLastBackendState == enmBackendState) + return enmBackendState; + return drvAudioStreamProcessBackendStateChange(pStreamEx, enmBackendState, pStreamEx->enmLastBackendState); +} + + +#ifdef VBOX_WITH_AUDIO_ENUM +/** + * Enumerates all host audio devices. + * + * This functionality might not be implemented by all backends and will return + * VERR_NOT_SUPPORTED if not being supported. + * + * @note Must not hold the driver's critical section! + * + * @returns VBox status code. + * @param pThis Driver instance to be called. + * @param fLog Whether to print the enumerated device to the release log or not. + * @param pDevEnum Where to store the device enumeration. + * + * @remarks This is currently ONLY used for release logging. + */ +static DECLCALLBACK(int) drvAudioDevicesEnumerateInternal(PDRVAUDIO pThis, bool fLog, PPDMAUDIOHOSTENUM pDevEnum) +{ + RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + + int rc; + + /* + * If the backend supports it, do a device enumeration. + */ + if (pThis->pHostDrvAudio->pfnGetDevices) + { + PDMAUDIOHOSTENUM DevEnum; + rc = pThis->pHostDrvAudio->pfnGetDevices(pThis->pHostDrvAudio, &DevEnum); + if (RT_SUCCESS(rc)) + { + if (fLog) + { + LogRel(("Audio: Found %RU16 devices for driver '%s'\n", DevEnum.cDevices, pThis->BackendCfg.szName)); + + PPDMAUDIOHOSTDEV pDev; + RTListForEach(&DevEnum.LstDevices, pDev, PDMAUDIOHOSTDEV, ListEntry) + { + char szFlags[PDMAUDIOHOSTDEV_MAX_FLAGS_STRING_LEN]; + LogRel(("Audio: Device '%s':\n" + "Audio: ID = %s\n" + "Audio: Usage = %s\n" + "Audio: Flags = %s\n" + "Audio: Input channels = %RU8\n" + "Audio: Output channels = %RU8\n", + pDev->pszName, pDev->pszId ? pDev->pszId : "", + PDMAudioDirGetName(pDev->enmUsage), PDMAudioHostDevFlagsToString(szFlags, pDev->fFlags), + pDev->cMaxInputChannels, pDev->cMaxOutputChannels)); + } + } + + if (pDevEnum) + rc = PDMAudioHostEnumCopy(pDevEnum, &DevEnum, PDMAUDIODIR_INVALID /*all*/, true /*fOnlyCoreData*/); + + PDMAudioHostEnumDelete(&DevEnum); + } + else + { + if (fLog) + LogRel(("Audio: Device enumeration for driver '%s' failed with %Rrc\n", pThis->BackendCfg.szName, rc)); + /* Not fatal. */ + } + } + else + { + rc = VERR_NOT_SUPPORTED; + + if (fLog) + LogRel2(("Audio: Host driver '%s' does not support audio device enumeration, skipping\n", pThis->BackendCfg.szName)); + } + + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + LogFunc(("Returning %Rrc\n", rc)); + return rc; +} +#endif /* VBOX_WITH_AUDIO_ENUM */ + + +/********************************************************************************************************************************* +* PDMIAUDIOCONNECTOR * +*********************************************************************************************************************************/ + +/** + * @interface_method_impl{PDMIAUDIOCONNECTOR,pfnEnable} + */ +static DECLCALLBACK(int) drvAudioEnable(PPDMIAUDIOCONNECTOR pInterface, PDMAUDIODIR enmDir, bool fEnable) +{ + PDRVAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVAUDIO, IAudioConnector); + AssertPtr(pThis); + LogFlowFunc(("enmDir=%s fEnable=%d\n", PDMAudioDirGetName(enmDir), fEnable)); + + /* + * Figure which status flag variable is being updated. + */ + bool *pfEnabled; + if (enmDir == PDMAUDIODIR_IN) + pfEnabled = &pThis->In.fEnabled; + else if (enmDir == PDMAUDIODIR_OUT) + pfEnabled = &pThis->Out.fEnabled; + else + AssertFailedReturn(VERR_INVALID_PARAMETER); + + /* + * Grab the driver wide lock and check it. Ignore call if no change. + */ + int rc = RTCritSectRwEnterExcl(&pThis->CritSectGlobals); + AssertRCReturn(rc, rc); + + if (fEnable != *pfEnabled) + { + LogRel(("Audio: %s %s for driver '%s'\n", + fEnable ? "Enabling" : "Disabling", enmDir == PDMAUDIODIR_IN ? "input" : "output", pThis->BackendCfg.szName)); + + /* + * When enabling, we must update flag before calling drvAudioStreamControlInternalBackend. + */ + if (fEnable) + *pfEnabled = true; + + /* + * Update the backend status for the streams in the given direction. + * + * The pThis->Out.fEnable / pThis->In.fEnable status flags only reflect in the + * direction of the backend, drivers and devices above us in the chain does not + * know about this. When disabled playback goes to /dev/null and we capture + * only silence. This means pStreamEx->fStatus holds the nominal status + * and we'll use it to restore the operation. (See also @bugref{9882}.) + */ + PDRVAUDIOSTREAM pStreamEx; + RTListForEach(&pThis->LstStreams, pStreamEx, DRVAUDIOSTREAM, ListEntry) + { + if (pStreamEx->Core.Cfg.enmDir == enmDir) + { + RTCritSectEnter(&pStreamEx->Core.CritSect); + + /* + * When (re-)enabling a stream, clear the disabled warning bit again. + */ + if (fEnable) + pStreamEx->Core.fWarningsShown &= ~PDMAUDIOSTREAM_WARN_FLAGS_DISABLED; + + /* + * We don't need to do anything unless the stream is enabled. + * Paused includes enabled, as does draining, but we only want the former. + */ + uint32_t const fStatus = pStreamEx->fStatus; + if (fStatus & PDMAUDIOSTREAM_STS_ENABLED) + { + const char *pszOperation; + int rc2; + if (fEnable) + { + if (!(fStatus & PDMAUDIOSTREAM_STS_PENDING_DISABLE)) + { + /** @todo r=bird: We need to redo pre-buffering OR switch to + * DRVAUDIOPLAYSTATE_PREBUF_SWITCHING playback mode when disabling + * output streams. The former is preferred if associated with + * reporting the stream as INACTIVE. */ + rc2 = drvAudioStreamControlInternalBackend(pThis, pStreamEx, PDMAUDIOSTREAMCMD_ENABLE); + pszOperation = "enable"; + if (RT_SUCCESS(rc2) && (fStatus & PDMAUDIOSTREAM_STS_PAUSED)) + { + rc2 = drvAudioStreamControlInternalBackend(pThis, pStreamEx, PDMAUDIOSTREAMCMD_PAUSE); + pszOperation = "pause"; + } + } + else + { + rc2 = VINF_SUCCESS; + pszOperation = NULL; + } + } + else + { + rc2 = drvAudioStreamControlInternalBackend(pThis, pStreamEx, PDMAUDIOSTREAMCMD_DISABLE); + pszOperation = "disable"; + } + if (RT_FAILURE(rc2)) + { + LogRel(("Audio: Failed to %s %s stream '%s': %Rrc\n", + pszOperation, enmDir == PDMAUDIODIR_IN ? "input" : "output", pStreamEx->Core.Cfg.szName, rc2)); + if (RT_SUCCESS(rc)) + rc = rc2; /** @todo r=bird: This isn't entirely helpful to the caller since we'll update the status + * regardless of the status code we return. And anyway, there is nothing that can be done + * about individual stream by the caller... */ + } + } + + RTCritSectLeave(&pStreamEx->Core.CritSect); + } + } + + /* + * When disabling, we must update the status flag after the + * drvAudioStreamControlInternalBackend(DISABLE) calls. + */ + *pfEnabled = fEnable; + } + + RTCritSectRwLeaveExcl(&pThis->CritSectGlobals); + LogFlowFuncLeaveRC(rc); + return rc; +} + + +/** + * @interface_method_impl{PDMIAUDIOCONNECTOR,pfnIsEnabled} + */ +static DECLCALLBACK(bool) drvAudioIsEnabled(PPDMIAUDIOCONNECTOR pInterface, PDMAUDIODIR enmDir) +{ + PDRVAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVAUDIO, IAudioConnector); + AssertPtr(pThis); + int rc = RTCritSectRwEnterShared(&pThis->CritSectGlobals); + AssertRCReturn(rc, false); + + bool fEnabled; + if (enmDir == PDMAUDIODIR_IN) + fEnabled = pThis->In.fEnabled; + else if (enmDir == PDMAUDIODIR_OUT) + fEnabled = pThis->Out.fEnabled; + else + AssertFailedStmt(fEnabled = false); + + RTCritSectRwLeaveShared(&pThis->CritSectGlobals); + return fEnabled; +} + + +/** + * @interface_method_impl{PDMIAUDIOCONNECTOR,pfnGetConfig} + */ +static DECLCALLBACK(int) drvAudioGetConfig(PPDMIAUDIOCONNECTOR pInterface, PPDMAUDIOBACKENDCFG pCfg) +{ + PDRVAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVAUDIO, IAudioConnector); + AssertPtr(pThis); + AssertPtrReturn(pCfg, VERR_INVALID_POINTER); + int rc = RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + AssertRCReturn(rc, rc); + + if (pThis->pHostDrvAudio) + rc = pThis->pHostDrvAudio->pfnGetConfig(pThis->pHostDrvAudio, pCfg); + else + rc = VERR_PDM_NO_ATTACHED_DRIVER; + + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + LogFlowFuncLeaveRC(rc); + return rc; +} + + +/** + * @interface_method_impl{PDMIAUDIOCONNECTOR,pfnGetStatus} + */ +static DECLCALLBACK(PDMAUDIOBACKENDSTS) drvAudioGetStatus(PPDMIAUDIOCONNECTOR pInterface, PDMAUDIODIR enmDir) +{ + PDRVAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVAUDIO, IAudioConnector); + AssertPtr(pThis); + int rc = RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + AssertRCReturn(rc, PDMAUDIOBACKENDSTS_UNKNOWN); + + PDMAUDIOBACKENDSTS fBackendStatus; + if (pThis->pHostDrvAudio) + { + if (pThis->pHostDrvAudio->pfnGetStatus) + fBackendStatus = pThis->pHostDrvAudio->pfnGetStatus(pThis->pHostDrvAudio, enmDir); + else + fBackendStatus = PDMAUDIOBACKENDSTS_UNKNOWN; + } + else + fBackendStatus = PDMAUDIOBACKENDSTS_NOT_ATTACHED; + + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + LogFlowFunc(("LEAVE - %#x\n", fBackendStatus)); + return fBackendStatus; +} + + +/** + * Frees an audio stream and its allocated resources. + * + * @param pStreamEx Audio stream to free. After this call the pointer will + * not be valid anymore. + */ +static void drvAudioStreamFree(PDRVAUDIOSTREAM pStreamEx) +{ + if (pStreamEx) + { + LogFunc(("[%s]\n", pStreamEx->Core.Cfg.szName)); + Assert(pStreamEx->Core.uMagic == PDMAUDIOSTREAM_MAGIC); + Assert(pStreamEx->uMagic == DRVAUDIOSTREAM_MAGIC); + + pStreamEx->Core.uMagic = ~PDMAUDIOSTREAM_MAGIC; + pStreamEx->pBackend = NULL; + pStreamEx->uMagic = DRVAUDIOSTREAM_MAGIC_DEAD; + + RTCritSectDelete(&pStreamEx->Core.CritSect); + + RTMemFree(pStreamEx); + } +} + + +/** + * Adjusts the request stream configuration, applying our settings. + * + * This also does some basic validations. + * + * Used by both the stream creation and stream configuration hinting code. + * + * @returns VBox status code. + * @param pThis Pointer to the DrvAudio instance data. + * @param pCfg The configuration that should be adjusted. + * @param pszName Stream name to use when logging warnings and errors. + */ +static int drvAudioStreamAdjustConfig(PCDRVAUDIO pThis, PPDMAUDIOSTREAMCFG pCfg, const char *pszName) +{ + /* Get the right configuration for the stream to be created. */ + PCDRVAUDIOCFG pDrvCfg = pCfg->enmDir == PDMAUDIODIR_IN ? &pThis->CfgIn: &pThis->CfgOut; + + /* Fill in the tweakable parameters into the requested host configuration. + * All parameters in principle can be changed and returned by the backend via the acquired configuration. */ + + /* + * PCM + */ + if (PDMAudioPropsSampleSize(&pDrvCfg->Props) != 0) /* Anything set via custom extra-data? */ + { + PDMAudioPropsSetSampleSize(&pCfg->Props, PDMAudioPropsSampleSize(&pDrvCfg->Props)); + LogRel2(("Audio: Using custom sample size of %RU8 bytes for stream '%s'\n", + PDMAudioPropsSampleSize(&pCfg->Props), pszName)); + } + + if (pDrvCfg->Props.uHz) /* Anything set via custom extra-data? */ + { + pCfg->Props.uHz = pDrvCfg->Props.uHz; + LogRel2(("Audio: Using custom Hz rate %RU32 for stream '%s'\n", pCfg->Props.uHz, pszName)); + } + + if (pDrvCfg->uSigned != UINT8_MAX) /* Anything set via custom extra-data? */ + { + pCfg->Props.fSigned = RT_BOOL(pDrvCfg->uSigned); + LogRel2(("Audio: Using custom %s sample format for stream '%s'\n", + pCfg->Props.fSigned ? "signed" : "unsigned", pszName)); + } + + if (pDrvCfg->uSwapEndian != UINT8_MAX) /* Anything set via custom extra-data? */ + { + pCfg->Props.fSwapEndian = RT_BOOL(pDrvCfg->uSwapEndian); + LogRel2(("Audio: Using custom %s endianess for samples of stream '%s'\n", + pCfg->Props.fSwapEndian ? "swapped" : "original", pszName)); + } + + if (PDMAudioPropsChannels(&pDrvCfg->Props) != 0) /* Anything set via custom extra-data? */ + { + PDMAudioPropsSetChannels(&pCfg->Props, PDMAudioPropsChannels(&pDrvCfg->Props)); + LogRel2(("Audio: Using custom %RU8 channel(s) for stream '%s'\n", PDMAudioPropsChannels(&pDrvCfg->Props), pszName)); + } + + /* Validate PCM properties. */ + if (!AudioHlpPcmPropsAreValidAndSupported(&pCfg->Props)) + { + LogRel(("Audio: Invalid custom PCM properties set for stream '%s', cannot create stream\n", pszName)); + return VERR_INVALID_PARAMETER; + } + + /* + * Buffer size + */ + const char *pszWhat = "device-specific"; + if (pDrvCfg->uBufferSizeMs) + { + pCfg->Backend.cFramesBufferSize = PDMAudioPropsMilliToFrames(&pCfg->Props, pDrvCfg->uBufferSizeMs); + pszWhat = "custom"; + } + + if (!pCfg->Backend.cFramesBufferSize) /* Set default buffer size if nothing explicitly is set. */ + { + pCfg->Backend.cFramesBufferSize = PDMAudioPropsMilliToFrames(&pCfg->Props, 300 /*ms*/); + pszWhat = "default"; + } + + LogRel2(("Audio: Using %s buffer size %RU64 ms / %RU32 frames for stream '%s'\n", + pszWhat, PDMAudioPropsFramesToMilli(&pCfg->Props, pCfg->Backend.cFramesBufferSize), + pCfg->Backend.cFramesBufferSize, pszName)); + + /* + * Period size + */ + pszWhat = "device-specific"; + if (pDrvCfg->uPeriodSizeMs) + { + pCfg->Backend.cFramesPeriod = PDMAudioPropsMilliToFrames(&pCfg->Props, pDrvCfg->uPeriodSizeMs); + pszWhat = "custom"; + } + + if (!pCfg->Backend.cFramesPeriod) /* Set default period size if nothing explicitly is set. */ + { + pCfg->Backend.cFramesPeriod = pCfg->Backend.cFramesBufferSize / 4; + pszWhat = "default"; + } + + if (pCfg->Backend.cFramesPeriod >= pCfg->Backend.cFramesBufferSize / 2) + { + LogRel(("Audio: Warning! Stream '%s': The stream period size (%RU64ms, %s) cannot be more than half the buffer size (%RU64ms)!\n", + pszName, PDMAudioPropsFramesToMilli(&pCfg->Props, pCfg->Backend.cFramesPeriod), pszWhat, + PDMAudioPropsFramesToMilli(&pCfg->Props, pCfg->Backend.cFramesBufferSize))); + pCfg->Backend.cFramesPeriod = pCfg->Backend.cFramesBufferSize / 2; + } + + LogRel2(("Audio: Using %s period size %RU64 ms / %RU32 frames for stream '%s'\n", + pszWhat, PDMAudioPropsFramesToMilli(&pCfg->Props, pCfg->Backend.cFramesPeriod), + pCfg->Backend.cFramesPeriod, pszName)); + + /* + * Pre-buffering size + */ + pszWhat = "device-specific"; + if (pDrvCfg->uPreBufSizeMs != UINT32_MAX) /* Anything set via global / per-VM extra-data? */ + { + pCfg->Backend.cFramesPreBuffering = PDMAudioPropsMilliToFrames(&pCfg->Props, pDrvCfg->uPreBufSizeMs); + pszWhat = "custom"; + } + else /* No, then either use the default or device-specific settings (if any). */ + { + if (pCfg->Backend.cFramesPreBuffering == UINT32_MAX) /* Set default pre-buffering size if nothing explicitly is set. */ + { + /* Pre-buffer 50% for both output & input. Capping both at 200ms. + The 50% reasoning being that we need to have sufficient slack space + in both directions as the guest DMA timer might be delayed by host + scheduling as well as sped up afterwards because of TM catch-up. */ + uint32_t const cFramesMax = PDMAudioPropsMilliToFrames(&pCfg->Props, 200); + pCfg->Backend.cFramesPreBuffering = pCfg->Backend.cFramesBufferSize / 2; + pCfg->Backend.cFramesPreBuffering = RT_MIN(pCfg->Backend.cFramesPreBuffering, cFramesMax); + pszWhat = "default"; + } + } + + if (pCfg->Backend.cFramesPreBuffering >= pCfg->Backend.cFramesBufferSize) + { + LogRel(("Audio: Warning! Stream '%s': Pre-buffering (%RU64ms, %s) cannot equal or exceed the buffer size (%RU64ms)!\n", + pszName, PDMAudioPropsFramesToMilli(&pCfg->Props, pCfg->Backend.cFramesBufferSize), pszWhat, + PDMAudioPropsFramesToMilli(&pCfg->Props, pCfg->Backend.cFramesPreBuffering) )); + pCfg->Backend.cFramesPreBuffering = pCfg->Backend.cFramesBufferSize - 1; + } + + LogRel2(("Audio: Using %s pre-buffering size %RU64 ms / %RU32 frames for stream '%s'\n", + pszWhat, PDMAudioPropsFramesToMilli(&pCfg->Props, pCfg->Backend.cFramesPreBuffering), + pCfg->Backend.cFramesPreBuffering, pszName)); + + return VINF_SUCCESS; +} + + +/** + * Worker thread function for drvAudioStreamConfigHint that's used when + * PDMAUDIOBACKEND_F_ASYNC_HINT is in effect. + */ +static DECLCALLBACK(void) drvAudioStreamConfigHintWorker(PDRVAUDIO pThis, PPDMAUDIOSTREAMCFG pCfg) +{ + LogFlowFunc(("pThis=%p pCfg=%p\n", pThis, pCfg)); + AssertPtrReturnVoid(pCfg); + int rc = RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + AssertRCReturnVoid(rc); + + PPDMIHOSTAUDIO const pHostDrvAudio = pThis->pHostDrvAudio; + if (pHostDrvAudio) + { + AssertPtr(pHostDrvAudio->pfnStreamConfigHint); + if (pHostDrvAudio->pfnStreamConfigHint) + pHostDrvAudio->pfnStreamConfigHint(pHostDrvAudio, pCfg); + } + PDMAudioStrmCfgFree(pCfg); + + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + LogFlowFunc(("returns\n")); +} + + +/** + * @interface_method_impl{PDMIAUDIOCONNECTOR,pfnStreamConfigHint} + */ +static DECLCALLBACK(void) drvAudioStreamConfigHint(PPDMIAUDIOCONNECTOR pInterface, PPDMAUDIOSTREAMCFG pCfg) +{ + PDRVAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVAUDIO, IAudioConnector); + AssertReturnVoid(pCfg->enmDir == PDMAUDIODIR_IN || pCfg->enmDir == PDMAUDIODIR_OUT); + + int rc = RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + AssertRCReturnVoid(rc); + + /* + * Don't do anything unless the backend has a pfnStreamConfigHint method + * and the direction is currently enabled. + */ + if ( pThis->pHostDrvAudio + && pThis->pHostDrvAudio->pfnStreamConfigHint) + { + if (pCfg->enmDir == PDMAUDIODIR_OUT ? pThis->Out.fEnabled : pThis->In.fEnabled) + { + /* + * Adjust the configuration (applying out settings) then call the backend driver. + */ + rc = drvAudioStreamAdjustConfig(pThis, pCfg, pCfg->szName); + AssertLogRelRC(rc); + if (RT_SUCCESS(rc)) + { + rc = VERR_CALLBACK_RETURN; + if (pThis->BackendCfg.fFlags & PDMAUDIOBACKEND_F_ASYNC_HINT) + { + PPDMAUDIOSTREAMCFG pDupCfg = PDMAudioStrmCfgDup(pCfg); + if (pDupCfg) + { + rc = RTReqPoolCallVoidNoWait(pThis->hReqPool, (PFNRT)drvAudioStreamConfigHintWorker, 2, pThis, pDupCfg); + if (RT_SUCCESS(rc)) + LogFlowFunc(("Asynchronous call running on worker thread.\n")); + else + PDMAudioStrmCfgFree(pDupCfg); + } + } + if (RT_FAILURE_NP(rc)) + { + LogFlowFunc(("Doing synchronous call...\n")); + pThis->pHostDrvAudio->pfnStreamConfigHint(pThis->pHostDrvAudio, pCfg); + } + } + } + else + LogFunc(("Ignoring hint because direction is not currently enabled\n")); + } + else + LogFlowFunc(("Ignoring hint because backend has no pfnStreamConfigHint method.\n")); + + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); +} + + +/** + * Common worker for synchronizing the ENABLED and PAUSED status bits with the + * backend after it becomes ready. + * + * Used by async init and re-init. + * + * @note Is sometimes called w/o having entered DRVAUDIO::CritSectHotPlug. + * Caller must however own the stream critsect. + */ +static int drvAudioStreamUpdateBackendOnStatus(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx, const char *pszWhen) +{ + int rc = VINF_SUCCESS; + if (pStreamEx->fStatus & PDMAUDIOSTREAM_STS_ENABLED) + { + rc = drvAudioStreamControlInternalBackend(pThis, pStreamEx, PDMAUDIOSTREAMCMD_ENABLE); + if (RT_SUCCESS(rc)) + { + if (pStreamEx->fStatus & PDMAUDIOSTREAM_STS_PAUSED) + { + rc = drvAudioStreamControlInternalBackend(pThis, pStreamEx, PDMAUDIOSTREAMCMD_PAUSE); + if (RT_FAILURE(rc)) + LogRelMax(64, ("Audio: Failed to pause stream '%s' after %s: %Rrc\n", pStreamEx->Core.Cfg.szName, pszWhen, rc)); + } + } + else + LogRelMax(64, ("Audio: Failed to enable stream '%s' after %s: %Rrc\n", pStreamEx->Core.Cfg.szName, pszWhen, rc)); + } + return rc; +} + + +/** + * For performing PDMIHOSTAUDIO::pfnStreamInitAsync on a worker thread. + * + * @param pThis Pointer to the DrvAudio instance data. + * @param pStreamEx The stream. One reference for us to release. + */ +static DECLCALLBACK(void) drvAudioStreamInitAsync(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx) +{ + LogFlow(("pThis=%p pStreamEx=%p (%s)\n", pThis, pStreamEx, pStreamEx->Core.Cfg.szName)); + + int rc = RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + AssertRCReturnVoid(rc); + + /* + * Do the init job. + */ + bool fDestroyed; + PPDMIHOSTAUDIO pIHostDrvAudio = pThis->pHostDrvAudio; + AssertPtr(pIHostDrvAudio); + if (pIHostDrvAudio && pIHostDrvAudio->pfnStreamInitAsync) + { + fDestroyed = pStreamEx->cRefs <= 1; + rc = pIHostDrvAudio->pfnStreamInitAsync(pIHostDrvAudio, pStreamEx->pBackend, fDestroyed); + LogFlow(("pfnStreamInitAsync returns %Rrc (on %p, fDestroyed=%d)\n", rc, pStreamEx, fDestroyed)); + } + else + { + fDestroyed = true; + rc = VERR_AUDIO_STREAM_COULD_NOT_CREATE; + } + + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + RTCritSectEnter(&pStreamEx->Core.CritSect); + + /* + * On success, update the backend on the stream status and mark it ready for business. + */ + if (RT_SUCCESS(rc) && !fDestroyed) + { + RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + + /* + * Update the backend state. + */ + pStreamEx->fStatus |= PDMAUDIOSTREAM_STS_BACKEND_READY; /* before the backend control call! */ + + rc = drvAudioStreamUpdateBackendOnStatus(pThis, pStreamEx, "asynchronous initialization completed"); + + /* + * Modify the play state if output stream. + */ + if (pStreamEx->Core.Cfg.enmDir == PDMAUDIODIR_OUT) + { + DRVAUDIOPLAYSTATE const enmPlayState = pStreamEx->Out.enmPlayState; + switch (enmPlayState) + { + case DRVAUDIOPLAYSTATE_PREBUF: + case DRVAUDIOPLAYSTATE_PREBUF_SWITCHING: + break; + case DRVAUDIOPLAYSTATE_PREBUF_OVERDUE: + pStreamEx->Out.enmPlayState = DRVAUDIOPLAYSTATE_PREBUF_COMMITTING; + break; + case DRVAUDIOPLAYSTATE_NOPLAY: + pStreamEx->Out.enmPlayState = DRVAUDIOPLAYSTATE_PREBUF; + break; + case DRVAUDIOPLAYSTATE_PLAY: + case DRVAUDIOPLAYSTATE_PREBUF_COMMITTING: + break; /* possible race here, so don't assert. */ + case DRVAUDIOPLAYSTATE_PLAY_PREBUF: + AssertFailedBreak(); + /* no default */ + case DRVAUDIOPLAYSTATE_END: + case DRVAUDIOPLAYSTATE_INVALID: + break; + } + LogFunc(("enmPlayState: %s -> %s\n", drvAudioPlayStateName(enmPlayState), + drvAudioPlayStateName(pStreamEx->Out.enmPlayState) )); + } + + /* + * Update the last backend state. + */ + pStreamEx->enmLastBackendState = drvAudioStreamGetBackendState(pThis, pStreamEx); + + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + } + /* + * Don't quite know what to do on failure... + */ + else if (!fDestroyed) + { + LogRelMax(64, ("Audio: Failed to initialize stream '%s': %Rrc\n", pStreamEx->Core.Cfg.szName, rc)); + } + + /* + * Release the request handle, must be done while inside the critical section. + */ + if (pStreamEx->hReqInitAsync != NIL_RTREQ) + { + LogFlowFunc(("Releasing hReqInitAsync=%p\n", pStreamEx->hReqInitAsync)); + RTReqRelease(pStreamEx->hReqInitAsync); + pStreamEx->hReqInitAsync = NIL_RTREQ; + } + + RTCritSectLeave(&pStreamEx->Core.CritSect); + + /* + * Release our stream reference. + */ + uint32_t cRefs = drvAudioStreamReleaseInternal(pThis, pStreamEx, true /*fMayDestroy*/); + LogFlowFunc(("returns (fDestroyed=%d, cRefs=%u)\n", fDestroyed, cRefs)); RT_NOREF(cRefs); +} + + +/** + * Worker for drvAudioStreamInitInternal and drvAudioStreamReInitInternal that + * creates the backend (host driver) side of an audio stream. + * + * @returns VBox status code. + * @param pThis Pointer to driver instance. + * @param pStreamEx Stream to create backend for. The Core.Cfg field + * contains the requested configuration when we're called + * and the actual configuration when successfully + * returning. + * + * @note Configuration precedence for requested audio stream configuration (first has highest priority, if set): + * - per global extra-data + * - per-VM extra-data + * - requested configuration (by pCfgReq) + * - default value + */ +static int drvAudioStreamCreateInternalBackend(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx) +{ + AssertMsg((pStreamEx->fStatus & PDMAUDIOSTREAM_STS_BACKEND_CREATED) == 0, + ("Stream '%s' already initialized in backend\n", pStreamEx->Core.Cfg.szName)); + + /* + * Adjust the stream config, applying defaults and any overriding settings. + */ + int rc = drvAudioStreamAdjustConfig(pThis, &pStreamEx->Core.Cfg, pStreamEx->Core.Cfg.szName); + if (RT_FAILURE(rc)) + return rc; + PDMAUDIOSTREAMCFG const CfgReq = pStreamEx->Core.Cfg; + + /* + * Call the host driver to create the stream. + */ + RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + + AssertLogRelMsgStmt(RT_VALID_PTR(pThis->pHostDrvAudio), + ("Audio: %p\n", pThis->pHostDrvAudio), rc = VERR_PDM_NO_ATTACHED_DRIVER); + if (RT_SUCCESS(rc)) + AssertLogRelMsgStmt(pStreamEx->Core.cbBackend == pThis->BackendCfg.cbStream, + ("Audio: Backend changed? cbBackend changed from %#x to %#x\n", + pStreamEx->Core.cbBackend, pThis->BackendCfg.cbStream), + rc = VERR_STATE_CHANGED); + if (RT_SUCCESS(rc)) + rc = pThis->pHostDrvAudio->pfnStreamCreate(pThis->pHostDrvAudio, pStreamEx->pBackend, &CfgReq, &pStreamEx->Core.Cfg); + if (RT_SUCCESS(rc)) + { + pStreamEx->enmLastBackendState = drvAudioStreamGetBackendState(pThis, pStreamEx); + + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + + AssertLogRelReturn(pStreamEx->pBackend->uMagic == PDMAUDIOBACKENDSTREAM_MAGIC, VERR_INTERNAL_ERROR_3); + AssertLogRelReturn(pStreamEx->pBackend->pStream == &pStreamEx->Core, VERR_INTERNAL_ERROR_3); + + /* Must set the backend-initialized flag now or the backend won't be + destroyed (this used to be done at the end of this function, with + several possible early return paths before it). */ + pStreamEx->fStatus |= PDMAUDIOSTREAM_STS_BACKEND_CREATED; + } + else + { + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + if (rc == VERR_NOT_SUPPORTED) + LogRel2(("Audio: Creating stream '%s' in backend not supported\n", pStreamEx->Core.Cfg.szName)); + else if (rc == VERR_AUDIO_STREAM_COULD_NOT_CREATE) + LogRel2(("Audio: Stream '%s' could not be created in backend because of missing hardware / drivers\n", + pStreamEx->Core.Cfg.szName)); + else + LogRel(("Audio: Creating stream '%s' in backend failed with %Rrc\n", pStreamEx->Core.Cfg.szName, rc)); + return rc; + } + + /* Remember if we need to call pfnStreamInitAsync. */ + pStreamEx->fNeedAsyncInit = rc == VINF_AUDIO_STREAM_ASYNC_INIT_NEEDED; + AssertStmt(rc != VINF_AUDIO_STREAM_ASYNC_INIT_NEEDED || pThis->pHostDrvAudio->pfnStreamInitAsync != NULL, + pStreamEx->fNeedAsyncInit = false); + AssertMsg( rc != VINF_AUDIO_STREAM_ASYNC_INIT_NEEDED + || pStreamEx->enmLastBackendState == PDMHOSTAUDIOSTREAMSTATE_INITIALIZING, + ("rc=%Rrc %s\n", rc, PDMHostAudioStreamStateGetName(pStreamEx->enmLastBackendState))); + + PPDMAUDIOSTREAMCFG const pCfgAcq = &pStreamEx->Core.Cfg; + + /* + * Validate acquired configuration. + */ + char szTmp[PDMAUDIOPROPSTOSTRING_MAX]; + LogFunc(("Backend returned: %s\n", PDMAudioStrmCfgToString(pCfgAcq, szTmp, sizeof(szTmp)) )); + AssertLogRelMsgReturn(AudioHlpStreamCfgIsValid(pCfgAcq), + ("Audio: Creating stream '%s' returned an invalid backend configuration (%s), skipping\n", + pCfgAcq->szName, PDMAudioPropsToString(&pCfgAcq->Props, szTmp, sizeof(szTmp))), + VERR_INVALID_PARAMETER); + + /* Let the user know that the backend changed one of the values requested above. */ + if (pCfgAcq->Backend.cFramesBufferSize != CfgReq.Backend.cFramesBufferSize) + LogRel2(("Audio: Backend changed buffer size from %RU64ms (%RU32 frames) to %RU64ms (%RU32 frames)\n", + PDMAudioPropsFramesToMilli(&CfgReq.Props, CfgReq.Backend.cFramesBufferSize), CfgReq.Backend.cFramesBufferSize, + PDMAudioPropsFramesToMilli(&pCfgAcq->Props, pCfgAcq->Backend.cFramesBufferSize), pCfgAcq->Backend.cFramesBufferSize)); + + if (pCfgAcq->Backend.cFramesPeriod != CfgReq.Backend.cFramesPeriod) + LogRel2(("Audio: Backend changed period size from %RU64ms (%RU32 frames) to %RU64ms (%RU32 frames)\n", + PDMAudioPropsFramesToMilli(&CfgReq.Props, CfgReq.Backend.cFramesPeriod), CfgReq.Backend.cFramesPeriod, + PDMAudioPropsFramesToMilli(&pCfgAcq->Props, pCfgAcq->Backend.cFramesPeriod), pCfgAcq->Backend.cFramesPeriod)); + + /* Was pre-buffering requested, but the acquired configuration from the backend told us something else? */ + if (CfgReq.Backend.cFramesPreBuffering) + { + if (pCfgAcq->Backend.cFramesPreBuffering != CfgReq.Backend.cFramesPreBuffering) + LogRel2(("Audio: Backend changed pre-buffering size from %RU64ms (%RU32 frames) to %RU64ms (%RU32 frames)\n", + PDMAudioPropsFramesToMilli(&CfgReq.Props, CfgReq.Backend.cFramesPreBuffering), CfgReq.Backend.cFramesPreBuffering, + PDMAudioPropsFramesToMilli(&pCfgAcq->Props, pCfgAcq->Backend.cFramesPreBuffering), pCfgAcq->Backend.cFramesPreBuffering)); + + if (pCfgAcq->Backend.cFramesPreBuffering > pCfgAcq->Backend.cFramesBufferSize) + { + pCfgAcq->Backend.cFramesPreBuffering = pCfgAcq->Backend.cFramesBufferSize; + LogRel2(("Audio: Pre-buffering size bigger than buffer size for stream '%s', adjusting to %RU64ms (%RU32 frames)\n", pCfgAcq->szName, + PDMAudioPropsFramesToMilli(&pCfgAcq->Props, pCfgAcq->Backend.cFramesPreBuffering), pCfgAcq->Backend.cFramesPreBuffering)); + } + } + else if (CfgReq.Backend.cFramesPreBuffering == 0) /* Was the pre-buffering requested as being disabeld? Tell the users. */ + { + LogRel2(("Audio: Pre-buffering is disabled for stream '%s'\n", pCfgAcq->szName)); + pCfgAcq->Backend.cFramesPreBuffering = 0; + } + + /* + * Check if the backend did return sane values and correct if necessary. + */ + uint32_t const cFramesPreBufferingMax = pCfgAcq->Backend.cFramesBufferSize - RT_MIN(16, pCfgAcq->Backend.cFramesBufferSize); + if (pCfgAcq->Backend.cFramesPreBuffering > cFramesPreBufferingMax) + { + LogRel2(("Audio: Warning! Pre-buffering size of %RU32 frames for stream '%s' is too close to or larger than the %RU32 frames buffer size, reducing it to %RU32 frames!\n", + pCfgAcq->Backend.cFramesPreBuffering, pCfgAcq->szName, pCfgAcq->Backend.cFramesBufferSize, cFramesPreBufferingMax)); + AssertMsgFailed(("cFramesPreBuffering=%#x vs cFramesPreBufferingMax=%#x\n", pCfgAcq->Backend.cFramesPreBuffering, cFramesPreBufferingMax)); + pCfgAcq->Backend.cFramesPreBuffering = cFramesPreBufferingMax; + } + + if (pCfgAcq->Backend.cFramesPeriod > pCfgAcq->Backend.cFramesBufferSize) + { + LogRel2(("Audio: Warning! Period size of %RU32 frames for stream '%s' is larger than the %RU32 frames buffer size, reducing it to %RU32 frames!\n", + pCfgAcq->Backend.cFramesPeriod, pCfgAcq->szName, pCfgAcq->Backend.cFramesBufferSize, pCfgAcq->Backend.cFramesBufferSize / 2)); + AssertMsgFailed(("cFramesPeriod=%#x vs cFramesBufferSize=%#x\n", pCfgAcq->Backend.cFramesPeriod, pCfgAcq->Backend.cFramesBufferSize)); + pCfgAcq->Backend.cFramesPeriod = pCfgAcq->Backend.cFramesBufferSize / 2; + } + + LogRel2(("Audio: Buffer size for stream '%s' is %RU64 ms / %RU32 frames\n", pCfgAcq->szName, + PDMAudioPropsFramesToMilli(&pCfgAcq->Props, pCfgAcq->Backend.cFramesBufferSize), pCfgAcq->Backend.cFramesBufferSize)); + LogRel2(("Audio: Pre-buffering size for stream '%s' is %RU64 ms / %RU32 frames\n", pCfgAcq->szName, + PDMAudioPropsFramesToMilli(&pCfgAcq->Props, pCfgAcq->Backend.cFramesPreBuffering), pCfgAcq->Backend.cFramesPreBuffering)); + LogRel2(("Audio: Scheduling hint for stream '%s' is %RU32ms / %RU32 frames\n", pCfgAcq->szName, + pCfgAcq->Device.cMsSchedulingHint, PDMAudioPropsMilliToFrames(&pCfgAcq->Props, pCfgAcq->Device.cMsSchedulingHint))); + + /* Make sure the configured buffer size by the backend at least can hold the configured latency. */ + uint32_t const cMsPeriod = PDMAudioPropsFramesToMilli(&pCfgAcq->Props, pCfgAcq->Backend.cFramesPeriod); + LogRel2(("Audio: Period size of stream '%s' is %RU64 ms / %RU32 frames\n", + pCfgAcq->szName, cMsPeriod, pCfgAcq->Backend.cFramesPeriod)); + /** @todo r=bird: This is probably a misleading/harmless warning as we'd just + * have to transfer more each time we move data. The period is generally + * pure irrelevant fiction anyway. A more relevant comparison would + * be to half the buffer size, i.e. making sure we get scheduled often + * enough to keep the buffer at least half full (probably more + * sensible if the buffer size was more than 2x scheduling periods). */ + if ( CfgReq.Device.cMsSchedulingHint /* Any scheduling hint set? */ + && CfgReq.Device.cMsSchedulingHint > cMsPeriod) /* This might lead to buffer underflows. */ + LogRel(("Audio: Warning: Scheduling hint of stream '%s' is bigger (%RU64ms) than used period size (%RU64ms)\n", + pCfgAcq->szName, CfgReq.Device.cMsSchedulingHint, cMsPeriod)); + + /* + * Done, just log the result: + */ + LogFunc(("Acquired stream config: %s\n", PDMAudioStrmCfgToString(&pStreamEx->Core.Cfg, szTmp, sizeof(szTmp)) )); + LogRel2(("Audio: Acquired stream config: %s\n", PDMAudioStrmCfgToString(&pStreamEx->Core.Cfg, szTmp, sizeof(szTmp)) )); + + return VINF_SUCCESS; +} + + +/** + * Worker for drvAudioStreamCreate that initializes the audio stream. + * + * @returns VBox status code. + * @param pThis Pointer to driver instance. + * @param pStreamEx Stream to initialize. Caller already set a few fields. + * The Core.Cfg field contains the requested configuration + * when we're called and the actual configuration when + * successfully returning. + */ +static int drvAudioStreamInitInternal(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx) +{ + /* + * Init host stream. + */ + pStreamEx->Core.uMagic = PDMAUDIOSTREAM_MAGIC; + + char szTmp[PDMAUDIOSTRMCFGTOSTRING_MAX]; + LogFunc(("Requested stream config: %s\n", PDMAudioStrmCfgToString(&pStreamEx->Core.Cfg, szTmp, sizeof(szTmp)) )); + LogRel2(("Audio: Creating stream: %s\n", PDMAudioStrmCfgToString(&pStreamEx->Core.Cfg, szTmp, sizeof(szTmp)) )); + + int rc = drvAudioStreamCreateInternalBackend(pThis, pStreamEx); + if (RT_FAILURE(rc)) + return rc; + + /* + * Configure host buffers. + */ + Assert(pStreamEx->cbPreBufThreshold == 0); + if (pStreamEx->Core.Cfg.Backend.cFramesPreBuffering != 0) + pStreamEx->cbPreBufThreshold = PDMAudioPropsFramesToBytes(&pStreamEx->Core.Cfg.Props, + pStreamEx->Core.Cfg.Backend.cFramesPreBuffering); + + /* Allocate space for pre-buffering of output stream w/o mixing buffers. */ + if (pStreamEx->Core.Cfg.enmDir == PDMAUDIODIR_OUT) + { + Assert(pStreamEx->Out.cbPreBufAlloc == 0); + Assert(pStreamEx->Out.cbPreBuffered == 0); + Assert(pStreamEx->Out.offPreBuf == 0); + if (pStreamEx->Core.Cfg.Backend.cFramesPreBuffering != 0) + { + uint32_t cbPreBufAlloc = PDMAudioPropsFramesToBytes(&pStreamEx->Core.Cfg.Props, + pStreamEx->Core.Cfg.Backend.cFramesBufferSize); + cbPreBufAlloc = RT_MIN(RT_ALIGN_32(pStreamEx->cbPreBufThreshold + _8K, _4K), cbPreBufAlloc); + cbPreBufAlloc = PDMAudioPropsFloorBytesToFrame(&pStreamEx->Core.Cfg.Props, cbPreBufAlloc); + pStreamEx->Out.cbPreBufAlloc = cbPreBufAlloc; + pStreamEx->Out.pbPreBuf = (uint8_t *)RTMemAllocZ(cbPreBufAlloc); + AssertReturn(pStreamEx->Out.pbPreBuf, VERR_NO_MEMORY); + } + pStreamEx->Out.enmPlayState = DRVAUDIOPLAYSTATE_NOPLAY; /* Changed upon enable. */ + } + + /* + * Register statistics. + */ + PPDMDRVINS const pDrvIns = pThis->pDrvIns; + /** @todo expose config and more. */ + PDMDrvHlpSTAMRegisterF(pDrvIns, &pStreamEx->Core.Cfg.Backend.cFramesBufferSize, STAMTYPE_U32, STAMVISIBILITY_USED, STAMUNIT_NONE, + "The size of the backend buffer (in frames)", "%s/0-HostBackendBufSize", pStreamEx->Core.Cfg.szName); + PDMDrvHlpSTAMRegisterF(pDrvIns, &pStreamEx->Core.Cfg.Backend.cFramesPeriod, STAMTYPE_U32, STAMVISIBILITY_USED, STAMUNIT_NONE, + "The size of the backend period (in frames)", "%s/0-HostBackendPeriodSize", pStreamEx->Core.Cfg.szName); + PDMDrvHlpSTAMRegisterF(pDrvIns, &pStreamEx->Core.Cfg.Backend.cFramesPreBuffering, STAMTYPE_U32, STAMVISIBILITY_USED, STAMUNIT_NONE, + "Pre-buffer size (in frames)", "%s/0-HostBackendPreBufferSize", pStreamEx->Core.Cfg.szName); + PDMDrvHlpSTAMRegisterF(pDrvIns, &pStreamEx->Core.Cfg.Device.cMsSchedulingHint, STAMTYPE_U32, STAMVISIBILITY_USED, STAMUNIT_NONE, + "Device DMA scheduling hint (in milliseconds)", "%s/0-DeviceSchedulingHint", pStreamEx->Core.Cfg.szName); + PDMDrvHlpSTAMRegisterF(pDrvIns, &pStreamEx->Core.Cfg.Props.uHz, STAMTYPE_U32, STAMVISIBILITY_USED, STAMUNIT_HZ, + "Backend stream frequency", "%s/Hz", pStreamEx->Core.Cfg.szName); + PDMDrvHlpSTAMRegisterF(pDrvIns, &pStreamEx->Core.Cfg.Props.cbFrame, STAMTYPE_U8, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "Backend frame size", "%s/Framesize", pStreamEx->Core.Cfg.szName); + if (pStreamEx->Core.Cfg.enmDir == PDMAUDIODIR_IN) + { + PDMDrvHlpSTAMRegisterF(pDrvIns, &pStreamEx->In.Stats.cbBackendReadableBefore, STAMTYPE_U32, STAMVISIBILITY_USED, STAMUNIT_NONE, + "Free space in backend buffer before play", "%s/0-HostBackendBufReadableBefore", pStreamEx->Core.Cfg.szName); + PDMDrvHlpSTAMRegisterF(pDrvIns, &pStreamEx->In.Stats.cbBackendReadableAfter, STAMTYPE_U32, STAMVISIBILITY_USED, STAMUNIT_NONE, + "Free space in backend buffer after play", "%s/0-HostBackendBufReadableAfter", pStreamEx->Core.Cfg.szName); +#ifdef VBOX_WITH_STATISTICS + PDMDrvHlpSTAMRegisterF(pDrvIns, &pStreamEx->In.Stats.ProfCapture, STAMTYPE_PROFILE, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + "Profiling time spent in StreamCapture", "%s/ProfStreamCapture", pStreamEx->Core.Cfg.szName); + PDMDrvHlpSTAMRegisterF(pDrvIns, &pStreamEx->In.Stats.ProfGetReadable, STAMTYPE_PROFILE, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + "Profiling time spent in StreamGetReadable", "%s/ProfStreamGetReadable", pStreamEx->Core.Cfg.szName); + PDMDrvHlpSTAMRegisterF(pDrvIns, &pStreamEx->In.Stats.ProfGetReadableBytes, STAMTYPE_PROFILE, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "Readable byte stats", "%s/ProfStreamGetReadableBytes", pStreamEx->Core.Cfg.szName); +#endif + } + else + { + PDMDrvHlpSTAMRegisterF(pDrvIns, &pStreamEx->Out.Stats.cbBackendWritableBefore, STAMTYPE_U32, STAMVISIBILITY_USED, STAMUNIT_NONE, + "Free space in backend buffer before play", "%s/0-HostBackendBufWritableBefore", pStreamEx->Core.Cfg.szName); + PDMDrvHlpSTAMRegisterF(pDrvIns, &pStreamEx->Out.Stats.cbBackendWritableAfter, STAMTYPE_U32, STAMVISIBILITY_USED, STAMUNIT_NONE, + "Free space in backend buffer after play", "%s/0-HostBackendBufWritableAfter", pStreamEx->Core.Cfg.szName); +#ifdef VBOX_WITH_STATISTICS + PDMDrvHlpSTAMRegisterF(pDrvIns, &pStreamEx->Out.Stats.ProfPlay, STAMTYPE_PROFILE, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + "Profiling time spent in StreamPlay", "%s/ProfStreamPlay", pStreamEx->Core.Cfg.szName); + PDMDrvHlpSTAMRegisterF(pDrvIns, &pStreamEx->Out.Stats.ProfGetWritable, STAMTYPE_PROFILE, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + "Profiling time spent in StreamGetWritable", "%s/ProfStreamGetWritable", pStreamEx->Core.Cfg.szName); + PDMDrvHlpSTAMRegisterF(pDrvIns, &pStreamEx->Out.Stats.ProfGetWritableBytes, STAMTYPE_PROFILE, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "Writeable byte stats", "%s/ProfStreamGetWritableBytes", pStreamEx->Core.Cfg.szName); +#endif + } +#ifdef VBOX_WITH_STATISTICS + PDMDrvHlpSTAMRegisterF(pDrvIns, &pStreamEx->StatProfGetState, STAMTYPE_PROFILE, STAMVISIBILITY_USED, STAMUNIT_OCCURENCES, + "Profiling time spent in StreamGetState", "%s/ProfStreamGetState", pStreamEx->Core.Cfg.szName); + PDMDrvHlpSTAMRegisterF(pDrvIns, &pStreamEx->StatXfer, STAMTYPE_PROFILE, STAMVISIBILITY_USED, STAMUNIT_BYTES, + "Byte transfer stats (excluding pre-buffering)", "%s/Transfers", pStreamEx->Core.Cfg.szName); +#endif + PDMDrvHlpSTAMRegisterF(pDrvIns, &pStreamEx->offInternal, STAMTYPE_U64, STAMVISIBILITY_USED, STAMUNIT_NONE, + "Internal stream offset", "%s/offInternal", pStreamEx->Core.Cfg.szName); + + LogFlowFunc(("[%s] Returning %Rrc\n", pStreamEx->Core.Cfg.szName, rc)); + return rc; +} + + +/** + * @interface_method_impl{PDMIAUDIOCONNECTOR,pfnStreamCreate} + */ +static DECLCALLBACK(int) drvAudioStreamCreate(PPDMIAUDIOCONNECTOR pInterface, uint32_t fFlags, PCPDMAUDIOSTREAMCFG pCfgReq, + PPDMAUDIOSTREAM *ppStream) +{ + PDRVAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVAUDIO, IAudioConnector); + AssertPtr(pThis); + + /* + * Assert sanity. + */ + AssertReturn(!(fFlags & ~PDMAUDIOSTREAM_CREATE_F_NO_MIXBUF), VERR_INVALID_FLAGS); + AssertPtrReturn(pCfgReq, VERR_INVALID_POINTER); + AssertPtrReturn(ppStream, VERR_INVALID_POINTER); + *ppStream = NULL; + LogFlowFunc(("pCfgReq=%s\n", pCfgReq->szName)); +#ifdef LOG_ENABLED + PDMAudioStrmCfgLog(pCfgReq); +#endif + AssertReturn(AudioHlpStreamCfgIsValid(pCfgReq), VERR_INVALID_PARAMETER); + AssertReturn(pCfgReq->enmDir == PDMAUDIODIR_IN || pCfgReq->enmDir == PDMAUDIODIR_OUT, VERR_NOT_SUPPORTED); + + /* + * Grab a free stream count now. + */ + int rc = RTCritSectRwEnterExcl(&pThis->CritSectGlobals); + AssertRCReturn(rc, rc); + + uint32_t * const pcFreeStreams = pCfgReq->enmDir == PDMAUDIODIR_IN ? &pThis->In.cStreamsFree : &pThis->Out.cStreamsFree; + if (*pcFreeStreams > 0) + *pcFreeStreams -= 1; + else + { + RTCritSectRwLeaveExcl(&pThis->CritSectGlobals); + LogFlowFunc(("Maximum number of host %s streams reached\n", PDMAudioDirGetName(pCfgReq->enmDir) )); + return pCfgReq->enmDir == PDMAUDIODIR_IN ? VERR_AUDIO_NO_FREE_INPUT_STREAMS : VERR_AUDIO_NO_FREE_OUTPUT_STREAMS; + } + + RTCritSectRwLeaveExcl(&pThis->CritSectGlobals); + + /* + * Get and check the backend size. + * + * Since we'll have to leave the hot-plug lock before we call the backend, + * we'll have revalidate the size at that time. + */ + RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + + size_t const cbHstStrm = pThis->BackendCfg.cbStream; + AssertStmt(cbHstStrm >= sizeof(PDMAUDIOBACKENDSTREAM), rc = VERR_OUT_OF_RANGE); + AssertStmt(cbHstStrm < _16M, rc = VERR_OUT_OF_RANGE); + + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + if (RT_SUCCESS(rc)) + { + /* + * Allocate and initialize common state. + */ + PDRVAUDIOSTREAM pStreamEx = (PDRVAUDIOSTREAM)RTMemAllocZ(sizeof(DRVAUDIOSTREAM) + RT_ALIGN_Z(cbHstStrm, 64)); + if (pStreamEx) + { + rc = RTCritSectInit(&pStreamEx->Core.CritSect); /* (drvAudioStreamFree assumes it's initailized) */ + if (RT_SUCCESS(rc)) + { + PPDMAUDIOBACKENDSTREAM pBackend = (PPDMAUDIOBACKENDSTREAM)(pStreamEx + 1); + pBackend->uMagic = PDMAUDIOBACKENDSTREAM_MAGIC; + pBackend->pStream = &pStreamEx->Core; + + pStreamEx->pBackend = pBackend; + pStreamEx->Core.Cfg = *pCfgReq; + pStreamEx->Core.cbBackend = (uint32_t)cbHstStrm; + pStreamEx->fDestroyImmediate = true; + pStreamEx->hReqInitAsync = NIL_RTREQ; + pStreamEx->uMagic = DRVAUDIOSTREAM_MAGIC; + + /* Make a unqiue stream name including the host (backend) driver name. */ + AssertPtr(pThis->pHostDrvAudio); + size_t cchName = RTStrPrintf(pStreamEx->Core.Cfg.szName, RT_ELEMENTS(pStreamEx->Core.Cfg.szName), "[%s] %s:0", + pThis->BackendCfg.szName, pCfgReq->szName[0] != '\0' ? pCfgReq->szName : "<NoName>"); + if (cchName < sizeof(pStreamEx->Core.Cfg.szName)) + { + RTCritSectRwEnterShared(&pThis->CritSectGlobals); + for (uint32_t i = 0; i < 256; i++) + { + bool fDone = true; + PDRVAUDIOSTREAM pIt; + RTListForEach(&pThis->LstStreams, pIt, DRVAUDIOSTREAM, ListEntry) + { + if (strcmp(pIt->Core.Cfg.szName, pStreamEx->Core.Cfg.szName) == 0) + { + RTStrPrintf(pStreamEx->Core.Cfg.szName, RT_ELEMENTS(pStreamEx->Core.Cfg.szName), "[%s] %s:%u", + pThis->BackendCfg.szName, pCfgReq->szName[0] != '\0' ? pCfgReq->szName : "<NoName>", + i); + fDone = false; + break; + } + } + if (fDone) + break; + } + RTCritSectRwLeaveShared(&pThis->CritSectGlobals); + } + + /* + * Try to init the rest. + */ + rc = drvAudioStreamInitInternal(pThis, pStreamEx); + if (RT_SUCCESS(rc)) + { + /* Set initial reference counts. */ + pStreamEx->cRefs = pStreamEx->fNeedAsyncInit ? 2 : 1; + + /* Add it to the list. */ + RTCritSectRwEnterExcl(&pThis->CritSectGlobals); + + RTListAppend(&pThis->LstStreams, &pStreamEx->ListEntry); + pThis->cStreams++; + STAM_REL_COUNTER_INC(&pThis->StatTotalStreamsCreated); + + RTCritSectRwLeaveExcl(&pThis->CritSectGlobals); + + /* + * Init debug stuff if enabled (ignore failures). + */ + if (pCfgReq->enmDir == PDMAUDIODIR_IN) + { + if (pThis->CfgIn.Dbg.fEnabled) + AudioHlpFileCreateAndOpen(&pStreamEx->In.Dbg.pFileCapture, pThis->CfgIn.Dbg.szPathOut, + "DrvAudioCapture", pThis->pDrvIns->iInstance, &pStreamEx->Core.Cfg.Props); + } + else /* Out */ + { + if (pThis->CfgOut.Dbg.fEnabled) + AudioHlpFileCreateAndOpen(&pStreamEx->Out.Dbg.pFilePlay, pThis->CfgOut.Dbg.szPathOut, + "DrvAudioPlay", pThis->pDrvIns->iInstance, &pStreamEx->Core.Cfg.Props); + } + + /* + * Kick off the asynchronous init. + */ + if (!pStreamEx->fNeedAsyncInit) + { + pStreamEx->fStatus |= PDMAUDIOSTREAM_STS_BACKEND_READY; + PDMAUDIOSTREAM_STS_ASSERT_VALID(pStreamEx->fStatus); + } + else + { + int rc2 = RTReqPoolCallEx(pThis->hReqPool, 0 /*cMillies*/, &pStreamEx->hReqInitAsync, + RTREQFLAGS_VOID | RTREQFLAGS_NO_WAIT, + (PFNRT)drvAudioStreamInitAsync, 2, pThis, pStreamEx); + LogFlowFunc(("hReqInitAsync=%p rc2=%Rrc\n", pStreamEx->hReqInitAsync, rc2)); + AssertRCStmt(rc2, drvAudioStreamInitAsync(pThis, pStreamEx)); + } + +#ifdef VBOX_STRICT + /* + * Assert lock order to make sure the lock validator picks up on it. + */ + RTCritSectRwEnterShared(&pThis->CritSectGlobals); + RTCritSectEnter(&pStreamEx->Core.CritSect); + RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + RTCritSectLeave(&pStreamEx->Core.CritSect); + RTCritSectRwLeaveShared(&pThis->CritSectGlobals); +#endif + + *ppStream = &pStreamEx->Core; + LogFlowFunc(("returns VINF_SUCCESS (pStreamEx=%p)\n", pStreamEx)); + return VINF_SUCCESS; + } + + LogFunc(("drvAudioStreamInitInternal failed: %Rrc\n", rc)); + int rc2 = drvAudioStreamUninitInternal(pThis, pStreamEx); + AssertRC(rc2); + drvAudioStreamFree(pStreamEx); + } + else + RTMemFree(pStreamEx); + } + else + rc = VERR_NO_MEMORY; + } + + /* + * Give back the stream count, we couldn't use it after all. + */ + RTCritSectRwEnterExcl(&pThis->CritSectGlobals); + *pcFreeStreams += 1; + RTCritSectRwLeaveExcl(&pThis->CritSectGlobals); + + LogFlowFuncLeaveRC(rc); + return rc; +} + + +/** + * Calls the backend to give it the chance to destroy its part of the audio stream. + * + * Called from drvAudioPowerOff, drvAudioStreamUninitInternal and + * drvAudioStreamReInitInternal. + * + * @returns VBox status code. + * @param pThis Pointer to driver instance. + * @param pStreamEx Audio stream destruct backend for. + */ +static int drvAudioStreamDestroyInternalBackend(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx) +{ + AssertPtr(pThis); + AssertPtr(pStreamEx); + + int rc = VINF_SUCCESS; + +#ifdef LOG_ENABLED + char szStreamSts[DRVAUDIO_STATUS_STR_MAX]; +#endif + LogFunc(("[%s] fStatus=%s\n", pStreamEx->Core.Cfg.szName, drvAudioStreamStatusToStr(szStreamSts, pStreamEx->fStatus))); + + if (pStreamEx->fStatus & PDMAUDIOSTREAM_STS_BACKEND_CREATED) + { + AssertPtr(pStreamEx->pBackend); + + /* Check if the pointer to the host audio driver is still valid. + * It can be NULL if we were called in drvAudioDestruct, for example. */ + RTCritSectRwEnterShared(&pThis->CritSectHotPlug); /** @todo needed? */ + if (pThis->pHostDrvAudio) + rc = pThis->pHostDrvAudio->pfnStreamDestroy(pThis->pHostDrvAudio, pStreamEx->pBackend, pStreamEx->fDestroyImmediate); + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + + pStreamEx->fStatus &= ~(PDMAUDIOSTREAM_STS_BACKEND_CREATED | PDMAUDIOSTREAM_STS_BACKEND_READY); + PDMAUDIOSTREAM_STS_ASSERT_VALID(pStreamEx->fStatus); + } + + LogFlowFunc(("[%s] Returning %Rrc\n", pStreamEx->Core.Cfg.szName, rc)); + return rc; +} + + +/** + * Uninitializes an audio stream - worker for drvAudioStreamDestroy, + * drvAudioDestruct and drvAudioStreamCreate. + * + * @returns VBox status code. + * @param pThis Pointer to driver instance. + * @param pStreamEx Pointer to audio stream to uninitialize. + */ +static int drvAudioStreamUninitInternal(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx) +{ + AssertPtrReturn(pThis, VERR_INVALID_POINTER); + AssertMsgReturn(pStreamEx->cRefs <= 1, + ("Stream '%s' still has %RU32 references held when uninitializing\n", pStreamEx->Core.Cfg.szName, pStreamEx->cRefs), + VERR_WRONG_ORDER); + LogFlowFunc(("[%s] cRefs=%RU32\n", pStreamEx->Core.Cfg.szName, pStreamEx->cRefs)); + + RTCritSectEnter(&pStreamEx->Core.CritSect); + + /* + * ... + */ + if (pStreamEx->fDestroyImmediate) + drvAudioStreamControlInternal(pThis, pStreamEx, PDMAUDIOSTREAMCMD_DISABLE); + int rc = drvAudioStreamDestroyInternalBackend(pThis, pStreamEx); + + /* Free pre-buffer space. */ + if ( pStreamEx->Core.Cfg.enmDir == PDMAUDIODIR_OUT + && pStreamEx->Out.pbPreBuf) + { + RTMemFree(pStreamEx->Out.pbPreBuf); + pStreamEx->Out.pbPreBuf = NULL; + pStreamEx->Out.cbPreBufAlloc = 0; + pStreamEx->Out.cbPreBuffered = 0; + pStreamEx->Out.offPreBuf = 0; + } + + if (RT_SUCCESS(rc)) + { +#ifdef LOG_ENABLED + if (pStreamEx->fStatus != PDMAUDIOSTREAM_STS_NONE) + { + char szStreamSts[DRVAUDIO_STATUS_STR_MAX]; + LogFunc(("[%s] Warning: Still has %s set when uninitializing\n", + pStreamEx->Core.Cfg.szName, drvAudioStreamStatusToStr(szStreamSts, pStreamEx->fStatus))); + } +#endif + pStreamEx->fStatus = PDMAUDIOSTREAM_STS_NONE; + } + + PPDMDRVINS const pDrvIns = pThis->pDrvIns; + PDMDrvHlpSTAMDeregisterByPrefix(pDrvIns, pStreamEx->Core.Cfg.szName); + + if (pStreamEx->Core.Cfg.enmDir == PDMAUDIODIR_IN) + { + if (pThis->CfgIn.Dbg.fEnabled) + { + AudioHlpFileDestroy(pStreamEx->In.Dbg.pFileCapture); + pStreamEx->In.Dbg.pFileCapture = NULL; + } + } + else + { + Assert(pStreamEx->Core.Cfg.enmDir == PDMAUDIODIR_OUT); + if (pThis->CfgOut.Dbg.fEnabled) + { + AudioHlpFileDestroy(pStreamEx->Out.Dbg.pFilePlay); + pStreamEx->Out.Dbg.pFilePlay = NULL; + } + } + + RTCritSectLeave(&pStreamEx->Core.CritSect); + LogFlowFunc(("Returning %Rrc\n", rc)); + return rc; +} + + +/** + * Internal release function. + * + * @returns New reference count, UINT32_MAX if bad stream. + * @param pThis Pointer to the DrvAudio instance data. + * @param pStreamEx The stream to reference. + * @param fMayDestroy Whether the caller is allowed to implicitly destroy + * the stream or not. + */ +static uint32_t drvAudioStreamReleaseInternal(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx, bool fMayDestroy) +{ + AssertPtrReturn(pStreamEx, UINT32_MAX); + AssertReturn(pStreamEx->Core.uMagic == PDMAUDIOSTREAM_MAGIC, UINT32_MAX); + AssertReturn(pStreamEx->uMagic == DRVAUDIOSTREAM_MAGIC, UINT32_MAX); + Assert(!RTCritSectIsOwner(&pStreamEx->Core.CritSect)); + + uint32_t cRefs = ASMAtomicDecU32(&pStreamEx->cRefs); + if (cRefs != 0) + Assert(cRefs < _1K); + else if (fMayDestroy) + { +/** @todo r=bird: Caching one stream in each direction for some time, + * depending on the time it took to create it. drvAudioStreamCreate can use it + * if the configuration matches, otherwise it'll throw it away. This will + * provide a general speedup independ of device (HDA used to do this, but + * doesn't) and backend implementation. Ofc, the backend probably needs an + * opt-out here. */ + int rc = drvAudioStreamUninitInternal(pThis, pStreamEx); + if (RT_SUCCESS(rc)) + { + RTCritSectRwEnterExcl(&pThis->CritSectGlobals); + + if (pStreamEx->Core.Cfg.enmDir == PDMAUDIODIR_IN) + pThis->In.cStreamsFree++; + else /* Out */ + pThis->Out.cStreamsFree++; + pThis->cStreams--; + + RTListNodeRemove(&pStreamEx->ListEntry); + + RTCritSectRwLeaveExcl(&pThis->CritSectGlobals); + + drvAudioStreamFree(pStreamEx); + } + else + { + LogRel(("Audio: Uninitializing stream '%s' failed with %Rrc\n", pStreamEx->Core.Cfg.szName, rc)); + /** @todo r=bird: What's the plan now? */ + } + } + else + { + cRefs = ASMAtomicIncU32(&pStreamEx->cRefs); + AssertFailed(); + } + + Log12Func(("returns %u (%s)\n", cRefs, cRefs > 0 ? pStreamEx->Core.Cfg.szName : "destroyed")); + return cRefs; +} + + +/** + * Asynchronous worker for drvAudioStreamDestroy. + * + * Does DISABLE and releases reference, possibly destroying the stream. + * + * @param pThis Pointer to the DrvAudio instance data. + * @param pStreamEx The stream. One reference for us to release. + * @param fImmediate How to treat draining streams. + */ +static DECLCALLBACK(void) drvAudioStreamDestroyAsync(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx, bool fImmediate) +{ + LogFlowFunc(("pThis=%p pStreamEx=%p (%s) fImmediate=%RTbool\n", pThis, pStreamEx, pStreamEx->Core.Cfg.szName, fImmediate)); +#ifdef LOG_ENABLED + uint64_t const nsStart = RTTimeNanoTS(); +#endif + RTCritSectEnter(&pStreamEx->Core.CritSect); + + pStreamEx->fDestroyImmediate = fImmediate; /* Do NOT adjust for draining status, just pass it as-is. CoreAudio needs this. */ + + if (!fImmediate && (pStreamEx->fStatus & PDMAUDIOSTREAM_STS_PENDING_DISABLE)) + LogFlowFunc(("No DISABLE\n")); + else + { + int rc2 = drvAudioStreamControlInternal(pThis, pStreamEx, PDMAUDIOSTREAMCMD_DISABLE); + LogFlowFunc(("DISABLE done: %Rrc\n", rc2)); + AssertRC(rc2); + } + + RTCritSectLeave(&pStreamEx->Core.CritSect); + + drvAudioStreamReleaseInternal(pThis, pStreamEx, true /*fMayDestroy*/); + + LogFlowFunc(("returning (after %'RU64 ns)\n", RTTimeNanoTS() - nsStart)); +} + + +/** + * @interface_method_impl{PDMIAUDIOCONNECTOR,pfnStreamDestroy} + */ +static DECLCALLBACK(int) drvAudioStreamDestroy(PPDMIAUDIOCONNECTOR pInterface, PPDMAUDIOSTREAM pStream, bool fImmediate) +{ + PDRVAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVAUDIO, IAudioConnector); + AssertPtr(pThis); + + /* Ignore NULL streams. */ + if (!pStream) + return VINF_SUCCESS; + + PDRVAUDIOSTREAM pStreamEx = (PDRVAUDIOSTREAM)pStream; /* Note! Do not touch pStream after this! */ + AssertPtrReturn(pStreamEx, VERR_INVALID_POINTER); + LogFlowFunc(("ENTER - %p (%s) fImmediate=%RTbool\n", pStreamEx, pStreamEx->Core.Cfg.szName, fImmediate)); + AssertReturn(pStreamEx->Core.uMagic == PDMAUDIOSTREAM_MAGIC, VERR_INVALID_MAGIC); + AssertReturn(pStreamEx->uMagic == DRVAUDIOSTREAM_MAGIC, VERR_INVALID_MAGIC); + AssertReturn(pStreamEx->pBackend && pStreamEx->pBackend->uMagic == PDMAUDIOBACKENDSTREAM_MAGIC, VERR_INVALID_MAGIC); + + /* + * The main difference from a regular release is that this will disable + * (or drain if we could) the stream and we can cancel any pending + * pfnStreamInitAsync call. + */ + int rc = RTCritSectEnter(&pStreamEx->Core.CritSect); + AssertRCReturn(rc, rc); + + if (pStreamEx->uMagic == DRVAUDIOSTREAM_MAGIC) + { + if (pStreamEx->cRefs > 0 && pStreamEx->cRefs < UINT32_MAX / 4) + { + char szStatus[DRVAUDIO_STATUS_STR_MAX]; + LogRel2(("Audio: Destroying stream '%s': cRefs=%u; status: %s; backend: %s; hReqInitAsync=%p\n", + pStreamEx->Core.Cfg.szName, pStreamEx->cRefs, drvAudioStreamStatusToStr(szStatus, pStreamEx->fStatus), + PDMHostAudioStreamStateGetName(drvAudioStreamGetBackendState(pThis, pStreamEx)), + pStreamEx->hReqInitAsync)); + + /* Try cancel pending async init request and release the it. */ + if (pStreamEx->hReqInitAsync != NIL_RTREQ) + { + Assert(pStreamEx->cRefs >= 2); + int rc2 = RTReqCancel(pStreamEx->hReqInitAsync); + + RTReqRelease(pStreamEx->hReqInitAsync); + pStreamEx->hReqInitAsync = NIL_RTREQ; + + RTCritSectLeave(&pStreamEx->Core.CritSect); /* (exit before releasing the stream to avoid assertion) */ + + if (RT_SUCCESS(rc2)) + { + LogFlowFunc(("Successfully cancelled pending pfnStreamInitAsync call (hReqInitAsync=%p).\n", + pStreamEx->hReqInitAsync)); + drvAudioStreamReleaseInternal(pThis, pStreamEx, true /*fMayDestroy*/); + } + else + { + LogFlowFunc(("Failed to cancel pending pfnStreamInitAsync call (hReqInitAsync=%p): %Rrc\n", + pStreamEx->hReqInitAsync, rc2)); + Assert(rc2 == VERR_RT_REQUEST_STATE); + } + } + else + RTCritSectLeave(&pStreamEx->Core.CritSect); + + /* + * Now, if the backend requests asynchronous disabling and destruction + * push the disabling and destroying over to a worker thread. + * + * This is a general offloading feature that all backends should make use of, + * however it's rather precarious on macs where stopping an already draining + * stream may take 8-10ms which naturally isn't something we should be doing + * on an EMT. + */ + if (!(pThis->BackendCfg.fFlags & PDMAUDIOBACKEND_F_ASYNC_STREAM_DESTROY)) + drvAudioStreamDestroyAsync(pThis, pStreamEx, fImmediate); + else + { + int rc2 = RTReqPoolCallEx(pThis->hReqPool, 0 /*cMillies*/, NULL /*phReq*/, + RTREQFLAGS_VOID | RTREQFLAGS_NO_WAIT, + (PFNRT)drvAudioStreamDestroyAsync, 3, pThis, pStreamEx, fImmediate); + LogFlowFunc(("hReqInitAsync=%p rc2=%Rrc\n", pStreamEx->hReqInitAsync, rc2)); + AssertRCStmt(rc2, drvAudioStreamDestroyAsync(pThis, pStreamEx, fImmediate)); + } + } + else + { + AssertLogRelMsgFailedStmt(("%p cRefs=%#x\n", pStreamEx, pStreamEx->cRefs), rc = VERR_CALLER_NO_REFERENCE); + RTCritSectLeave(&pStreamEx->Core.CritSect); /*??*/ + } + } + else + { + AssertLogRelMsgFailedStmt(("%p uMagic=%#x\n", pStreamEx, pStreamEx->uMagic), rc = VERR_INVALID_MAGIC); + RTCritSectLeave(&pStreamEx->Core.CritSect); /*??*/ + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + + +/** + * Drops all audio data (and associated state) of a stream. + * + * Used by drvAudioStreamIterateInternal(), drvAudioStreamResetOnDisable(), and + * drvAudioStreamReInitInternal(). + * + * @param pStreamEx Stream to drop data for. + */ +static void drvAudioStreamResetInternal(PDRVAUDIOSTREAM pStreamEx) +{ + LogFunc(("[%s]\n", pStreamEx->Core.Cfg.szName)); + Assert(RTCritSectIsOwner(&pStreamEx->Core.CritSect)); + + pStreamEx->nsLastIterated = 0; + pStreamEx->nsLastPlayedCaptured = 0; + pStreamEx->nsLastReadWritten = 0; + if (pStreamEx->Core.Cfg.enmDir == PDMAUDIODIR_OUT) + { + pStreamEx->Out.cbPreBuffered = 0; + pStreamEx->Out.offPreBuf = 0; + pStreamEx->Out.enmPlayState = pStreamEx->cbPreBufThreshold > 0 + ? DRVAUDIOPLAYSTATE_PREBUF : DRVAUDIOPLAYSTATE_PLAY; + } + else + pStreamEx->In.enmCaptureState = pStreamEx->cbPreBufThreshold > 0 + ? DRVAUDIOCAPTURESTATE_PREBUF : DRVAUDIOCAPTURESTATE_CAPTURING; +} + + +/** + * Re-initializes an audio stream with its existing host and guest stream + * configuration. + * + * This might be the case if the backend told us we need to re-initialize + * because something on the host side has changed. + * + * @note Does not touch the stream's status flags. + * + * @returns VBox status code. + * @param pThis Pointer to driver instance. + * @param pStreamEx Stream to re-initialize. + */ +static int drvAudioStreamReInitInternal(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx) +{ + char szTmp[RT_MAX(PDMAUDIOSTRMCFGTOSTRING_MAX, DRVAUDIO_STATUS_STR_MAX)]; + LogFlowFunc(("[%s] status: %s\n", pStreamEx->Core.Cfg.szName, drvAudioStreamStatusToStr(szTmp, pStreamEx->fStatus) )); + Assert(RTCritSectIsOwner(&pStreamEx->Core.CritSect)); + RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + + /* + * Destroy and re-create stream on backend side. + */ + if ( (pStreamEx->fStatus & (PDMAUDIOSTREAM_STS_ENABLED | PDMAUDIOSTREAM_STS_BACKEND_CREATED | PDMAUDIOSTREAM_STS_BACKEND_READY)) + == (PDMAUDIOSTREAM_STS_ENABLED | PDMAUDIOSTREAM_STS_BACKEND_CREATED | PDMAUDIOSTREAM_STS_BACKEND_READY)) + drvAudioStreamControlInternalBackend(pThis, pStreamEx, PDMAUDIOSTREAMCMD_DISABLE); + + if (pStreamEx->fStatus & PDMAUDIOSTREAM_STS_BACKEND_CREATED) + drvAudioStreamDestroyInternalBackend(pThis, pStreamEx); + + int rc = VERR_AUDIO_STREAM_NOT_READY; + if (!(pStreamEx->fStatus & PDMAUDIOSTREAM_STS_BACKEND_CREATED)) + { + drvAudioStreamResetInternal(pStreamEx); + + RT_BZERO(pStreamEx->pBackend + 1, pStreamEx->Core.cbBackend - sizeof(*pStreamEx->pBackend)); + + rc = drvAudioStreamCreateInternalBackend(pThis, pStreamEx); + if (RT_SUCCESS(rc)) + { + LogFunc(("Acquired host config: %s\n", PDMAudioStrmCfgToString(&pStreamEx->Core.Cfg, szTmp, sizeof(szTmp)) )); + /** @todo Validate (re-)acquired configuration with pStreamEx->Core.Core.Cfg? + * drvAudioStreamInitInternal() does some setup and a bunch of + * validations + adjustments of the stream config, so this surely is quite + * optimistic. */ + if (true) + { + /* + * Kick off the asynchronous init. + */ + if (!pStreamEx->fNeedAsyncInit) + { + pStreamEx->fStatus |= PDMAUDIOSTREAM_STS_BACKEND_READY; + PDMAUDIOSTREAM_STS_ASSERT_VALID(pStreamEx->fStatus); + } + else + { + drvAudioStreamRetainInternal(pStreamEx); + int rc2 = RTReqPoolCallEx(pThis->hReqPool, 0 /*cMillies*/, &pStreamEx->hReqInitAsync, + RTREQFLAGS_VOID | RTREQFLAGS_NO_WAIT, + (PFNRT)drvAudioStreamInitAsync, 2, pThis, pStreamEx); + LogFlowFunc(("hReqInitAsync=%p rc2=%Rrc\n", pStreamEx->hReqInitAsync, rc2)); + AssertRCStmt(rc2, drvAudioStreamInitAsync(pThis, pStreamEx)); + } + + /* + * Update the backend on the stream state if it's ready, otherwise + * let the worker thread do it after the async init has completed. + */ + if ( (pStreamEx->fStatus & (PDMAUDIOSTREAM_STS_BACKEND_READY | PDMAUDIOSTREAM_STS_BACKEND_CREATED)) + == (PDMAUDIOSTREAM_STS_BACKEND_READY | PDMAUDIOSTREAM_STS_BACKEND_CREATED)) + { + rc = drvAudioStreamUpdateBackendOnStatus(pThis, pStreamEx, "re-initializing"); + /** @todo not sure if we really need to care about this status code... */ + } + else if (pStreamEx->fStatus & PDMAUDIOSTREAM_STS_BACKEND_CREATED) + { + Assert(pStreamEx->hReqInitAsync != NIL_RTREQ); + LogFunc(("Asynchronous stream init (%p) ...\n", pStreamEx->hReqInitAsync)); + } + else + { + LogRel(("Audio: Re-initializing stream '%s' somehow failed, status: %s\n", pStreamEx->Core.Cfg.szName, + drvAudioStreamStatusToStr(szTmp, pStreamEx->fStatus) )); + AssertFailed(); + rc = VERR_AUDIO_STREAM_COULD_NOT_CREATE; + } + } + } + else + LogRel(("Audio: Re-initializing stream '%s' failed with %Rrc\n", pStreamEx->Core.Cfg.szName, rc)); + } + else + { + LogRel(("Audio: Re-initializing stream '%s' failed to destroy previous backend.\n", pStreamEx->Core.Cfg.szName)); + AssertFailed(); + } + + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + LogFunc(("[%s] Returning %Rrc\n", pStreamEx->Core.Cfg.szName, rc)); + return rc; +} + + +/** + * @interface_method_impl{PDMIAUDIOCONNECTOR,pfnStreamReInit} + */ +static DECLCALLBACK(int) drvAudioStreamReInit(PPDMIAUDIOCONNECTOR pInterface, PPDMAUDIOSTREAM pStream) +{ + PDRVAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVAUDIO, IAudioConnector); + PDRVAUDIOSTREAM pStreamEx = (PDRVAUDIOSTREAM)pStream; + AssertPtrReturn(pStreamEx, VERR_INVALID_POINTER); + AssertReturn(pStreamEx->Core.uMagic == PDMAUDIOSTREAM_MAGIC, VERR_INVALID_MAGIC); + AssertReturn(pStreamEx->uMagic == DRVAUDIOSTREAM_MAGIC, VERR_INVALID_MAGIC); + AssertReturn(pStreamEx->fStatus & PDMAUDIOSTREAM_STS_NEED_REINIT, VERR_INVALID_STATE); + LogFlowFunc(("\n")); + + int rc = RTCritSectEnter(&pStreamEx->Core.CritSect); + AssertRCReturn(rc, rc); + + if (pStreamEx->fStatus & PDMAUDIOSTREAM_STS_NEED_REINIT) + { + const unsigned cMaxTries = 5; + const uint64_t nsNow = RTTimeNanoTS(); + + /* Throttle re-initializing streams on failure. */ + if ( pStreamEx->cTriesReInit < cMaxTries + && pStreamEx->hReqInitAsync == NIL_RTREQ + && ( pStreamEx->nsLastReInit == 0 + || nsNow - pStreamEx->nsLastReInit >= RT_NS_1SEC * pStreamEx->cTriesReInit)) + { + rc = drvAudioStreamReInitInternal(pThis, pStreamEx); + if (RT_SUCCESS(rc)) + { + /* Remove the pending re-init flag on success. */ + pStreamEx->fStatus &= ~PDMAUDIOSTREAM_STS_NEED_REINIT; + PDMAUDIOSTREAM_STS_ASSERT_VALID(pStreamEx->fStatus); + } + else + { + pStreamEx->nsLastReInit = nsNow; + pStreamEx->cTriesReInit++; + + /* Did we exceed our tries re-initializing the stream? + * Then this one is dead-in-the-water, so disable it for further use. */ + if (pStreamEx->cTriesReInit >= cMaxTries) + { + LogRel(("Audio: Re-initializing stream '%s' exceeded maximum retries (%u), leaving as disabled\n", + pStreamEx->Core.Cfg.szName, cMaxTries)); + + /* Don't try to re-initialize anymore and mark as disabled. */ + /** @todo should mark it as not-initialized too, shouldn't we? */ + pStreamEx->fStatus &= ~(PDMAUDIOSTREAM_STS_NEED_REINIT | PDMAUDIOSTREAM_STS_ENABLED); + PDMAUDIOSTREAM_STS_ASSERT_VALID(pStreamEx->fStatus); + + /* Note: Further writes to this stream go to / will be read from the bit bucket (/dev/null) from now on. */ + } + } + } + else + Log8Func(("cTriesReInit=%d hReqInitAsync=%p nsLast=%RU64 nsNow=%RU64 nsDelta=%RU64\n", pStreamEx->cTriesReInit, + pStreamEx->hReqInitAsync, pStreamEx->nsLastReInit, nsNow, nsNow - pStreamEx->nsLastReInit)); + +#ifdef LOG_ENABLED + char szStreamSts[DRVAUDIO_STATUS_STR_MAX]; +#endif + Log3Func(("[%s] fStatus=%s\n", pStreamEx->Core.Cfg.szName, drvAudioStreamStatusToStr(szStreamSts, pStreamEx->fStatus))); + } + else + { + AssertFailed(); + rc = VERR_INVALID_STATE; + } + + RTCritSectLeave(&pStreamEx->Core.CritSect); + + LogFlowFuncLeaveRC(rc); + return rc; +} + + +/** + * Internal retain function. + * + * @returns New reference count, UINT32_MAX if bad stream. + * @param pStreamEx The stream to reference. + */ +static uint32_t drvAudioStreamRetainInternal(PDRVAUDIOSTREAM pStreamEx) +{ + AssertPtrReturn(pStreamEx, UINT32_MAX); + AssertReturn(pStreamEx->Core.uMagic == PDMAUDIOSTREAM_MAGIC, UINT32_MAX); + AssertReturn(pStreamEx->uMagic == DRVAUDIOSTREAM_MAGIC, UINT32_MAX); + + uint32_t const cRefs = ASMAtomicIncU32(&pStreamEx->cRefs); + Assert(cRefs > 1); + Assert(cRefs < _1K); + + Log12Func(("returns %u (%s)\n", cRefs, pStreamEx->Core.Cfg.szName)); + return cRefs; +} + + +/** + * @interface_method_impl{PDMIAUDIOCONNECTOR,pfnStreamRetain} + */ +static DECLCALLBACK(uint32_t) drvAudioStreamRetain(PPDMIAUDIOCONNECTOR pInterface, PPDMAUDIOSTREAM pStream) +{ + RT_NOREF(pInterface); + return drvAudioStreamRetainInternal((PDRVAUDIOSTREAM)pStream); +} + + +/** + * @interface_method_impl{PDMIAUDIOCONNECTOR,pfnStreamRelease} + */ +static DECLCALLBACK(uint32_t) drvAudioStreamRelease(PPDMIAUDIOCONNECTOR pInterface, PPDMAUDIOSTREAM pStream) +{ + return drvAudioStreamReleaseInternal(RT_FROM_MEMBER(pInterface, DRVAUDIO, IAudioConnector), + (PDRVAUDIOSTREAM)pStream, + false /*fMayDestroy*/); +} + + +/** + * Controls a stream's backend. + * + * @returns VBox status code. + * @param pThis Pointer to driver instance. + * @param pStreamEx Stream to control. + * @param enmStreamCmd Control command. + * + * @note Caller has entered the critical section of the stream. + * @note Can be called w/o having entered DRVAUDIO::CritSectHotPlug. + */ +static int drvAudioStreamControlInternalBackend(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx, PDMAUDIOSTREAMCMD enmStreamCmd) +{ + AssertPtr(pThis); + AssertPtr(pStreamEx); + Assert(RTCritSectIsOwner(&pStreamEx->Core.CritSect)); + + int rc = RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + AssertRCReturn(rc, rc); + + /* + * Whether to propagate commands down to the backend. + * + * 1. If the stream direction is disabled on the driver level, we should + * obviously not call the backend. Our stream status will reflect the + * actual state so drvAudioEnable() can tell the backend if the user + * re-enables the stream direction. + * + * 2. If the backend hasn't finished initializing yet, don't try call + * it to start/stop/pause/whatever the stream. (Better to do it here + * than to replicate this in the relevant backends.) When the backend + * finish initializing the stream, we'll update it about the stream state. + */ + bool const fDirEnabled = pStreamEx->Core.Cfg.enmDir == PDMAUDIODIR_IN + ? pThis->In.fEnabled : pThis->Out.fEnabled; + PDMHOSTAUDIOSTREAMSTATE const enmBackendState = drvAudioStreamGetBackendState(pThis, pStreamEx); + /* ^^^ (checks pThis->pHostDrvAudio != NULL too) */ + + char szStreamSts[DRVAUDIO_STATUS_STR_MAX]; + LogRel2(("Audio: %s stream '%s' backend (%s is %s; status: %s; backend-status: %s)\n", + PDMAudioStrmCmdGetName(enmStreamCmd), pStreamEx->Core.Cfg.szName, PDMAudioDirGetName(pStreamEx->Core.Cfg.enmDir), + fDirEnabled ? "enabled" : "disabled", drvAudioStreamStatusToStr(szStreamSts, pStreamEx->fStatus), + PDMHostAudioStreamStateGetName(enmBackendState) )); + + if (fDirEnabled) + { + if ( (pStreamEx->fStatus & PDMAUDIOSTREAM_STS_BACKEND_READY /* don't really need this check, do we? */) + && ( enmBackendState == PDMHOSTAUDIOSTREAMSTATE_OKAY + || enmBackendState == PDMHOSTAUDIOSTREAMSTATE_DRAINING) ) + { + switch (enmStreamCmd) + { + case PDMAUDIOSTREAMCMD_ENABLE: + rc = pThis->pHostDrvAudio->pfnStreamEnable(pThis->pHostDrvAudio, pStreamEx->pBackend); + break; + + case PDMAUDIOSTREAMCMD_DISABLE: + rc = pThis->pHostDrvAudio->pfnStreamDisable(pThis->pHostDrvAudio, pStreamEx->pBackend); + break; + + case PDMAUDIOSTREAMCMD_PAUSE: + rc = pThis->pHostDrvAudio->pfnStreamPause(pThis->pHostDrvAudio, pStreamEx->pBackend); + break; + + case PDMAUDIOSTREAMCMD_RESUME: + rc = pThis->pHostDrvAudio->pfnStreamResume(pThis->pHostDrvAudio, pStreamEx->pBackend); + break; + + case PDMAUDIOSTREAMCMD_DRAIN: + if (pThis->pHostDrvAudio->pfnStreamDrain) + rc = pThis->pHostDrvAudio->pfnStreamDrain(pThis->pHostDrvAudio, pStreamEx->pBackend); + else + rc = VERR_NOT_SUPPORTED; + break; + + default: + AssertMsgFailedBreakStmt(("Command %RU32 not implemented\n", enmStreamCmd), rc = VERR_INTERNAL_ERROR_2); + } + if (RT_SUCCESS(rc)) + Log2Func(("[%s] %s succeeded (%Rrc)\n", pStreamEx->Core.Cfg.szName, PDMAudioStrmCmdGetName(enmStreamCmd), rc)); + else + { + LogFunc(("[%s] %s failed with %Rrc\n", pStreamEx->Core.Cfg.szName, PDMAudioStrmCmdGetName(enmStreamCmd), rc)); + if ( rc != VERR_NOT_IMPLEMENTED + && rc != VERR_NOT_SUPPORTED + && rc != VERR_AUDIO_STREAM_NOT_READY) + LogRel(("Audio: %s stream '%s' failed with %Rrc\n", PDMAudioStrmCmdGetName(enmStreamCmd), pStreamEx->Core.Cfg.szName, rc)); + } + } + else + LogFlowFunc(("enmBackendStat(=%s) != OKAY || !(fStatus(=%#x) & BACKEND_READY)\n", + PDMHostAudioStreamStateGetName(enmBackendState), pStreamEx->fStatus)); + } + else + LogFlowFunc(("fDirEnabled=false\n")); + + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + return rc; +} + + +/** + * Resets the given audio stream. + * + * @param pStreamEx Stream to reset. + */ +static void drvAudioStreamResetOnDisable(PDRVAUDIOSTREAM pStreamEx) +{ + drvAudioStreamResetInternal(pStreamEx); + + LogFunc(("[%s]\n", pStreamEx->Core.Cfg.szName)); + + pStreamEx->fStatus &= PDMAUDIOSTREAM_STS_BACKEND_CREATED | PDMAUDIOSTREAM_STS_BACKEND_READY; + pStreamEx->Core.fWarningsShown = PDMAUDIOSTREAM_WARN_FLAGS_NONE; + +#ifdef VBOX_WITH_STATISTICS + /* + * Reset statistics. + */ + if (pStreamEx->Core.Cfg.enmDir == PDMAUDIODIR_IN) + { + } + else if (pStreamEx->Core.Cfg.enmDir == PDMAUDIODIR_OUT) + { + } + else + AssertFailed(); +#endif +} + + +/** + * Controls an audio stream. + * + * @returns VBox status code. + * @param pThis Pointer to driver instance. + * @param pStreamEx Stream to control. + * @param enmStreamCmd Control command. + */ +static int drvAudioStreamControlInternal(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx, PDMAUDIOSTREAMCMD enmStreamCmd) +{ + AssertPtr(pThis); + AssertPtr(pStreamEx); + Assert(RTCritSectIsOwner(&pStreamEx->Core.CritSect)); + +#ifdef LOG_ENABLED + char szStreamSts[DRVAUDIO_STATUS_STR_MAX]; +#endif + LogFunc(("[%s] enmStreamCmd=%s fStatus=%s\n", pStreamEx->Core.Cfg.szName, PDMAudioStrmCmdGetName(enmStreamCmd), + drvAudioStreamStatusToStr(szStreamSts, pStreamEx->fStatus))); + + int rc = VINF_SUCCESS; + + switch (enmStreamCmd) + { + case PDMAUDIOSTREAMCMD_ENABLE: + if (!(pStreamEx->fStatus & PDMAUDIOSTREAM_STS_ENABLED)) + { + /* Are we still draining this stream? Then we must disable it first. */ + if (pStreamEx->fStatus & PDMAUDIOSTREAM_STS_PENDING_DISABLE) + { + LogFunc(("Stream '%s' is still draining - disabling...\n", pStreamEx->Core.Cfg.szName)); + rc = drvAudioStreamControlInternalBackend(pThis, pStreamEx, PDMAUDIOSTREAMCMD_DISABLE); + AssertRC(rc); + if (drvAudioStreamGetBackendState(pThis, pStreamEx) != PDMHOSTAUDIOSTREAMSTATE_DRAINING) + { + pStreamEx->fStatus &= ~(PDMAUDIOSTREAM_STS_ENABLED | PDMAUDIOSTREAM_STS_PENDING_DISABLE); + drvAudioStreamResetInternal(pStreamEx); + rc = VINF_SUCCESS; + } + } + + if (RT_SUCCESS(rc)) + { + /* Reset the state before we try to start. */ + PDMHOSTAUDIOSTREAMSTATE const enmBackendState = drvAudioStreamGetBackendState(pThis, pStreamEx); + pStreamEx->enmLastBackendState = enmBackendState; + pStreamEx->offInternal = 0; + + if (pStreamEx->Core.Cfg.enmDir == PDMAUDIODIR_OUT) + { + pStreamEx->Out.cbPreBuffered = 0; + pStreamEx->Out.offPreBuf = 0; + pStreamEx->Out.enmPlayState = DRVAUDIOPLAYSTATE_NOPLAY; + switch (enmBackendState) + { + case PDMHOSTAUDIOSTREAMSTATE_INITIALIZING: + if (pStreamEx->cbPreBufThreshold > 0) + pStreamEx->Out.enmPlayState = DRVAUDIOPLAYSTATE_PREBUF; + break; + case PDMHOSTAUDIOSTREAMSTATE_DRAINING: + AssertFailed(); + RT_FALL_THROUGH(); + case PDMHOSTAUDIOSTREAMSTATE_OKAY: + pStreamEx->Out.enmPlayState = pStreamEx->cbPreBufThreshold > 0 + ? DRVAUDIOPLAYSTATE_PREBUF : DRVAUDIOPLAYSTATE_PLAY; + break; + case PDMHOSTAUDIOSTREAMSTATE_NOT_WORKING: + case PDMHOSTAUDIOSTREAMSTATE_INACTIVE: + break; + /* no default */ + case PDMHOSTAUDIOSTREAMSTATE_INVALID: + case PDMHOSTAUDIOSTREAMSTATE_END: + case PDMHOSTAUDIOSTREAMSTATE_32BIT_HACK: + break; + } + LogFunc(("ENABLE: enmBackendState=%s enmPlayState=%s\n", PDMHostAudioStreamStateGetName(enmBackendState), + drvAudioPlayStateName(pStreamEx->Out.enmPlayState))); + } + else + { + pStreamEx->In.enmCaptureState = DRVAUDIOCAPTURESTATE_NO_CAPTURE; + switch (enmBackendState) + { + case PDMHOSTAUDIOSTREAMSTATE_INITIALIZING: + pStreamEx->In.enmCaptureState = DRVAUDIOCAPTURESTATE_PREBUF; + break; + case PDMHOSTAUDIOSTREAMSTATE_DRAINING: + AssertFailed(); + RT_FALL_THROUGH(); + case PDMHOSTAUDIOSTREAMSTATE_OKAY: + pStreamEx->In.enmCaptureState = pStreamEx->cbPreBufThreshold > 0 + ? DRVAUDIOCAPTURESTATE_PREBUF : DRVAUDIOCAPTURESTATE_CAPTURING; + break; + case PDMHOSTAUDIOSTREAMSTATE_NOT_WORKING: + case PDMHOSTAUDIOSTREAMSTATE_INACTIVE: + break; + /* no default */ + case PDMHOSTAUDIOSTREAMSTATE_INVALID: + case PDMHOSTAUDIOSTREAMSTATE_END: + case PDMHOSTAUDIOSTREAMSTATE_32BIT_HACK: + break; + } + LogFunc(("ENABLE: enmBackendState=%s enmCaptureState=%s\n", PDMHostAudioStreamStateGetName(enmBackendState), + drvAudioCaptureStateName(pStreamEx->In.enmCaptureState))); + } + + rc = drvAudioStreamControlInternalBackend(pThis, pStreamEx, PDMAUDIOSTREAMCMD_ENABLE); + if (RT_SUCCESS(rc)) + { + pStreamEx->nsStarted = RTTimeNanoTS(); + pStreamEx->fStatus |= PDMAUDIOSTREAM_STS_ENABLED; + PDMAUDIOSTREAM_STS_ASSERT_VALID(pStreamEx->fStatus); + } + } + } + break; + + case PDMAUDIOSTREAMCMD_DISABLE: + if (pStreamEx->fStatus & PDMAUDIOSTREAM_STS_ENABLED) + { + rc = drvAudioStreamControlInternalBackend(pThis, pStreamEx, PDMAUDIOSTREAMCMD_DISABLE); + LogFunc(("DISABLE '%s': Backend DISABLE -> %Rrc\n", pStreamEx->Core.Cfg.szName, rc)); + if (RT_SUCCESS(rc)) /** @todo ignore this and reset it anyway? */ + drvAudioStreamResetOnDisable(pStreamEx); + } + break; + + case PDMAUDIOSTREAMCMD_PAUSE: + if ((pStreamEx->fStatus & (PDMAUDIOSTREAM_STS_ENABLED | PDMAUDIOSTREAM_STS_PAUSED)) == PDMAUDIOSTREAM_STS_ENABLED) + { + rc = drvAudioStreamControlInternalBackend(pThis, pStreamEx, PDMAUDIOSTREAMCMD_PAUSE); + if (RT_SUCCESS(rc)) + { + pStreamEx->fStatus |= PDMAUDIOSTREAM_STS_PAUSED; + PDMAUDIOSTREAM_STS_ASSERT_VALID(pStreamEx->fStatus); + } + } + break; + + case PDMAUDIOSTREAMCMD_RESUME: + if (pStreamEx->fStatus & PDMAUDIOSTREAM_STS_PAUSED) + { + Assert(pStreamEx->fStatus & PDMAUDIOSTREAM_STS_ENABLED); + rc = drvAudioStreamControlInternalBackend(pThis, pStreamEx, PDMAUDIOSTREAMCMD_RESUME); + if (RT_SUCCESS(rc)) + { + pStreamEx->fStatus &= ~PDMAUDIOSTREAM_STS_PAUSED; + PDMAUDIOSTREAM_STS_ASSERT_VALID(pStreamEx->fStatus); + } + } + break; + + case PDMAUDIOSTREAMCMD_DRAIN: + /* + * Only for output streams and we don't want this command more than once. + */ + AssertReturn(pStreamEx->Core.Cfg.enmDir == PDMAUDIODIR_OUT, VERR_INVALID_FUNCTION); + AssertBreak(!(pStreamEx->fStatus & PDMAUDIOSTREAM_STS_PENDING_DISABLE)); + if (pStreamEx->fStatus & PDMAUDIOSTREAM_STS_ENABLED) + { + rc = VERR_INTERNAL_ERROR_2; + switch (pStreamEx->Out.enmPlayState) + { + case DRVAUDIOPLAYSTATE_PREBUF: + if (pStreamEx->Out.cbPreBuffered > 0) + { + LogFunc(("DRAIN '%s': Initiating draining of pre-buffered data...\n", pStreamEx->Core.Cfg.szName)); + pStreamEx->Out.enmPlayState = DRVAUDIOPLAYSTATE_PREBUF_COMMITTING; + pStreamEx->fStatus |= PDMAUDIOSTREAM_STS_PENDING_DISABLE; + PDMAUDIOSTREAM_STS_ASSERT_VALID(pStreamEx->fStatus); + rc = VINF_SUCCESS; + break; + } + RT_FALL_THROUGH(); + case DRVAUDIOPLAYSTATE_NOPLAY: + case DRVAUDIOPLAYSTATE_PREBUF_SWITCHING: + case DRVAUDIOPLAYSTATE_PREBUF_OVERDUE: + LogFunc(("DRAIN '%s': Nothing to drain (enmPlayState=%s)\n", + pStreamEx->Core.Cfg.szName, drvAudioPlayStateName(pStreamEx->Out.enmPlayState))); + rc = drvAudioStreamControlInternalBackend(pThis, pStreamEx, PDMAUDIOSTREAMCMD_DISABLE); + AssertRC(rc); + drvAudioStreamResetOnDisable(pStreamEx); + break; + + case DRVAUDIOPLAYSTATE_PLAY: + case DRVAUDIOPLAYSTATE_PLAY_PREBUF: + LogFunc(("DRAIN '%s': Initiating backend draining (enmPlayState=%s -> NOPLAY) ...\n", + pStreamEx->Core.Cfg.szName, drvAudioPlayStateName(pStreamEx->Out.enmPlayState))); + pStreamEx->Out.enmPlayState = DRVAUDIOPLAYSTATE_NOPLAY; + rc = drvAudioStreamControlInternalBackend(pThis, pStreamEx, PDMAUDIOSTREAMCMD_DRAIN); + if (RT_SUCCESS(rc)) + { + pStreamEx->fStatus |= PDMAUDIOSTREAM_STS_PENDING_DISABLE; + PDMAUDIOSTREAM_STS_ASSERT_VALID(pStreamEx->fStatus); + } + else + { + LogFunc(("DRAIN '%s': Backend DRAIN failed with %Rrc, disabling the stream instead...\n", + pStreamEx->Core.Cfg.szName, rc)); + rc = drvAudioStreamControlInternalBackend(pThis, pStreamEx, PDMAUDIOSTREAMCMD_DISABLE); + AssertRC(rc); + drvAudioStreamResetOnDisable(pStreamEx); + } + break; + + case DRVAUDIOPLAYSTATE_PREBUF_COMMITTING: + LogFunc(("DRAIN '%s': Initiating draining of pre-buffered data (already committing)...\n", + pStreamEx->Core.Cfg.szName)); + pStreamEx->fStatus |= PDMAUDIOSTREAM_STS_PENDING_DISABLE; + PDMAUDIOSTREAM_STS_ASSERT_VALID(pStreamEx->fStatus); + rc = VINF_SUCCESS; + break; + + /* no default */ + case DRVAUDIOPLAYSTATE_INVALID: + case DRVAUDIOPLAYSTATE_END: + AssertFailedBreak(); + } + } + break; + + default: + rc = VERR_NOT_IMPLEMENTED; + break; + } + + if (RT_FAILURE(rc)) + LogFunc(("[%s] Failed with %Rrc\n", pStreamEx->Core.Cfg.szName, rc)); + + return rc; +} + + +/** + * @interface_method_impl{PDMIAUDIOCONNECTOR,pfnStreamControl} + */ +static DECLCALLBACK(int) drvAudioStreamControl(PPDMIAUDIOCONNECTOR pInterface, + PPDMAUDIOSTREAM pStream, PDMAUDIOSTREAMCMD enmStreamCmd) +{ + PDRVAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVAUDIO, IAudioConnector); + AssertPtr(pThis); + + /** @todo r=bird: why? It's not documented to ignore NULL streams. */ + if (!pStream) + return VINF_SUCCESS; + PDRVAUDIOSTREAM pStreamEx = (PDRVAUDIOSTREAM)pStream; + AssertPtrReturn(pStreamEx, VERR_INVALID_POINTER); + AssertReturn(pStreamEx->Core.uMagic == PDMAUDIOSTREAM_MAGIC, VERR_INVALID_MAGIC); + AssertReturn(pStreamEx->uMagic == DRVAUDIOSTREAM_MAGIC, VERR_INVALID_MAGIC); + + int rc = RTCritSectEnter(&pStreamEx->Core.CritSect); + AssertRCReturn(rc, rc); + + LogFlowFunc(("[%s] enmStreamCmd=%s\n", pStreamEx->Core.Cfg.szName, PDMAudioStrmCmdGetName(enmStreamCmd))); + + rc = drvAudioStreamControlInternal(pThis, pStreamEx, enmStreamCmd); + + RTCritSectLeave(&pStreamEx->Core.CritSect); + return rc; +} + + +/** + * Copy data to the pre-buffer, ring-buffer style. + * + * The @a cbMax parameter is almost always set to the threshold size, the + * exception is when commiting the buffer and we want to top it off to reduce + * the number of transfers to the backend (the first transfer may start + * playback, so more data is better). + */ +static int drvAudioStreamPreBuffer(PDRVAUDIOSTREAM pStreamEx, const uint8_t *pbBuf, uint32_t cbBuf, uint32_t cbMax) +{ + uint32_t const cbAlloc = pStreamEx->Out.cbPreBufAlloc; + AssertReturn(cbAlloc >= cbMax, VERR_INTERNAL_ERROR_3); + AssertReturn(cbAlloc >= 8, VERR_INTERNAL_ERROR_4); + AssertReturn(cbMax >= 8, VERR_INTERNAL_ERROR_5); + + uint32_t offRead = pStreamEx->Out.offPreBuf; + uint32_t cbCur = pStreamEx->Out.cbPreBuffered; + AssertStmt(offRead < cbAlloc, offRead %= cbAlloc); + AssertStmt(cbCur <= cbMax, offRead = (offRead + cbCur - cbMax) % cbAlloc; cbCur = cbMax); + + /* + * First chunk. + */ + uint32_t offWrite = (offRead + cbCur) % cbAlloc; + uint32_t cbToCopy = RT_MIN(cbAlloc - offWrite, cbBuf); + memcpy(&pStreamEx->Out.pbPreBuf[offWrite], pbBuf, cbToCopy); + + /* Advance. */ + offWrite = (offWrite + cbToCopy) % cbAlloc; + for (;;) + { + pbBuf += cbToCopy; + cbCur += cbToCopy; + if (cbCur > cbMax) + offRead = (offRead + cbCur - cbMax) % cbAlloc; + cbBuf -= cbToCopy; + if (!cbBuf) + break; + + /* + * Second+ chunk, from the start of the buffer. + * + * Note! It is assumed very unlikely that we will ever see a cbBuf larger than + * cbMax, so we don't waste space on clipping cbBuf here (can happen with + * custom pre-buffer sizes). + */ + Assert(offWrite == 0); + cbToCopy = RT_MIN(cbAlloc, cbBuf); + memcpy(pStreamEx->Out.pbPreBuf, pbBuf, cbToCopy); + } + + /* + * Update the pre-buffering size and position. + */ + pStreamEx->Out.cbPreBuffered = RT_MIN(cbCur, cbMax); + pStreamEx->Out.offPreBuf = offRead; + return VINF_SUCCESS; +} + + +/** + * Worker for drvAudioStreamPlay() and drvAudioStreamPreBufComitting(). + * + * Caller owns the lock. + */ +static int drvAudioStreamPlayLocked(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx, + const uint8_t *pbBuf, uint32_t cbBuf, uint32_t *pcbWritten) +{ + Log3Func(("%s: @%#RX64: cbBuf=%#x\n", pStreamEx->Core.Cfg.szName, pStreamEx->offInternal, cbBuf)); + + uint32_t cbWritable = pThis->pHostDrvAudio->pfnStreamGetWritable(pThis->pHostDrvAudio, pStreamEx->pBackend); + pStreamEx->Out.Stats.cbBackendWritableBefore = cbWritable; + + uint32_t cbWritten = 0; + int rc = VINF_SUCCESS; + uint8_t const cbFrame = PDMAudioPropsFrameSize(&pStreamEx->Core.Cfg.Props); + while (cbBuf >= cbFrame && cbWritable >= cbFrame) + { + uint32_t const cbToWrite = PDMAudioPropsFloorBytesToFrame(&pStreamEx->Core.Cfg.Props, RT_MIN(cbBuf, cbWritable)); + uint32_t cbWrittenNow = 0; + rc = pThis->pHostDrvAudio->pfnStreamPlay(pThis->pHostDrvAudio, pStreamEx->pBackend, pbBuf, cbToWrite, &cbWrittenNow); + if (RT_SUCCESS(rc)) + { + if (cbWrittenNow != cbToWrite) + Log3Func(("%s: @%#RX64: Wrote fewer bytes than requested: %#x, requested %#x\n", + pStreamEx->Core.Cfg.szName, pStreamEx->offInternal, cbWrittenNow, cbToWrite)); +#ifdef DEBUG_bird + Assert(cbWrittenNow == cbToWrite); +#endif + AssertStmt(cbWrittenNow <= cbToWrite, cbWrittenNow = cbToWrite); + cbWritten += cbWrittenNow; + cbBuf -= cbWrittenNow; + pbBuf += cbWrittenNow; + pStreamEx->offInternal += cbWrittenNow; + } + else + { + *pcbWritten = cbWritten; + LogFunc(("%s: @%#RX64: pfnStreamPlay failed writing %#x bytes (%#x previous written, %#x writable): %Rrc\n", + pStreamEx->Core.Cfg.szName, pStreamEx->offInternal, cbToWrite, cbWritten, cbWritable, rc)); + return cbWritten ? VINF_SUCCESS : rc; + } + + cbWritable = pThis->pHostDrvAudio->pfnStreamGetWritable(pThis->pHostDrvAudio, pStreamEx->pBackend); + } + + STAM_PROFILE_ADD_PERIOD(&pStreamEx->StatXfer, cbWritten); + *pcbWritten = cbWritten; + pStreamEx->Out.Stats.cbBackendWritableAfter = cbWritable; + if (cbWritten) + pStreamEx->nsLastPlayedCaptured = RTTimeNanoTS(); + + Log3Func(("%s: @%#RX64: Wrote %#x bytes (%#x bytes left)\n", pStreamEx->Core.Cfg.szName, pStreamEx->offInternal, cbWritten, cbBuf)); + return rc; +} + + +/** + * Worker for drvAudioStreamPlay() and drvAudioStreamPreBufComitting(). + */ +static int drvAudioStreamPlayToPreBuffer(PDRVAUDIOSTREAM pStreamEx, const void *pvBuf, uint32_t cbBuf, uint32_t cbMax, + uint32_t *pcbWritten) +{ + int rc = drvAudioStreamPreBuffer(pStreamEx, (uint8_t const *)pvBuf, cbBuf, cbMax); + if (RT_SUCCESS(rc)) + { + *pcbWritten = cbBuf; + pStreamEx->offInternal += cbBuf; + Log3Func(("[%s] Pre-buffering (%s): wrote %#x bytes => %#x bytes / %u%%\n", + pStreamEx->Core.Cfg.szName, drvAudioPlayStateName(pStreamEx->Out.enmPlayState), cbBuf, pStreamEx->Out.cbPreBuffered, + pStreamEx->Out.cbPreBuffered * 100 / RT_MAX(pStreamEx->cbPreBufThreshold, 1))); + + } + else + *pcbWritten = 0; + return rc; +} + + +/** + * Used when we're committing (transfering) the pre-buffered bytes to the + * device. + * + * This is called both from drvAudioStreamPlay() and + * drvAudioStreamIterateInternal(). + * + * @returns VBox status code. + * @param pThis Pointer to the DrvAudio instance data. + * @param pStreamEx The stream to commit the pre-buffering for. + * @param pbBuf Buffer with new bytes to write. Can be NULL when called + * in the PENDING_DISABLE state from + * drvAudioStreamIterateInternal(). + * @param cbBuf Number of new bytes. Can be zero. + * @param pcbWritten Where to return the number of bytes written. + * + * @note Locking: Stream critsect and hot-plug in shared mode. + */ +static int drvAudioStreamPreBufComitting(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx, + const uint8_t *pbBuf, uint32_t cbBuf, uint32_t *pcbWritten) +{ + /* + * First, top up the buffer with new data from pbBuf. + */ + *pcbWritten = 0; + if (cbBuf > 0) + { + uint32_t const cbToCopy = RT_MIN(pStreamEx->Out.cbPreBufAlloc - pStreamEx->Out.cbPreBuffered, cbBuf); + if (cbToCopy > 0) + { + int rc = drvAudioStreamPlayToPreBuffer(pStreamEx, pbBuf, cbBuf, pStreamEx->Out.cbPreBufAlloc, pcbWritten); + AssertRCReturn(rc, rc); + pbBuf += cbToCopy; + cbBuf -= cbToCopy; + } + } + + AssertReturn(pThis->pHostDrvAudio, VERR_AUDIO_BACKEND_NOT_ATTACHED); + + /* + * Write the pre-buffered chunk. + */ + int rc = VINF_SUCCESS; + uint32_t const cbAlloc = pStreamEx->Out.cbPreBufAlloc; + AssertReturn(cbAlloc > 0, VERR_INTERNAL_ERROR_2); + uint32_t off = pStreamEx->Out.offPreBuf; + AssertStmt(off < pStreamEx->Out.cbPreBufAlloc, off %= cbAlloc); + uint32_t cbLeft = pStreamEx->Out.cbPreBuffered; + while (cbLeft > 0) + { + uint32_t const cbToWrite = RT_MIN(cbAlloc - off, cbLeft); + Assert(cbToWrite > 0); + + uint32_t cbPreBufWritten = 0; + rc = pThis->pHostDrvAudio->pfnStreamPlay(pThis->pHostDrvAudio, pStreamEx->pBackend, &pStreamEx->Out.pbPreBuf[off], + cbToWrite, &cbPreBufWritten); + AssertRCBreak(rc); + if (!cbPreBufWritten) + break; + AssertStmt(cbPreBufWritten <= cbToWrite, cbPreBufWritten = cbToWrite); + off = (off + cbPreBufWritten) % cbAlloc; + cbLeft -= cbPreBufWritten; + } + + if (cbLeft == 0) + { + LogFunc(("@%#RX64: Wrote all %#x bytes of pre-buffered audio data. %s -> PLAY\n", pStreamEx->offInternal, + pStreamEx->Out.cbPreBuffered, drvAudioPlayStateName(pStreamEx->Out.enmPlayState) )); + pStreamEx->Out.cbPreBuffered = 0; + pStreamEx->Out.offPreBuf = 0; + pStreamEx->Out.enmPlayState = DRVAUDIOPLAYSTATE_PLAY; + + if (cbBuf > 0) + { + uint32_t cbWritten2 = 0; + rc = drvAudioStreamPlayLocked(pThis, pStreamEx, pbBuf, cbBuf, &cbWritten2); + if (RT_SUCCESS(rc)) + *pcbWritten += cbWritten2; + } + else + pStreamEx->nsLastPlayedCaptured = RTTimeNanoTS(); + } + else + { + if (cbLeft != pStreamEx->Out.cbPreBuffered) + pStreamEx->nsLastPlayedCaptured = RTTimeNanoTS(); + + LogRel2(("Audio: @%#RX64: Stream '%s' pre-buffering commit problem: wrote %#x out of %#x + %#x - rc=%Rrc *pcbWritten=%#x %s -> PREBUF_COMMITTING\n", + pStreamEx->offInternal, pStreamEx->Core.Cfg.szName, pStreamEx->Out.cbPreBuffered - cbLeft, + pStreamEx->Out.cbPreBuffered, cbBuf, rc, *pcbWritten, drvAudioPlayStateName(pStreamEx->Out.enmPlayState) )); + AssertMsg( pStreamEx->Out.enmPlayState == DRVAUDIOPLAYSTATE_PREBUF_COMMITTING + || pStreamEx->Out.enmPlayState == DRVAUDIOPLAYSTATE_PREBUF + || RT_FAILURE(rc), + ("Buggy host driver buffer reporting? cbLeft=%#x cbPreBuffered=%#x enmPlayState=%s\n", + cbLeft, pStreamEx->Out.cbPreBuffered, drvAudioPlayStateName(pStreamEx->Out.enmPlayState) )); + + pStreamEx->Out.cbPreBuffered = cbLeft; + pStreamEx->Out.offPreBuf = off; + pStreamEx->Out.enmPlayState = DRVAUDIOPLAYSTATE_PREBUF_COMMITTING; + } + + return *pcbWritten ? VINF_SUCCESS : rc; +} + + +/** + * Does one iteration of an audio stream. + * + * This function gives the backend the chance of iterating / altering data and + * does the actual mixing between the guest <-> host mixing buffers. + * + * @returns VBox status code. + * @param pThis Pointer to driver instance. + * @param pStreamEx Stream to iterate. + */ +static int drvAudioStreamIterateInternal(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx) +{ + AssertPtrReturn(pThis, VERR_INVALID_POINTER); + +#ifdef LOG_ENABLED + char szStreamSts[DRVAUDIO_STATUS_STR_MAX]; +#endif + Log3Func(("[%s] fStatus=%s\n", pStreamEx->Core.Cfg.szName, drvAudioStreamStatusToStr(szStreamSts, pStreamEx->fStatus))); + + /* Not enabled or paused? Skip iteration. */ + if ((pStreamEx->fStatus & (PDMAUDIOSTREAM_STS_ENABLED | PDMAUDIOSTREAM_STS_PAUSED)) != PDMAUDIOSTREAM_STS_ENABLED) + return VINF_SUCCESS; + + /* + * Pending disable is really what we're here for. + * + * This only happens to output streams. We ASSUME the caller (MixerBuffer) + * implements a timeout on the draining, so we skip that here. + */ + if (!(pStreamEx->fStatus & PDMAUDIOSTREAM_STS_PENDING_DISABLE)) + { /* likely until we get to the end of the stream at least. */ } + else + { + AssertReturn(pStreamEx->Core.Cfg.enmDir == PDMAUDIODIR_OUT, VINF_SUCCESS); + RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + + /* + * Move pre-buffered samples to the backend. + */ + if (pStreamEx->Out.enmPlayState == DRVAUDIOPLAYSTATE_PREBUF_COMMITTING) + { + if (pStreamEx->Out.cbPreBuffered > 0) + { + uint32_t cbIgnored = 0; + drvAudioStreamPreBufComitting(pThis, pStreamEx, NULL, 0, &cbIgnored); + Log3Func(("Stream '%s': Transferred %#x bytes\n", pStreamEx->Core.Cfg.szName, cbIgnored)); + } + if (pStreamEx->Out.cbPreBuffered == 0) + { + Log3Func(("Stream '%s': No more pre-buffered data -> NOPLAY + backend DRAIN\n", pStreamEx->Core.Cfg.szName)); + pStreamEx->Out.enmPlayState = DRVAUDIOPLAYSTATE_NOPLAY; + + int rc = drvAudioStreamControlInternalBackend(pThis, pStreamEx, PDMAUDIOSTREAMCMD_DRAIN); + if (RT_FAILURE(rc)) + { + LogFunc(("Stream '%s': Backend DRAIN failed with %Rrc, disabling the stream instead...\n", + pStreamEx->Core.Cfg.szName, rc)); + rc = drvAudioStreamControlInternalBackend(pThis, pStreamEx, PDMAUDIOSTREAMCMD_DISABLE); + AssertRC(rc); + drvAudioStreamResetOnDisable(pStreamEx); + } + } + } + else + Assert(pStreamEx->Out.enmPlayState == DRVAUDIOPLAYSTATE_NOPLAY); + + /* + * Check the backend status to see if it's still draining and to + * update our status when it stops doing so. + */ + PDMHOSTAUDIOSTREAMSTATE const enmBackendState = drvAudioStreamGetBackendState(pThis, pStreamEx); + if (enmBackendState == PDMHOSTAUDIOSTREAMSTATE_DRAINING) + { + uint32_t cbIgnored = 0; + pThis->pHostDrvAudio->pfnStreamPlay(pThis->pHostDrvAudio, pStreamEx->pBackend, NULL, 0, &cbIgnored); + } + else + { + LogFunc(("Stream '%s': Backend finished draining.\n", pStreamEx->Core.Cfg.szName)); + drvAudioStreamResetOnDisable(pStreamEx); + } + + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + } + + /* Update timestamps. */ + pStreamEx->nsLastIterated = RTTimeNanoTS(); + + return VINF_SUCCESS; /** @todo r=bird: What can the caller do with an error status here? */ +} + + +/** + * @interface_method_impl{PDMIAUDIOCONNECTOR,pfnStreamIterate} + */ +static DECLCALLBACK(int) drvAudioStreamIterate(PPDMIAUDIOCONNECTOR pInterface, PPDMAUDIOSTREAM pStream) +{ + PDRVAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVAUDIO, IAudioConnector); + AssertPtr(pThis); + PDRVAUDIOSTREAM pStreamEx = (PDRVAUDIOSTREAM)pStream; + AssertPtrReturn(pStreamEx, VERR_INVALID_POINTER); + AssertReturn(pStreamEx->Core.uMagic == PDMAUDIOSTREAM_MAGIC, VERR_INVALID_MAGIC); + AssertReturn(pStreamEx->uMagic == DRVAUDIOSTREAM_MAGIC, VERR_INVALID_MAGIC); + + int rc = RTCritSectEnter(&pStreamEx->Core.CritSect); + AssertRCReturn(rc, rc); + + rc = drvAudioStreamIterateInternal(pThis, pStreamEx); + + RTCritSectLeave(&pStreamEx->Core.CritSect); + + if (RT_FAILURE(rc)) + LogFlowFuncLeaveRC(rc); + return rc; +} + + +/** + * @interface_method_impl{PDMIAUDIOCONNECTOR,pfnStreamGetState} + */ +static DECLCALLBACK(PDMAUDIOSTREAMSTATE) drvAudioStreamGetState(PPDMIAUDIOCONNECTOR pInterface, PPDMAUDIOSTREAM pStream) +{ + PDRVAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVAUDIO, IAudioConnector); + PDRVAUDIOSTREAM pStreamEx = (PDRVAUDIOSTREAM)pStream; + AssertPtrReturn(pStreamEx, PDMAUDIOSTREAMSTATE_INVALID); + AssertReturn(pStreamEx->Core.uMagic == PDMAUDIOSTREAM_MAGIC, PDMAUDIOSTREAMSTATE_INVALID); + AssertReturn(pStreamEx->uMagic == DRVAUDIOSTREAM_MAGIC, PDMAUDIOSTREAMSTATE_INVALID); + STAM_PROFILE_START(&pStreamEx->StatProfGetState, a); + + /* + * Get the status mask. + */ + int rc = RTCritSectEnter(&pStreamEx->Core.CritSect); + AssertRCReturn(rc, PDMAUDIOSTREAMSTATE_INVALID); + RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + + PDMHOSTAUDIOSTREAMSTATE const enmBackendState = drvAudioStreamGetBackendStateAndProcessChanges(pThis, pStreamEx); + uint32_t const fStrmStatus = pStreamEx->fStatus; + PDMAUDIODIR const enmDir = pStreamEx->Core.Cfg.enmDir; + Assert(enmDir == PDMAUDIODIR_IN || enmDir == PDMAUDIODIR_OUT); + + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + RTCritSectLeave(&pStreamEx->Core.CritSect); + + /* + * Translate it to state enum value. + */ + PDMAUDIOSTREAMSTATE enmState; + if (!(fStrmStatus & PDMAUDIOSTREAM_STS_NEED_REINIT)) + { + if (fStrmStatus & PDMAUDIOSTREAM_STS_BACKEND_CREATED) + { + if ( (fStrmStatus & PDMAUDIOSTREAM_STS_ENABLED) + && (enmDir == PDMAUDIODIR_IN ? pThis->In.fEnabled : pThis->Out.fEnabled) + && ( enmBackendState == PDMHOSTAUDIOSTREAMSTATE_OKAY + || enmBackendState == PDMHOSTAUDIOSTREAMSTATE_DRAINING + || enmBackendState == PDMHOSTAUDIOSTREAMSTATE_INITIALIZING )) + enmState = enmDir == PDMAUDIODIR_IN ? PDMAUDIOSTREAMSTATE_ENABLED_READABLE : PDMAUDIOSTREAMSTATE_ENABLED_WRITABLE; + else + enmState = PDMAUDIOSTREAMSTATE_INACTIVE; + } + else + enmState = PDMAUDIOSTREAMSTATE_NOT_WORKING; + } + else + enmState = PDMAUDIOSTREAMSTATE_NEED_REINIT; + + STAM_PROFILE_STOP(&pStreamEx->StatProfGetState, a); +#ifdef LOG_ENABLED + char szStreamSts[DRVAUDIO_STATUS_STR_MAX]; +#endif + Log3Func(("[%s] returns %s (status: %s)\n", pStreamEx->Core.Cfg.szName, PDMAudioStreamStateGetName(enmState), + drvAudioStreamStatusToStr(szStreamSts, fStrmStatus))); + return enmState; +} + + +/** + * @interface_method_impl{PDMIAUDIOCONNECTOR,pfnStreamGetWritable} + */ +static DECLCALLBACK(uint32_t) drvAudioStreamGetWritable(PPDMIAUDIOCONNECTOR pInterface, PPDMAUDIOSTREAM pStream) +{ + PDRVAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVAUDIO, IAudioConnector); + AssertPtr(pThis); + PDRVAUDIOSTREAM pStreamEx = (PDRVAUDIOSTREAM)pStream; + AssertPtrReturn(pStreamEx, 0); + AssertReturn(pStreamEx->Core.uMagic == PDMAUDIOSTREAM_MAGIC, 0); + AssertReturn(pStreamEx->uMagic == DRVAUDIOSTREAM_MAGIC, 0); + AssertMsgReturn(pStreamEx->Core.Cfg.enmDir == PDMAUDIODIR_OUT, ("Can't write to a non-output stream\n"), 0); + STAM_PROFILE_START(&pStreamEx->Out.Stats.ProfGetWritable, a); + + int rc = RTCritSectEnter(&pStreamEx->Core.CritSect); + AssertRCReturn(rc, 0); + RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + + /* + * Use the playback and backend states to determin how much can be written, if anything. + */ + uint32_t cbWritable = 0; + DRVAUDIOPLAYSTATE const enmPlayMode = pStreamEx->Out.enmPlayState; + PDMHOSTAUDIOSTREAMSTATE const enmBackendState = drvAudioStreamGetBackendState(pThis, pStreamEx); + if ( PDMAudioStrmStatusCanWrite(pStreamEx->fStatus) + && pThis->pHostDrvAudio != NULL + && enmBackendState != PDMHOSTAUDIOSTREAMSTATE_DRAINING) + { + switch (enmPlayMode) + { + /* + * Whatever the backend can hold. + */ + case DRVAUDIOPLAYSTATE_PLAY: + case DRVAUDIOPLAYSTATE_PLAY_PREBUF: + Assert(pStreamEx->fStatus & PDMAUDIOSTREAM_STS_BACKEND_READY); + Assert(enmBackendState == PDMHOSTAUDIOSTREAMSTATE_OKAY /* potential unplug race */); + cbWritable = pThis->pHostDrvAudio->pfnStreamGetWritable(pThis->pHostDrvAudio, pStreamEx->pBackend); + break; + + /* + * Whatever we've got of available space in the pre-buffer. + * Note! For the last round when we pass the pre-buffering threshold, we may + * report fewer bytes than what a DMA timer period for the guest device + * typically produces, however that should be transfered in the following + * round that goes directly to the backend buffer. + */ + case DRVAUDIOPLAYSTATE_PREBUF: + cbWritable = pStreamEx->Out.cbPreBufAlloc - pStreamEx->Out.cbPreBuffered; + if (!cbWritable) + cbWritable = PDMAudioPropsFramesToBytes(&pStreamEx->Core.Cfg.Props, 2); + break; + + /* + * These are slightly more problematic and can go wrong if the pre-buffer is + * manually configured to be smaller than the output of a typeical DMA timer + * period for the guest device. So, to overcompensate, we just report back + * the backend buffer size (the pre-buffer is circular, so no overflow issue). + */ + case DRVAUDIOPLAYSTATE_PREBUF_OVERDUE: + case DRVAUDIOPLAYSTATE_PREBUF_SWITCHING: + cbWritable = PDMAudioPropsFramesToBytes(&pStreamEx->Core.Cfg.Props, + RT_MAX(pStreamEx->Core.Cfg.Backend.cFramesBufferSize, + pStreamEx->Core.Cfg.Backend.cFramesPreBuffering)); + break; + + case DRVAUDIOPLAYSTATE_PREBUF_COMMITTING: + { + /* Buggy backend: We weren't able to copy all the pre-buffered data to it + when reaching the threshold. Try escape this situation, or at least + keep the extra buffering to a minimum. We must try write something + as long as there is space for it, as we need the pfnStreamWrite call + to move the data. */ + Assert(pStreamEx->fStatus & PDMAUDIOSTREAM_STS_BACKEND_READY); + Assert(enmBackendState == PDMHOSTAUDIOSTREAMSTATE_OKAY /* potential unplug race */); + uint32_t const cbMin = PDMAudioPropsFramesToBytes(&pStreamEx->Core.Cfg.Props, 8); + cbWritable = pThis->pHostDrvAudio->pfnStreamGetWritable(pThis->pHostDrvAudio, pStreamEx->pBackend); + if (cbWritable >= pStreamEx->Out.cbPreBuffered + cbMin) + cbWritable -= pStreamEx->Out.cbPreBuffered + cbMin / 2; + else + cbWritable = RT_MIN(cbMin, pStreamEx->Out.cbPreBufAlloc - pStreamEx->Out.cbPreBuffered); + AssertLogRel(cbWritable); + break; + } + + case DRVAUDIOPLAYSTATE_NOPLAY: + break; + case DRVAUDIOPLAYSTATE_INVALID: + case DRVAUDIOPLAYSTATE_END: + AssertFailed(); + break; + } + + /* Make sure to align the writable size to the host's frame size. */ + cbWritable = PDMAudioPropsFloorBytesToFrame(&pStreamEx->Core.Cfg.Props, cbWritable); + } + + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + STAM_PROFILE_ADD_PERIOD(&pStreamEx->Out.Stats.ProfGetWritableBytes, cbWritable); + STAM_PROFILE_STOP(&pStreamEx->Out.Stats.ProfGetWritable, a); + RTCritSectLeave(&pStreamEx->Core.CritSect); + Log3Func(("[%s] cbWritable=%#RX32 (%RU64ms) enmPlayMode=%s enmBackendState=%s\n", + pStreamEx->Core.Cfg.szName, cbWritable, PDMAudioPropsBytesToMilli(&pStreamEx->Core.Cfg.Props, cbWritable), + drvAudioPlayStateName(enmPlayMode), PDMHostAudioStreamStateGetName(enmBackendState) )); + return cbWritable; +} + + +/** + * @interface_method_impl{PDMIAUDIOCONNECTOR,pfnStreamPlay} + */ +static DECLCALLBACK(int) drvAudioStreamPlay(PPDMIAUDIOCONNECTOR pInterface, PPDMAUDIOSTREAM pStream, + const void *pvBuf, uint32_t cbBuf, uint32_t *pcbWritten) +{ + PDRVAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVAUDIO, IAudioConnector); + AssertPtr(pThis); + + /* + * Check input and sanity. + */ + AssertPtrReturn(pInterface, VERR_INVALID_POINTER); + PDRVAUDIOSTREAM pStreamEx = (PDRVAUDIOSTREAM)pStream; + AssertPtrReturn(pStreamEx, VERR_INVALID_POINTER); + AssertPtrReturn(pvBuf, VERR_INVALID_POINTER); + AssertReturn(cbBuf, VERR_INVALID_PARAMETER); + uint32_t uTmp; + if (pcbWritten) + AssertPtrReturn(pcbWritten, VERR_INVALID_PARAMETER); + else + pcbWritten = &uTmp; + + AssertReturn(pStreamEx->Core.uMagic == PDMAUDIOSTREAM_MAGIC, VERR_INVALID_MAGIC); + AssertReturn(pStreamEx->uMagic == DRVAUDIOSTREAM_MAGIC, VERR_INVALID_MAGIC); + AssertMsgReturn(pStreamEx->Core.Cfg.enmDir == PDMAUDIODIR_OUT, + ("Stream '%s' is not an output stream and therefore cannot be written to (direction is '%s')\n", + pStreamEx->Core.Cfg.szName, PDMAudioDirGetName(pStreamEx->Core.Cfg.enmDir)), VERR_ACCESS_DENIED); + + AssertMsg(PDMAudioPropsIsSizeAligned(&pStreamEx->Core.Cfg.Props, cbBuf), + ("Stream '%s' got a non-frame-aligned write (%#RX32 bytes)\n", pStreamEx->Core.Cfg.szName, cbBuf)); + STAM_PROFILE_START(&pStreamEx->Out.Stats.ProfPlay, a); + + int rc = RTCritSectEnter(&pStreamEx->Core.CritSect); + AssertRCReturn(rc, rc); + + /* + * First check that we can write to the stream, and if not, + * whether to just drop the input into the bit bucket. + */ + if (PDMAudioStrmStatusIsReady(pStreamEx->fStatus)) + { + RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + if ( pThis->Out.fEnabled /* (see @bugref{9882}) */ + && pThis->pHostDrvAudio != NULL) + { + /* + * Get the backend state and process changes to it since last time we checked. + */ + PDMHOSTAUDIOSTREAMSTATE const enmBackendState = drvAudioStreamGetBackendStateAndProcessChanges(pThis, pStreamEx); + + /* + * Do the transfering. + */ + switch (pStreamEx->Out.enmPlayState) + { + case DRVAUDIOPLAYSTATE_PLAY: + Assert(pStreamEx->fStatus & PDMAUDIOSTREAM_STS_BACKEND_READY); + Assert(enmBackendState == PDMHOSTAUDIOSTREAMSTATE_OKAY); + rc = drvAudioStreamPlayLocked(pThis, pStreamEx, (uint8_t const *)pvBuf, cbBuf, pcbWritten); + break; + + case DRVAUDIOPLAYSTATE_PLAY_PREBUF: + Assert(pStreamEx->fStatus & PDMAUDIOSTREAM_STS_BACKEND_READY); + Assert(enmBackendState == PDMHOSTAUDIOSTREAMSTATE_OKAY); + rc = drvAudioStreamPlayLocked(pThis, pStreamEx, (uint8_t const *)pvBuf, cbBuf, pcbWritten); + drvAudioStreamPreBuffer(pStreamEx, (uint8_t const *)pvBuf, *pcbWritten, pStreamEx->cbPreBufThreshold); + break; + + case DRVAUDIOPLAYSTATE_PREBUF: + if (cbBuf + pStreamEx->Out.cbPreBuffered < pStreamEx->cbPreBufThreshold) + rc = drvAudioStreamPlayToPreBuffer(pStreamEx, pvBuf, cbBuf, pStreamEx->cbPreBufThreshold, pcbWritten); + else if ( enmBackendState == PDMHOSTAUDIOSTREAMSTATE_OKAY + && (pStreamEx->fStatus & PDMAUDIOSTREAM_STS_BACKEND_READY)) + { + Log3Func(("[%s] Pre-buffering completing: cbBuf=%#x cbPreBuffered=%#x => %#x vs cbPreBufThreshold=%#x\n", + pStreamEx->Core.Cfg.szName, cbBuf, pStreamEx->Out.cbPreBuffered, + cbBuf + pStreamEx->Out.cbPreBuffered, pStreamEx->cbPreBufThreshold)); + rc = drvAudioStreamPreBufComitting(pThis, pStreamEx, (uint8_t const *)pvBuf, cbBuf, pcbWritten); + } + else + { + Log3Func(("[%s] Pre-buffering completing but device not ready: cbBuf=%#x cbPreBuffered=%#x => %#x vs cbPreBufThreshold=%#x; PREBUF -> PREBUF_OVERDUE\n", + pStreamEx->Core.Cfg.szName, cbBuf, pStreamEx->Out.cbPreBuffered, + cbBuf + pStreamEx->Out.cbPreBuffered, pStreamEx->cbPreBufThreshold)); + pStreamEx->Out.enmPlayState = DRVAUDIOPLAYSTATE_PREBUF_OVERDUE; + rc = drvAudioStreamPlayToPreBuffer(pStreamEx, pvBuf, cbBuf, pStreamEx->cbPreBufThreshold, pcbWritten); + } + break; + + case DRVAUDIOPLAYSTATE_PREBUF_OVERDUE: + Assert( !(pStreamEx->fStatus & PDMAUDIOSTREAM_STS_BACKEND_READY) + || enmBackendState != PDMHOSTAUDIOSTREAMSTATE_OKAY); + RT_FALL_THRU(); + case DRVAUDIOPLAYSTATE_PREBUF_SWITCHING: + rc = drvAudioStreamPlayToPreBuffer(pStreamEx, pvBuf, cbBuf, pStreamEx->cbPreBufThreshold, pcbWritten); + break; + + case DRVAUDIOPLAYSTATE_PREBUF_COMMITTING: + Assert(pStreamEx->fStatus & PDMAUDIOSTREAM_STS_BACKEND_READY); + Assert(enmBackendState == PDMHOSTAUDIOSTREAMSTATE_OKAY); + rc = drvAudioStreamPreBufComitting(pThis, pStreamEx, (uint8_t const *)pvBuf, cbBuf, pcbWritten); + break; + + case DRVAUDIOPLAYSTATE_NOPLAY: + *pcbWritten = cbBuf; + pStreamEx->offInternal += cbBuf; + Log3Func(("[%s] Discarding the data, backend state: %s\n", pStreamEx->Core.Cfg.szName, + PDMHostAudioStreamStateGetName(enmBackendState) )); + break; + + default: + *pcbWritten = cbBuf; + AssertMsgFailedBreak(("%d; cbBuf=%#x\n", pStreamEx->Out.enmPlayState, cbBuf)); + } + + if (!pStreamEx->Out.Dbg.pFilePlay || RT_FAILURE(rc)) + { /* likely */ } + else + AudioHlpFileWrite(pStreamEx->Out.Dbg.pFilePlay, pvBuf, *pcbWritten); + } + else + { + *pcbWritten = cbBuf; + pStreamEx->offInternal += cbBuf; + Log3Func(("[%s] Backend stream %s, discarding the data\n", pStreamEx->Core.Cfg.szName, + !pThis->Out.fEnabled ? "disabled" : !pThis->pHostDrvAudio ? "not attached" : "not ready yet")); + } + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + } + else + rc = VERR_AUDIO_STREAM_NOT_READY; + + STAM_PROFILE_STOP(&pStreamEx->Out.Stats.ProfPlay, a); + RTCritSectLeave(&pStreamEx->Core.CritSect); + return rc; +} + + +/** + * @interface_method_impl{PDMIAUDIOCONNECTOR,pfnStreamGetReadable} + */ +static DECLCALLBACK(uint32_t) drvAudioStreamGetReadable(PPDMIAUDIOCONNECTOR pInterface, PPDMAUDIOSTREAM pStream) +{ + PDRVAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVAUDIO, IAudioConnector); + AssertPtr(pThis); + PDRVAUDIOSTREAM pStreamEx = (PDRVAUDIOSTREAM)pStream; + AssertPtrReturn(pStreamEx, 0); + AssertReturn(pStreamEx->Core.uMagic == PDMAUDIOSTREAM_MAGIC, 0); + AssertReturn(pStreamEx->uMagic == DRVAUDIOSTREAM_MAGIC, 0); + AssertMsg(pStreamEx->Core.Cfg.enmDir == PDMAUDIODIR_IN, ("Can't read from a non-input stream\n")); + STAM_PROFILE_START(&pStreamEx->In.Stats.ProfGetReadable, a); + + int rc = RTCritSectEnter(&pStreamEx->Core.CritSect); + AssertRCReturn(rc, 0); + RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + + /* + * Use the capture state to determin how much can be written, if anything. + */ + uint32_t cbReadable = 0; + DRVAUDIOCAPTURESTATE const enmCaptureState = pStreamEx->In.enmCaptureState; + PDMHOSTAUDIOSTREAMSTATE const enmBackendState = drvAudioStreamGetBackendState(pThis, pStreamEx); RT_NOREF(enmBackendState); + if ( PDMAudioStrmStatusCanRead(pStreamEx->fStatus) + && pThis->pHostDrvAudio != NULL) + { + switch (enmCaptureState) + { + /* + * Whatever the backend has to offer when in capture mode. + */ + case DRVAUDIOCAPTURESTATE_CAPTURING: + Assert(pStreamEx->fStatus & PDMAUDIOSTREAM_STS_BACKEND_READY); + Assert(enmBackendState == PDMHOSTAUDIOSTREAMSTATE_OKAY /* potential unplug race */); + cbReadable = pThis->pHostDrvAudio->pfnStreamGetReadable(pThis->pHostDrvAudio, pStreamEx->pBackend); + break; + + /* + * Same calculation as in drvAudioStreamCaptureSilence, only we cap it + * at the pre-buffering threshold so we don't get into trouble when we + * switch to capture mode between now and pfnStreamCapture. + */ + case DRVAUDIOCAPTURESTATE_PREBUF: + { + uint64_t const cNsStream = RTTimeNanoTS() - pStreamEx->nsStarted; + uint64_t const offCur = PDMAudioPropsNanoToBytes64(&pStreamEx->Core.Cfg.Props, cNsStream); + if (offCur > pStreamEx->offInternal) + { + uint64_t const cbUnread = offCur - pStreamEx->offInternal; + cbReadable = (uint32_t)RT_MIN(pStreamEx->cbPreBufThreshold, cbUnread); + } + break; + } + + case DRVAUDIOCAPTURESTATE_NO_CAPTURE: + break; + + case DRVAUDIOCAPTURESTATE_INVALID: + case DRVAUDIOCAPTURESTATE_END: + AssertFailed(); + break; + } + + /* Make sure to align the readable size to the host's frame size. */ + cbReadable = PDMAudioPropsFloorBytesToFrame(&pStreamEx->Core.Cfg.Props, cbReadable); + } + + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + STAM_PROFILE_ADD_PERIOD(&pStreamEx->In.Stats.ProfGetReadableBytes, cbReadable); + STAM_PROFILE_STOP(&pStreamEx->In.Stats.ProfGetReadable, a); + RTCritSectLeave(&pStreamEx->Core.CritSect); + Log3Func(("[%s] cbReadable=%#RX32 (%RU64ms) enmCaptureMode=%s enmBackendState=%s\n", + pStreamEx->Core.Cfg.szName, cbReadable, PDMAudioPropsBytesToMilli(&pStreamEx->Core.Cfg.Props, cbReadable), + drvAudioCaptureStateName(enmCaptureState), PDMHostAudioStreamStateGetName(enmBackendState) )); + return cbReadable; +} + + +/** + * Worker for drvAudioStreamCapture that returns silence. + * + * The amount of silence returned is a function of how long the stream has been + * enabled. + * + * @returns VINF_SUCCESS + * @param pStreamEx The stream to commit the pre-buffering for. + * @param pbBuf The output buffer. + * @param cbBuf The size of the output buffer. + * @param pcbRead Where to return the number of bytes actually read. + */ +static int drvAudioStreamCaptureSilence(PDRVAUDIOSTREAM pStreamEx, uint8_t *pbBuf, uint32_t cbBuf, uint32_t *pcbRead) +{ + /** @todo Does not take paused time into account... */ + uint64_t const cNsStream = RTTimeNanoTS() - pStreamEx->nsStarted; + uint64_t const offCur = PDMAudioPropsNanoToBytes64(&pStreamEx->Core.Cfg.Props, cNsStream); + if (offCur > pStreamEx->offInternal) + { + uint64_t const cbUnread = offCur - pStreamEx->offInternal; + uint32_t const cbToClear = (uint32_t)RT_MIN(cbBuf, cbUnread); + *pcbRead = cbToClear; + pStreamEx->offInternal += cbToClear; + cbBuf -= cbToClear; + PDMAudioPropsClearBuffer(&pStreamEx->Core.Cfg.Props, pbBuf, cbToClear, + PDMAudioPropsBytesToFrames(&pStreamEx->Core.Cfg.Props, cbToClear)); + } + else + *pcbRead = 0; + Log4Func(("%s: @%#RX64: Read %#x bytes of silence (%#x bytes left)\n", + pStreamEx->Core.Cfg.szName, pStreamEx->offInternal, *pcbRead, cbBuf)); + return VINF_SUCCESS; +} + + +/** + * Worker for drvAudioStreamCapture. + */ +static int drvAudioStreamCaptureLocked(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx, + uint8_t *pbBuf, uint32_t cbBuf, uint32_t *pcbRead) +{ + Log4Func(("%s: @%#RX64: cbBuf=%#x\n", pStreamEx->Core.Cfg.szName, pStreamEx->offInternal, cbBuf)); + + uint32_t cbReadable = pThis->pHostDrvAudio->pfnStreamGetReadable(pThis->pHostDrvAudio, pStreamEx->pBackend); + pStreamEx->In.Stats.cbBackendReadableBefore = cbReadable; + + uint32_t cbRead = 0; + int rc = VINF_SUCCESS; + uint8_t const cbFrame = PDMAudioPropsFrameSize(&pStreamEx->Core.Cfg.Props); + while (cbBuf >= cbFrame && cbReadable >= cbFrame) + { + uint32_t const cbToRead = PDMAudioPropsFloorBytesToFrame(&pStreamEx->Core.Cfg.Props, RT_MIN(cbBuf, cbReadable)); + uint32_t cbReadNow = 0; + rc = pThis->pHostDrvAudio->pfnStreamCapture(pThis->pHostDrvAudio, pStreamEx->pBackend, pbBuf, cbToRead, &cbReadNow); + if (RT_SUCCESS(rc)) + { + if (cbReadNow != cbToRead) + Log4Func(("%s: @%#RX64: Read fewer bytes than requested: %#x, requested %#x\n", + pStreamEx->Core.Cfg.szName, pStreamEx->offInternal, cbReadNow, cbToRead)); +#ifdef DEBUG_bird + Assert(cbReadNow == cbToRead); +#endif + AssertStmt(cbReadNow <= cbToRead, cbReadNow = cbToRead); + cbRead += cbReadNow; + cbBuf -= cbReadNow; + pbBuf += cbReadNow; + pStreamEx->offInternal += cbReadNow; + } + else + { + *pcbRead = cbRead; + LogFunc(("%s: @%#RX64: pfnStreamCapture failed read %#x bytes (%#x previous read, %#x readable): %Rrc\n", + pStreamEx->Core.Cfg.szName, pStreamEx->offInternal, cbToRead, cbRead, cbReadable, rc)); + return cbRead ? VINF_SUCCESS : rc; + } + + cbReadable = pThis->pHostDrvAudio->pfnStreamGetReadable(pThis->pHostDrvAudio, pStreamEx->pBackend); + } + + STAM_PROFILE_ADD_PERIOD(&pStreamEx->StatXfer, cbRead); + *pcbRead = cbRead; + pStreamEx->In.Stats.cbBackendReadableAfter = cbReadable; + if (cbRead) + pStreamEx->nsLastPlayedCaptured = RTTimeNanoTS(); + + Log4Func(("%s: @%#RX64: Read %#x bytes (%#x bytes left)\n", pStreamEx->Core.Cfg.szName, pStreamEx->offInternal, cbRead, cbBuf)); + return rc; +} + + +/** + * @interface_method_impl{PDMIAUDIOCONNECTOR,pfnStreamCapture} + */ +static DECLCALLBACK(int) drvAudioStreamCapture(PPDMIAUDIOCONNECTOR pInterface, PPDMAUDIOSTREAM pStream, + void *pvBuf, uint32_t cbBuf, uint32_t *pcbRead) +{ + PDRVAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVAUDIO, IAudioConnector); + AssertPtr(pThis); + + /* + * Check input and sanity. + */ + AssertPtrReturn(pInterface, VERR_INVALID_POINTER); + PDRVAUDIOSTREAM pStreamEx = (PDRVAUDIOSTREAM)pStream; + AssertPtrReturn(pStreamEx, VERR_INVALID_POINTER); + AssertPtrReturn(pvBuf, VERR_INVALID_POINTER); + AssertReturn(cbBuf, VERR_INVALID_PARAMETER); + uint32_t uTmp; + if (pcbRead) + AssertPtrReturn(pcbRead, VERR_INVALID_PARAMETER); + else + pcbRead = &uTmp; + + AssertReturn(pStreamEx->Core.uMagic == PDMAUDIOSTREAM_MAGIC, VERR_INVALID_MAGIC); + AssertReturn(pStreamEx->uMagic == DRVAUDIOSTREAM_MAGIC, VERR_INVALID_MAGIC); + AssertMsgReturn(pStreamEx->Core.Cfg.enmDir == PDMAUDIODIR_IN, + ("Stream '%s' is not an input stream and therefore cannot be read from (direction is '%s')\n", + pStreamEx->Core.Cfg.szName, PDMAudioDirGetName(pStreamEx->Core.Cfg.enmDir)), VERR_ACCESS_DENIED); + + AssertMsg(PDMAudioPropsIsSizeAligned(&pStreamEx->Core.Cfg.Props, cbBuf), + ("Stream '%s' got a non-frame-aligned write (%#RX32 bytes)\n", pStreamEx->Core.Cfg.szName, cbBuf)); + STAM_PROFILE_START(&pStreamEx->In.Stats.ProfCapture, a); + + int rc = RTCritSectEnter(&pStreamEx->Core.CritSect); + AssertRCReturn(rc, rc); + + /* + * First check that we can read from the stream, and if not, + * whether to just drop the input into the bit bucket. + */ + if (PDMAudioStrmStatusIsReady(pStreamEx->fStatus)) + { + RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + if ( pThis->In.fEnabled /* (see @bugref{9882}) */ + && pThis->pHostDrvAudio != NULL) + { + /* + * Get the backend state and process changes to it since last time we checked. + */ + PDMHOSTAUDIOSTREAMSTATE const enmBackendState = drvAudioStreamGetBackendStateAndProcessChanges(pThis, pStreamEx); + + /* + * Do the transfering. + */ + switch (pStreamEx->In.enmCaptureState) + { + case DRVAUDIOCAPTURESTATE_CAPTURING: + Assert(pStreamEx->fStatus & PDMAUDIOSTREAM_STS_BACKEND_READY); + Assert(enmBackendState == PDMHOSTAUDIOSTREAMSTATE_OKAY); + rc = drvAudioStreamCaptureLocked(pThis, pStreamEx, (uint8_t *)pvBuf, cbBuf, pcbRead); + break; + + case DRVAUDIOCAPTURESTATE_PREBUF: + if (enmBackendState == PDMHOSTAUDIOSTREAMSTATE_OKAY) + { + uint32_t const cbReadable = pThis->pHostDrvAudio->pfnStreamGetReadable(pThis->pHostDrvAudio, + pStreamEx->pBackend); + if (cbReadable >= pStreamEx->cbPreBufThreshold) + { + Log4Func(("[%s] Pre-buffering completed: cbReadable=%#x vs cbPreBufThreshold=%#x (cbBuf=%#x)\n", + pStreamEx->Core.Cfg.szName, cbReadable, pStreamEx->cbPreBufThreshold, cbBuf)); + pStreamEx->In.enmCaptureState = DRVAUDIOCAPTURESTATE_CAPTURING; + rc = drvAudioStreamCaptureLocked(pThis, pStreamEx, (uint8_t *)pvBuf, cbBuf, pcbRead); + break; + } + pStreamEx->In.Stats.cbBackendReadableBefore = cbReadable; + pStreamEx->In.Stats.cbBackendReadableAfter = cbReadable; + Log4Func(("[%s] Pre-buffering: Got %#x out of %#x\n", + pStreamEx->Core.Cfg.szName, cbReadable, pStreamEx->cbPreBufThreshold)); + } + else + Log4Func(("[%s] Pre-buffering: Backend status %s\n", + pStreamEx->Core.Cfg.szName, PDMHostAudioStreamStateGetName(enmBackendState) )); + drvAudioStreamCaptureSilence(pStreamEx, (uint8_t *)pvBuf, cbBuf, pcbRead); + break; + + case DRVAUDIOCAPTURESTATE_NO_CAPTURE: + *pcbRead = 0; + Log4Func(("[%s] Not capturing - backend state: %s\n", + pStreamEx->Core.Cfg.szName, PDMHostAudioStreamStateGetName(enmBackendState) )); + break; + + default: + *pcbRead = 0; + AssertMsgFailedBreak(("%d; cbBuf=%#x\n", pStreamEx->In.enmCaptureState, cbBuf)); + } + + if (!pStreamEx->In.Dbg.pFileCapture || RT_FAILURE(rc)) + { /* likely */ } + else + AudioHlpFileWrite(pStreamEx->In.Dbg.pFileCapture, pvBuf, *pcbRead); + } + else + { + *pcbRead = 0; + Log4Func(("[%s] Backend stream %s, returning no data\n", pStreamEx->Core.Cfg.szName, + !pThis->Out.fEnabled ? "disabled" : !pThis->pHostDrvAudio ? "not attached" : "not ready yet")); + } + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + } + else + rc = VERR_AUDIO_STREAM_NOT_READY; + + STAM_PROFILE_STOP(&pStreamEx->In.Stats.ProfCapture, a); + RTCritSectLeave(&pStreamEx->Core.CritSect); + return rc; +} + + +/********************************************************************************************************************************* +* PDMIHOSTAUDIOPORT interface implementation. * +*********************************************************************************************************************************/ + +/** + * Worker for drvAudioHostPort_DoOnWorkerThread with stream argument, called on + * worker thread. + */ +static DECLCALLBACK(void) drvAudioHostPort_DoOnWorkerThreadStreamWorker(PDRVAUDIO pThis, PDRVAUDIOSTREAM pStreamEx, + uintptr_t uUser, void *pvUser) +{ + LogFlowFunc(("pThis=%p uUser=%#zx pvUser=%p\n", pThis, uUser, pvUser)); + AssertPtrReturnVoid(pThis); + AssertPtrReturnVoid(pStreamEx); + AssertReturnVoid(pStreamEx->uMagic == DRVAUDIOSTREAM_MAGIC); + + /* + * The CritSectHotPlug lock should not be needed here as detach will destroy + * the thread pool. So, we'll leave taking the stream lock to the worker we're + * calling as there are no lock order concerns. + */ + PPDMIHOSTAUDIO const pIHostDrvAudio = pThis->pHostDrvAudio; + AssertPtrReturnVoid(pIHostDrvAudio); + AssertPtrReturnVoid(pIHostDrvAudio->pfnDoOnWorkerThread); + pIHostDrvAudio->pfnDoOnWorkerThread(pIHostDrvAudio, pStreamEx->pBackend, uUser, pvUser); + + drvAudioStreamReleaseInternal(pThis, pStreamEx, true /*fMayDestroy*/); + LogFlowFunc(("returns\n")); +} + + +/** + * Worker for drvAudioHostPort_DoOnWorkerThread without stream argument, called + * on worker thread. + * + * This wrapper isn't technically required, but it helps with logging and a few + * extra sanity checks. + */ +static DECLCALLBACK(void) drvAudioHostPort_DoOnWorkerThreadWorker(PDRVAUDIO pThis, uintptr_t uUser, void *pvUser) +{ + LogFlowFunc(("pThis=%p uUser=%#zx pvUser=%p\n", pThis, uUser, pvUser)); + AssertPtrReturnVoid(pThis); + + /* + * The CritSectHotPlug lock should not be needed here as detach will destroy + * the thread pool. + */ + PPDMIHOSTAUDIO const pIHostDrvAudio = pThis->pHostDrvAudio; + AssertPtrReturnVoid(pIHostDrvAudio); + AssertPtrReturnVoid(pIHostDrvAudio->pfnDoOnWorkerThread); + + pIHostDrvAudio->pfnDoOnWorkerThread(pIHostDrvAudio, NULL, uUser, pvUser); + + LogFlowFunc(("returns\n")); +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIOPORT,pfnDoOnWorkerThread} + */ +static DECLCALLBACK(int) drvAudioHostPort_DoOnWorkerThread(PPDMIHOSTAUDIOPORT pInterface, PPDMAUDIOBACKENDSTREAM pStream, + uintptr_t uUser, void *pvUser) +{ + PDRVAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVAUDIO, IHostAudioPort); + LogFlowFunc(("pStream=%p uUser=%#zx pvUser=%p\n", pStream, uUser, pvUser)); + + /* + * Assert some sanity. + */ + PDRVAUDIOSTREAM pStreamEx; + if (!pStream) + pStreamEx = NULL; + else + { + AssertPtrReturn(pStream, VERR_INVALID_POINTER); + AssertReturn(pStream->uMagic == PDMAUDIOBACKENDSTREAM_MAGIC, VERR_INVALID_MAGIC); + pStreamEx = (PDRVAUDIOSTREAM)pStream->pStream; + AssertPtrReturn(pStreamEx, VERR_INVALID_POINTER); + AssertReturn(pStreamEx->uMagic == DRVAUDIOSTREAM_MAGIC, VERR_INVALID_MAGIC); + AssertReturn(pStreamEx->Core.uMagic == PDMAUDIOSTREAM_MAGIC, VERR_INVALID_MAGIC); + } + + int rc = RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + AssertRCReturn(rc, rc); + + Assert(pThis->hReqPool != NIL_RTREQPOOL); + AssertPtr(pThis->pHostDrvAudio); + if ( pThis->hReqPool != NIL_RTREQPOOL + && pThis->pHostDrvAudio != NULL) + { + AssertPtr(pThis->pHostDrvAudio->pfnDoOnWorkerThread); + if (pThis->pHostDrvAudio->pfnDoOnWorkerThread) + { + /* + * Try do the work. + */ + if (!pStreamEx) + { + rc = RTReqPoolCallEx(pThis->hReqPool, 0 /*cMillies*/, NULL /*phReq*/, RTREQFLAGS_VOID | RTREQFLAGS_NO_WAIT, + (PFNRT)drvAudioHostPort_DoOnWorkerThreadWorker, 3, pThis, uUser, pvUser); + AssertRC(rc); + } + else + { + uint32_t cRefs = drvAudioStreamRetainInternal(pStreamEx); + if (cRefs != UINT32_MAX) + { + rc = RTReqPoolCallEx(pThis->hReqPool, 0 /*cMillies*/, NULL, RTREQFLAGS_VOID | RTREQFLAGS_NO_WAIT, + (PFNRT)drvAudioHostPort_DoOnWorkerThreadStreamWorker, + 4, pThis, pStreamEx, uUser, pvUser); + AssertRC(rc); + if (RT_FAILURE(rc)) + { + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + drvAudioStreamReleaseInternal(pThis, pStreamEx, true /*fMayDestroy*/); + RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + } + } + else + rc = VERR_INVALID_PARAMETER; + } + } + else + rc = VERR_INVALID_FUNCTION; + } + else + rc = VERR_INVALID_STATE; + + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + LogFlowFunc(("returns %Rrc\n", rc)); + return rc; +} + + +/** + * Marks a stream for re-init. + */ +static void drvAudioStreamMarkNeedReInit(PDRVAUDIOSTREAM pStreamEx, const char *pszCaller) +{ + LogFlow((LOG_FN_FMT ": Flagging %s for re-init.\n", pszCaller, pStreamEx->Core.Cfg.szName)); RT_NOREF(pszCaller); + Assert(RTCritSectIsOwner(&pStreamEx->Core.CritSect)); + + pStreamEx->fStatus |= PDMAUDIOSTREAM_STS_NEED_REINIT; + PDMAUDIOSTREAM_STS_ASSERT_VALID(pStreamEx->fStatus); + pStreamEx->cTriesReInit = 0; + pStreamEx->nsLastReInit = 0; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIOPORT,pfnNotifyDeviceChanged} + */ +static DECLCALLBACK(void) drvAudioHostPort_NotifyDeviceChanged(PPDMIHOSTAUDIOPORT pInterface, PDMAUDIODIR enmDir, void *pvUser) +{ + PDRVAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVAUDIO, IHostAudioPort); + AssertReturnVoid(enmDir == PDMAUDIODIR_IN || enmDir == PDMAUDIODIR_OUT); + LogRel(("Audio: The %s device for %s is changing.\n", enmDir == PDMAUDIODIR_IN ? "input" : "output", pThis->BackendCfg.szName)); + + /* + * Grab the list lock in shared mode and do the work. + */ + int rc = RTCritSectRwEnterShared(&pThis->CritSectGlobals); + AssertRCReturnVoid(rc); + + PDRVAUDIOSTREAM pStreamEx; + RTListForEach(&pThis->LstStreams, pStreamEx, DRVAUDIOSTREAM, ListEntry) + { + if (pStreamEx->Core.Cfg.enmDir == enmDir) + { + RTCritSectEnter(&pStreamEx->Core.CritSect); + RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + + if (pThis->pHostDrvAudio->pfnStreamNotifyDeviceChanged) + { + LogFlowFunc(("Calling pfnStreamNotifyDeviceChanged on %s, old backend state: %s...\n", pStreamEx->Core.Cfg.szName, + PDMHostAudioStreamStateGetName(drvAudioStreamGetBackendState(pThis, pStreamEx)) )); + pThis->pHostDrvAudio->pfnStreamNotifyDeviceChanged(pThis->pHostDrvAudio, pStreamEx->pBackend, pvUser); + LogFlowFunc(("New stream backend state: %s\n", + PDMHostAudioStreamStateGetName(drvAudioStreamGetBackendState(pThis, pStreamEx)) )); + } + else + drvAudioStreamMarkNeedReInit(pStreamEx, __PRETTY_FUNCTION__); + + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + RTCritSectLeave(&pStreamEx->Core.CritSect); + } + } + + RTCritSectRwLeaveShared(&pThis->CritSectGlobals); +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIOPORT,pfnStreamNotifyPreparingDeviceSwitch} + */ +static DECLCALLBACK(void) drvAudioHostPort_StreamNotifyPreparingDeviceSwitch(PPDMIHOSTAUDIOPORT pInterface, + PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + + /* + * Backend stream to validated DrvAudio stream: + */ + AssertPtrReturnVoid(pStream); + AssertReturnVoid(pStream->uMagic == PDMAUDIOBACKENDSTREAM_MAGIC); + PDRVAUDIOSTREAM pStreamEx = (PDRVAUDIOSTREAM)pStream->pStream; + AssertPtrReturnVoid(pStreamEx); + AssertReturnVoid(pStreamEx->Core.uMagic == PDMAUDIOSTREAM_MAGIC); + AssertReturnVoid(pStreamEx->uMagic == DRVAUDIOSTREAM_MAGIC); + LogFlowFunc(("pStreamEx=%p '%s'\n", pStreamEx, pStreamEx->Core.Cfg.szName)); + + /* + * Grab the lock and do switch the state (only needed for output streams for now). + */ + RTCritSectEnter(&pStreamEx->Core.CritSect); + AssertReturnVoidStmt(pStreamEx->uMagic == DRVAUDIOSTREAM_MAGIC, RTCritSectLeave(&pStreamEx->Core.CritSect)); /* paranoia */ + + if (pStreamEx->Core.Cfg.enmDir == PDMAUDIODIR_OUT) + { + if (pStreamEx->cbPreBufThreshold > 0) + { + DRVAUDIOPLAYSTATE const enmPlayState = pStreamEx->Out.enmPlayState; + switch (enmPlayState) + { + case DRVAUDIOPLAYSTATE_PREBUF: + case DRVAUDIOPLAYSTATE_PREBUF_OVERDUE: + case DRVAUDIOPLAYSTATE_NOPLAY: + case DRVAUDIOPLAYSTATE_PREBUF_COMMITTING: /* simpler */ + pStreamEx->Out.enmPlayState = DRVAUDIOPLAYSTATE_PREBUF_SWITCHING; + break; + case DRVAUDIOPLAYSTATE_PLAY: + pStreamEx->Out.enmPlayState = DRVAUDIOPLAYSTATE_PLAY_PREBUF; + break; + case DRVAUDIOPLAYSTATE_PREBUF_SWITCHING: + case DRVAUDIOPLAYSTATE_PLAY_PREBUF: + break; + /* no default */ + case DRVAUDIOPLAYSTATE_END: + case DRVAUDIOPLAYSTATE_INVALID: + break; + } + LogFunc(("%s -> %s\n", drvAudioPlayStateName(enmPlayState), drvAudioPlayStateName(pStreamEx->Out.enmPlayState) )); + } + else + LogFunc(("No pre-buffering configured.\n")); + } + else + LogFunc(("input stream, nothing to do.\n")); + + RTCritSectLeave(&pStreamEx->Core.CritSect); +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIOPORT,pfnStreamNotifyDeviceChanged} + */ +static DECLCALLBACK(void) drvAudioHostPort_StreamNotifyDeviceChanged(PPDMIHOSTAUDIOPORT pInterface, + PPDMAUDIOBACKENDSTREAM pStream, bool fReInit) +{ + PDRVAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVAUDIO, IHostAudioPort); + + /* + * Backend stream to validated DrvAudio stream: + */ + AssertPtrReturnVoid(pStream); + AssertReturnVoid(pStream->uMagic == PDMAUDIOBACKENDSTREAM_MAGIC); + PDRVAUDIOSTREAM pStreamEx = (PDRVAUDIOSTREAM)pStream->pStream; + AssertPtrReturnVoid(pStreamEx); + AssertReturnVoid(pStreamEx->Core.uMagic == PDMAUDIOSTREAM_MAGIC); + AssertReturnVoid(pStreamEx->uMagic == DRVAUDIOSTREAM_MAGIC); + + /* + * Grab the lock and do the requested work. + */ + RTCritSectEnter(&pStreamEx->Core.CritSect); + AssertReturnVoidStmt(pStreamEx->uMagic == DRVAUDIOSTREAM_MAGIC, RTCritSectLeave(&pStreamEx->Core.CritSect)); /* paranoia */ + + if (fReInit) + drvAudioStreamMarkNeedReInit(pStreamEx, __PRETTY_FUNCTION__); + else + { + /* + * Adjust the stream state now that the device has (perhaps finally) been switched. + * + * For enabled output streams, we must update the play state. We could try commit + * pre-buffered data here, but it's really not worth the hazzle and risk (don't + * know which thread we're on, do we now). + */ + AssertStmt(!(pStreamEx->fStatus & PDMAUDIOSTREAM_STS_NEED_REINIT), + pStreamEx->fStatus &= ~PDMAUDIOSTREAM_STS_NEED_REINIT); + + + if (pStreamEx->Core.Cfg.enmDir == PDMAUDIODIR_OUT) + { + DRVAUDIOPLAYSTATE const enmPlayState = pStreamEx->Out.enmPlayState; + pStreamEx->Out.enmPlayState = DRVAUDIOPLAYSTATE_PREBUF; + LogFunc(("%s: %s -> %s\n", pStreamEx->Core.Cfg.szName, drvAudioPlayStateName(enmPlayState), + drvAudioPlayStateName(pStreamEx->Out.enmPlayState) )); + RT_NOREF(enmPlayState); + } + + /* Disable and then fully resync. */ + /** @todo This doesn't work quite reliably if we're in draining mode + * (PENDING_DISABLE, so the backend needs to take care of that prior to calling + * us. Sigh. The idea was to avoid extra state mess in the backend... */ + drvAudioStreamControlInternalBackend(pThis, pStreamEx, PDMAUDIOSTREAMCMD_DISABLE); + drvAudioStreamUpdateBackendOnStatus(pThis, pStreamEx, "device changed"); + } + + RTCritSectLeave(&pStreamEx->Core.CritSect); +} + + +#ifdef VBOX_WITH_AUDIO_ENUM +/** + * @callback_method_impl{FNTMTIMERDRV, Re-enumerate backend devices.} + * + * Used to do/trigger re-enumeration of backend devices with a delay after we + * got notification as there can be further notifications following shortly + * after the first one. Also good to get it of random COM/whatever threads. + */ +static DECLCALLBACK(void) drvAudioEnumerateTimer(PPDMDRVINS pDrvIns, TMTIMERHANDLE hTimer, void *pvUser) +{ + PDRVAUDIO pThis = PDMINS_2_DATA(pDrvIns, PDRVAUDIO); + RT_NOREF(hTimer, pvUser); + + /* Try push the work over to the thread-pool if we've got one. */ + RTCritSectRwEnterShared(&pThis->CritSectHotPlug); + if (pThis->hReqPool != NIL_RTREQPOOL) + { + int rc = RTReqPoolCallEx(pThis->hReqPool, 0 /*cMillies*/, NULL, RTREQFLAGS_VOID | RTREQFLAGS_NO_WAIT, + (PFNRT)drvAudioDevicesEnumerateInternal, + 3, pThis, true /*fLog*/, (PPDMAUDIOHOSTENUM)NULL /*pDevEnum*/); + LogFunc(("RTReqPoolCallEx: %Rrc\n", rc)); + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + if (RT_SUCCESS(rc)) + return; + } + else + RTCritSectRwLeaveShared(&pThis->CritSectHotPlug); + + LogFunc(("Calling drvAudioDevicesEnumerateInternal...\n")); + drvAudioDevicesEnumerateInternal(pThis, true /* fLog */, NULL /* pDevEnum */); +} +#endif /* VBOX_WITH_AUDIO_ENUM */ + + +/** + * @interface_method_impl{PDMIHOSTAUDIOPORT,pfnNotifyDevicesChanged} + */ +static DECLCALLBACK(void) drvAudioHostPort_NotifyDevicesChanged(PPDMIHOSTAUDIOPORT pInterface) +{ + PDRVAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVAUDIO, IHostAudioPort); + LogRel(("Audio: Device configuration of driver '%s' has changed\n", pThis->BackendCfg.szName)); + +#ifdef RT_OS_DARWIN /** @todo Remove legacy behaviour: */ + /* Mark all host streams to re-initialize. */ + int rc2 = RTCritSectRwEnterShared(&pThis->CritSectGlobals); + AssertRCReturnVoid(rc2); + PDRVAUDIOSTREAM pStreamEx; + RTListForEach(&pThis->LstStreams, pStreamEx, DRVAUDIOSTREAM, ListEntry) + { + RTCritSectEnter(&pStreamEx->Core.CritSect); + drvAudioStreamMarkNeedReInit(pStreamEx, __PRETTY_FUNCTION__); + RTCritSectLeave(&pStreamEx->Core.CritSect); + } + RTCritSectRwLeaveShared(&pThis->CritSectGlobals); +#endif + +#ifdef VBOX_WITH_AUDIO_ENUM + /* + * Re-enumerate all host devices with a tiny delay to avoid re-doing this + * when a bunch of changes happens at once (they typically do on windows). + * We'll keep postponing it till it quiesces for a fraction of a second. + */ + int rc = PDMDrvHlpTimerSetMillies(pThis->pDrvIns, pThis->hEnumTimer, RT_MS_1SEC / 3); + AssertRC(rc); +#endif +} + + +/********************************************************************************************************************************* +* PDMIBASE interface implementation. * +*********************************************************************************************************************************/ + +/** + * @interface_method_impl{PDMIBASE,pfnQueryInterface} + */ +static DECLCALLBACK(void *) drvAudioQueryInterface(PPDMIBASE pInterface, const char *pszIID) +{ + LogFlowFunc(("pInterface=%p, pszIID=%s\n", pInterface, pszIID)); + + PPDMDRVINS pDrvIns = PDMIBASE_2_PDMDRV(pInterface); + PDRVAUDIO pThis = PDMINS_2_DATA(pDrvIns, PDRVAUDIO); + + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIBASE, &pDrvIns->IBase); + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIAUDIOCONNECTOR, &pThis->IAudioConnector); + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIHOSTAUDIOPORT, &pThis->IHostAudioPort); + + return NULL; +} + + +/********************************************************************************************************************************* +* PDMDRVREG interface implementation. * +*********************************************************************************************************************************/ + +/** + * Power Off notification. + * + * @param pDrvIns The driver instance data. + */ +static DECLCALLBACK(void) drvAudioPowerOff(PPDMDRVINS pDrvIns) +{ + PDRVAUDIO pThis = PDMINS_2_DATA(pDrvIns, PDRVAUDIO); + + LogFlowFuncEnter(); + + /** @todo locking? */ + if (pThis->pHostDrvAudio) /* If not lower driver is configured, bail out. */ + { + /* + * Just destroy the host stream on the backend side. + * The rest will either be destructed by the device emulation or + * in drvAudioDestruct(). + */ + int rc = RTCritSectRwEnterShared(&pThis->CritSectGlobals); + AssertRCReturnVoid(rc); + + PDRVAUDIOSTREAM pStreamEx; + RTListForEach(&pThis->LstStreams, pStreamEx, DRVAUDIOSTREAM, ListEntry) + { + RTCritSectEnter(&pStreamEx->Core.CritSect); + drvAudioStreamControlInternalBackend(pThis, pStreamEx, PDMAUDIOSTREAMCMD_DISABLE); + RTCritSectLeave(&pStreamEx->Core.CritSect); + } + + RTCritSectRwLeaveShared(&pThis->CritSectGlobals); + } + + LogFlowFuncLeave(); +} + + +/** + * Detach notification. + * + * @param pDrvIns The driver instance data. + * @param fFlags Detach flags. + */ +static DECLCALLBACK(void) drvAudioDetach(PPDMDRVINS pDrvIns, uint32_t fFlags) +{ + PDMDRV_CHECK_VERSIONS_RETURN_VOID(pDrvIns); + PDRVAUDIO pThis = PDMINS_2_DATA(pDrvIns, PDRVAUDIO); + RT_NOREF(fFlags); + + int rc = RTCritSectRwEnterExcl(&pThis->CritSectHotPlug); + AssertLogRelRCReturnVoid(rc); + + LogFunc(("%s (detached %p, hReqPool=%p)\n", pThis->BackendCfg.szName, pThis->pHostDrvAudio, pThis->hReqPool)); + + /* + * Must first destroy the thread pool first so we are certain no threads + * are still using the instance being detached. Release lock while doing + * this as the thread functions may need to take it to complete. + */ + if (pThis->pHostDrvAudio && pThis->hReqPool != NIL_RTREQPOOL) + { + RTREQPOOL hReqPool = pThis->hReqPool; + pThis->hReqPool = NIL_RTREQPOOL; + + RTCritSectRwLeaveExcl(&pThis->CritSectHotPlug); + + RTReqPoolRelease(hReqPool); + + RTCritSectRwEnterExcl(&pThis->CritSectHotPlug); + } + + /* + * Now we can safely set pHostDrvAudio to NULL. + */ + pThis->pHostDrvAudio = NULL; + + RTCritSectRwLeaveExcl(&pThis->CritSectHotPlug); +} + + +/** + * Initializes the host backend and queries its initial configuration. + * + * @returns VBox status code. + * @param pThis Driver instance to be called. + */ +static int drvAudioHostInit(PDRVAUDIO pThis) +{ + LogFlowFuncEnter(); + + /* + * Check the function pointers, make sure the ones we define as + * mandatory are present. + */ + PPDMIHOSTAUDIO pIHostDrvAudio = pThis->pHostDrvAudio; + AssertPtrReturn(pIHostDrvAudio, VERR_INVALID_POINTER); + AssertPtrReturn(pIHostDrvAudio->pfnGetConfig, VERR_INVALID_POINTER); + AssertPtrNullReturn(pIHostDrvAudio->pfnGetDevices, VERR_INVALID_POINTER); + AssertPtrNullReturn(pIHostDrvAudio->pfnSetDevice, VERR_INVALID_POINTER); + AssertPtrNullReturn(pIHostDrvAudio->pfnGetStatus, VERR_INVALID_POINTER); + AssertPtrNullReturn(pIHostDrvAudio->pfnDoOnWorkerThread, VERR_INVALID_POINTER); + AssertPtrNullReturn(pIHostDrvAudio->pfnStreamConfigHint, VERR_INVALID_POINTER); + AssertPtrReturn(pIHostDrvAudio->pfnStreamCreate, VERR_INVALID_POINTER); + AssertPtrNullReturn(pIHostDrvAudio->pfnStreamInitAsync, VERR_INVALID_POINTER); + AssertPtrReturn(pIHostDrvAudio->pfnStreamDestroy, VERR_INVALID_POINTER); + AssertPtrNullReturn(pIHostDrvAudio->pfnStreamNotifyDeviceChanged, VERR_INVALID_POINTER); + AssertPtrReturn(pIHostDrvAudio->pfnStreamEnable, VERR_INVALID_POINTER); + AssertPtrReturn(pIHostDrvAudio->pfnStreamDisable, VERR_INVALID_POINTER); + AssertPtrReturn(pIHostDrvAudio->pfnStreamPause, VERR_INVALID_POINTER); + AssertPtrReturn(pIHostDrvAudio->pfnStreamResume, VERR_INVALID_POINTER); + AssertPtrNullReturn(pIHostDrvAudio->pfnStreamDrain, VERR_INVALID_POINTER); + AssertPtrReturn(pIHostDrvAudio->pfnStreamGetReadable, VERR_INVALID_POINTER); + AssertPtrReturn(pIHostDrvAudio->pfnStreamGetWritable, VERR_INVALID_POINTER); + AssertPtrNullReturn(pIHostDrvAudio->pfnStreamGetPending, VERR_INVALID_POINTER); + AssertPtrReturn(pIHostDrvAudio->pfnStreamGetState, VERR_INVALID_POINTER); + AssertPtrReturn(pIHostDrvAudio->pfnStreamPlay, VERR_INVALID_POINTER); + AssertPtrReturn(pIHostDrvAudio->pfnStreamCapture, VERR_INVALID_POINTER); + + /* + * Get the backend configuration. + * + * Note! Limit the number of streams to max 128 in each direction to + * prevent wasting resources. + * Note! Take care not to wipe the DriverName config value on failure. + */ + PDMAUDIOBACKENDCFG BackendCfg; + RT_ZERO(BackendCfg); + int rc = pIHostDrvAudio->pfnGetConfig(pIHostDrvAudio, &BackendCfg); + if (RT_SUCCESS(rc)) + { + if (LogIsEnabled() && strcmp(BackendCfg.szName, pThis->BackendCfg.szName) != 0) + LogFunc(("BackendCfg.szName: '%s' -> '%s'\n", pThis->BackendCfg.szName, BackendCfg.szName)); + pThis->BackendCfg = BackendCfg; + pThis->In.cStreamsFree = RT_MIN(BackendCfg.cMaxStreamsIn, 128); + pThis->Out.cStreamsFree = RT_MIN(BackendCfg.cMaxStreamsOut, 128); + + LogFlowFunc(("cStreamsFreeIn=%RU8, cStreamsFreeOut=%RU8\n", pThis->In.cStreamsFree, pThis->Out.cStreamsFree)); + } + else + { + LogRel(("Audio: Getting configuration for driver '%s' failed with %Rrc\n", pThis->BackendCfg.szName, rc)); + return VERR_AUDIO_BACKEND_INIT_FAILED; + } + + LogRel2(("Audio: Host driver '%s' supports %RU32 input streams and %RU32 output streams at once.\n", + pThis->BackendCfg.szName, pThis->In.cStreamsFree, pThis->Out.cStreamsFree)); + +#ifdef VBOX_WITH_AUDIO_ENUM + int rc2 = drvAudioDevicesEnumerateInternal(pThis, true /* fLog */, NULL /* pDevEnum */); + if (rc2 != VERR_NOT_SUPPORTED) /* Some backends don't implement device enumeration. */ + AssertRC(rc2); + /* Ignore rc2. */ +#endif + + /* + * Create a thread pool if stream creation can be asynchronous. + * + * The pool employs no pushback as the caller is typically EMT and + * shouldn't be delayed. + * + * The number of threads limits and the device implementations use + * of pfnStreamDestroy limits the number of streams pending async + * init. We use RTReqCancel in drvAudioStreamDestroy to allow us + * to release extra reference held by the pfnStreamInitAsync call + * if successful. Cancellation will only be possible if the call + * hasn't been picked up by a worker thread yet, so the max number + * of threads in the pool defines how many destroyed streams that + * can be lingering. (We must keep this under control, otherwise + * an evil guest could just rapidly trigger stream creation and + * destruction to consume host heap and hog CPU resources for + * configuring audio backends.) + */ + if ( pThis->hReqPool == NIL_RTREQPOOL + && ( pIHostDrvAudio->pfnStreamInitAsync + || pIHostDrvAudio->pfnDoOnWorkerThread + || (pThis->BackendCfg.fFlags & (PDMAUDIOBACKEND_F_ASYNC_HINT | PDMAUDIOBACKEND_F_ASYNC_STREAM_DESTROY)) )) + { + char szName[16]; + RTStrPrintf(szName, sizeof(szName), "Aud%uWr", pThis->pDrvIns->iInstance); + RTREQPOOL hReqPool = NIL_RTREQPOOL; + rc = RTReqPoolCreate(3 /*cMaxThreads*/, RT_MS_30SEC /*cMsMinIdle*/, UINT32_MAX /*cThreadsPushBackThreshold*/, + 1 /*cMsMaxPushBack*/, szName, &hReqPool); + LogFlowFunc(("Creating thread pool '%s': %Rrc, hReqPool=%p\n", szName, rc, hReqPool)); + AssertRCReturn(rc, rc); + + rc = RTReqPoolSetCfgVar(hReqPool, RTREQPOOLCFGVAR_THREAD_FLAGS, RTTHREADFLAGS_COM_MTA); + AssertRCReturnStmt(rc, RTReqPoolRelease(hReqPool), rc); + + rc = RTReqPoolSetCfgVar(hReqPool, RTREQPOOLCFGVAR_MIN_THREADS, 1); + AssertRC(rc); /* harmless */ + + pThis->hReqPool = hReqPool; + } + else + LogFlowFunc(("No thread pool.\n")); + + LogFlowFuncLeave(); + return VINF_SUCCESS; +} + + +/** + * Does the actual backend driver attaching and queries the backend's interface. + * + * This is a worker for both drvAudioAttach and drvAudioConstruct. + * + * @returns VBox status code. + * @param pDrvIns The driver instance. + * @param pThis Pointer to driver instance. + * @param fFlags Attach flags; see PDMDrvHlpAttach(). + */ +static int drvAudioDoAttachInternal(PPDMDRVINS pDrvIns, PDRVAUDIO pThis, uint32_t fFlags) +{ + Assert(pThis->pHostDrvAudio == NULL); /* No nested attaching. */ + + /* + * Attach driver below and query its connector interface. + */ + PPDMIBASE pDownBase; + int rc = PDMDrvHlpAttach(pDrvIns, fFlags, &pDownBase); + if (RT_SUCCESS(rc)) + { + pThis->pHostDrvAudio = PDMIBASE_QUERY_INTERFACE(pDownBase, PDMIHOSTAUDIO); + if (pThis->pHostDrvAudio) + { + /* + * If everything went well, initialize the lower driver. + */ + rc = drvAudioHostInit(pThis); + if (RT_FAILURE(rc)) + pThis->pHostDrvAudio = NULL; + } + else + { + LogRel(("Audio: Failed to query interface for underlying host driver '%s'\n", pThis->BackendCfg.szName)); + rc = PDMDRV_SET_ERROR(pThis->pDrvIns, VERR_PDM_MISSING_INTERFACE_BELOW, + N_("The host audio driver does not implement PDMIHOSTAUDIO!")); + } + } + /* + * If the host driver below us failed to construct for some beningn reason, + * we'll report it as a runtime error and replace it with the Null driver. + * + * Note! We do NOT change anything in PDM (or CFGM), so pDrvIns->pDownBase + * will remain NULL in this case. + */ + else if ( rc == VERR_AUDIO_BACKEND_INIT_FAILED + || rc == VERR_MODULE_NOT_FOUND + || rc == VERR_SYMBOL_NOT_FOUND + || rc == VERR_FILE_NOT_FOUND + || rc == VERR_PATH_NOT_FOUND) + { + /* Complain: */ + LogRel(("DrvAudio: Host audio driver '%s' init failed with %Rrc. Switching to the NULL driver for now.\n", + pThis->BackendCfg.szName, rc)); + PDMDrvHlpVMSetRuntimeError(pDrvIns, 0 /*fFlags*/, "HostAudioNotResponding", + N_("Host audio backend (%s) initialization has failed. Selecting the NULL audio backend with the consequence that no sound is audible"), + pThis->BackendCfg.szName); + + /* Replace with null audio: */ + pThis->pHostDrvAudio = (PPDMIHOSTAUDIO)&g_DrvHostAudioNull; + RTStrCopy(pThis->BackendCfg.szName, sizeof(pThis->BackendCfg.szName), "NULL"); + rc = drvAudioHostInit(pThis); + AssertRC(rc); + } + + LogFunc(("[%s] rc=%Rrc\n", pThis->BackendCfg.szName, rc)); + return rc; +} + + +/** + * Attach notification. + * + * @param pDrvIns The driver instance data. + * @param fFlags Attach flags. + */ +static DECLCALLBACK(int) drvAudioAttach(PPDMDRVINS pDrvIns, uint32_t fFlags) +{ + PDMDRV_CHECK_VERSIONS_RETURN(pDrvIns); + PDRVAUDIO pThis = PDMINS_2_DATA(pDrvIns, PDRVAUDIO); + LogFunc(("%s\n", pThis->BackendCfg.szName)); + + int rc = RTCritSectRwEnterExcl(&pThis->CritSectHotPlug); + AssertRCReturn(rc, rc); + + rc = drvAudioDoAttachInternal(pDrvIns, pThis, fFlags); + + RTCritSectRwLeaveExcl(&pThis->CritSectHotPlug); + return rc; +} + + +/** + * Handles state changes for all audio streams. + * + * @param pDrvIns Pointer to driver instance. + * @param enmCmd Stream command to set for all streams. + */ +static void drvAudioStateHandler(PPDMDRVINS pDrvIns, PDMAUDIOSTREAMCMD enmCmd) +{ + PDMDRV_CHECK_VERSIONS_RETURN_VOID(pDrvIns); + PDRVAUDIO pThis = PDMINS_2_DATA(pDrvIns, PDRVAUDIO); + LogFlowFunc(("enmCmd=%s\n", PDMAudioStrmCmdGetName(enmCmd))); + + int rc2 = RTCritSectRwEnterShared(&pThis->CritSectGlobals); + AssertRCReturnVoid(rc2); + + PDRVAUDIOSTREAM pStreamEx; + RTListForEach(&pThis->LstStreams, pStreamEx, DRVAUDIOSTREAM, ListEntry) + { + RTCritSectEnter(&pStreamEx->Core.CritSect); + drvAudioStreamControlInternal(pThis, pStreamEx, enmCmd); + RTCritSectLeave(&pStreamEx->Core.CritSect); + } + + RTCritSectRwLeaveShared(&pThis->CritSectGlobals); +} + + +/** + * Resume notification. + * + * @param pDrvIns The driver instance data. + */ +static DECLCALLBACK(void) drvAudioResume(PPDMDRVINS pDrvIns) +{ + drvAudioStateHandler(pDrvIns, PDMAUDIOSTREAMCMD_RESUME); +} + + +/** + * Suspend notification. + * + * @param pDrvIns The driver instance data. + */ +static DECLCALLBACK(void) drvAudioSuspend(PPDMDRVINS pDrvIns) +{ + drvAudioStateHandler(pDrvIns, PDMAUDIOSTREAMCMD_PAUSE); +} + + +/** + * Destructs an audio driver instance. + * + * @copydoc FNPDMDRVDESTRUCT + */ +static DECLCALLBACK(void) drvAudioDestruct(PPDMDRVINS pDrvIns) +{ + PDMDRV_CHECK_VERSIONS_RETURN_VOID(pDrvIns); + PDRVAUDIO pThis = PDMINS_2_DATA(pDrvIns, PDRVAUDIO); + + LogFlowFuncEnter(); + + /* + * We must start by setting pHostDrvAudio to NULL here as the anything below + * us has already been destroyed at this point. + */ + if (RTCritSectRwIsInitialized(&pThis->CritSectHotPlug)) + { + RTCritSectRwEnterExcl(&pThis->CritSectHotPlug); + pThis->pHostDrvAudio = NULL; + RTCritSectRwLeaveExcl(&pThis->CritSectHotPlug); + } + else + { + Assert(pThis->pHostDrvAudio == NULL); + pThis->pHostDrvAudio = NULL; + } + + /* + * Make sure the thread pool is out of the picture before we terminate all the streams. + */ + if (pThis->hReqPool != NIL_RTREQPOOL) + { + uint32_t cRefs = RTReqPoolRelease(pThis->hReqPool); + Assert(cRefs == 0); RT_NOREF(cRefs); + pThis->hReqPool = NIL_RTREQPOOL; + } + + /* + * Destroy all streams. + */ + if (RTCritSectRwIsInitialized(&pThis->CritSectGlobals)) + { + RTCritSectRwEnterExcl(&pThis->CritSectGlobals); + + PDRVAUDIOSTREAM pStreamEx, pStreamExNext; + RTListForEachSafe(&pThis->LstStreams, pStreamEx, pStreamExNext, DRVAUDIOSTREAM, ListEntry) + { + int rc = drvAudioStreamUninitInternal(pThis, pStreamEx); + if (RT_SUCCESS(rc)) + { + RTListNodeRemove(&pStreamEx->ListEntry); + drvAudioStreamFree(pStreamEx); + } + } + + RTCritSectRwLeaveExcl(&pThis->CritSectGlobals); + RTCritSectRwDelete(&pThis->CritSectGlobals); + } + + + /* Sanity. */ + Assert(RTListIsEmpty(&pThis->LstStreams)); + + if (RTCritSectRwIsInitialized(&pThis->CritSectHotPlug)) + RTCritSectRwDelete(&pThis->CritSectHotPlug); + + PDMDrvHlpSTAMDeregisterByPrefix(pDrvIns, ""); + + LogFlowFuncLeave(); +} + + +/** + * Constructs an audio driver instance. + * + * @copydoc FNPDMDRVCONSTRUCT + */ +static DECLCALLBACK(int) drvAudioConstruct(PPDMDRVINS pDrvIns, PCFGMNODE pCfg, uint32_t fFlags) +{ + PDMDRV_CHECK_VERSIONS_RETURN(pDrvIns); + PDRVAUDIO pThis = PDMINS_2_DATA(pDrvIns, PDRVAUDIO); + PCPDMDRVHLPR3 pHlp = pDrvIns->pHlpR3; + LogFlowFunc(("pDrvIns=%#p, pCfgHandle=%#p, fFlags=%x\n", pDrvIns, pCfg, fFlags)); + + /* + * Basic instance init. + */ + RTListInit(&pThis->LstStreams); + pThis->hReqPool = NIL_RTREQPOOL; + + /* + * Read configuration. + */ + PDMDRV_VALIDATE_CONFIG_RETURN(pDrvIns, + "DriverName|" + "InputEnabled|" + "OutputEnabled|" + "DebugEnabled|" + "DebugPathOut|" + /* Deprecated: */ + "PCMSampleBitIn|" + "PCMSampleBitOut|" + "PCMSampleHzIn|" + "PCMSampleHzOut|" + "PCMSampleSignedIn|" + "PCMSampleSignedOut|" + "PCMSampleSwapEndianIn|" + "PCMSampleSwapEndianOut|" + "PCMSampleChannelsIn|" + "PCMSampleChannelsOut|" + "PeriodSizeMsIn|" + "PeriodSizeMsOut|" + "BufferSizeMsIn|" + "BufferSizeMsOut|" + "PreBufferSizeMsIn|" + "PreBufferSizeMsOut", + "In|Out"); + + int rc = pHlp->pfnCFGMQueryStringDef(pCfg, "DriverName", pThis->BackendCfg.szName, sizeof(pThis->BackendCfg.szName), "Untitled"); + AssertLogRelRCReturn(rc, rc); + + /* Neither input nor output by default for security reasons. */ + rc = pHlp->pfnCFGMQueryBoolDef(pCfg, "InputEnabled", &pThis->In.fEnabled, false); + AssertLogRelRCReturn(rc, rc); + + rc = pHlp->pfnCFGMQueryBoolDef(pCfg, "OutputEnabled", &pThis->Out.fEnabled, false); + AssertLogRelRCReturn(rc, rc); + + /* Debug stuff (same for both directions). */ + rc = pHlp->pfnCFGMQueryBoolDef(pCfg, "DebugEnabled", &pThis->CfgIn.Dbg.fEnabled, false); + AssertLogRelRCReturn(rc, rc); + + rc = pHlp->pfnCFGMQueryStringDef(pCfg, "DebugPathOut", pThis->CfgIn.Dbg.szPathOut, sizeof(pThis->CfgIn.Dbg.szPathOut), ""); + AssertLogRelRCReturn(rc, rc); + if (pThis->CfgIn.Dbg.szPathOut[0] == '\0') + { + rc = RTPathTemp(pThis->CfgIn.Dbg.szPathOut, sizeof(pThis->CfgIn.Dbg.szPathOut)); + if (RT_FAILURE(rc)) + { + LogRel(("Audio: Warning! Failed to retrieve temporary directory: %Rrc - disabling debugging.\n", rc)); + pThis->CfgIn.Dbg.szPathOut[0] = '\0'; + pThis->CfgIn.Dbg.fEnabled = false; + } + } + if (pThis->CfgIn.Dbg.fEnabled) + LogRel(("Audio: Debugging for driver '%s' enabled (audio data written to '%s')\n", + pThis->BackendCfg.szName, pThis->CfgIn.Dbg.szPathOut)); + + /* Copy debug setup to the output direction. */ + pThis->CfgOut.Dbg = pThis->CfgIn.Dbg; + + LogRel2(("Audio: Verbose logging for driver '%s' is probably enabled too.\n", pThis->BackendCfg.szName)); + /* This ^^^^^^^ is the *WRONG* place for that kind of statement. Verbose logging might only be enabled for DrvAudio. */ + LogRel2(("Audio: Initial status for driver '%s' is: input is %s, output is %s\n", + pThis->BackendCfg.szName, pThis->In.fEnabled ? "enabled" : "disabled", pThis->Out.fEnabled ? "enabled" : "disabled")); + + /* + * Per direction configuration. A bit complicated as + * these wasn't originally in sub-nodes. + */ + for (unsigned iDir = 0; iDir < 2; iDir++) + { + char szNm[48]; + PDRVAUDIOCFG pAudioCfg = iDir == 0 ? &pThis->CfgIn : &pThis->CfgOut; + const char *pszDir = iDir == 0 ? "In" : "Out"; + +#define QUERY_VAL_RET(a_Width, a_szName, a_pValue, a_uDefault, a_ExprValid, a_szValidRange) \ + do { \ + rc = RT_CONCAT(pHlp->pfnCFGMQueryU,a_Width)(pDirNode, strcpy(szNm, a_szName), a_pValue); \ + if (rc == VERR_CFGM_VALUE_NOT_FOUND || rc == VERR_CFGM_NO_PARENT) \ + { \ + rc = RT_CONCAT(pHlp->pfnCFGMQueryU,a_Width)(pCfg, strcat(szNm, pszDir), a_pValue); \ + if (rc == VERR_CFGM_VALUE_NOT_FOUND || rc == VERR_CFGM_NO_PARENT) \ + { \ + *(a_pValue) = a_uDefault; \ + rc = VINF_SUCCESS; \ + } \ + else \ + LogRel(("DrvAudio: Warning! Please use '%s/" a_szName "' instead of '%s' for your VBoxInternal hacks\n", pszDir, szNm)); \ + } \ + AssertRCReturn(rc, PDMDrvHlpVMSetError(pDrvIns, rc, RT_SRC_POS, \ + N_("Configuration error: Failed to read %s config value '%s'"), pszDir, szNm)); \ + if (!(a_ExprValid)) \ + return PDMDrvHlpVMSetError(pDrvIns, VERR_OUT_OF_RANGE, RT_SRC_POS, \ + N_("Configuration error: Unsupported %s value %u. " a_szValidRange), szNm, *(a_pValue)); \ + } while (0) + + PCFGMNODE const pDirNode = pHlp->pfnCFGMGetChild(pCfg, pszDir); + rc = pHlp->pfnCFGMValidateConfig(pDirNode, iDir == 0 ? "In/" : "Out/", + "PCMSampleBit|" + "PCMSampleHz|" + "PCMSampleSigned|" + "PCMSampleSwapEndian|" + "PCMSampleChannels|" + "PeriodSizeMs|" + "BufferSizeMs|" + "PreBufferSizeMs", + "", pDrvIns->pReg->szName, pDrvIns->iInstance); + AssertRCReturn(rc, rc); + + uint8_t cSampleBits = 0; + QUERY_VAL_RET(8, "PCMSampleBit", &cSampleBits, 0, + cSampleBits == 0 + || cSampleBits == 8 + || cSampleBits == 16 + || cSampleBits == 32 + || cSampleBits == 64, + "Must be either 0, 8, 16, 32 or 64"); + if (cSampleBits) + PDMAudioPropsSetSampleSize(&pAudioCfg->Props, cSampleBits / 8); + + uint8_t cChannels; + QUERY_VAL_RET(8, "PCMSampleChannels", &cChannels, 0, cChannels <= 16, "Max 16"); + if (cChannels) + PDMAudioPropsSetChannels(&pAudioCfg->Props, cChannels); + + QUERY_VAL_RET(32, "PCMSampleHz", &pAudioCfg->Props.uHz, 0, + pAudioCfg->Props.uHz == 0 || (pAudioCfg->Props.uHz >= 6000 && pAudioCfg->Props.uHz <= 768000), + "In the range 6000 thru 768000, or 0"); + + QUERY_VAL_RET(8, "PCMSampleSigned", &pAudioCfg->uSigned, UINT8_MAX, + pAudioCfg->uSigned == 0 || pAudioCfg->uSigned == 1 || pAudioCfg->uSigned == UINT8_MAX, + "Must be either 0, 1, or 255"); + + QUERY_VAL_RET(8, "PCMSampleSwapEndian", &pAudioCfg->uSwapEndian, UINT8_MAX, + pAudioCfg->uSwapEndian == 0 || pAudioCfg->uSwapEndian == 1 || pAudioCfg->uSwapEndian == UINT8_MAX, + "Must be either 0, 1, or 255"); + + QUERY_VAL_RET(32, "PeriodSizeMs", &pAudioCfg->uPeriodSizeMs, 0, + pAudioCfg->uPeriodSizeMs <= RT_MS_1SEC, "Max 1000"); + + QUERY_VAL_RET(32, "BufferSizeMs", &pAudioCfg->uBufferSizeMs, 0, + pAudioCfg->uBufferSizeMs <= RT_MS_5SEC, "Max 5000"); + + QUERY_VAL_RET(32, "PreBufferSizeMs", &pAudioCfg->uPreBufSizeMs, UINT32_MAX, + pAudioCfg->uPreBufSizeMs <= RT_MS_1SEC || pAudioCfg->uPreBufSizeMs == UINT32_MAX, + "Max 1000, or 0xffffffff"); +#undef QUERY_VAL_RET + } + + /* + * Init the rest of the driver instance data. + */ + rc = RTCritSectRwInit(&pThis->CritSectHotPlug); + AssertRCReturn(rc, rc); + rc = RTCritSectRwInit(&pThis->CritSectGlobals); + AssertRCReturn(rc, rc); +#ifdef VBOX_STRICT + /* Define locking order: */ + RTCritSectRwEnterExcl(&pThis->CritSectGlobals); + RTCritSectRwEnterExcl(&pThis->CritSectHotPlug); + RTCritSectRwLeaveExcl(&pThis->CritSectHotPlug); + RTCritSectRwLeaveExcl(&pThis->CritSectGlobals); +#endif + + pThis->pDrvIns = pDrvIns; + /* IBase. */ + pDrvIns->IBase.pfnQueryInterface = drvAudioQueryInterface; + /* IAudioConnector. */ + pThis->IAudioConnector.pfnEnable = drvAudioEnable; + pThis->IAudioConnector.pfnIsEnabled = drvAudioIsEnabled; + pThis->IAudioConnector.pfnGetConfig = drvAudioGetConfig; + pThis->IAudioConnector.pfnGetStatus = drvAudioGetStatus; + pThis->IAudioConnector.pfnStreamConfigHint = drvAudioStreamConfigHint; + pThis->IAudioConnector.pfnStreamCreate = drvAudioStreamCreate; + pThis->IAudioConnector.pfnStreamDestroy = drvAudioStreamDestroy; + pThis->IAudioConnector.pfnStreamReInit = drvAudioStreamReInit; + pThis->IAudioConnector.pfnStreamRetain = drvAudioStreamRetain; + pThis->IAudioConnector.pfnStreamRelease = drvAudioStreamRelease; + pThis->IAudioConnector.pfnStreamControl = drvAudioStreamControl; + pThis->IAudioConnector.pfnStreamIterate = drvAudioStreamIterate; + pThis->IAudioConnector.pfnStreamGetState = drvAudioStreamGetState; + pThis->IAudioConnector.pfnStreamGetWritable = drvAudioStreamGetWritable; + pThis->IAudioConnector.pfnStreamPlay = drvAudioStreamPlay; + pThis->IAudioConnector.pfnStreamGetReadable = drvAudioStreamGetReadable; + pThis->IAudioConnector.pfnStreamCapture = drvAudioStreamCapture; + /* IHostAudioPort */ + pThis->IHostAudioPort.pfnDoOnWorkerThread = drvAudioHostPort_DoOnWorkerThread; + pThis->IHostAudioPort.pfnNotifyDeviceChanged = drvAudioHostPort_NotifyDeviceChanged; + pThis->IHostAudioPort.pfnStreamNotifyPreparingDeviceSwitch = drvAudioHostPort_StreamNotifyPreparingDeviceSwitch; + pThis->IHostAudioPort.pfnStreamNotifyDeviceChanged = drvAudioHostPort_StreamNotifyDeviceChanged; + pThis->IHostAudioPort.pfnNotifyDevicesChanged = drvAudioHostPort_NotifyDevicesChanged; + +#ifdef VBOX_WITH_AUDIO_ENUM + /* + * Create a timer to trigger delayed device enumeration on device changes. + */ + RTStrPrintf(pThis->szEnumTimerName, sizeof(pThis->szEnumTimerName), "AudioEnum-%u", pDrvIns->iInstance); + rc = PDMDrvHlpTMTimerCreate(pDrvIns, TMCLOCK_REAL, drvAudioEnumerateTimer, NULL /*pvUser*/, + 0 /*fFlags*/, pThis->szEnumTimerName, &pThis->hEnumTimer); + AssertRCReturn(rc, rc); +#endif + + /* + * Attach the host driver, if present. + */ + rc = drvAudioDoAttachInternal(pDrvIns, pThis, fFlags); + if (rc == VERR_PDM_NO_ATTACHED_DRIVER) + rc = VINF_SUCCESS; + + /* + * Statistics (afte driver attach for name). + */ + PDMDrvHlpSTAMRegister(pDrvIns, &pThis->BackendCfg.fFlags, STAMTYPE_U32, "BackendFlags", STAMUNIT_COUNT, pThis->BackendCfg.szName); /* Mainly for the name. */ + PDMDrvHlpSTAMRegister(pDrvIns, &pThis->cStreams, STAMTYPE_U32, "Streams", STAMUNIT_COUNT, "Current streams count."); + PDMDrvHlpSTAMRegCounter(pDrvIns, &pThis->StatTotalStreamsCreated, "TotalStreamsCreated", "Number of stream ever created."); + PDMDrvHlpSTAMRegister(pDrvIns, &pThis->In.fEnabled, STAMTYPE_BOOL, "InputEnabled", STAMUNIT_NONE, "Whether input is enabled or not."); + PDMDrvHlpSTAMRegister(pDrvIns, &pThis->In.cStreamsFree, STAMTYPE_U32, "InputStreamFree", STAMUNIT_COUNT, "Number of free input stream slots"); + PDMDrvHlpSTAMRegister(pDrvIns, &pThis->Out.fEnabled, STAMTYPE_BOOL, "OutputEnabled", STAMUNIT_NONE, "Whether output is enabled or not."); + PDMDrvHlpSTAMRegister(pDrvIns, &pThis->Out.cStreamsFree, STAMTYPE_U32, "OutputStreamFree", STAMUNIT_COUNT, "Number of free output stream slots"); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Audio driver registration record. + */ +const PDMDRVREG g_DrvAUDIO = +{ + /* u32Version */ + PDM_DRVREG_VERSION, + /* szName */ + "AUDIO", + /* szRCMod */ + "", + /* szR0Mod */ + "", + /* pszDescription */ + "Audio connector driver", + /* fFlags */ + PDM_DRVREG_FLAGS_HOST_BITS_DEFAULT, + /* fClass */ + PDM_DRVREG_CLASS_AUDIO, + /* cMaxInstances */ + UINT32_MAX, + /* cbInstance */ + sizeof(DRVAUDIO), + /* pfnConstruct */ + drvAudioConstruct, + /* pfnDestruct */ + drvAudioDestruct, + /* pfnRelocate */ + NULL, + /* pfnIOCtl */ + NULL, + /* pfnPowerOn */ + NULL, + /* pfnReset */ + NULL, + /* pfnSuspend */ + drvAudioSuspend, + /* pfnResume */ + drvAudioResume, + /* pfnAttach */ + drvAudioAttach, + /* pfnDetach */ + drvAudioDetach, + /* pfnPowerOff */ + drvAudioPowerOff, + /* pfnSoftReset */ + NULL, + /* u32EndVersion */ + PDM_DRVREG_VERSION +}; + diff --git a/src/VBox/Devices/Audio/DrvHostAudioAlsa.cpp b/src/VBox/Devices/Audio/DrvHostAudioAlsa.cpp new file mode 100644 index 00000000..0f64b487 --- /dev/null +++ b/src/VBox/Devices/Audio/DrvHostAudioAlsa.cpp @@ -0,0 +1,1611 @@ +/* $Id: DrvHostAudioAlsa.cpp $ */ +/** @file + * Host audio driver - Advanced Linux Sound Architecture (ALSA). + */ + +/* + * Copyright (C) 2006-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 + * -------------------------------------------------------------------- + * + * This code is based on: alsaaudio.c + * + * QEMU ALSA audio driver + * + * Copyright (c) 2005 Vassili Karpov (malc) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_DRV_HOST_AUDIO +#include <VBox/log.h> +#include <iprt/alloc.h> +#include <iprt/uuid.h> /* For PDMIBASE_2_PDMDRV. */ +#include <VBox/vmm/pdmaudioifs.h> +#include <VBox/vmm/pdmaudioinline.h> +#include <VBox/vmm/pdmaudiohostenuminline.h> + +#include "DrvHostAudioAlsaStubsMangling.h" +#include <alsa/asoundlib.h> +#include <alsa/control.h> /* For device enumeration. */ +#include <alsa/version.h> +#include "DrvHostAudioAlsaStubs.h" + +#include "VBoxDD.h" + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +/** Maximum number of tries to recover a broken pipe. */ +#define ALSA_RECOVERY_TRIES_MAX 5 + + +/********************************************************************************************************************************* +* Structures * +*********************************************************************************************************************************/ +/** + * ALSA host audio specific stream data. + */ +typedef struct DRVHSTAUDALSASTREAM +{ + /** Common part. */ + PDMAUDIOBACKENDSTREAM Core; + + /** Handle to the ALSA PCM stream. */ + snd_pcm_t *hPCM; + /** Internal stream offset (for debugging). */ + uint64_t offInternal; + + /** The stream's acquired configuration. */ + PDMAUDIOSTREAMCFG Cfg; +} DRVHSTAUDALSASTREAM; +/** Pointer to the ALSA host audio specific stream data. */ +typedef DRVHSTAUDALSASTREAM *PDRVHSTAUDALSASTREAM; + + +/** + * Host Alsa audio driver instance data. + * @implements PDMIAUDIOCONNECTOR + */ +typedef struct DRVHSTAUDALSA +{ + /** Pointer to the driver instance structure. */ + PPDMDRVINS pDrvIns; + /** Pointer to host audio interface. */ + PDMIHOSTAUDIO IHostAudio; + /** Error count for not flooding the release log. + * UINT32_MAX for unlimited logging. */ + uint32_t cLogErrors; + + /** Critical section protecting the default device strings. */ + RTCRITSECT CritSect; + /** Default input device name. */ + char szInputDev[256]; + /** Default output device name. */ + char szOutputDev[256]; + /** Upwards notification interface. */ + PPDMIHOSTAUDIOPORT pIHostAudioPort; +} DRVHSTAUDALSA; +/** Pointer to the instance data of an ALSA host audio driver. */ +typedef DRVHSTAUDALSA *PDRVHSTAUDALSA; + + + +/** + * Closes an ALSA stream + * + * @returns VBox status code. + * @param phPCM Pointer to the ALSA stream handle to close. Will be set to + * NULL. + */ +static int drvHstAudAlsaStreamClose(snd_pcm_t **phPCM) +{ + if (!phPCM || !*phPCM) + return VINF_SUCCESS; + + LogRelFlowFuncEnter(); + + int rc; + int rc2 = snd_pcm_close(*phPCM); + if (rc2 == 0) + { + *phPCM = NULL; + rc = VINF_SUCCESS; + } + else + { + rc = RTErrConvertFromErrno(-rc2); + LogRel(("ALSA: Closing PCM descriptor failed: %s (%d, %Rrc)\n", snd_strerror(rc2), rc2, rc)); + } + + LogRelFlowFuncLeaveRC(rc); + return rc; +} + + +#ifdef DEBUG +static void drvHstAudAlsaDbgErrorHandler(const char *file, int line, const char *function, + int err, const char *fmt, ...) +{ + /** @todo Implement me! */ + RT_NOREF(file, line, function, err, fmt); +} +#endif + + +/** + * Tries to recover an ALSA stream. + * + * @returns VBox status code. + * @param hPCM ALSA stream handle. + */ +static int drvHstAudAlsaStreamRecover(snd_pcm_t *hPCM) +{ + AssertPtrReturn(hPCM, VERR_INVALID_POINTER); + + int rc = snd_pcm_prepare(hPCM); + if (rc >= 0) + { + LogFlowFunc(("Successfully recovered %p.\n", hPCM)); + return VINF_SUCCESS; + } + LogFunc(("Failed to recover stream %p: %s (%d)\n", hPCM, snd_strerror(rc), rc)); + return RTErrConvertFromErrno(-rc); +} + + +/** + * Resumes an ALSA stream. + * + * Used by drvHstAudAlsaHA_StreamPlay() and drvHstAudAlsaHA_StreamCapture(). + * + * @returns VBox status code. + * @param hPCM ALSA stream to resume. + */ +static int drvHstAudAlsaStreamResume(snd_pcm_t *hPCM) +{ + AssertPtrReturn(hPCM, VERR_INVALID_POINTER); + + int rc = snd_pcm_resume(hPCM); + if (rc >= 0) + { + LogFlowFunc(("Successfuly resumed %p.\n", hPCM)); + return VINF_SUCCESS; + } + LogFunc(("Failed to resume stream %p: %s (%d)\n", hPCM, snd_strerror(rc), rc)); + return RTErrConvertFromErrno(-rc); +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnGetConfig} + */ +static DECLCALLBACK(int) drvHstAudAlsaHA_GetConfig(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDCFG pBackendCfg) +{ + RT_NOREF(pInterface); + AssertPtrReturn(pBackendCfg, VERR_INVALID_POINTER); + + /* + * Fill in the config structure. + */ + RTStrCopy(pBackendCfg->szName, sizeof(pBackendCfg->szName), "ALSA"); + pBackendCfg->cbStream = sizeof(DRVHSTAUDALSASTREAM); + pBackendCfg->fFlags = 0; + /* ALSA allows exactly one input and one output used at a time for the selected device(s). */ + pBackendCfg->cMaxStreamsIn = 1; + pBackendCfg->cMaxStreamsOut = 1; + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnGetDevices} + */ +static DECLCALLBACK(int) drvHstAudAlsaHA_GetDevices(PPDMIHOSTAUDIO pInterface, PPDMAUDIOHOSTENUM pDeviceEnum) +{ + RT_NOREF(pInterface); + PDMAudioHostEnumInit(pDeviceEnum); + + char **papszHints = NULL; + int rc = snd_device_name_hint(-1 /* All cards */, "pcm", (void***)&papszHints); + if (rc == 0) + { + rc = VINF_SUCCESS; + for (size_t iHint = 0; papszHints[iHint] != NULL && RT_SUCCESS(rc); iHint++) + { + /* + * Retrieve the available info: + */ + const char * const pszHint = papszHints[iHint]; + char * const pszDev = snd_device_name_get_hint(pszHint, "NAME"); + char * const pszInOutId = snd_device_name_get_hint(pszHint, "IOID"); + char * const pszDesc = snd_device_name_get_hint(pszHint, "DESC"); + + if (pszDev && RTStrICmpAscii(pszDev, "null") != 0) + { + /* Detect and log presence of pulse audio plugin. */ + if (RTStrIStr("pulse", pszDev) != NULL) + LogRel(("ALSA: The ALSAAudio plugin for pulse audio is being used (%s).\n", pszDev)); + + /* + * Add an entry to the enumeration result. + * We engage in some trickery here to deal with device names that + * are more than 63 characters long. + */ + size_t const cbId = pszDev ? strlen(pszDev) + 1 : 1; + size_t const cbName = pszDesc ? strlen(pszDesc) + 2 + 1 : cbId; + PPDMAUDIOHOSTDEV pDev = PDMAudioHostDevAlloc(sizeof(*pDev), cbName, cbId); + if (pDev) + { + RTStrCopy(pDev->pszId, cbId, pszDev); + if (pDev->pszId) + { + pDev->fFlags = PDMAUDIOHOSTDEV_F_NONE; + pDev->enmType = PDMAUDIODEVICETYPE_UNKNOWN; + + if (pszInOutId == NULL) + { + pDev->enmUsage = PDMAUDIODIR_DUPLEX; + pDev->cMaxInputChannels = 2; + pDev->cMaxOutputChannels = 2; + } + else if (RTStrICmpAscii(pszInOutId, "Input") == 0) + { + pDev->enmUsage = PDMAUDIODIR_IN; + pDev->cMaxInputChannels = 2; + pDev->cMaxOutputChannels = 0; + } + else + { + AssertMsg(RTStrICmpAscii(pszInOutId, "Output") == 0, ("%s (%s)\n", pszInOutId, pszHint)); + pDev->enmUsage = PDMAUDIODIR_OUT; + pDev->cMaxInputChannels = 0; + pDev->cMaxOutputChannels = 2; + } + + if (pszDesc && *pszDesc) + { + char *pszDesc2 = strchr(pszDesc, '\n'); + if (!pszDesc2) + RTStrCopy(pDev->pszName, cbName, pszDesc); + else + { + *pszDesc2++ = '\0'; + char *psz; + while ((psz = strchr(pszDesc2, '\n')) != NULL) + *psz = ' '; + RTStrPrintf(pDev->pszName, cbName, "%s (%s)", pszDesc2, pszDesc); + } + } + else + RTStrCopy(pDev->pszName, cbName, pszDev); + + PDMAudioHostEnumAppend(pDeviceEnum, pDev); + + LogRel2(("ALSA: Device #%u: '%s' enmDir=%s: %s\n", iHint, pszDev, + PDMAudioDirGetName(pDev->enmUsage), pszDesc)); + } + else + { + PDMAudioHostDevFree(pDev); + rc = VERR_NO_STR_MEMORY; + } + } + else + rc = VERR_NO_MEMORY; + } + + /* + * Clean up. + */ + if (pszInOutId) + free(pszInOutId); + if (pszDesc) + free(pszDesc); + if (pszDev) + free(pszDev); + } + + snd_device_name_free_hint((void **)papszHints); + + if (RT_FAILURE(rc)) + { + PDMAudioHostEnumDelete(pDeviceEnum); + PDMAudioHostEnumInit(pDeviceEnum); + } + } + else + { + int rc2 = RTErrConvertFromErrno(-rc); + LogRel2(("ALSA: Error enumerating PCM devices: %Rrc (%d)\n", rc2, rc)); + rc = rc2; + } + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnSetDevice} + */ +static DECLCALLBACK(int) drvHstAudAlsaHA_SetDevice(PPDMIHOSTAUDIO pInterface, PDMAUDIODIR enmDir, const char *pszId) +{ + PDRVHSTAUDALSA pThis = RT_FROM_MEMBER(pInterface, DRVHSTAUDALSA, 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 = "default"; + 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) + { + int rc = RTCritSectEnter(&pThis->CritSect); + AssertRCReturn(rc, rc); + if (strcmp(pThis->szInputDev, pszId) == 0) + RTCritSectLeave(&pThis->CritSect); + else + { + LogRel(("ALSA: Changing input device: '%s' -> '%s'\n", pThis->szInputDev, pszId)); + RTStrCopy(pThis->szInputDev, sizeof(pThis->szInputDev), pszId); + PPDMIHOSTAUDIOPORT pIHostAudioPort = pThis->pIHostAudioPort; + RTCritSectLeave(&pThis->CritSect); + 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) + { + int rc = RTCritSectEnter(&pThis->CritSect); + AssertRCReturn(rc, rc); + if (strcmp(pThis->szOutputDev, pszId) == 0) + RTCritSectLeave(&pThis->CritSect); + else + { + LogRel(("ALSA: Changing output device: '%s' -> '%s'\n", pThis->szOutputDev, pszId)); + RTStrCopy(pThis->szOutputDev, sizeof(pThis->szOutputDev), pszId); + PPDMIHOSTAUDIOPORT pIHostAudioPort = pThis->pIHostAudioPort; + RTCritSectLeave(&pThis->CritSect); + 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) drvHstAudAlsaHA_GetStatus(PPDMIHOSTAUDIO pInterface, PDMAUDIODIR enmDir) +{ + RT_NOREF(enmDir); + AssertPtrReturn(pInterface, PDMAUDIOBACKENDSTS_UNKNOWN); + + return PDMAUDIOBACKENDSTS_RUNNING; +} + + +/** + * Converts internal audio PCM properties to an ALSA PCM format. + * + * @returns Converted ALSA PCM format. + * @param pProps Internal audio PCM configuration to convert. + */ +static snd_pcm_format_t alsaAudioPropsToALSA(PCPDMAUDIOPCMPROPS pProps) +{ + switch (PDMAudioPropsSampleSize(pProps)) + { + case 1: + return pProps->fSigned ? SND_PCM_FORMAT_S8 : SND_PCM_FORMAT_U8; + + case 2: + if (PDMAudioPropsIsLittleEndian(pProps)) + return pProps->fSigned ? SND_PCM_FORMAT_S16_LE : SND_PCM_FORMAT_U16_LE; + return pProps->fSigned ? SND_PCM_FORMAT_S16_BE : SND_PCM_FORMAT_U16_BE; + + case 4: + if (PDMAudioPropsIsLittleEndian(pProps)) + return pProps->fSigned ? SND_PCM_FORMAT_S32_LE : SND_PCM_FORMAT_U32_LE; + return pProps->fSigned ? SND_PCM_FORMAT_S32_BE : SND_PCM_FORMAT_U32_BE; + + default: + AssertLogRelMsgFailed(("%RU8 bytes not supported\n", PDMAudioPropsSampleSize(pProps))); + return SND_PCM_FORMAT_UNKNOWN; + } +} + + +/** + * Sets the software parameters of an ALSA stream. + * + * @returns 0 on success, negative errno on failure. + * @param hPCM ALSA stream to set software parameters for. + * @param pCfgReq Requested stream configuration (PDM). + * @param pCfgAcq The actual stream configuration (PDM). Updated as + * needed. + */ +static int alsaStreamSetSWParams(snd_pcm_t *hPCM, PCPDMAUDIOSTREAMCFG pCfgReq, PPDMAUDIOSTREAMCFG pCfgAcq) +{ + if (pCfgReq->enmDir == PDMAUDIODIR_IN) /* For input streams there's nothing to do in here right now. */ + return 0; + + snd_pcm_sw_params_t *pSWParms = NULL; + snd_pcm_sw_params_alloca(&pSWParms); + AssertReturn(pSWParms, -ENOMEM); + + int err = snd_pcm_sw_params_current(hPCM, pSWParms); + AssertLogRelMsgReturn(err >= 0, ("ALSA: Failed to get current software parameters: %s\n", snd_strerror(err)), err); + + /* Under normal circumstance, we don't need to set a playback threshold + because DrvAudio will do the pre-buffering and hand us everything in + one continuous chunk when we should start playing. But since it is + configurable, we'll set a reasonable minimum of two DMA periods or + max 50 milliseconds (the pAlsaCfgReq->threshold value). + + Of course we also have to make sure the threshold is below the buffer + size, or ALSA will never start playing. */ + unsigned long const cFramesMax = PDMAudioPropsMilliToFrames(&pCfgAcq->Props, 50); + unsigned long cFramesThreshold = RT_MIN(pCfgAcq->Backend.cFramesPeriod * 2, cFramesMax); + if (cFramesThreshold >= pCfgAcq->Backend.cFramesBufferSize - pCfgAcq->Backend.cFramesBufferSize / 16) + cFramesThreshold = pCfgAcq->Backend.cFramesBufferSize - pCfgAcq->Backend.cFramesBufferSize / 16; + + err = snd_pcm_sw_params_set_start_threshold(hPCM, pSWParms, cFramesThreshold); + AssertLogRelMsgReturn(err >= 0, ("ALSA: Failed to set software threshold to %lu: %s\n", cFramesThreshold, snd_strerror(err)), err); + + err = snd_pcm_sw_params_set_avail_min(hPCM, pSWParms, pCfgReq->Backend.cFramesPeriod); + AssertLogRelMsgReturn(err >= 0, ("ALSA: Failed to set available minimum to %u: %s\n", + pCfgReq->Backend.cFramesPeriod, snd_strerror(err)), err); + + /* Commit the software parameters: */ + err = snd_pcm_sw_params(hPCM, pSWParms); + AssertLogRelMsgReturn(err >= 0, ("ALSA: Failed to set new software parameters: %s\n", snd_strerror(err)), err); + + /* Get the actual parameters: */ + snd_pcm_uframes_t cFramesThresholdActual = cFramesThreshold; + err = snd_pcm_sw_params_get_start_threshold(pSWParms, &cFramesThresholdActual); + AssertLogRelMsgStmt(err >= 0, ("ALSA: Failed to get start threshold: %s\n", snd_strerror(err)), + cFramesThresholdActual = cFramesThreshold); + + LogRel2(("ALSA: SW params: %lu frames threshold, %u frames avail minimum\n", + cFramesThresholdActual, pCfgAcq->Backend.cFramesPeriod)); + return 0; +} + + +/** + * Maps a PDM channel ID to an ASLA channel map position. + */ +static unsigned int drvHstAudAlsaPdmChToAlsa(PDMAUDIOCHANNELID enmId, uint8_t cChannels) +{ + switch (enmId) + { + case PDMAUDIOCHANNELID_UNKNOWN: return SND_CHMAP_UNKNOWN; + case PDMAUDIOCHANNELID_UNUSED_ZERO: return SND_CHMAP_NA; + case PDMAUDIOCHANNELID_UNUSED_SILENCE: return SND_CHMAP_NA; + + case PDMAUDIOCHANNELID_FRONT_LEFT: return SND_CHMAP_FL; + case PDMAUDIOCHANNELID_FRONT_RIGHT: return SND_CHMAP_FR; + case PDMAUDIOCHANNELID_FRONT_CENTER: return cChannels == 1 ? SND_CHMAP_MONO : SND_CHMAP_FC; + case PDMAUDIOCHANNELID_LFE: return SND_CHMAP_LFE; + case PDMAUDIOCHANNELID_REAR_LEFT: return SND_CHMAP_RL; + case PDMAUDIOCHANNELID_REAR_RIGHT: return SND_CHMAP_RR; + case PDMAUDIOCHANNELID_FRONT_LEFT_OF_CENTER: return SND_CHMAP_FLC; + case PDMAUDIOCHANNELID_FRONT_RIGHT_OF_CENTER: return SND_CHMAP_FRC; + case PDMAUDIOCHANNELID_REAR_CENTER: return SND_CHMAP_RC; + case PDMAUDIOCHANNELID_SIDE_LEFT: return SND_CHMAP_SL; + case PDMAUDIOCHANNELID_SIDE_RIGHT: return SND_CHMAP_SR; + case PDMAUDIOCHANNELID_TOP_CENTER: return SND_CHMAP_TC; + case PDMAUDIOCHANNELID_FRONT_LEFT_HEIGHT: return SND_CHMAP_TFL; + case PDMAUDIOCHANNELID_FRONT_CENTER_HEIGHT: return SND_CHMAP_TFC; + case PDMAUDIOCHANNELID_FRONT_RIGHT_HEIGHT: return SND_CHMAP_TFR; + case PDMAUDIOCHANNELID_REAR_LEFT_HEIGHT: return SND_CHMAP_TRL; + case PDMAUDIOCHANNELID_REAR_CENTER_HEIGHT: return SND_CHMAP_TRC; + case PDMAUDIOCHANNELID_REAR_RIGHT_HEIGHT: return SND_CHMAP_TRR; + + case PDMAUDIOCHANNELID_INVALID: + case PDMAUDIOCHANNELID_END: + case PDMAUDIOCHANNELID_32BIT_HACK: + break; + } + AssertFailed(); + return SND_CHMAP_NA; +} + + +/** + * Sets the hardware parameters of an ALSA stream. + * + * @returns 0 on success, negative errno on failure. + * @param hPCM ALSA stream to set software parameters for. + * @param enmAlsaFmt The ALSA format to use. + * @param pCfgReq Requested stream configuration (PDM). + * @param pCfgAcq The actual stream configuration (PDM). This is assumed + * to be a copy of pCfgReq on input, at least for + * properties handled here. On output some of the + * properties may be updated to match the actual stream + * configuration. + */ +static int alsaStreamSetHwParams(snd_pcm_t *hPCM, snd_pcm_format_t enmAlsaFmt, + PCPDMAUDIOSTREAMCFG pCfgReq, PPDMAUDIOSTREAMCFG pCfgAcq) +{ + /* + * Get the current hardware parameters. + */ + snd_pcm_hw_params_t *pHWParms = NULL; + snd_pcm_hw_params_alloca(&pHWParms); + AssertReturn(pHWParms, -ENOMEM); + + int err = snd_pcm_hw_params_any(hPCM, pHWParms); + AssertLogRelMsgReturn(err >= 0, ("ALSA: Failed to initialize hardware parameters: %s\n", snd_strerror(err)), err); + + /* + * Modify them according to pAlsaCfgReq. + * We update pAlsaCfgObt as we go for parameters set by "near" methods. + */ + /* We'll use snd_pcm_writei/snd_pcm_readi: */ + err = snd_pcm_hw_params_set_access(hPCM, pHWParms, SND_PCM_ACCESS_RW_INTERLEAVED); + AssertLogRelMsgReturn(err >= 0, ("ALSA: Failed to set access type: %s\n", snd_strerror(err)), err); + + /* Set the format and frequency. */ + err = snd_pcm_hw_params_set_format(hPCM, pHWParms, enmAlsaFmt); + AssertLogRelMsgReturn(err >= 0, ("ALSA: Failed to set audio format to %d: %s\n", enmAlsaFmt, snd_strerror(err)), err); + + unsigned int uFreq = PDMAudioPropsHz(&pCfgReq->Props); + err = snd_pcm_hw_params_set_rate_near(hPCM, pHWParms, &uFreq, NULL /*dir*/); + AssertLogRelMsgReturn(err >= 0, ("ALSA: Failed to set frequency to %uHz: %s\n", + PDMAudioPropsHz(&pCfgReq->Props), snd_strerror(err)), err); + pCfgAcq->Props.uHz = uFreq; + + /* Channel count currently does not change with the mapping translations, + as ALSA can express both silent and unknown channel positions. */ + union + { + snd_pcm_chmap_t Map; + unsigned int padding[1 + PDMAUDIO_MAX_CHANNELS]; + } u; + uint8_t aidSrcChannels[PDMAUDIO_MAX_CHANNELS]; + unsigned int *aidDstChannels = u.Map.pos; + unsigned int cChannels = u.Map.channels = PDMAudioPropsChannels(&pCfgReq->Props); + unsigned int iDst = 0; + for (unsigned int iSrc = 0; iSrc < cChannels; iSrc++) + { + uint8_t const idSrc = pCfgReq->Props.aidChannels[iSrc]; + aidSrcChannels[iDst] = idSrc; + aidDstChannels[iDst] = drvHstAudAlsaPdmChToAlsa((PDMAUDIOCHANNELID)idSrc, cChannels); + iDst++; + } + u.Map.channels = cChannels = iDst; + for (; iDst < PDMAUDIO_MAX_CHANNELS; iDst++) + { + aidSrcChannels[iDst] = PDMAUDIOCHANNELID_INVALID; + aidDstChannels[iDst] = SND_CHMAP_NA; + } + + err = snd_pcm_hw_params_set_channels_near(hPCM, pHWParms, &cChannels); + AssertLogRelMsgReturn(err >= 0, ("ALSA: Failed to set number of channels to %d\n", PDMAudioPropsChannels(&pCfgReq->Props)), + err); + if (cChannels == PDMAudioPropsChannels(&pCfgReq->Props)) + memcpy(pCfgAcq->Props.aidChannels, aidSrcChannels, sizeof(pCfgAcq->Props.aidChannels)); + else + { + LogRel2(("ALSA: Requested %u channels, got %u\n", u.Map.channels, cChannels)); + AssertLogRelMsgReturn(cChannels > 0 && cChannels <= PDMAUDIO_MAX_CHANNELS, + ("ALSA: Unsupported channel count: %u (requested %d)\n", + cChannels, PDMAudioPropsChannels(&pCfgReq->Props)), -ERANGE); + PDMAudioPropsSetChannels(&pCfgAcq->Props, (uint8_t)cChannels); + /** @todo Can we somehow guess channel IDs? snd_pcm_get_chmap? */ + } + + /* The period size (reportedly frame count per hw interrupt): */ + int dir = 0; + snd_pcm_uframes_t minval = pCfgReq->Backend.cFramesPeriod; + err = snd_pcm_hw_params_get_period_size_min(pHWParms, &minval, &dir); + AssertLogRelMsgReturn(err >= 0, ("ALSA: Could not determine minimal period size: %s\n", snd_strerror(err)), err); + + snd_pcm_uframes_t period_size_f = pCfgReq->Backend.cFramesPeriod; + if (period_size_f < minval) + period_size_f = minval; + err = snd_pcm_hw_params_set_period_size_near(hPCM, pHWParms, &period_size_f, 0); + LogRel2(("ALSA: Period size is: %lu frames (min %lu, requested %u)\n", period_size_f, minval, pCfgReq->Backend.cFramesPeriod)); + AssertLogRelMsgReturn(err >= 0, ("ALSA: Failed to set period size %d (%s)\n", period_size_f, snd_strerror(err)), err); + + /* The buffer size: */ + minval = pCfgReq->Backend.cFramesBufferSize; + err = snd_pcm_hw_params_get_buffer_size_min(pHWParms, &minval); + AssertLogRelMsgReturn(err >= 0, ("ALSA: Could not retrieve minimal buffer size: %s\n", snd_strerror(err)), err); + + snd_pcm_uframes_t buffer_size_f = pCfgReq->Backend.cFramesBufferSize; + if (buffer_size_f < minval) + buffer_size_f = minval; + err = snd_pcm_hw_params_set_buffer_size_near(hPCM, pHWParms, &buffer_size_f); + LogRel2(("ALSA: Buffer size is: %lu frames (min %lu, requested %u)\n", buffer_size_f, minval, pCfgReq->Backend.cFramesBufferSize)); + AssertLogRelMsgReturn(err >= 0, ("ALSA: Failed to set near buffer size %RU32: %s\n", buffer_size_f, snd_strerror(err)), err); + + /* + * Set the hardware parameters. + */ + err = snd_pcm_hw_params(hPCM, pHWParms); + AssertLogRelMsgReturn(err >= 0, ("ALSA: Failed to apply audio parameters: %s\n", snd_strerror(err)), err); + + /* + * Get relevant parameters and put them in the pAlsaCfgObt structure. + */ + snd_pcm_uframes_t obt_buffer_size = buffer_size_f; + err = snd_pcm_hw_params_get_buffer_size(pHWParms, &obt_buffer_size); + AssertLogRelMsgStmt(err >= 0, ("ALSA: Failed to get buffer size: %s\n", snd_strerror(err)), obt_buffer_size = buffer_size_f); + pCfgAcq->Backend.cFramesBufferSize = obt_buffer_size; + + snd_pcm_uframes_t obt_period_size = period_size_f; + err = snd_pcm_hw_params_get_period_size(pHWParms, &obt_period_size, &dir); + AssertLogRelMsgStmt(err >= 0, ("ALSA: Failed to get period size: %s\n", snd_strerror(err)), obt_period_size = period_size_f); + pCfgAcq->Backend.cFramesPeriod = obt_period_size; + + LogRel2(("ALSA: HW params: %u Hz, %u frames period, %u frames buffer, %u channel(s), enmAlsaFmt=%d\n", + PDMAudioPropsHz(&pCfgAcq->Props), pCfgAcq->Backend.cFramesPeriod, pCfgAcq->Backend.cFramesBufferSize, + PDMAudioPropsChannels(&pCfgAcq->Props), enmAlsaFmt)); + +#if 0 /* Disabled in the hope to resolve testboxes not being able to drain + crashing when closing the PCM streams. */ + /* + * Channel config (not fatal). + */ + if (PDMAudioPropsChannels(&pCfgAcq->Props) == PDMAudioPropsChannels(&pCfgReq->Props)) + { + err = snd_pcm_set_chmap(hPCM, &u.Map); + if (err < 0) + { + if (err == -ENXIO) + LogRel2(("ALSA: Audio device does not support channel maps, skipping\n")); + else + LogRel2(("ALSA: snd_pcm_set_chmap failed: %s (%d)\n", snd_strerror(err), err)); + } + } +#endif + + return 0; +} + + +/** + * Opens (creates) an ALSA stream. + * + * @returns VBox status code. + * @param pThis The alsa driver instance data. + * @param enmAlsaFmt The ALSA format to use. + * @param pCfgReq Requested configuration to create stream with (PDM). + * @param pCfgAcq The actual stream configuration (PDM). This is assumed + * to be a copy of pCfgReq on input, at least for + * properties handled here. On output some of the + * properties may be updated to match the actual stream + * configuration. + * @param phPCM Where to store the ALSA stream handle on success. + */ +static int alsaStreamOpen(PDRVHSTAUDALSA pThis, snd_pcm_format_t enmAlsaFmt, PCPDMAUDIOSTREAMCFG pCfgReq, + PPDMAUDIOSTREAMCFG pCfgAcq, snd_pcm_t **phPCM) +{ + /* + * Open the stream. + */ + int rc = VERR_AUDIO_STREAM_COULD_NOT_CREATE; + const char * const pszType = pCfgReq->enmDir == PDMAUDIODIR_IN ? "input" : "output"; + const char * const pszDev = pCfgReq->enmDir == PDMAUDIODIR_IN ? pThis->szInputDev : pThis->szOutputDev; + snd_pcm_stream_t enmType = pCfgReq->enmDir == PDMAUDIODIR_IN ? SND_PCM_STREAM_CAPTURE : SND_PCM_STREAM_PLAYBACK; + + snd_pcm_t *hPCM = NULL; + LogRel(("ALSA: Using %s device \"%s\"\n", pszType, pszDev)); + int err = snd_pcm_open(&hPCM, pszDev, enmType, SND_PCM_NONBLOCK); + if (err >= 0) + { + err = snd_pcm_nonblock(hPCM, 1); + if (err >= 0) + { + /* + * Configure hardware stream parameters. + */ + err = alsaStreamSetHwParams(hPCM, enmAlsaFmt, pCfgReq, pCfgAcq); + if (err >= 0) + { + /* + * Prepare it. + */ + rc = VERR_AUDIO_BACKEND_INIT_FAILED; + err = snd_pcm_prepare(hPCM); + if (err >= 0) + { + /* + * Configure software stream parameters. + */ + rc = alsaStreamSetSWParams(hPCM, pCfgReq, pCfgAcq); + if (RT_SUCCESS(rc)) + { + *phPCM = hPCM; + return VINF_SUCCESS; + } + } + else + LogRel(("ALSA: snd_pcm_prepare failed: %s\n", snd_strerror(err))); + } + } + else + LogRel(("ALSA: Error setting non-blocking mode for %s stream: %s\n", pszType, snd_strerror(err))); + drvHstAudAlsaStreamClose(&hPCM); + } + else + LogRel(("ALSA: Failed to open \"%s\" as %s device: %s\n", pszDev, pszType, snd_strerror(err))); + *phPCM = NULL; + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamCreate} + */ +static DECLCALLBACK(int) drvHstAudAlsaHA_StreamCreate(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + PCPDMAUDIOSTREAMCFG pCfgReq, PPDMAUDIOSTREAMCFG pCfgAcq) +{ + PDRVHSTAUDALSA pThis = RT_FROM_MEMBER(pInterface, DRVHSTAUDALSA, IHostAudio); + AssertPtrReturn(pInterface, VERR_INVALID_POINTER); + AssertPtrReturn(pStream, VERR_INVALID_POINTER); + AssertPtrReturn(pCfgReq, VERR_INVALID_POINTER); + AssertPtrReturn(pCfgAcq, VERR_INVALID_POINTER); + + PDRVHSTAUDALSASTREAM pStreamALSA = (PDRVHSTAUDALSASTREAM)pStream; + PDMAudioStrmCfgCopy(&pStreamALSA->Cfg, pCfgReq); + + int rc; + snd_pcm_format_t const enmFmt = alsaAudioPropsToALSA(&pCfgReq->Props); + if (enmFmt != SND_PCM_FORMAT_UNKNOWN) + { + rc = alsaStreamOpen(pThis, enmFmt, pCfgReq, pCfgAcq, &pStreamALSA->hPCM); + if (RT_SUCCESS(rc)) + { + /* We have no objections to the pre-buffering that DrvAudio applies, + only we need to adjust it relative to the actual buffer size. */ + pCfgAcq->Backend.cFramesPreBuffering = (uint64_t)pCfgReq->Backend.cFramesPreBuffering + * pCfgAcq->Backend.cFramesBufferSize + / RT_MAX(pCfgReq->Backend.cFramesBufferSize, 1); + + PDMAudioStrmCfgCopy(&pStreamALSA->Cfg, pCfgAcq); + LogFlowFunc(("returns success - hPCM=%p\n", pStreamALSA->hPCM)); + return rc; + } + } + else + rc = VERR_AUDIO_STREAM_COULD_NOT_CREATE; + LogFunc(("returns %Rrc\n", rc)); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDestroy} + */ +static DECLCALLBACK(int) drvHstAudAlsaHA_StreamDestroy(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, bool fImmediate) +{ + RT_NOREF(pInterface); + PDRVHSTAUDALSASTREAM pStreamALSA = (PDRVHSTAUDALSASTREAM)pStream; + AssertPtrReturn(pStreamALSA, VERR_INVALID_POINTER); + RT_NOREF(fImmediate); + + LogRelFlowFunc(("Stream '%s' state is '%s'\n", pStreamALSA->Cfg.szName, snd_pcm_state_name(snd_pcm_state(pStreamALSA->hPCM)))); + + /** @todo r=bird: It's not like we can do much with a bad status... Check + * what the caller does... */ + int rc = drvHstAudAlsaStreamClose(&pStreamALSA->hPCM); + + LogRelFlowFunc(("returns %Rrc\n", rc)); + + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamEnable} + */ +static DECLCALLBACK(int) drvHstAudAlsaHA_StreamEnable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PDRVHSTAUDALSASTREAM pStreamALSA = (PDRVHSTAUDALSASTREAM)pStream; + + /* + * Prepare the stream. + */ + int rc = snd_pcm_prepare(pStreamALSA->hPCM); + if (rc >= 0) + { + Assert(snd_pcm_state(pStreamALSA->hPCM) == SND_PCM_STATE_PREPARED); + + /* + * Input streams should be started now, whereas output streams must + * pre-buffer sufficent data before starting. + */ + if (pStreamALSA->Cfg.enmDir == PDMAUDIODIR_IN) + { + rc = snd_pcm_start(pStreamALSA->hPCM); + if (rc >= 0) + rc = VINF_SUCCESS; + else + { + LogRel(("ALSA: Error starting input stream '%s': %s (%d)\n", pStreamALSA->Cfg.szName, snd_strerror(rc), rc)); + rc = RTErrConvertFromErrno(-rc); + } + } + else + rc = VINF_SUCCESS; + } + else + { + LogRel(("ALSA: Error preparing stream '%s': %s (%d)\n", pStreamALSA->Cfg.szName, snd_strerror(rc), rc)); + rc = RTErrConvertFromErrno(-rc); + } + LogFlowFunc(("returns %Rrc (state %s)\n", rc, snd_pcm_state_name(snd_pcm_state(pStreamALSA->hPCM)))); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDisable} + */ +static DECLCALLBACK(int) drvHstAudAlsaHA_StreamDisable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PDRVHSTAUDALSASTREAM pStreamALSA = (PDRVHSTAUDALSASTREAM)pStream; + + int rc = snd_pcm_drop(pStreamALSA->hPCM); + if (rc >= 0) + rc = VINF_SUCCESS; + else + { + LogRel(("ALSA: Error stopping stream '%s': %s (%d)\n", pStreamALSA->Cfg.szName, snd_strerror(rc), rc)); + rc = RTErrConvertFromErrno(-rc); + } + LogFlowFunc(("returns %Rrc (state %s)\n", rc, snd_pcm_state_name(snd_pcm_state(pStreamALSA->hPCM)))); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamPause} + */ +static DECLCALLBACK(int) drvHstAudAlsaHA_StreamPause(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + /* Same as disable. */ + /** @todo r=bird: Try use pause and fallback on disable/enable if it isn't + * supported or doesn't work. */ + return drvHstAudAlsaHA_StreamDisable(pInterface, pStream); +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamResume} + */ +static DECLCALLBACK(int) drvHstAudAlsaHA_StreamResume(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + /* Same as enable. */ + return drvHstAudAlsaHA_StreamEnable(pInterface, pStream); +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDrain} + */ +static DECLCALLBACK(int) drvHstAudAlsaHA_StreamDrain(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PDRVHSTAUDALSASTREAM pStreamALSA = (PDRVHSTAUDALSASTREAM)pStream; + + snd_pcm_state_t const enmState = snd_pcm_state(pStreamALSA->hPCM); + LogRelFlowFunc(("Stream '%s' input state: %s (%d)\n", pStreamALSA->Cfg.szName, snd_pcm_state_name(enmState), enmState)); + + /* Only for output streams. */ + AssertReturn(pStreamALSA->Cfg.enmDir == PDMAUDIODIR_OUT, VERR_WRONG_ORDER); + + int rc; + switch (enmState) + { + case SND_PCM_STATE_RUNNING: + case SND_PCM_STATE_PREPARED: /* not yet started */ + { + /* Do not change to blocking here! */ + rc = snd_pcm_drain(pStreamALSA->hPCM); + if (rc >= 0 || rc == -EAGAIN) + rc = VINF_SUCCESS; + else + { + snd_pcm_state_t const enmState2 = snd_pcm_state(pStreamALSA->hPCM); + if (rc == -EPIPE && enmState2 == enmState) + { + /* Not entirely sure, but possibly an underrun, so just disable the stream. */ + LogRel2(("ALSA: snd_pcm_drain failed with -EPIPE, stopping stream (%s)\n", pStreamALSA->Cfg.szName)); + rc = snd_pcm_drop(pStreamALSA->hPCM); + if (rc >= 0) + rc = VINF_SUCCESS; + else + { + LogRel(("ALSA: Error draining/stopping stream '%s': %s (%d)\n", pStreamALSA->Cfg.szName, snd_strerror(rc), rc)); + rc = RTErrConvertFromErrno(-rc); + } + } + else + { + LogRel(("ALSA: Error draining output of '%s': %s (%d; %s -> %s)\n", pStreamALSA->Cfg.szName, snd_strerror(rc), + rc, snd_pcm_state_name(enmState), snd_pcm_state_name(enmState2))); + rc = RTErrConvertFromErrno(-rc); + } + } + break; + } + + default: + rc = VINF_SUCCESS; + break; + } + LogRelFlowFunc(("returns %Rrc (state %s)\n", rc, snd_pcm_state_name(snd_pcm_state(pStreamALSA->hPCM)))); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetState} + */ +static DECLCALLBACK(PDMHOSTAUDIOSTREAMSTATE) drvHstAudAlsaHA_StreamGetState(PPDMIHOSTAUDIO pInterface, + PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PDRVHSTAUDALSASTREAM pStreamALSA = (PDRVHSTAUDALSASTREAM)pStream; + AssertPtrReturn(pStreamALSA, PDMHOSTAUDIOSTREAMSTATE_INVALID); + + PDMHOSTAUDIOSTREAMSTATE enmStreamState = PDMHOSTAUDIOSTREAMSTATE_OKAY; + snd_pcm_state_t enmAlsaState = snd_pcm_state(pStreamALSA->hPCM); + if (enmAlsaState == SND_PCM_STATE_DRAINING) + { + /* We're operating in non-blocking mode, so we must (at least for a demux + config) call snd_pcm_drain again to drive it forward. Otherwise we + might be stuck in the drain state forever. */ + Log5Func(("Calling snd_pcm_drain again...\n")); + snd_pcm_drain(pStreamALSA->hPCM); + enmAlsaState = snd_pcm_state(pStreamALSA->hPCM); + } + + if (enmAlsaState == SND_PCM_STATE_DRAINING) + enmStreamState = PDMHOSTAUDIOSTREAMSTATE_DRAINING; +#if (((SND_LIB_MAJOR) << 16) | ((SND_LIB_MAJOR) << 8) | (SND_LIB_SUBMINOR)) >= 0x10002 /* was added in 1.0.2 */ + else if (enmAlsaState == SND_PCM_STATE_DISCONNECTED) + enmStreamState = PDMHOSTAUDIOSTREAMSTATE_NOT_WORKING; +#endif + + Log5Func(("Stream '%s': ALSA state=%s -> %s\n", + pStreamALSA->Cfg.szName, snd_pcm_state_name(enmAlsaState), PDMHostAudioStreamStateGetName(enmStreamState) )); + return enmStreamState; +} + + +/** + * Returns the available audio frames queued. + * + * @returns VBox status code. + * @param hPCM ALSA stream handle. + * @param pcFramesAvail Where to store the available frames. + */ +static int alsaStreamGetAvail(snd_pcm_t *hPCM, snd_pcm_sframes_t *pcFramesAvail) +{ + AssertPtr(hPCM); + AssertPtr(pcFramesAvail); + + int rc; + snd_pcm_sframes_t cFramesAvail = snd_pcm_avail_update(hPCM); + if (cFramesAvail > 0) + { + LogFunc(("cFramesAvail=%ld\n", cFramesAvail)); + *pcFramesAvail = cFramesAvail; + return VINF_SUCCESS; + } + + /* + * We can maybe recover from an EPIPE... + */ + if (cFramesAvail == -EPIPE) + { + rc = drvHstAudAlsaStreamRecover(hPCM); + if (RT_SUCCESS(rc)) + { + cFramesAvail = snd_pcm_avail_update(hPCM); + if (cFramesAvail >= 0) + { + LogFunc(("cFramesAvail=%ld\n", cFramesAvail)); + *pcFramesAvail = cFramesAvail; + return VINF_SUCCESS; + } + } + else + { + *pcFramesAvail = 0; + return rc; + } + } + + rc = RTErrConvertFromErrno(-(int)cFramesAvail); + LogFunc(("failed - cFramesAvail=%ld rc=%Rrc\n", cFramesAvail, rc)); + *pcFramesAvail = 0; + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetPending} + */ +static DECLCALLBACK(uint32_t) drvHstAudAlsaHA_StreamGetPending(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PDRVHSTAUDALSASTREAM pStreamALSA = (PDRVHSTAUDALSASTREAM)pStream; + AssertPtrReturn(pStreamALSA, 0); + + /* + * This is only relevant to output streams (input streams can't have + * any pending, unplayed data). + */ + uint32_t cbPending = 0; + if (pStreamALSA->Cfg.enmDir == PDMAUDIODIR_OUT) + { + /* + * Getting the delay (in audio frames) reports the time it will take + * to hear a new sample after all queued samples have been played out. + * + * We use snd_pcm_avail_delay instead of snd_pcm_delay here as it will + * update the buffer positions, and we can use the extra value against + * the buffer size to double check since the delay value may include + * fixed built-in delays in the processing chain and hardware. + */ + snd_pcm_sframes_t cFramesAvail = 0; + snd_pcm_sframes_t cFramesDelay = 0; + int rc = snd_pcm_avail_delay(pStreamALSA->hPCM, &cFramesAvail, &cFramesDelay); + + /* + * We now also get the state as the pending value should be zero when + * we're not in a playing state. + */ + snd_pcm_state_t enmState = snd_pcm_state(pStreamALSA->hPCM); + switch (enmState) + { + case SND_PCM_STATE_RUNNING: + case SND_PCM_STATE_DRAINING: + if (rc >= 0) + { + if ((uint32_t)cFramesAvail >= pStreamALSA->Cfg.Backend.cFramesBufferSize) + cbPending = 0; + else + cbPending = PDMAudioPropsFramesToBytes(&pStreamALSA->Cfg.Props, cFramesDelay); + } + break; + + default: + break; + } + Log2Func(("returns %u (%#x) - cFramesBufferSize=%RU32 cFramesAvail=%ld cFramesDelay=%ld rc=%d; enmState=%s (%d) \n", + cbPending, cbPending, pStreamALSA->Cfg.Backend.cFramesBufferSize, cFramesAvail, cFramesDelay, rc, + snd_pcm_state_name(enmState), enmState)); + } + return cbPending; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetWritable} + */ +static DECLCALLBACK(uint32_t) drvHstAudAlsaHA_StreamGetWritable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PDRVHSTAUDALSASTREAM pStreamALSA = (PDRVHSTAUDALSASTREAM)pStream; + + uint32_t cbAvail = 0; + snd_pcm_sframes_t cFramesAvail = 0; + int rc = alsaStreamGetAvail(pStreamALSA->hPCM, &cFramesAvail); + if (RT_SUCCESS(rc)) + cbAvail = PDMAudioPropsFramesToBytes(&pStreamALSA->Cfg.Props, cFramesAvail); + + return cbAvail; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamPlay} + */ +static DECLCALLBACK(int) drvHstAudAlsaHA_StreamPlay(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + const void *pvBuf, uint32_t cbBuf, uint32_t *pcbWritten) +{ + PDRVHSTAUDALSASTREAM pStreamALSA = (PDRVHSTAUDALSASTREAM)pStream; + AssertPtrReturn(pInterface, VERR_INVALID_POINTER); + AssertPtrReturn(pStream, VERR_INVALID_POINTER); + AssertPtrReturn(pcbWritten, VERR_INVALID_POINTER); + Log4Func(("@%#RX64: pvBuf=%p cbBuf=%#x (%u) state=%s - %s\n", pStreamALSA->offInternal, pvBuf, cbBuf, cbBuf, + snd_pcm_state_name(snd_pcm_state(pStreamALSA->hPCM)), pStreamALSA->Cfg.szName)); + if (cbBuf) + AssertPtrReturn(pvBuf, VERR_INVALID_POINTER); + else + { + /* Fend off draining calls. */ + *pcbWritten = 0; + return VINF_SUCCESS; + } + + /* + * Determine how much we can write (caller actually did this + * already, but we repeat it just to be sure or something). + */ + snd_pcm_sframes_t cFramesAvail; + int rc = alsaStreamGetAvail(pStreamALSA->hPCM, &cFramesAvail); + if (RT_SUCCESS(rc)) + { + Assert(cFramesAvail); + if (cFramesAvail) + { + PCPDMAUDIOPCMPROPS pProps = &pStreamALSA->Cfg.Props; + uint32_t cbToWrite = PDMAudioPropsFramesToBytes(pProps, (uint32_t)cFramesAvail); + if (cbToWrite) + { + if (cbToWrite > cbBuf) + cbToWrite = cbBuf; + + /* + * Try write the data. + */ + uint32_t cFramesToWrite = PDMAudioPropsBytesToFrames(pProps, cbToWrite); + snd_pcm_sframes_t cFramesWritten = snd_pcm_writei(pStreamALSA->hPCM, pvBuf, cFramesToWrite); + if (cFramesWritten > 0) + { + Log4Func(("snd_pcm_writei w/ cbToWrite=%u -> %ld (frames) [cFramesAvail=%ld]\n", + cbToWrite, cFramesWritten, cFramesAvail)); + *pcbWritten = PDMAudioPropsFramesToBytes(pProps, cFramesWritten); + pStreamALSA->offInternal += *pcbWritten; + return VINF_SUCCESS; + } + LogFunc(("snd_pcm_writei w/ cbToWrite=%u -> %ld [cFramesAvail=%ld]\n", cbToWrite, cFramesWritten, cFramesAvail)); + + + /* + * There are a couple of error we can recover from, try to do so. + * Only don't try too many times. + */ + for (unsigned iTry = 0; + (cFramesWritten == -EPIPE || cFramesWritten == -ESTRPIPE) && iTry < ALSA_RECOVERY_TRIES_MAX; + iTry++) + { + if (cFramesWritten == -EPIPE) + { + /* Underrun occurred. */ + rc = drvHstAudAlsaStreamRecover(pStreamALSA->hPCM); + if (RT_FAILURE(rc)) + break; + LogFlowFunc(("Recovered from playback (iTry=%u)\n", iTry)); + } + else + { + /* An suspended event occurred, needs resuming. */ + rc = drvHstAudAlsaStreamResume(pStreamALSA->hPCM); + if (RT_FAILURE(rc)) + { + LogRel(("ALSA: Failed to resume output stream (iTry=%u, rc=%Rrc)\n", iTry, rc)); + break; + } + LogFlowFunc(("Resumed suspended output stream (iTry=%u)\n", iTry)); + } + + cFramesWritten = snd_pcm_writei(pStreamALSA->hPCM, pvBuf, cFramesToWrite); + if (cFramesWritten > 0) + { + Log4Func(("snd_pcm_writei w/ cbToWrite=%u -> %ld (frames) [cFramesAvail=%ld]\n", + cbToWrite, cFramesWritten, cFramesAvail)); + *pcbWritten = PDMAudioPropsFramesToBytes(pProps, cFramesWritten); + pStreamALSA->offInternal += *pcbWritten; + return VINF_SUCCESS; + } + LogFunc(("snd_pcm_writei w/ cbToWrite=%u -> %ld [cFramesAvail=%ld, iTry=%d]\n", cbToWrite, cFramesWritten, cFramesAvail, iTry)); + } + + /* Make sure we return with an error status. */ + if (RT_SUCCESS_NP(rc)) + { + if (cFramesWritten == 0) + rc = VERR_ACCESS_DENIED; + else + { + rc = RTErrConvertFromErrno(-(int)cFramesWritten); + LogFunc(("Failed to write %RU32 bytes: %ld (%Rrc)\n", cbToWrite, cFramesWritten, rc)); + } + } + } + } + } + else + LogFunc(("Error getting number of playback frames, rc=%Rrc\n", rc)); + *pcbWritten = 0; + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetReadable} + */ +static DECLCALLBACK(uint32_t) drvHstAudAlsaHA_StreamGetReadable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PDRVHSTAUDALSASTREAM pStreamALSA = (PDRVHSTAUDALSASTREAM)pStream; + + uint32_t cbAvail = 0; + snd_pcm_sframes_t cFramesAvail = 0; + int rc = alsaStreamGetAvail(pStreamALSA->hPCM, &cFramesAvail); + if (RT_SUCCESS(rc)) + cbAvail = PDMAudioPropsFramesToBytes(&pStreamALSA->Cfg.Props, cFramesAvail); + + return cbAvail; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamCapture} + */ +static DECLCALLBACK(int) drvHstAudAlsaHA_StreamCapture(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + void *pvBuf, uint32_t cbBuf, uint32_t *pcbRead) +{ + RT_NOREF_PV(pInterface); + PDRVHSTAUDALSASTREAM pStreamALSA = (PDRVHSTAUDALSASTREAM)pStream; + AssertPtrReturn(pStreamALSA, VERR_INVALID_POINTER); + AssertPtrReturn(pvBuf, VERR_INVALID_POINTER); + AssertReturn(cbBuf, VERR_INVALID_PARAMETER); + AssertPtrReturn(pcbRead, VERR_INVALID_POINTER); + Log4Func(("@%#RX64: pvBuf=%p cbBuf=%#x (%u) state=%s - %s\n", pStreamALSA->offInternal, pvBuf, cbBuf, cbBuf, + snd_pcm_state_name(snd_pcm_state(pStreamALSA->hPCM)), pStreamALSA->Cfg.szName)); + + /* + * Figure out how much we can read without trouble (we're doing + * non-blocking reads, but whatever). + */ + snd_pcm_sframes_t cAvail; + int rc = alsaStreamGetAvail(pStreamALSA->hPCM, &cAvail); + if (RT_SUCCESS(rc)) + { + if (!cAvail) /* No data yet? */ + { + snd_pcm_state_t enmState = snd_pcm_state(pStreamALSA->hPCM); + switch (enmState) + { + case SND_PCM_STATE_PREPARED: + /** @todo r=bird: explain the logic here... */ + cAvail = PDMAudioPropsBytesToFrames(&pStreamALSA->Cfg.Props, cbBuf); + break; + + case SND_PCM_STATE_SUSPENDED: + rc = drvHstAudAlsaStreamResume(pStreamALSA->hPCM); + if (RT_SUCCESS(rc)) + { + LogFlowFunc(("Resumed suspended input stream.\n")); + break; + } + LogFunc(("Failed resuming suspended input stream: %Rrc\n", rc)); + return rc; + + default: + LogFlow(("No frames available: state=%s (%d)\n", snd_pcm_state_name(enmState), enmState)); + break; + } + if (!cAvail) + { + *pcbRead = 0; + return VINF_SUCCESS; + } + } + } + else + { + LogFunc(("Error getting number of captured frames, rc=%Rrc\n", rc)); + return rc; + } + + size_t cbToRead = PDMAudioPropsFramesToBytes(&pStreamALSA->Cfg.Props, cAvail); + cbToRead = RT_MIN(cbToRead, cbBuf); + LogFlowFunc(("cbToRead=%zu, cAvail=%RI32\n", cbToRead, cAvail)); + + /* + * Read loop. + */ + uint32_t cbReadTotal = 0; + while (cbToRead > 0) + { + /* + * Do the reading. + */ + snd_pcm_uframes_t const cFramesToRead = PDMAudioPropsBytesToFrames(&pStreamALSA->Cfg.Props, cbToRead); + AssertBreakStmt(cFramesToRead > 0, rc = VERR_NO_DATA); + + snd_pcm_sframes_t cFramesRead = snd_pcm_readi(pStreamALSA->hPCM, pvBuf, cFramesToRead); + if (cFramesRead > 0) + { + /* + * We should not run into a full mixer buffer or we lose samples and + * run into an endless loop if ALSA keeps producing samples ("null" + * capture device for example). + */ + uint32_t const cbRead = PDMAudioPropsFramesToBytes(&pStreamALSA->Cfg.Props, cFramesRead); + Assert(cbRead <= cbToRead); + + cbToRead -= cbRead; + cbReadTotal += cbRead; + pvBuf = (uint8_t *)pvBuf + cbRead; + pStreamALSA->offInternal += cbRead; + } + else + { + /* + * Try recover from overrun and re-try. + * Other conditions/errors we cannot and will just quit the loop. + */ + if (cFramesRead == -EPIPE) + { + rc = drvHstAudAlsaStreamRecover(pStreamALSA->hPCM); + if (RT_SUCCESS(rc)) + { + LogFlowFunc(("Successfully recovered from overrun\n")); + continue; + } + LogFunc(("Failed to recover from overrun: %Rrc\n", rc)); + } + else if (cFramesRead == -EAGAIN) + LogFunc(("No input frames available (EAGAIN)\n")); + else if (cFramesRead == 0) + LogFunc(("No input frames available (0)\n")); + else + { + rc = RTErrConvertFromErrno(-(int)cFramesRead); + LogFunc(("Failed to read input frames: %s (%ld, %Rrc)\n", snd_strerror(cFramesRead), cFramesRead, rc)); + } + + /* If we've read anything, suppress the error. */ + if (RT_FAILURE(rc) && cbReadTotal > 0) + { + LogFunc(("Suppressing %Rrc because %#x bytes has been read already\n", rc, cbReadTotal)); + rc = VINF_SUCCESS; + } + break; + } + } + + LogFlowFunc(("returns %Rrc and %#x (%d) bytes (%u bytes left); state %s\n", + rc, cbReadTotal, cbReadTotal, cbToRead, snd_pcm_state_name(snd_pcm_state(pStreamALSA->hPCM)))); + *pcbRead = cbReadTotal; + return rc; +} + + +/********************************************************************************************************************************* +* PDMIBASE * +*********************************************************************************************************************************/ + +/** + * @interface_method_impl{PDMIBASE,pfnQueryInterface} + */ +static DECLCALLBACK(void *) drvHstAudAlsaQueryInterface(PPDMIBASE pInterface, const char *pszIID) +{ + PPDMDRVINS pDrvIns = PDMIBASE_2_PDMDRV(pInterface); + PDRVHSTAUDALSA pThis = PDMINS_2_DATA(pDrvIns, PDRVHSTAUDALSA); + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIBASE, &pDrvIns->IBase); + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIHOSTAUDIO, &pThis->IHostAudio); + return NULL; +} + + +/********************************************************************************************************************************* +* PDMDRVREG * +*********************************************************************************************************************************/ + +/** + * @interface_method_impl{PDMDRVREG,pfnDestruct, + * Destructs an ALSA host audio driver instance.} + */ +static DECLCALLBACK(void) drvHstAudAlsaDestruct(PPDMDRVINS pDrvIns) +{ + PDMDRV_CHECK_VERSIONS_RETURN_VOID(pDrvIns); + PDRVHSTAUDALSA pThis = PDMINS_2_DATA(pDrvIns, PDRVHSTAUDALSA); + LogFlowFuncEnter(); + + if (RTCritSectIsInitialized(&pThis->CritSect)) + { + RTCritSectEnter(&pThis->CritSect); + pThis->pIHostAudioPort = NULL; + RTCritSectLeave(&pThis->CritSect); + RTCritSectDelete(&pThis->CritSect); + } + + LogFlowFuncLeave(); +} + + +/** + * @interface_method_impl{PDMDRVREG,pfnConstruct, + * Construct an ALSA host audio driver instance.} + */ +static DECLCALLBACK(int) drvHstAudAlsaConstruct(PPDMDRVINS pDrvIns, PCFGMNODE pCfg, uint32_t fFlags) +{ + RT_NOREF(fFlags); + PDMDRV_CHECK_VERSIONS_RETURN(pDrvIns); + PDRVHSTAUDALSA pThis = PDMINS_2_DATA(pDrvIns, PDRVHSTAUDALSA); + PCPDMDRVHLPR3 pHlp = pDrvIns->pHlpR3; + LogRel(("Audio: Initializing ALSA driver\n")); + + /* + * Init the static parts. + */ + pThis->pDrvIns = pDrvIns; + int rc = RTCritSectInit(&pThis->CritSect); + AssertRCReturn(rc, rc); + /* IBase */ + pDrvIns->IBase.pfnQueryInterface = drvHstAudAlsaQueryInterface; + /* IHostAudio */ + pThis->IHostAudio.pfnGetConfig = drvHstAudAlsaHA_GetConfig; + pThis->IHostAudio.pfnGetDevices = drvHstAudAlsaHA_GetDevices; + pThis->IHostAudio.pfnSetDevice = drvHstAudAlsaHA_SetDevice; + pThis->IHostAudio.pfnGetStatus = drvHstAudAlsaHA_GetStatus; + pThis->IHostAudio.pfnDoOnWorkerThread = NULL; + pThis->IHostAudio.pfnStreamConfigHint = NULL; + pThis->IHostAudio.pfnStreamCreate = drvHstAudAlsaHA_StreamCreate; + pThis->IHostAudio.pfnStreamInitAsync = NULL; + pThis->IHostAudio.pfnStreamDestroy = drvHstAudAlsaHA_StreamDestroy; + pThis->IHostAudio.pfnStreamNotifyDeviceChanged = NULL; + pThis->IHostAudio.pfnStreamEnable = drvHstAudAlsaHA_StreamEnable; + pThis->IHostAudio.pfnStreamDisable = drvHstAudAlsaHA_StreamDisable; + pThis->IHostAudio.pfnStreamPause = drvHstAudAlsaHA_StreamPause; + pThis->IHostAudio.pfnStreamResume = drvHstAudAlsaHA_StreamResume; + pThis->IHostAudio.pfnStreamDrain = drvHstAudAlsaHA_StreamDrain; + pThis->IHostAudio.pfnStreamGetPending = drvHstAudAlsaHA_StreamGetPending; + pThis->IHostAudio.pfnStreamGetState = drvHstAudAlsaHA_StreamGetState; + pThis->IHostAudio.pfnStreamGetWritable = drvHstAudAlsaHA_StreamGetWritable; + pThis->IHostAudio.pfnStreamPlay = drvHstAudAlsaHA_StreamPlay; + pThis->IHostAudio.pfnStreamGetReadable = drvHstAudAlsaHA_StreamGetReadable; + pThis->IHostAudio.pfnStreamCapture = drvHstAudAlsaHA_StreamCapture; + + /* + * Read configuration. + */ + PDMDRV_VALIDATE_CONFIG_RETURN(pDrvIns, "OutputDeviceID|InputDeviceID", ""); + + rc = pHlp->pfnCFGMQueryStringDef(pCfg, "InputDeviceID", pThis->szInputDev, sizeof(pThis->szInputDev), "default"); + AssertRCReturn(rc, rc); + rc = pHlp->pfnCFGMQueryStringDef(pCfg, "OutputDeviceID", pThis->szOutputDev, sizeof(pThis->szOutputDev), "default"); + AssertRCReturn(rc, rc); + + /* + * Init the alsa library. + */ + rc = audioLoadAlsaLib(); + if (RT_FAILURE(rc)) + { + LogRel(("ALSA: Failed to load the ALSA shared library: %Rrc\n", rc)); + return 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); + +#ifdef DEBUG + /* + * Some debug stuff we don't use for anything at all. + */ + snd_lib_error_set_handler(drvHstAudAlsaDbgErrorHandler); +#endif + return VINF_SUCCESS; +} + + +/** + * ALSA audio driver registration record. + */ +const PDMDRVREG g_DrvHostALSAAudio = +{ + /* u32Version */ + PDM_DRVREG_VERSION, + /* szName */ + "ALSAAudio", + /* szRCMod */ + "", + /* szR0Mod */ + "", + /* pszDescription */ + "ALSA host audio driver", + /* fFlags */ + PDM_DRVREG_FLAGS_HOST_BITS_DEFAULT, + /* fClass. */ + PDM_DRVREG_CLASS_AUDIO, + /* cMaxInstances */ + ~0U, + /* cbInstance */ + sizeof(DRVHSTAUDALSA), + /* pfnConstruct */ + drvHstAudAlsaConstruct, + /* pfnDestruct */ + drvHstAudAlsaDestruct, + /* pfnRelocate */ + NULL, + /* pfnIOCtl */ + NULL, + /* pfnPowerOn */ + NULL, + /* pfnReset */ + NULL, + /* pfnSuspend */ + NULL, + /* pfnResume */ + NULL, + /* pfnAttach */ + NULL, + /* pfnDetach */ + NULL, + /* pfnPowerOff */ + NULL, + /* pfnSoftReset */ + NULL, + /* u32EndVersion */ + PDM_DRVREG_VERSION +}; + diff --git a/src/VBox/Devices/Audio/DrvHostAudioAlsaStubs.cpp b/src/VBox/Devices/Audio/DrvHostAudioAlsaStubs.cpp new file mode 100644 index 00000000..70d6b9c1 --- /dev/null +++ b/src/VBox/Devices/Audio/DrvHostAudioAlsaStubs.cpp @@ -0,0 +1,364 @@ +/* $Id: DrvHostAudioAlsaStubs.cpp $ */ +/** @file + * Stubs for libasound. + */ + +/* + * Copyright (C) 2006-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 + */ + +#define LOG_GROUP LOG_GROUP_DRV_HOST_AUDIO +#include <iprt/assert.h> +#include <iprt/err.h> +#include <iprt/ldr.h> +#include <VBox/log.h> +#include <iprt/once.h> + +#include <alsa/asoundlib.h> +#include <errno.h> + +#include "DrvHostAudioAlsaStubs.h" + +#define VBOX_ALSA_LIB "libasound.so.2" + +#define PROXY_STUB(function, rettype, signature, shortsig) \ + static rettype (*pfn_ ## function) signature; \ + \ + extern "C" rettype VBox_##function signature; \ + rettype VBox_##function signature \ + { \ + return pfn_ ## function shortsig; \ + } + +PROXY_STUB(snd_lib_error_set_handler, int, (snd_lib_error_handler_t handler), + (handler)) +PROXY_STUB(snd_strerror, const char *, (int errnum), (errnum)) + +PROXY_STUB(snd_device_name_hint, int, + (int card, const char *iface, void ***hints), + (card, iface, hints)) +PROXY_STUB(snd_device_name_free_hint, int, + (void **hints), + (hints)) +PROXY_STUB(snd_device_name_get_hint, char *, + (const void *hint, const char *id), + (hint, id)) + +static int fallback_snd_device_name_hint(int card, const char *iface, void ***hints) +{ + RT_NOREF(card, iface); + *hints = NULL; + return -ENOSYS; +} + +static int fallback_snd_device_name_free_hint(void **hints) +{ + RT_NOREF(hints); + return 0; +} + +static char *fallback_snd_device_name_get_hint(const void *hint, const char *id) +{ + RT_NOREF(hint, id); + return NULL; +} + +/* + * PCM + */ + +PROXY_STUB(snd_pcm_avail_update, snd_pcm_sframes_t, (snd_pcm_t *pcm), (pcm)) +PROXY_STUB(snd_pcm_avail_delay, int, + (snd_pcm_t *pcm, snd_pcm_sframes_t *availp, snd_pcm_sframes_t *delayp), + (pcm, availp, delayp)) +PROXY_STUB(snd_pcm_close, int, (snd_pcm_t *pcm), (pcm)) +PROXY_STUB(snd_pcm_delay, int, (snd_pcm_t *pcm, snd_pcm_sframes_t *delayp), (pcm, delayp)) +PROXY_STUB(snd_pcm_nonblock, int, (snd_pcm_t *pcm, int *onoff), + (pcm, onoff)) +PROXY_STUB(snd_pcm_drain, int, (snd_pcm_t *pcm), + (pcm)) +PROXY_STUB(snd_pcm_drop, int, (snd_pcm_t *pcm), (pcm)) +PROXY_STUB(snd_pcm_open, int, + (snd_pcm_t **pcm, const char *name, snd_pcm_stream_t stream, int mode), + (pcm, name, stream, mode)) +PROXY_STUB(snd_pcm_prepare, int, (snd_pcm_t *pcm), (pcm)) +PROXY_STUB(snd_pcm_readi, snd_pcm_sframes_t, + (snd_pcm_t *pcm, void *buffer, snd_pcm_uframes_t size), + (pcm, buffer, size)) +PROXY_STUB(snd_pcm_resume, int, (snd_pcm_t *pcm), (pcm)) +PROXY_STUB(snd_pcm_set_chmap, int, (snd_pcm_t *pcm, snd_pcm_chmap_t const *map), (pcm, map)) +PROXY_STUB(snd_pcm_state, snd_pcm_state_t, (snd_pcm_t *pcm), (pcm)) +PROXY_STUB(snd_pcm_state_name, const char *, (snd_pcm_state_t state), (state)) +PROXY_STUB(snd_pcm_writei, snd_pcm_sframes_t, + (snd_pcm_t *pcm, const void *buffer, snd_pcm_uframes_t size), + (pcm, buffer, size)) +PROXY_STUB(snd_pcm_start, int, (snd_pcm_t *pcm), (pcm)) + +static int fallback_snd_pcm_avail_delay(snd_pcm_t *pcm, snd_pcm_sframes_t *availp, snd_pcm_sframes_t *delayp) +{ + *availp = pfn_snd_pcm_avail_update(pcm); + int ret = pfn_snd_pcm_delay(pcm, delayp); + if (ret >= 0 && *availp < 0) + ret = (int)*availp; + return ret; +} + +static int fallback_snd_pcm_set_chmap(snd_pcm_t *pcm, snd_pcm_chmap_t const *map) +{ + RT_NOREF(pcm, map); + return 0; +} + +/* + * HW + */ + +PROXY_STUB(snd_pcm_hw_params, int, + (snd_pcm_t *pcm, snd_pcm_hw_params_t *params), + (pcm, params)) +PROXY_STUB(snd_pcm_hw_params_any, int, + (snd_pcm_t *pcm, snd_pcm_hw_params_t *params), + (pcm, params)) +PROXY_STUB(snd_pcm_hw_params_get_buffer_size, int, + (const snd_pcm_hw_params_t *params, snd_pcm_uframes_t *val), + (params, val)) +PROXY_STUB(snd_pcm_hw_params_get_buffer_size_min, int, + (const snd_pcm_hw_params_t *params, snd_pcm_uframes_t *val), + (params, val)) +PROXY_STUB(snd_pcm_hw_params_get_period_size, int, + (const snd_pcm_hw_params_t *params, snd_pcm_uframes_t *frames, int *dir), + (params, frames, dir)) +PROXY_STUB(snd_pcm_hw_params_get_period_size_min, int, + (const snd_pcm_hw_params_t *params, snd_pcm_uframes_t *frames, int *dir), + (params, frames, dir)) +PROXY_STUB(snd_pcm_hw_params_set_rate_near, int, + (snd_pcm_t *pcm, snd_pcm_hw_params_t *params, unsigned int *val, int *dir), + (pcm, params, val, dir)) +PROXY_STUB(snd_pcm_hw_params_set_access, int, + (snd_pcm_t *pcm, snd_pcm_hw_params_t *params, snd_pcm_access_t _access), + (pcm, params, _access)) +PROXY_STUB(snd_pcm_hw_params_set_buffer_time_near, int, + (snd_pcm_t *pcm, snd_pcm_hw_params_t *params, unsigned int *val, int *dir), + (pcm, params, val, dir)) +PROXY_STUB(snd_pcm_hw_params_set_buffer_size_near, int, + (snd_pcm_t *pcm, snd_pcm_hw_params_t *params, snd_pcm_uframes_t *val), + (pcm, params, val)) +PROXY_STUB(snd_pcm_hw_params_set_channels_near, int, + (snd_pcm_t *pcm, snd_pcm_hw_params_t *params, unsigned int *val), + (pcm, params, val)) +PROXY_STUB(snd_pcm_hw_params_set_period_size_near, int, + (snd_pcm_t *pcm, snd_pcm_hw_params_t *params, snd_pcm_uframes_t *val, int *dir), + (pcm, params, val, dir)) +PROXY_STUB(snd_pcm_hw_params_set_period_time_near, int, + (snd_pcm_t *pcm, snd_pcm_hw_params_t *params, unsigned int *val, int *dir), + (pcm, params, val, dir)) +PROXY_STUB(snd_pcm_hw_params_sizeof, size_t, (void), ()) +PROXY_STUB(snd_pcm_hw_params_set_format, int, + (snd_pcm_t *pcm, snd_pcm_hw_params_t *params, snd_pcm_format_t val), + (pcm, params, val)) + +/* + * SW + */ + +PROXY_STUB(snd_pcm_sw_params, int, + (snd_pcm_t *pcm, snd_pcm_sw_params_t *params), + (pcm, params)) +PROXY_STUB(snd_pcm_sw_params_current, int, + (snd_pcm_t *pcm, snd_pcm_sw_params_t *params), + (pcm, params)) +PROXY_STUB(snd_pcm_sw_params_get_start_threshold, int, + (const snd_pcm_sw_params_t *params, snd_pcm_uframes_t *val), + (params, val)) +PROXY_STUB(snd_pcm_sw_params_set_avail_min, int, + (snd_pcm_t *pcm, snd_pcm_sw_params_t *params, snd_pcm_uframes_t val), + (pcm, params, val)) +PROXY_STUB(snd_pcm_sw_params_set_start_threshold, int, + (snd_pcm_t *pcm, snd_pcm_sw_params_t *params, snd_pcm_uframes_t val), + (pcm, params, val)) +PROXY_STUB(snd_pcm_sw_params_sizeof, size_t, (void), ()) + +/* + * Mixer + */ + +PROXY_STUB(snd_mixer_selem_id_sizeof, size_t, + (void), ()) +PROXY_STUB(snd_mixer_open, int, + (snd_mixer_t **mixer, int mode), + (mixer, mode)) +PROXY_STUB(snd_mixer_attach, int, + (snd_mixer_t *mixer, const char *name), + (mixer, name)) +PROXY_STUB(snd_mixer_close, int, + (snd_mixer_t *mixer), + (mixer)) +PROXY_STUB(snd_mixer_selem_id_set_index, void, + (snd_mixer_selem_id_t *obj, unsigned int val), + (obj, val)) +PROXY_STUB(snd_mixer_selem_id_set_name, void, + (snd_mixer_selem_id_t *obj, const char *val), + (obj, val)) +PROXY_STUB(snd_mixer_selem_set_playback_volume, int, + (snd_mixer_elem_t *elem, snd_mixer_selem_channel_id_t channel, long value), + (elem, channel, value)) +PROXY_STUB(snd_mixer_selem_get_playback_volume_range, int, + (snd_mixer_elem_t *elem, long *min, long *max), + (elem, min, max)) +PROXY_STUB(snd_mixer_selem_set_capture_volume, int, + (snd_mixer_elem_t *elem, snd_mixer_selem_channel_id_t channel, long value), + (elem, channel, value)) +PROXY_STUB(snd_mixer_selem_get_capture_volume_range, int, + (snd_mixer_elem_t *elem, long *min, long *max), + (elem, min, max)) +PROXY_STUB(snd_mixer_selem_register, int, + (snd_mixer_t *mixer, snd_mixer_selem_regopt *options, snd_mixer_class_t **classp), + (mixer, options, classp)) +PROXY_STUB(snd_mixer_load, int, + (snd_mixer_t *mixer), + (mixer)) +PROXY_STUB(snd_mixer_find_selem, snd_mixer_elem_t *, + (snd_mixer_t *mixer, const snd_mixer_selem_id_t *id), + (mixer, id)) + +typedef struct +{ + const char *name; + void (**pfn)(void); + void (*pfnFallback)(void); +} SHARED_FUNC; + +#define ELEMENT(function) { #function , (void (**)(void)) & pfn_ ## function, NULL } +#define ELEMENT_FALLBACK(function) { #function , (void (**)(void)) & pfn_ ## function, (void (*)(void))fallback_ ## function } +static SHARED_FUNC SharedFuncs[] = +{ + ELEMENT(snd_lib_error_set_handler), + ELEMENT(snd_strerror), + + ELEMENT_FALLBACK(snd_device_name_hint), + ELEMENT_FALLBACK(snd_device_name_get_hint), + ELEMENT_FALLBACK(snd_device_name_free_hint), + + ELEMENT(snd_pcm_avail_update), + ELEMENT_FALLBACK(snd_pcm_avail_delay), + ELEMENT(snd_pcm_close), + ELEMENT(snd_pcm_delay), + ELEMENT(snd_pcm_drain), + ELEMENT(snd_pcm_drop), + ELEMENT(snd_pcm_nonblock), + ELEMENT(snd_pcm_open), + ELEMENT(snd_pcm_prepare), + ELEMENT(snd_pcm_resume), + ELEMENT_FALLBACK(snd_pcm_set_chmap), + ELEMENT(snd_pcm_state), + ELEMENT(snd_pcm_state_name), + + ELEMENT(snd_pcm_readi), + ELEMENT(snd_pcm_start), + ELEMENT(snd_pcm_writei), + + ELEMENT(snd_pcm_hw_params), + ELEMENT(snd_pcm_hw_params_any), + ELEMENT(snd_pcm_hw_params_sizeof), + ELEMENT(snd_pcm_hw_params_get_buffer_size), + ELEMENT(snd_pcm_hw_params_get_buffer_size_min), + ELEMENT(snd_pcm_hw_params_get_period_size_min), + ELEMENT(snd_pcm_hw_params_set_access), + ELEMENT(snd_pcm_hw_params_set_buffer_size_near), + ELEMENT(snd_pcm_hw_params_set_buffer_time_near), + ELEMENT(snd_pcm_hw_params_set_channels_near), + ELEMENT(snd_pcm_hw_params_set_format), + ELEMENT(snd_pcm_hw_params_get_period_size), + ELEMENT(snd_pcm_hw_params_set_period_size_near), + ELEMENT(snd_pcm_hw_params_set_period_time_near), + ELEMENT(snd_pcm_hw_params_set_rate_near), + + ELEMENT(snd_pcm_sw_params), + ELEMENT(snd_pcm_sw_params_current), + ELEMENT(snd_pcm_sw_params_get_start_threshold), + ELEMENT(snd_pcm_sw_params_set_avail_min), + ELEMENT(snd_pcm_sw_params_set_start_threshold), + ELEMENT(snd_pcm_sw_params_sizeof), + + ELEMENT(snd_mixer_selem_id_sizeof), + ELEMENT(snd_mixer_open), + ELEMENT(snd_mixer_attach), + ELEMENT(snd_mixer_close), + ELEMENT(snd_mixer_selem_id_set_index), + ELEMENT(snd_mixer_selem_id_set_name), + ELEMENT(snd_mixer_selem_set_playback_volume), + ELEMENT(snd_mixer_selem_get_playback_volume_range), + ELEMENT(snd_mixer_selem_set_capture_volume), + ELEMENT(snd_mixer_selem_get_capture_volume_range), + ELEMENT(snd_mixer_selem_register), + ELEMENT(snd_mixer_load), + ELEMENT(snd_mixer_find_selem), + +}; +#undef ELEMENT + +/** Init once. */ +static RTONCE g_AlsaLibInitOnce = RTONCE_INITIALIZER; + + +/** @callback_method_impl{FNRTONCE} */ +static DECLCALLBACK(int32_t) drvHostAudioAlsaLibInitOnce(void *pvUser) +{ + RT_NOREF(pvUser); + LogFlowFunc(("\n")); + + RTLDRMOD hMod = NIL_RTLDRMOD; + int rc = RTLdrLoadSystemEx(VBOX_ALSA_LIB, RTLDRLOAD_FLAGS_NO_UNLOAD, &hMod); + if (RT_SUCCESS(rc)) + { + for (uintptr_t i = 0; i < RT_ELEMENTS(SharedFuncs); i++) + { + rc = RTLdrGetSymbol(hMod, SharedFuncs[i].name, (void **)SharedFuncs[i].pfn); + if (RT_SUCCESS(rc)) + { /* likely */ } + else if (SharedFuncs[i].pfnFallback && rc == VERR_SYMBOL_NOT_FOUND) + *SharedFuncs[i].pfn = SharedFuncs[i].pfnFallback; + else + { + LogRelFunc(("Failed to load library %s: Getting symbol %s failed: %Rrc\n", VBOX_ALSA_LIB, SharedFuncs[i].name, rc)); + return rc; + } + } + + RTLdrClose(hMod); + } + else + LogRelFunc(("Failed to load library %s (%Rrc)\n", VBOX_ALSA_LIB, rc)); + return rc; +} + + +/** + * Try to dynamically load the ALSA libraries. + * + * @returns VBox status code. + */ +int audioLoadAlsaLib(void) +{ + LogFlowFunc(("\n")); + return RTOnce(&g_AlsaLibInitOnce, drvHostAudioAlsaLibInitOnce, NULL); +} + diff --git a/src/VBox/Devices/Audio/DrvHostAudioAlsaStubs.h b/src/VBox/Devices/Audio/DrvHostAudioAlsaStubs.h new file mode 100644 index 00000000..00cc3942 --- /dev/null +++ b/src/VBox/Devices/Audio/DrvHostAudioAlsaStubs.h @@ -0,0 +1,72 @@ +/* $Id: DrvHostAudioAlsaStubs.h $ */ +/** @file + * Stubs for libasound. + */ + +/* + * Copyright (C) 2006-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 + */ + +#ifndef VBOX_INCLUDED_SRC_Audio_DrvHostAudioAlsaStubs_h +#define VBOX_INCLUDED_SRC_Audio_DrvHostAudioAlsaStubs_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include <iprt/cdefs.h> +#include <alsa/version.h> + +#define VBOX_ALSA_MAKE_VER(a,b,c) ( ((a) << 24) | ((b) << 16) | (c) ) +#define VBOX_ALSA_VER VBOX_ALSA_MAKE_VER(SND_LIB_MAJOR, SND_LIB_MINOR, SND_LIB_SUBMINOR) + +RT_C_DECLS_BEGIN +extern int audioLoadAlsaLib(void); + +#if VBOX_ALSA_VER < VBOX_ALSA_MAKE_VER(1,0,18) /* added in 1.0.18 */ +extern int snd_pcm_avail_delay(snd_pcm_t *, snd_pcm_sframes_t *, snd_pcm_sframes_t *); +#endif + +#if VBOX_ALSA_VER < VBOX_ALSA_MAKE_VER(1,0,14) /* added in 1.0.14a */ +extern int snd_device_name_hint(int, const char *, void ***); +extern int snd_device_name_free_hint(void **); +extern char *snd_device_name_get_hint(const void *, const char *); +#endif + +#if VBOX_ALSA_VER < VBOX_ALSA_MAKE_VER(1,0,27) /* added in 1.0.27 */ +enum snd_pcm_chmap_position { SND_CHMAP_UNKNOWN = 0, SND_CHMAP_NA, SND_CHMAP_MONO, SND_CHMAP_FL, SND_CHMAP_FR, + SND_CHMAP_RL, SND_CHMAP_RR, SND_CHMAP_FC, SND_CHMAP_LFE, SND_CHMAP_SL, SND_CHMAP_SR, SND_CHMAP_RC, + SND_CHMAP_FLC, SND_CHMAP_FRC, SND_CHMAP_RLC, SND_CHMAP_RRC, SND_CHMAP_FLW, SND_CHMAP_FRW, SND_CHMAP_FLH, + SND_CHMAP_FCH, SND_CHMAP_FRH, SND_CHMAP_TC, SND_CHMAP_TFL, SND_CHMAP_TFR, SND_CHMAP_TFC, SND_CHMAP_TRL, + SND_CHMAP_TRR, SND_CHMAP_TRC, SND_CHMAP_TFLC, SND_CHMAP_TFRC, SND_CHMAP_TSL, SND_CHMAP_TSR, SND_CHMAP_LLFE, + SND_CHMAP_RLFE, SND_CHMAP_BC, SND_CHMAP_BLC, SND_CHMAP_BRC }; +typedef struct snd_pcm_chmap +{ + unsigned int channels; + RT_GCC_EXTENSION + unsigned int pos[RT_FLEXIBLE_ARRAY_IN_NESTED_UNION]; +} snd_pcm_chmap_t; +extern int snd_pcm_set_chmap(snd_pcm_t *, snd_pcm_chmap_t const *); +#endif + +RT_C_DECLS_END + +#endif /* !VBOX_INCLUDED_SRC_Audio_DrvHostAudioAlsaStubs_h */ + diff --git a/src/VBox/Devices/Audio/DrvHostAudioAlsaStubsMangling.h b/src/VBox/Devices/Audio/DrvHostAudioAlsaStubsMangling.h new file mode 100644 index 00000000..d9e6cb03 --- /dev/null +++ b/src/VBox/Devices/Audio/DrvHostAudioAlsaStubsMangling.h @@ -0,0 +1,99 @@ +/* $Id: DrvHostAudioAlsaStubsMangling.h $ */ +/** @file + * Mangle libasound symbols. + * + * This is necessary on hosts which don't support the -fvisibility gcc switch. + */ + +/* + * Copyright (C) 2013-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 + */ + +#ifndef VBOX_INCLUDED_SRC_Audio_DrvHostAudioAlsaStubsMangling_h +#define VBOX_INCLUDED_SRC_Audio_DrvHostAudioAlsaStubsMangling_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#define ALSA_MANGLER(symbol) VBox_##symbol + +#define snd_lib_error_set_handler ALSA_MANGLER(snd_lib_error_set_handler) +#define snd_strerror ALSA_MANGLER(snd_strerror) + +#define snd_device_name_hint ALSA_MANGLER(snd_device_name_hint) +#define snd_device_name_get_hint ALSA_MANGLER(snd_device_name_get_hint) +#define snd_device_name_free_hint ALSA_MANGLER(snd_device_name_free_hint) + +#define snd_pcm_avail_update ALSA_MANGLER(snd_pcm_avail_update) +#define snd_pcm_close ALSA_MANGLER(snd_pcm_close) +#define snd_pcm_avail_delay ALSA_MANGLER(snd_pcm_avail_delay) +#define snd_pcm_delay ALSA_MANGLER(snd_pcm_delay) +#define snd_pcm_drain ALSA_MANGLER(snd_pcm_drain) +#define snd_pcm_drop ALSA_MANGLER(snd_pcm_drop) +#define snd_pcm_nonblock ALSA_MANGLER(snd_pcm_nonblock) +#define snd_pcm_open ALSA_MANGLER(snd_pcm_open) +#define snd_pcm_prepare ALSA_MANGLER(snd_pcm_prepare) +#define snd_pcm_readi ALSA_MANGLER(snd_pcm_readi) +#define snd_pcm_resume ALSA_MANGLER(snd_pcm_resume) +#define snd_pcm_set_chmap ALSA_MANGLER(snd_pcm_set_chmap) +#define snd_pcm_start ALSA_MANGLER(snd_pcm_start) +#define snd_pcm_state ALSA_MANGLER(snd_pcm_state) +#define snd_pcm_state_name ALSA_MANGLER(snd_pcm_state_name) +#define snd_pcm_writei ALSA_MANGLER(snd_pcm_writei) + +#define snd_pcm_hw_params ALSA_MANGLER(snd_pcm_hw_params) +#define snd_pcm_hw_params_any ALSA_MANGLER(snd_pcm_hw_params_any) +#define snd_pcm_hw_params_sizeof ALSA_MANGLER(snd_pcm_hw_params_sizeof) +#define snd_pcm_hw_params_get_buffer_size ALSA_MANGLER(snd_pcm_hw_params_get_buffer_size) +#define snd_pcm_hw_params_get_period_size_min ALSA_MANGLER(snd_pcm_hw_params_get_period_size_min) +#define snd_pcm_hw_params_set_rate_near ALSA_MANGLER(snd_pcm_hw_params_set_rate_near) +#define snd_pcm_hw_params_set_access ALSA_MANGLER(snd_pcm_hw_params_set_access) +#define snd_pcm_hw_params_set_buffer_time_near ALSA_MANGLER(snd_pcm_hw_params_set_buffer_time_near) +#define snd_pcm_hw_params_set_buffer_size_near ALSA_MANGLER(snd_pcm_hw_params_set_buffer_size_near) +#define snd_pcm_hw_params_get_buffer_size_min ALSA_MANGLER(snd_pcm_hw_params_get_buffer_size_min) +#define snd_pcm_hw_params_set_channels_near ALSA_MANGLER(snd_pcm_hw_params_set_channels_near) +#define snd_pcm_hw_params_set_format ALSA_MANGLER(snd_pcm_hw_params_set_format) +#define snd_pcm_hw_params_get_period_size ALSA_MANGLER(snd_pcm_hw_params_get_period_size) +#define snd_pcm_hw_params_set_period_size_near ALSA_MANGLER(snd_pcm_hw_params_set_period_size_near) +#define snd_pcm_hw_params_set_period_time_near ALSA_MANGLER(snd_pcm_hw_params_set_period_time_near) + +#define snd_pcm_sw_params ALSA_MANGLER(snd_pcm_sw_params) +#define snd_pcm_sw_params_current ALSA_MANGLER(snd_pcm_sw_params_current) +#define snd_pcm_sw_params_get_start_threshold ALSA_MANGLER(snd_pcm_sw_params_get_start_threshold) +#define snd_pcm_sw_params_set_avail_min ALSA_MANGLER(snd_pcm_sw_params_set_avail_min) +#define snd_pcm_sw_params_set_start_threshold ALSA_MANGLER(snd_pcm_sw_params_set_start_threshold) +#define snd_pcm_sw_params_sizeof ALSA_MANGLER(snd_pcm_sw_params_sizeof) + +#define snd_mixer_selem_id_sizeof ALSA_MANGLER(snd_mixer_selem_id_sizeof) +#define snd_mixer_open ALSA_MANGLER(snd_mixer_open) +#define snd_mixer_attach ALSA_MANGLER(snd_mixer_attach) +#define snd_mixer_close ALSA_MANGLER(snd_mixer_close) +#define snd_mixer_selem_id_set_index ALSA_MANGLER(snd_mixer_selem_id_set_index) +#define snd_mixer_selem_id_set_name ALSA_MANGLER(snd_mixer_selem_id_set_name) +#define snd_mixer_selem_set_playback_volume ALSA_MANGLER(snd_mixer_selem_set_playback_volume) +#define snd_mixer_selem_get_playback_volume_range ALSA_MANGLER(snd_mixer_selem_get_playback_volume_range) +#define snd_mixer_selem_set_capture_volume ALSA_MANGLER(snd_mixer_selem_set_capture_volume) +#define snd_mixer_selem_get_capture_volume_range ALSA_MANGLER(snd_mixer_selem_get_capture_volume_range) +#define snd_mixer_selem_register ALSA_MANGLER(snd_mixer_selem_register) +#define snd_mixer_load ALSA_MANGLER(snd_mixer_load) +#define snd_mixer_find_selem ALSA_MANGLER(snd_mixer_find_selem) + +#endif /* !VBOX_INCLUDED_SRC_Audio_DrvHostAudioAlsaStubsMangling_h */ diff --git a/src/VBox/Devices/Audio/DrvHostAudioCoreAudio.cpp b/src/VBox/Devices/Audio/DrvHostAudioCoreAudio.cpp new file mode 100644 index 00000000..5a366cc1 --- /dev/null +++ b/src/VBox/Devices/Audio/DrvHostAudioCoreAudio.cpp @@ -0,0 +1,2925 @@ +/* $Id: DrvHostAudioCoreAudio.cpp $ */ +/** @file + * Host audio driver - Mac OS X CoreAudio. + * + * For relevant Apple documentation, here are some starters: + * - Core Audio Essentials + * https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/CoreAudioOverview/CoreAudioEssentials/CoreAudioEssentials.html + * - TN2097: Playing a sound file using the Default Output Audio Unit + * https://developer.apple.com/library/archive/technotes/tn2097/ + * - TN2091: Device input using the HAL Output Audio Unit + * https://developer.apple.com/library/archive/technotes/tn2091/ + * - Audio Component Services + * https://developer.apple.com/documentation/audiounit/audio_component_services?language=objc + * - QA1533: How to handle kAudioUnitProperty_MaximumFramesPerSlice + * https://developer.apple.com/library/archive/qa/qa1533/ + * - QA1317: Signaling the end of data when using AudioConverterFillComplexBuffer + * https://developer.apple.com/library/archive/qa/qa1317/ + */ + +/* + * Copyright (C) 2010-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 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_DRV_HOST_AUDIO +#include <VBox/log.h> +#include <VBox/vmm/pdmaudioinline.h> +#include <VBox/vmm/pdmaudiohostenuminline.h> + +#include "VBoxDD.h" + +#include <iprt/asm.h> +#include <iprt/cdefs.h> +#include <iprt/circbuf.h> +#include <iprt/mem.h> +#include <iprt/uuid.h> +#include <iprt/timer.h> + +#include <CoreAudio/CoreAudio.h> +#include <CoreServices/CoreServices.h> +#include <AudioToolbox/AudioQueue.h> +#include <AudioUnit/AudioUnit.h> + +#if MAC_OS_X_VERSION_MIN_REQUIRED < 1090 /* possibly 1080 */ +# define kAudioHardwarePropertyTranslateUIDToDevice (AudioObjectPropertySelector)'uidd' +#endif + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +/** The max number of queue buffers we'll use. */ +#define COREAUDIO_MAX_BUFFERS 1024 +/** The minimum number of queue buffers. */ +#define COREAUDIO_MIN_BUFFERS 4 + +/** Enables the worker thread. + * This saves CoreAudio from creating an additional thread upon queue + * creation. (It does not help with the slow AudioQueueDispose fun.) */ +#define CORE_AUDIO_WITH_WORKER_THREAD +#if 0 +/** Enables the AudioQueueDispose breakpoint timer (debugging help). */ +# define CORE_AUDIO_WITH_BREAKPOINT_TIMER +#endif + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +/** Pointer to the instance data for a Core Audio driver instance. */ +typedef struct DRVHOSTCOREAUDIO *PDRVHOSTCOREAUDIO; +/** Pointer to the Core Audio specific backend data for an audio stream. */ +typedef struct COREAUDIOSTREAM *PCOREAUDIOSTREAM; + +/** + * Core Audio device entry (enumeration). + * + * @note This is definitely not safe to just copy! + */ +typedef struct COREAUDIODEVICEDATA +{ + /** The core PDM structure. */ + PDMAUDIOHOSTDEV Core; + + /** The audio device ID of the currently used device (UInt32 typedef). */ + AudioDeviceID idDevice; +} COREAUDIODEVICEDATA; +/** Pointer to a Core Audio device entry (enumeration). */ +typedef COREAUDIODEVICEDATA *PCOREAUDIODEVICEDATA; + + +/** + * Audio device information. + * + * We do not use COREAUDIODEVICEDATA here as it contains lots more than what we + * need and care to query. We also don't want to depend on DrvAudio making + * PDMIHOSTAUDIO::pfnGetDevices callbacks to keep this information up to date. + */ +typedef struct DRVHSTAUDCADEVICE +{ + /** The audio device ID. kAudioDeviceUnknown if not available. */ + AudioObjectID idDevice; + /** Indicates whether we've registered device change listener. */ + bool fRegisteredListeners; + /** The UID string (must release). NULL if not available. */ + CFStringRef hStrUid; + /** The UID string for a specific device, NULL if we're using the default device. */ + char *pszSpecific; +} DRVHSTAUDCADEVICE; +/** Pointer to info about a default device. */ +typedef DRVHSTAUDCADEVICE *PDRVHSTAUDCADEVICE; + + +/** + * Core Audio stream state. + */ +typedef enum COREAUDIOINITSTATE +{ + /** The device is uninitialized. */ + COREAUDIOINITSTATE_UNINIT = 0, + /** The device is currently initializing. */ + COREAUDIOINITSTATE_IN_INIT, + /** The device is initialized. */ + COREAUDIOINITSTATE_INIT, + /** The device is currently uninitializing. */ + COREAUDIOINITSTATE_IN_UNINIT, + /** The usual 32-bit hack. */ + COREAUDIOINITSTATE_32BIT_HACK = 0x7fffffff +} COREAUDIOINITSTATE; + + +/** + * Core audio buffer tracker. + * + * For output buffer we'll be using AudioQueueBuffer::mAudioDataByteSize to + * track how much we've written. When a buffer is full, or if we run low on + * queued bufferes, it will be queued. + * + * For input buffer we'll be using offRead to track how much we've read. + * + * The queued/not-queued state is stored in the first bit of + * AudioQueueBuffer::mUserData. While bits 8 and up holds the index into + * COREAUDIOSTREAM::paBuffers. + */ +typedef struct COREAUDIOBUF +{ + /** The buffer. */ + AudioQueueBufferRef pBuf; + /** The buffer read offset (input only). */ + uint32_t offRead; +} COREAUDIOBUF; +/** Pointer to a core audio buffer tracker. */ +typedef COREAUDIOBUF *PCOREAUDIOBUF; + + +/** + * Core Audio specific data for an audio stream. + */ +typedef struct COREAUDIOSTREAM +{ + /** Common part. */ + PDMAUDIOBACKENDSTREAM Core; + + /** The stream's acquired configuration. */ + PDMAUDIOSTREAMCFG Cfg; + /** List node for the device's stream list. */ + RTLISTNODE Node; + /** The acquired (final) audio format for this stream. + * @note This what the device requests, we don't alter anything. */ + AudioStreamBasicDescription BasicStreamDesc; + /** The actual audio queue being used. */ + AudioQueueRef hAudioQueue; + + /** Number of buffers. */ + uint32_t cBuffers; + /** The array of buffer. */ + PCOREAUDIOBUF paBuffers; + + /** Initialization status tracker, actually COREAUDIOINITSTATE. + * Used when some of the device parameters or the device itself is changed + * during the runtime. */ + volatile uint32_t enmInitState; + /** The current buffer being written to / read from. */ + uint32_t idxBuffer; + /** Set if the stream is enabled. */ + bool fEnabled; + /** Set if the stream is started (playing/capturing). */ + bool fStarted; + /** Set if the stream is draining (output only). */ + bool fDraining; + /** Set if we should restart the stream on resume (saved pause state). */ + bool fRestartOnResume; +// /** Set if we're switching to a new output/input device. */ +// bool fSwitchingDevice; + /** Internal stream offset (bytes). */ + uint64_t offInternal; + /** The RTTimeMilliTS() at the end of the last transfer. */ + uint64_t msLastTransfer; + + /** Critical section for serializing access between thread + callbacks. */ + RTCRITSECT CritSect; + /** Buffer that drvHstAudCaStreamStatusString uses. */ + char szStatus[64]; +} COREAUDIOSTREAM; + + +/** + * Instance data for a Core Audio host audio driver. + * + * @implements PDMIAUDIOCONNECTOR + */ +typedef struct DRVHOSTCOREAUDIO +{ + /** Pointer to the driver instance structure. */ + PPDMDRVINS pDrvIns; + /** Pointer to host audio interface. */ + PDMIHOSTAUDIO IHostAudio; + /** The input device. */ + DRVHSTAUDCADEVICE InputDevice; + /** The output device. */ + DRVHSTAUDCADEVICE OutputDevice; + /** Upwards notification interface. */ + PPDMIHOSTAUDIOPORT pIHostAudioPort; + /** Indicates whether we've registered default input device change listener. */ + bool fRegisteredDefaultInputListener; + /** Indicates whether we've registered default output device change listener. */ + bool fRegisteredDefaultOutputListener; + +#ifdef CORE_AUDIO_WITH_WORKER_THREAD + /** @name Worker Thread For Queue callbacks and stuff. + * @{ */ + /** The worker thread. */ + RTTHREAD hThread; + /** The runloop of the worker thread. */ + CFRunLoopRef hThreadRunLoop; + /** The message port we use to talk to the thread. + * @note While we don't currently use the port, it is necessary to prevent + * the thread from spinning or stopping prematurely because of + * CFRunLoopRunInMode returning kCFRunLoopRunFinished. */ + CFMachPortRef hThreadPort; + /** Runloop source for hThreadPort. */ + CFRunLoopSourceRef hThreadPortSrc; + /** @} */ +#endif + + /** Critical section to serialize access. */ + RTCRITSECT CritSect; +#ifdef CORE_AUDIO_WITH_BREAKPOINT_TIMER + /** Timder for debugging AudioQueueDispose slowness. */ + RTTIMERLR hBreakpointTimer; +#endif +} DRVHOSTCOREAUDIO; + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ +static void drvHstAudCaUpdateOneDefaultDevice(PDRVHOSTCOREAUDIO pThis, PDRVHSTAUDCADEVICE pDevice, bool fInput, bool fNotify); + +/* DrvHostAudioCoreAudioAuth.mm: */ +DECLHIDDEN(int) coreAudioInputPermissionCheck(void); + + +#ifdef LOG_ENABLED +/** + * Gets the stream status. + * + * @returns Pointer to stream status string. + * @param pStreamCA The stream to get the status for. + */ +static const char *drvHstAudCaStreamStatusString(PCOREAUDIOSTREAM pStreamCA) +{ + static RTSTRTUPLE const s_aInitState[5] = + { + { RT_STR_TUPLE("UNINIT") }, + { RT_STR_TUPLE("IN_INIT") }, + { RT_STR_TUPLE("INIT") }, + { RT_STR_TUPLE("IN_UNINIT") }, + { RT_STR_TUPLE("BAD") }, + }; + uint32_t enmInitState = pStreamCA->enmInitState; + PCRTSTRTUPLE pTuple = &s_aInitState[RT_MIN(enmInitState, RT_ELEMENTS(s_aInitState) - 1)]; + memcpy(pStreamCA->szStatus, pTuple->psz, pTuple->cch); + size_t off = pTuple->cch; + + static RTSTRTUPLE const s_aEnable[2] = + { + { RT_STR_TUPLE("DISABLED") }, + { RT_STR_TUPLE("ENABLED ") }, + }; + pTuple = &s_aEnable[pStreamCA->fEnabled]; + memcpy(pStreamCA->szStatus, pTuple->psz, pTuple->cch); + off += pTuple->cch; + + static RTSTRTUPLE const s_aStarted[2] = + { + { RT_STR_TUPLE(" STOPPED") }, + { RT_STR_TUPLE(" STARTED") }, + }; + pTuple = &s_aStarted[pStreamCA->fStarted]; + memcpy(&pStreamCA->szStatus[off], pTuple->psz, pTuple->cch); + off += pTuple->cch; + + static RTSTRTUPLE const s_aDraining[2] = + { + { RT_STR_TUPLE("") }, + { RT_STR_TUPLE(" DRAINING") }, + }; + pTuple = &s_aDraining[pStreamCA->fDraining]; + memcpy(&pStreamCA->szStatus[off], pTuple->psz, pTuple->cch); + off += pTuple->cch; + + Assert(off < sizeof(pStreamCA->szStatus)); + pStreamCA->szStatus[off] = '\0'; + return pStreamCA->szStatus; +} +#endif /*LOG_ENABLED*/ + + + + +#if 0 /* unused */ +static int drvHstAudCaCFStringToCString(const CFStringRef pCFString, char **ppszString) +{ + CFIndex cLen = CFStringGetLength(pCFString) + 1; + char *pszResult = (char *)RTMemAllocZ(cLen * sizeof(char)); + if (!CFStringGetCString(pCFString, pszResult, cLen, kCFStringEncodingUTF8)) + { + RTMemFree(pszResult); + return VERR_NOT_FOUND; + } + + *ppszString = pszResult; + return VINF_SUCCESS; +} + +static AudioDeviceID drvHstAudCaDeviceUIDtoID(const char* pszUID) +{ + /* Create a CFString out of our CString. */ + CFStringRef strUID = CFStringCreateWithCString(NULL, pszUID, kCFStringEncodingMacRoman); + + /* Fill the translation structure. */ + AudioDeviceID deviceID; + + AudioValueTranslation translation; + translation.mInputData = &strUID; + translation.mInputDataSize = sizeof(CFStringRef); + translation.mOutputData = &deviceID; + translation.mOutputDataSize = sizeof(AudioDeviceID); + + /* Fetch the translation from the UID to the device ID. */ + AudioObjectPropertyAddress PropAddr = + { + kAudioHardwarePropertyDeviceForUID, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + + UInt32 uSize = sizeof(AudioValueTranslation); + OSStatus err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &PropAddr, 0, NULL, &uSize, &translation); + + /* Release the temporary CFString */ + CFRelease(strUID); + + if (RT_LIKELY(err == noErr)) + return deviceID; + + /* Return the unknown device on error. */ + return kAudioDeviceUnknown; +} +#endif /* unused */ + + +/** + * Wrapper around AudioObjectGetPropertyData and AudioObjectGetPropertyDataSize. + * + * @returns Pointer to temp heap allocation with the data on success, free using + * RTMemTmpFree. NULL on failure, fully logged. + */ +static void *drvHstAudCaGetPropertyDataEx(AudioObjectID idObject, AudioObjectPropertySelector enmSelector, + AudioObjectPropertyScope enmScope, AudioObjectPropertyElement enmElement, + const char *pszWhat, UInt32 *pcb) +{ + AudioObjectPropertyAddress const PropAddr = + { + /*.mSelector = */ enmSelector, + /*.mScope = */ enmScope, + /*.mElement = */ enmElement + }; + + /* + * Have to retry here in case the size isn't stable (like if a new device/whatever is added). + */ + for (uint32_t iTry = 0; ; iTry++) + { + UInt32 cb = 0; + OSStatus orc = AudioObjectGetPropertyDataSize(idObject, &PropAddr, 0, NULL, &cb); + if (orc == noErr) + { + cb = RT_MAX(cb, 1); /* we're allergic to zero allocations. */ + void *pv = RTMemTmpAllocZ(cb); + if (pv) + { + orc = AudioObjectGetPropertyData(idObject, &PropAddr, 0, NULL, &cb, pv); + if (orc == noErr) + { + Log9Func(("%u/%#x/%#x/%x/%s: returning %p LB %#x\n", + idObject, enmSelector, enmScope, enmElement, pszWhat, pv, cb)); + if (pcb) + *pcb = cb; + return pv; + } + + RTMemTmpFree(pv); + LogFunc(("AudioObjectGetPropertyData(%u/%#x/%#x/%x/%s, cb=%#x) -> %#x, iTry=%d\n", + idObject, enmSelector, enmScope, enmElement, pszWhat, cb, orc, iTry)); + if (iTry < 3) + continue; + LogRelMax(32, ("CoreAudio: AudioObjectGetPropertyData(%u/%#x/%#x/%x/%s, cb=%#x) failed: %#x\n", + idObject, enmSelector, enmScope, enmElement, pszWhat, cb, orc)); + } + else + LogRelMax(32, ("CoreAudio: Failed to allocate %#x bytes (to get %s for %s).\n", cb, pszWhat, idObject)); + } + else + LogRelMax(32, ("CoreAudio: Failed to get %s for %u: %#x\n", pszWhat, idObject, orc)); + if (pcb) + *pcb = 0; + return NULL; + } +} + + +/** + * Wrapper around AudioObjectGetPropertyData. + * + * @returns Success indicator. Failures (@c false) are fully logged. + */ +static bool drvHstAudCaGetPropertyData(AudioObjectID idObject, AudioObjectPropertySelector enmSelector, + AudioObjectPropertyScope enmScope, AudioObjectPropertyElement enmElement, + const char *pszWhat, void *pv, UInt32 cb) +{ + AudioObjectPropertyAddress const PropAddr = + { + /*.mSelector = */ enmSelector, + /*.mScope = */ enmScope, + /*.mElement = */ enmElement + }; + + OSStatus orc = AudioObjectGetPropertyData(idObject, &PropAddr, 0, NULL, &cb, pv); + if (orc == noErr) + { + Log9Func(("%u/%#x/%#x/%x/%s: returning %p LB %#x\n", idObject, enmSelector, enmScope, enmElement, pszWhat, pv, cb)); + return true; + } + LogRelMax(64, ("CoreAudio: Failed to query %s (%u/%#x/%#x/%x, cb=%#x): %#x\n", + pszWhat, idObject, enmSelector, enmScope, enmElement, cb, orc)); + return false; +} + + +/** + * Count the number of channels in one direction. + * + * @returns Channel count. + */ +static uint32_t drvHstAudCaEnumCountChannels(AudioObjectID idObject, AudioObjectPropertyScope enmScope) +{ + uint32_t cChannels = 0; + + AudioBufferList *pBufs + = (AudioBufferList *)drvHstAudCaGetPropertyDataEx(idObject, kAudioDevicePropertyStreamConfiguration, + enmScope, kAudioObjectPropertyElementMaster, "stream config", NULL); + if (pBufs) + { + UInt32 idxBuf = pBufs->mNumberBuffers; + while (idxBuf-- > 0) + { + Log9Func(("%u/%#x[%u]: %u\n", idObject, enmScope, idxBuf, pBufs->mBuffers[idxBuf].mNumberChannels)); + cChannels += pBufs->mBuffers[idxBuf].mNumberChannels; + } + + RTMemTmpFree(pBufs); + } + + return cChannels; +} + + +/** + * Translates a UID to an audio device ID. + * + * @returns Audio device ID on success, kAudioDeviceUnknown on failure. + * @param hStrUid The UID string to convert. + * @param pszUid The C-string vresion of @a hStrUid. + * @param pszWhat What we're converting (for logging). + */ +static AudioObjectID drvHstAudCaDeviceUidToId(CFStringRef hStrUid, const char *pszUid, const char *pszWhat) +{ + AudioObjectPropertyAddress const PropAddr = + { + /*.mSelector = */ kAudioHardwarePropertyTranslateUIDToDevice, + /*.mScope = */ kAudioObjectPropertyScopeGlobal, + /*.mElement = */ kAudioObjectPropertyElementMaster + }; + AudioObjectID idDevice = 0; + UInt32 cb = sizeof(idDevice); + OSStatus orc = AudioObjectGetPropertyData(kAudioObjectSystemObject, &PropAddr, + sizeof(hStrUid), &hStrUid, &cb, &idDevice); + if (orc == noErr) + { + Log9Func(("%s device UID '%s' -> %RU32\n", pszWhat, pszUid, idDevice)); + return idDevice; + } + /** @todo test on < 10.9, see which status code and do a fallback using the + * enumeration code. */ + LogRelMax(64, ("CoreAudio: Failed to translate %s device UID '%s' to audio device ID: %#x\n", pszWhat, pszUid, orc)); + return kAudioDeviceUnknown; +} + + +/** + * Copies a CFString to a buffer (UTF-8). + * + * @returns VBox status code. In the case of a buffer overflow, the buffer will + * contain data and be correctly terminated (provided @a cbDst is not + * zero.) + */ +static int drvHstAudCaCFStringToBuf(CFStringRef hStr, char *pszDst, size_t cbDst) +{ + AssertReturn(cbDst > 0, VERR_BUFFER_OVERFLOW); + + if (CFStringGetCString(hStr, pszDst, cbDst, kCFStringEncodingUTF8)) + return VINF_SUCCESS; + + /* First fallback: */ + const char *pszSrc = CFStringGetCStringPtr(hStr, kCFStringEncodingUTF8); + if (pszSrc) + return RTStrCopy(pszDst, cbDst, pszSrc); + + /* Second fallback: */ + CFIndex cbMax = CFStringGetMaximumSizeForEncoding(CFStringGetLength(hStr), kCFStringEncodingUTF8) + 1; + AssertReturn(cbMax > 0, VERR_INVALID_UTF8_ENCODING); + AssertReturn(cbMax < (CFIndex)_16M, VERR_OUT_OF_RANGE); + + char *pszTmp = (char *)RTMemTmpAlloc(cbMax); + AssertReturn(pszTmp, VERR_NO_TMP_MEMORY); + + int rc; + if (CFStringGetCString(hStr, pszTmp, cbMax, kCFStringEncodingUTF8)) + rc = RTStrCopy(pszDst, cbDst, pszTmp); + else + { + *pszDst = '\0'; + rc = VERR_INVALID_UTF8_ENCODING; + } + + RTMemTmpFree(pszTmp); + return rc; +} + + +/** + * Copies a CFString to a heap buffer (UTF-8). + * + * @returns Pointer to the heap buffer on success, NULL if out of heap or some + * conversion/extraction problem + */ +static char *drvHstAudCaCFStringToHeap(CFStringRef hStr) +{ + const char *pszSrc = CFStringGetCStringPtr(hStr, kCFStringEncodingUTF8); + if (pszSrc) + return RTStrDup(pszSrc); + + /* Fallback: */ + CFIndex cbMax = CFStringGetMaximumSizeForEncoding(CFStringGetLength(hStr), kCFStringEncodingUTF8) + 1; + AssertReturn(cbMax > 0, NULL); + AssertReturn(cbMax < (CFIndex)_16M, NULL); + + char *pszDst = RTStrAlloc(cbMax); + if (pszDst) + { + AssertReturnStmt(CFStringGetCString(hStr, pszDst, cbMax, kCFStringEncodingUTF8), RTStrFree(pszDst), NULL); + size_t const cchDst = strlen(pszDst); + if (cbMax - cchDst > 32) + RTStrRealloc(&pszDst, cchDst + 1); + } + return pszDst; +} + + +/********************************************************************************************************************************* +* Device Change Notification Callbacks * +*********************************************************************************************************************************/ + +#ifdef LOG_ENABLED +/** + * Called when the kAudioDevicePropertyNominalSampleRate or + * kAudioDeviceProcessorOverload properties changes on a default device. + * + * Registered on default devices after device enumeration. + * Not sure on which thread/runloop this runs. + * + * (See AudioObjectPropertyListenerProc in the SDK headers.) + */ +static OSStatus drvHstAudCaDevicePropertyChangedCallback(AudioObjectID idObject, UInt32 cAddresses, + const AudioObjectPropertyAddress paAddresses[], void *pvUser) +{ + LogFlowFunc(("idObject=%#x (%u) cAddresses=%u pvUser=%p\n", idObject, idObject, cAddresses, pvUser)); + for (UInt32 idx = 0; idx < cAddresses; idx++) + LogFlowFunc((" #%u: sel=%#x scope=%#x element=%#x\n", + idx, paAddresses[idx].mSelector, paAddresses[idx].mScope, paAddresses[idx].mElement)); + +/** @todo r=bird: What's the plan here exactly? I've changed it to + * LOG_ENABLED only for now, as this has no other purpose. */ + switch (idObject) + { + case kAudioDeviceProcessorOverload: + LogFunc(("Processor overload detected!\n")); + break; + case kAudioDevicePropertyNominalSampleRate: + LogFunc(("kAudioDevicePropertyNominalSampleRate!\n")); + break; + default: + /* Just skip. */ + break; + } + + return noErr; +} +#endif /* LOG_ENABLED */ + + +/** + * Called when the kAudioDevicePropertyDeviceIsAlive property changes on a + * default device. + * + * The purpose is mainly to log the event. There isn't much we can do about + * active streams or future one, other than waiting for a default device change + * notification callback. In the mean time, active streams should start failing + * to work and new ones fail on creation. This is the same for when we're + * configure to use specific devices, only we don't get any device change + * callback like for default ones. + * + * Not sure on which thread/runloop this runs. + * + * (See AudioObjectPropertyListenerProc in the SDK headers.) + */ +static OSStatus drvHstAudCaDeviceIsAliveChangedCallback(AudioObjectID idObject, UInt32 cAddresses, + const AudioObjectPropertyAddress paAddresses[], void *pvUser) +{ + PDRVHOSTCOREAUDIO pThis = (PDRVHOSTCOREAUDIO)pvUser; + AssertPtr(pThis); + RT_NOREF(cAddresses, paAddresses); + + /* + * Log everything. + */ + LogFlowFunc(("idObject=%#x (%u) cAddresses=%u\n", idObject, idObject, cAddresses)); + for (UInt32 idx = 0; idx < cAddresses; idx++) + LogFlowFunc((" #%u: sel=%#x scope=%#x element=%#x\n", + idx, paAddresses[idx].mSelector, paAddresses[idx].mScope, paAddresses[idx].mElement)); + + /* + * Check which devices are affected. + */ + int rc = RTCritSectEnter(&pThis->CritSect); + AssertRCReturn(rc, noErr); /* could be a destruction race */ + + for (unsigned i = 0; i < 2; i++) + { + if (idObject == (i == 0 ? pThis->InputDevice.idDevice : pThis->OutputDevice.idDevice)) + { + AudioObjectPropertyAddress const PropAddr = + { + kAudioDevicePropertyDeviceIsAlive, + i == 0 ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMaster + }; + UInt32 fAlive = 0; + UInt32 cb = sizeof(fAlive); + OSStatus orc = AudioObjectGetPropertyData(idObject, &PropAddr, 0, NULL, &cb, &fAlive); + if ( orc == kAudioHardwareBadDeviceError + || (orc == noErr && !fAlive)) + { + LogRel(("CoreAudio: The default %s device (%u) stopped functioning.\n", idObject, i == 0 ? "input" : "output")); +#if 0 /* This will only cause an extra re-init (in addition to the default device change) and likely do no good even if that + default device change callback doesn't arrive. So, don't do it! (bird) */ + PPDMIHOSTAUDIOPORT pIHostAudioPort = pThis->pIHostAudioPort; + if (pIHostAudioPort) + { + RTCritSectLeave(&pThis->CritSect); + + pIHostAudioPort->pfnNotifyDeviceChanged(pIHostAudioPort, i == 0 ? PDMAUDIODIR_IN : PDMAUDIODIR_OUT, NULL); + + rc = RTCritSectEnter(&pThis->CritSect); + AssertRCReturn(rc, noErr); /* could be a destruction race */ + } +#endif + } + } + } + + RTCritSectLeave(&pThis->CritSect); + return noErr; +} + + +/** + * Called when the default recording or playback device has changed. + * + * Registered by the constructor. Not sure on which thread/runloop this runs. + * + * (See AudioObjectPropertyListenerProc in the SDK headers.) + */ +static OSStatus drvHstAudCaDefaultDeviceChangedCallback(AudioObjectID idObject, UInt32 cAddresses, + const AudioObjectPropertyAddress *paAddresses, void *pvUser) + +{ + PDRVHOSTCOREAUDIO pThis = (PDRVHOSTCOREAUDIO)pvUser; + AssertPtr(pThis); + RT_NOREF(idObject, cAddresses, paAddresses); + + /* + * Log everything. + */ + LogFlowFunc(("idObject=%#x (%u) cAddresses=%u\n", idObject, idObject, cAddresses)); + for (UInt32 idx = 0; idx < cAddresses; idx++) + LogFlowFunc((" #%u: sel=%#x scope=%#x element=%#x\n", + idx, paAddresses[idx].mSelector, paAddresses[idx].mScope, paAddresses[idx].mElement)); + + /* + * Update the default devices and notify parent driver if anything actually changed. + */ + drvHstAudCaUpdateOneDefaultDevice(pThis, &pThis->OutputDevice, false /*fInput*/, true /*fNotify*/); + drvHstAudCaUpdateOneDefaultDevice(pThis, &pThis->InputDevice, true /*fInput*/, true /*fNotify*/); + + return noErr; +} + + +/** + * Registers callbacks for a specific Core Audio device. + * + * @returns true if idDevice isn't kAudioDeviceUnknown and callbacks were + * registered, otherwise false. + * @param pThis The core audio driver instance data. + * @param idDevice The device ID to deregister callbacks for. + */ +static bool drvHstAudCaDeviceRegisterCallbacks(PDRVHOSTCOREAUDIO pThis, AudioObjectID idDevice) +{ + if (idDevice != kAudioDeviceUnknown) + { + LogFunc(("idDevice=%RU32\n", idDevice)); + AudioObjectPropertyAddress PropAddr = + { + kAudioDevicePropertyDeviceIsAlive, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + OSStatus orc; + orc = AudioObjectAddPropertyListener(idDevice, &PropAddr, drvHstAudCaDeviceIsAliveChangedCallback, pThis); + unsigned cRegistrations = orc == noErr; + if ( orc != noErr + && orc != kAudioHardwareIllegalOperationError) + LogRel(("CoreAudio: Failed to add the recording device state changed listener (%#x)\n", orc)); + +#ifdef LOG_ENABLED + PropAddr.mSelector = kAudioDeviceProcessorOverload; + PropAddr.mScope = kAudioUnitScope_Global; + orc = AudioObjectAddPropertyListener(idDevice, &PropAddr, drvHstAudCaDevicePropertyChangedCallback, pThis); + cRegistrations += orc == noErr; + if (orc != noErr) + LogRel(("CoreAudio: Failed to register processor overload listener (%#x)\n", orc)); + + PropAddr.mSelector = kAudioDevicePropertyNominalSampleRate; + PropAddr.mScope = kAudioUnitScope_Global; + orc = AudioObjectAddPropertyListener(idDevice, &PropAddr, drvHstAudCaDevicePropertyChangedCallback, pThis); + cRegistrations += orc == noErr; + if (orc != noErr) + LogRel(("CoreAudio: Failed to register sample rate changed listener (%#x)\n", orc)); +#endif + return cRegistrations > 0; + } + return false; +} + + +/** + * Undoes what drvHstAudCaDeviceRegisterCallbacks() did. + * + * @param pThis The core audio driver instance data. + * @param idDevice The device ID to deregister callbacks for. + */ +static void drvHstAudCaDeviceUnregisterCallbacks(PDRVHOSTCOREAUDIO pThis, AudioObjectID idDevice) +{ + if (idDevice != kAudioDeviceUnknown) + { + LogFunc(("idDevice=%RU32\n", idDevice)); + AudioObjectPropertyAddress PropAddr = + { + kAudioDevicePropertyDeviceIsAlive, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + OSStatus orc; + orc = AudioObjectRemovePropertyListener(idDevice, &PropAddr, drvHstAudCaDeviceIsAliveChangedCallback, pThis); + if ( orc != noErr + && orc != kAudioHardwareBadObjectError) + LogRel(("CoreAudio: Failed to remove the device alive listener (%#x)\n", orc)); + +#ifdef LOG_ENABLED + PropAddr.mSelector = kAudioDeviceProcessorOverload; + orc = AudioObjectRemovePropertyListener(idDevice, &PropAddr, drvHstAudCaDevicePropertyChangedCallback, pThis); + if ( orc != noErr + && orc != kAudioHardwareBadObjectError) + LogRel(("CoreAudio: Failed to remove the recording processor overload listener (%#x)\n", orc)); + + PropAddr.mSelector = kAudioDevicePropertyNominalSampleRate; + orc = AudioObjectRemovePropertyListener(idDevice, &PropAddr, drvHstAudCaDevicePropertyChangedCallback, pThis); + if ( orc != noErr + && orc != kAudioHardwareBadObjectError) + LogRel(("CoreAudio: Failed to remove the sample rate changed listener (%#x)\n", orc)); +#endif + } +} + + +/** + * Updates the default device for one direction. + * + * @param pThis The core audio driver instance data. + * @param pDevice The device information to update. + * @param fInput Set if input device, clear if output. + * @param fNotify Whether to notify the parent driver if something + * changed. + */ +static void drvHstAudCaUpdateOneDefaultDevice(PDRVHOSTCOREAUDIO pThis, PDRVHSTAUDCADEVICE pDevice, bool fInput, bool fNotify) +{ + /* + * Skip if there is a specific device we should use for this direction. + */ + if (pDevice->pszSpecific) + return; + + /* + * Get the information before we enter the critical section. + * + * (Yeah, this may make us get things wrong if the defaults changes really + * fast and we get notifications in parallel on multiple threads. However, + * the first is a don't-do-that situation and the latter is unlikely.) + */ + AudioDeviceID idDefaultDev = kAudioDeviceUnknown; + if (!drvHstAudCaGetPropertyData(kAudioObjectSystemObject, + fInput ? kAudioHardwarePropertyDefaultInputDevice : kAudioHardwarePropertyDefaultOutputDevice, + kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster, + fInput ? "default input device" : "default output device", + &idDefaultDev, sizeof(idDefaultDev))) + idDefaultDev = kAudioDeviceUnknown; + + CFStringRef hStrUid = NULL; + if (idDefaultDev != kAudioDeviceUnknown) + { + if (!drvHstAudCaGetPropertyData(idDefaultDev, kAudioDevicePropertyDeviceUID, + fInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMaster, + fInput ? "default input device UID" : "default output device UID", + &hStrUid, sizeof(hStrUid))) + hStrUid = NULL; + } + char szUid[128]; + if (hStrUid) + drvHstAudCaCFStringToBuf(hStrUid, szUid, sizeof(szUid)); + else + szUid[0] = '\0'; + + /* + * Grab the lock and do the updating. + * + * We're a little paranoid wrt the locking in case there turn out to be some kind + * of race around destruction (there really can't be, but better play safe). + */ + PPDMIHOSTAUDIOPORT pIHostAudioPort = NULL; + + int rc = RTCritSectEnter(&pThis->CritSect); + AssertRC(rc); + if (RT_SUCCESS(rc)) + { + if (idDefaultDev != pDevice->idDevice) + { + if (idDefaultDev != kAudioDeviceUnknown) + { + LogRel(("CoreAudio: Default %s device: %u (was %u), ID '%s'\n", + fInput ? "input" : "output", idDefaultDev, pDevice->idDevice, szUid)); + pIHostAudioPort = fNotify ? pThis->pIHostAudioPort : NULL; /* (only if there is a new device) */ + } + else + LogRel(("CoreAudio: Default %s device is gone (was %u)\n", fInput ? "input" : "output", pDevice->idDevice)); + + if (pDevice->hStrUid) + CFRelease(pDevice->hStrUid); + if (pDevice->fRegisteredListeners) + drvHstAudCaDeviceUnregisterCallbacks(pThis, pDevice->idDevice); + pDevice->hStrUid = hStrUid; + pDevice->idDevice = idDefaultDev; + pDevice->fRegisteredListeners = drvHstAudCaDeviceRegisterCallbacks(pThis, pDevice->idDevice); + hStrUid = NULL; + } + RTCritSectLeave(&pThis->CritSect); + } + + if (hStrUid != NULL) + CFRelease(hStrUid); + + /* + * Notify parent driver to trigger a re-init of any associated streams. + */ + if (pIHostAudioPort) + { + LogFlowFunc(("Notifying parent driver about %s default device change...\n", fInput ? "input" : "output")); + pIHostAudioPort->pfnNotifyDeviceChanged(pIHostAudioPort, fInput ? PDMAUDIODIR_IN : PDMAUDIODIR_OUT, NULL /*pvUser*/); + } +} + + +/** + * Sets the device to use in one or the other direction (@a fInput). + * + * @returns VBox status code. + * @param pThis The core audio driver instance data. + * @param pDevice The device info structure to update. + * @param fInput Set if input, clear if output. + * @param fNotify Whether to notify the parent driver if something + * changed. + * @param pszUid The UID string for the device to use. NULL or empty + * string if default should be used. + */ +static int drvHstAudCaSetDevice(PDRVHOSTCOREAUDIO pThis, PDRVHSTAUDCADEVICE pDevice, bool fInput, bool fNotify, + const char *pszUid) +{ + if (!pszUid || !*pszUid) + { + /* + * Use default. Always refresh the given default device. + */ + int rc = RTCritSectEnter(&pThis->CritSect); + AssertRCReturn(rc, rc); + + if (pDevice->pszSpecific) + { + LogRel(("CoreAudio: Changing %s device from '%s' to default.\n", fInput ? "input" : "output", pDevice->pszSpecific)); + RTStrFree(pDevice->pszSpecific); + pDevice->pszSpecific = NULL; + } + + RTCritSectLeave(&pThis->CritSect); + + drvHstAudCaUpdateOneDefaultDevice(pThis, pDevice, fInput, fNotify); + } + else + { + /* + * Use device specified by pszUid. If not change, search for the device + * again if idDevice is unknown. + */ + int rc = RTCritSectEnter(&pThis->CritSect); + AssertRCReturn(rc, rc); + + bool fSkip = false; + bool fSame = false; + if (pDevice->pszSpecific) + { + if (strcmp(pszUid, pDevice->pszSpecific) != 0) + { + LogRel(("CoreAudio: Changing %s device from '%s' to '%s'.\n", + fInput ? "input" : "output", pDevice->pszSpecific, pszUid)); + RTStrFree(pDevice->pszSpecific); + pDevice->pszSpecific = NULL; + } + else + { + fSkip = pDevice->idDevice != kAudioDeviceUnknown; + fSame = true; + } + } + else + LogRel(("CoreAudio: Changing %s device from default to '%s'.\n", fInput ? "input" : "output", pszUid)); + + /* + * Allocate and swap the strings. This is the bit that might fail. + */ + if (!fSame) + { + CFStringRef hStrUid = CFStringCreateWithBytes(NULL /*allocator*/, (UInt8 const *)pszUid, (CFIndex)strlen(pszUid), + kCFStringEncodingUTF8, false /*isExternalRepresentation*/); + char *pszSpecific = RTStrDup(pszUid); + if (hStrUid && pszSpecific) + { + if (pDevice->hStrUid) + CFRelease(pDevice->hStrUid); + pDevice->hStrUid = hStrUid; + RTStrFree(pDevice->pszSpecific); + pDevice->pszSpecific = pszSpecific; + } + else + { + RTCritSectLeave(&pThis->CritSect); + + LogFunc(("returns VERR_NO_STR_MEMORY!\n")); + if (hStrUid) + CFRelease(hStrUid); + RTStrFree(pszSpecific); + return VERR_NO_STR_MEMORY; + } + + if (pDevice->fRegisteredListeners) + { + drvHstAudCaDeviceUnregisterCallbacks(pThis, pDevice->idDevice); + pDevice->fRegisteredListeners = false; + } + } + + /* + * Locate the device ID corresponding to the UID string. + */ + if (!fSkip) + { + pDevice->idDevice = drvHstAudCaDeviceUidToId(pDevice->hStrUid, pszUid, fInput ? "input" : "output"); + pDevice->fRegisteredListeners = drvHstAudCaDeviceRegisterCallbacks(pThis, pDevice->idDevice); + } + + PPDMIHOSTAUDIOPORT pIHostAudioPort = fNotify && !fSame ? pThis->pIHostAudioPort : NULL; + RTCritSectLeave(&pThis->CritSect); + + /* + * Notify parent driver to trigger a re-init of any associated streams. + */ + if (pIHostAudioPort) + { + LogFlowFunc(("Notifying parent driver about %s device change...\n", fInput ? "input" : "output")); + pIHostAudioPort->pfnNotifyDeviceChanged(pIHostAudioPort, fInput ? PDMAUDIODIR_IN : PDMAUDIODIR_OUT, NULL /*pvUser*/); + } + } + return VINF_SUCCESS; +} + + +/********************************************************************************************************************************* +* Worker Thread * +*********************************************************************************************************************************/ +#ifdef CORE_AUDIO_WITH_WORKER_THREAD + +/** + * Message handling callback for CFMachPort. + */ +static void drvHstAudCaThreadPortCallback(CFMachPortRef hPort, void *pvMsg, CFIndex cbMsg, void *pvUser) +{ + RT_NOREF(hPort, pvMsg, cbMsg, pvUser); + LogFunc(("hPort=%p pvMsg=%p cbMsg=%#x pvUser=%p\n", hPort, pvMsg, cbMsg, pvUser)); +} + + +/** + * @callback_method_impl{FNRTTHREAD, Worker thread for buffer callbacks.} + */ +static DECLCALLBACK(int) drvHstAudCaThread(RTTHREAD hThreadSelf, void *pvUser) +{ + PDRVHOSTCOREAUDIO pThis = (PDRVHOSTCOREAUDIO)pvUser; + + /* + * Get the runloop, add the mach port to it and signal the constructor thread that we're ready. + */ + pThis->hThreadRunLoop = CFRunLoopGetCurrent(); + CFRetain(pThis->hThreadRunLoop); + + CFRunLoopAddSource(pThis->hThreadRunLoop, pThis->hThreadPortSrc, kCFRunLoopDefaultMode); + + int rc = RTThreadUserSignal(hThreadSelf); + AssertRCReturn(rc, rc); + + /* + * Do work. + */ + for (;;) + { + SInt32 rcRunLoop = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 30.0, TRUE); + Log8Func(("CFRunLoopRunInMode -> %d\n", rcRunLoop)); + Assert(rcRunLoop != kCFRunLoopRunFinished); + if (rcRunLoop != kCFRunLoopRunStopped && rcRunLoop != kCFRunLoopRunFinished) + { /* likely */ } + else + break; + } + + /* + * Clean up. + */ + CFRunLoopRemoveSource(pThis->hThreadRunLoop, pThis->hThreadPortSrc, kCFRunLoopDefaultMode); + LogFunc(("The thread quits!\n")); + return VINF_SUCCESS; +} + +#endif /* CORE_AUDIO_WITH_WORKER_THREAD */ + + + +/********************************************************************************************************************************* +* PDMIHOSTAUDIO * +*********************************************************************************************************************************/ + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnGetConfig} + */ +static DECLCALLBACK(int) drvHstAudCaHA_GetConfig(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDCFG pBackendCfg) +{ + PDRVHOSTCOREAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVHOSTCOREAUDIO, IHostAudio); + AssertPtrReturn(pBackendCfg, VERR_INVALID_POINTER); + + /* + * Fill in the config structure. + */ + RTStrCopy(pBackendCfg->szName, sizeof(pBackendCfg->szName), "Core Audio"); + pBackendCfg->cbStream = sizeof(COREAUDIOSTREAM); + pBackendCfg->fFlags = PDMAUDIOBACKEND_F_ASYNC_STREAM_DESTROY; + + RTCritSectEnter(&pThis->CritSect); +#if 0 /** @todo r=bird: This looks like complete utter non-sense to me. */ + /* For Core Audio we provide one stream per device for now. */ + pBackendCfg->cMaxStreamsIn = PDMAudioHostEnumCountMatching(&pThis->Devices, PDMAUDIODIR_IN); + pBackendCfg->cMaxStreamsOut = PDMAudioHostEnumCountMatching(&pThis->Devices, PDMAUDIODIR_OUT); +#else + pBackendCfg->cMaxStreamsIn = UINT32_MAX; + pBackendCfg->cMaxStreamsOut = UINT32_MAX; +#endif + RTCritSectLeave(&pThis->CritSect); + + LogFlowFunc(("Returning %Rrc\n", VINF_SUCCESS)); + return VINF_SUCCESS; +} + + +/** + * Creates an enumeration of the host's playback and capture devices. + * + * @returns VBox status code. + * @param pDevEnm Where to store the enumerated devices. Caller is + * expected to clean this up on failure, if so desired. + * + * @note Handling of out-of-memory conditions isn't perhaps as good as it + * could be, but it was done so to make the drvHstAudCaGetPropertyData* + * functions as uncomplicated as possible. + */ +static int drvHstAudCaDevicesEnumerateAll(PPDMAUDIOHOSTENUM pDevEnm) +{ + AssertPtr(pDevEnm); + + /* + * First get the UIDs for the default devices. + */ + AudioDeviceID idDefaultDevIn = kAudioDeviceUnknown; + if (!drvHstAudCaGetPropertyData(kAudioObjectSystemObject, kAudioHardwarePropertyDefaultInputDevice, + kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster, + "default input device", &idDefaultDevIn, sizeof(idDefaultDevIn))) + idDefaultDevIn = kAudioDeviceUnknown; + if (idDefaultDevIn == kAudioDeviceUnknown) + LogFunc(("No default input device\n")); + + AudioDeviceID idDefaultDevOut = kAudioDeviceUnknown; + if (!drvHstAudCaGetPropertyData(kAudioObjectSystemObject, kAudioHardwarePropertyDefaultOutputDevice, + kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster, + "default output device", &idDefaultDevOut, sizeof(idDefaultDevOut))) + idDefaultDevOut = kAudioDeviceUnknown; + if (idDefaultDevOut == kAudioDeviceUnknown) + LogFunc(("No default output device\n")); + + /* + * Get a list of all audio devices. + * (We have to retry as the we may race new devices being inserted.) + */ + UInt32 cDevices = 0; + AudioDeviceID *paidDevices + = (AudioDeviceID *)drvHstAudCaGetPropertyDataEx(kAudioObjectSystemObject, kAudioHardwarePropertyDevices, + kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster, + "devices", &cDevices); + cDevices /= sizeof(paidDevices[0]); + + /* + * Try get details on each device and try add them to the enumeration result. + */ + for (uint32_t i = 0; i < cDevices; i++) + { + AudioDeviceID const idDevice = paidDevices[i]; + + /* + * Allocate a new device entry and populate it. + * + * The only relevant information here is channel counts and the UID(s), + * everything else is just extras we can live without. + */ + PCOREAUDIODEVICEDATA pDevEntry = (PCOREAUDIODEVICEDATA)PDMAudioHostDevAlloc(sizeof(*pDevEntry), 0, 0); + AssertReturnStmt(pDevEntry, RTMemTmpFree(paidDevices), VERR_NO_MEMORY); + + pDevEntry->idDevice = idDevice; + if (idDevice != kAudioDeviceUnknown) + { + if (idDevice == idDefaultDevIn) + pDevEntry->Core.fFlags |= PDMAUDIOHOSTDEV_F_DEFAULT_IN; + if (idDevice == idDefaultDevOut) + pDevEntry->Core.fFlags |= PDMAUDIOHOSTDEV_F_DEFAULT_OUT; + } + + /* Count channels and determin the usage. */ + pDevEntry->Core.cMaxInputChannels = drvHstAudCaEnumCountChannels(idDevice, kAudioDevicePropertyScopeInput); + pDevEntry->Core.cMaxOutputChannels = drvHstAudCaEnumCountChannels(idDevice, kAudioDevicePropertyScopeOutput); + if (pDevEntry->Core.cMaxInputChannels > 0 && pDevEntry->Core.cMaxOutputChannels > 0) + pDevEntry->Core.enmUsage = PDMAUDIODIR_DUPLEX; + else if (pDevEntry->Core.cMaxInputChannels > 0) + pDevEntry->Core.enmUsage = PDMAUDIODIR_IN; + else if (pDevEntry->Core.cMaxOutputChannels > 0) + pDevEntry->Core.enmUsage = PDMAUDIODIR_OUT; + else + { + pDevEntry->Core.enmUsage = PDMAUDIODIR_UNKNOWN; + pDevEntry->Core.fFlags |= PDMAUDIOHOSTDEV_F_IGNORE; + /** @todo drop & skip? */ + } + + /* Get the device UID. (We ASSUME this is the same for both input and + output sides of the device.) */ + CFStringRef hStrUid; + if (!drvHstAudCaGetPropertyData(idDevice, kAudioDevicePropertyDeviceUID, kAudioDevicePropertyDeviceUID, + kAudioObjectPropertyElementMaster, + "device UID", &hStrUid, sizeof(hStrUid))) + hStrUid = NULL; + + if (hStrUid) + { + pDevEntry->Core.pszId = drvHstAudCaCFStringToHeap(hStrUid); + pDevEntry->Core.fFlags |= PDMAUDIOHOSTDEV_F_ID_ALLOC; + } + else + pDevEntry->Core.fFlags |= PDMAUDIOHOSTDEV_F_IGNORE; + + /* Get the device name (ignore failures). */ + CFStringRef hStrName = NULL; + if (drvHstAudCaGetPropertyData(idDevice, kAudioObjectPropertyName, + pDevEntry->Core.enmUsage == PDMAUDIODIR_IN + ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMaster, "device name", &hStrName, sizeof(hStrName))) + { + pDevEntry->Core.pszName = drvHstAudCaCFStringToHeap(hStrName); + pDevEntry->Core.fFlags |= PDMAUDIOHOSTDEV_F_NAME_ALLOC; + CFRelease(hStrName); + } + + /* Check if the device is alive for the intended usage. For duplex + devices we'll flag it as dead if either of the directions are dead, + as there is no convenient way of saying otherwise. It's acadmic as + nobody currently 2021-05-22) uses the flag for anything. */ + UInt32 fAlive = 0; + if (drvHstAudCaGetPropertyData(idDevice, kAudioDevicePropertyDeviceIsAlive, + pDevEntry->Core.enmUsage == PDMAUDIODIR_IN + ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMaster, "is-alive", &fAlive, sizeof(fAlive))) + if (!fAlive) + pDevEntry->Core.fFlags |= PDMAUDIOHOSTDEV_F_DEAD; + fAlive = 0; + if ( pDevEntry->Core.enmUsage == PDMAUDIODIR_DUPLEX + && !(pDevEntry->Core.fFlags == PDMAUDIOHOSTDEV_F_DEAD) + && drvHstAudCaGetPropertyData(idDevice, kAudioDevicePropertyDeviceIsAlive, kAudioDevicePropertyScopeInput, + kAudioObjectPropertyElementMaster, "is-alive", &fAlive, sizeof(fAlive))) + if (!fAlive) + pDevEntry->Core.fFlags |= PDMAUDIOHOSTDEV_F_DEAD; + + /* Check if the device is being hogged by someone else. */ + pid_t pidHogger = -2; + if (drvHstAudCaGetPropertyData(idDevice, kAudioDevicePropertyHogMode, kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster, "hog-mode", &pidHogger, sizeof(pidHogger))) + if (pidHogger >= 0) + pDevEntry->Core.fFlags |= PDMAUDIOHOSTDEV_F_LOCKED; + + /* + * Try make sure we've got a name... Only add it to the enumeration if we have one. + */ + if (!pDevEntry->Core.pszName) + { + pDevEntry->Core.pszName = pDevEntry->Core.pszId; + pDevEntry->Core.fFlags &= ~PDMAUDIOHOSTDEV_F_NAME_ALLOC; + } + + if (pDevEntry->Core.pszName) + PDMAudioHostEnumAppend(pDevEnm, &pDevEntry->Core); + else + PDMAudioHostDevFree(&pDevEntry->Core); + } + + RTMemTmpFree(paidDevices); + + LogFunc(("Returning %u devices\n", pDevEnm->cDevices)); + PDMAudioHostEnumLog(pDevEnm, "Core Audio"); + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnGetDevices} + */ +static DECLCALLBACK(int) drvHstAudCaHA_GetDevices(PPDMIHOSTAUDIO pInterface, PPDMAUDIOHOSTENUM pDeviceEnum) +{ + RT_NOREF(pInterface); + AssertPtrReturn(pDeviceEnum, VERR_INVALID_POINTER); + + PDMAudioHostEnumInit(pDeviceEnum); + int rc = drvHstAudCaDevicesEnumerateAll(pDeviceEnum); + if (RT_FAILURE(rc)) + PDMAudioHostEnumDelete(pDeviceEnum); + + LogFlowFunc(("returns %Rrc\n", rc)); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnSetDevice} + */ +static DECLCALLBACK(int) drvHstAudCaHA_SetDevice(PPDMIHOSTAUDIO pInterface, PDMAUDIODIR enmDir, const char *pszId) +{ + PDRVHOSTCOREAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVHOSTCOREAUDIO, IHostAudio); + AssertPtrNullReturn(pszId, VERR_INVALID_POINTER); + if (pszId && !*pszId) + pszId = NULL; + AssertMsgReturn(enmDir == PDMAUDIODIR_IN || enmDir == PDMAUDIODIR_OUT || enmDir == PDMAUDIODIR_DUPLEX, + ("enmDir=%d\n", enmDir, pszId), VERR_INVALID_PARAMETER); + + /* + * Make the change. + */ + int rc = VINF_SUCCESS; + if (enmDir == PDMAUDIODIR_IN || enmDir == PDMAUDIODIR_DUPLEX) + rc = drvHstAudCaSetDevice(pThis, &pThis->InputDevice, true /*fInput*/, true /*fNotify*/, pszId); + if (enmDir == PDMAUDIODIR_OUT || (enmDir == PDMAUDIODIR_DUPLEX && RT_SUCCESS(rc))) + rc = drvHstAudCaSetDevice(pThis, &pThis->OutputDevice, false /*fInput*/, true /*fNotify*/, pszId); + + LogFlowFunc(("returns %Rrc\n", rc)); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnGetStatus} + */ +static DECLCALLBACK(PDMAUDIOBACKENDSTS) drvHstAudCaHA_GetStatus(PPDMIHOSTAUDIO pInterface, PDMAUDIODIR enmDir) +{ + RT_NOREF(pInterface, enmDir); + return PDMAUDIOBACKENDSTS_RUNNING; +} + + +/** + * Marks the given buffer as queued or not-queued. + * + * @returns Old queued value. + * @param pAudioBuffer The buffer. + * @param fQueued The new queued state. + */ +DECLINLINE(bool) drvHstAudCaSetBufferQueued(AudioQueueBufferRef pAudioBuffer, bool fQueued) +{ + if (fQueued) + return ASMAtomicBitTestAndSet(&pAudioBuffer->mUserData, 0); + return ASMAtomicBitTestAndClear(&pAudioBuffer->mUserData, 0); +} + + +/** + * Gets the queued state of the buffer. + * @returns true if queued, false if not. + * @param pAudioBuffer The buffer. + */ +DECLINLINE(bool) drvHstAudCaIsBufferQueued(AudioQueueBufferRef pAudioBuffer) +{ + return ((uintptr_t)pAudioBuffer->mUserData & 1) == 1; +} + + +/** + * Output audio queue buffer callback. + * + * Called whenever an audio queue is done processing a buffer. This routine + * will set the data fill size to zero and mark it as unqueued so that + * drvHstAudCaHA_StreamPlay knowns it can use it. + * + * @param pvUser User argument. + * @param hAudioQueue Audio queue to process output data for. + * @param pAudioBuffer Audio buffer to store output data in. + * + * @thread queue thread. + */ +static void drvHstAudCaOutputQueueBufferCallback(void *pvUser, AudioQueueRef hAudioQueue, AudioQueueBufferRef pAudioBuffer) +{ +#if defined(VBOX_STRICT) || defined(LOG_ENABLED) + PCOREAUDIOSTREAM pStreamCA = (PCOREAUDIOSTREAM)pvUser; + AssertPtr(pStreamCA); + Assert(pStreamCA->hAudioQueue == hAudioQueue); + + uintptr_t idxBuf = (uintptr_t)pAudioBuffer->mUserData >> 8; + Log4Func(("Got back buffer #%zu (%p)\n", idxBuf, pAudioBuffer)); + AssertReturnVoid( idxBuf < pStreamCA->cBuffers + && pStreamCA->paBuffers[idxBuf].pBuf == pAudioBuffer); +#endif + + pAudioBuffer->mAudioDataByteSize = 0; + bool fWasQueued = drvHstAudCaSetBufferQueued(pAudioBuffer, false /*fQueued*/); + Assert(!drvHstAudCaIsBufferQueued(pAudioBuffer)); + Assert(fWasQueued); RT_NOREF(fWasQueued); + + RT_NOREF(pvUser, hAudioQueue); +} + + +/** + * Input audio queue buffer callback. + * + * Called whenever input data from the audio queue becomes available. This + * routine will mark the buffer unqueued so that drvHstAudCaHA_StreamCapture can + * read the data from it. + * + * @param pvUser User argument. + * @param hAudioQueue Audio queue to process input data from. + * @param pAudioBuffer Audio buffer to process input data from. + * @param pAudioTS Audio timestamp. + * @param cPacketDesc Number of packet descriptors. + * @param paPacketDesc Array of packet descriptors. + */ +static void drvHstAudCaInputQueueBufferCallback(void *pvUser, AudioQueueRef hAudioQueue, + AudioQueueBufferRef pAudioBuffer, const AudioTimeStamp *pAudioTS, + UInt32 cPacketDesc, const AudioStreamPacketDescription *paPacketDesc) +{ +#if defined(VBOX_STRICT) || defined(LOG_ENABLED) + PCOREAUDIOSTREAM pStreamCA = (PCOREAUDIOSTREAM)pvUser; + AssertPtr(pStreamCA); + Assert(pStreamCA->hAudioQueue == hAudioQueue); + + uintptr_t idxBuf = (uintptr_t)pAudioBuffer->mUserData >> 8; + Log4Func(("Got back buffer #%zu (%p) with %#x bytes\n", idxBuf, pAudioBuffer, pAudioBuffer->mAudioDataByteSize)); + AssertReturnVoid( idxBuf < pStreamCA->cBuffers + && pStreamCA->paBuffers[idxBuf].pBuf == pAudioBuffer); +#endif + + bool fWasQueued = drvHstAudCaSetBufferQueued(pAudioBuffer, false /*fQueued*/); + Assert(!drvHstAudCaIsBufferQueued(pAudioBuffer)); + Assert(fWasQueued); RT_NOREF(fWasQueued); + + RT_NOREF(pvUser, hAudioQueue, pAudioTS, cPacketDesc, paPacketDesc); +} + + +static void drvHstAudCaLogAsbd(const char *pszDesc, const AudioStreamBasicDescription *pASBD) +{ + LogRel2(("CoreAudio: %s description:\n", pszDesc)); + LogRel2(("CoreAudio: Format ID: %#RX32 (%c%c%c%c)\n", pASBD->mFormatID, + RT_BYTE4(pASBD->mFormatID), RT_BYTE3(pASBD->mFormatID), + RT_BYTE2(pASBD->mFormatID), RT_BYTE1(pASBD->mFormatID))); + LogRel2(("CoreAudio: Flags: %#RX32", pASBD->mFormatFlags)); + if (pASBD->mFormatFlags & kAudioFormatFlagIsFloat) + LogRel2((" Float")); + if (pASBD->mFormatFlags & kAudioFormatFlagIsBigEndian) + LogRel2((" BigEndian")); + if (pASBD->mFormatFlags & kAudioFormatFlagIsSignedInteger) + LogRel2((" SignedInteger")); + if (pASBD->mFormatFlags & kAudioFormatFlagIsPacked) + LogRel2((" Packed")); + if (pASBD->mFormatFlags & kAudioFormatFlagIsAlignedHigh) + LogRel2((" AlignedHigh")); + if (pASBD->mFormatFlags & kAudioFormatFlagIsNonInterleaved) + LogRel2((" NonInterleaved")); + if (pASBD->mFormatFlags & kAudioFormatFlagIsNonMixable) + LogRel2((" NonMixable")); + if (pASBD->mFormatFlags & kAudioFormatFlagsAreAllClear) + LogRel2((" AllClear")); + LogRel2(("\n")); + LogRel2(("CoreAudio: SampleRate : %RU64.%02u Hz\n", + (uint64_t)pASBD->mSampleRate, (unsigned)(pASBD->mSampleRate * 100) % 100)); + LogRel2(("CoreAudio: ChannelsPerFrame: %RU32\n", pASBD->mChannelsPerFrame)); + LogRel2(("CoreAudio: FramesPerPacket : %RU32\n", pASBD->mFramesPerPacket)); + LogRel2(("CoreAudio: BitsPerChannel : %RU32\n", pASBD->mBitsPerChannel)); + LogRel2(("CoreAudio: BytesPerFrame : %RU32\n", pASBD->mBytesPerFrame)); + LogRel2(("CoreAudio: BytesPerPacket : %RU32\n", pASBD->mBytesPerPacket)); +} + + +static void drvHstAudCaPropsToAsbd(PCPDMAUDIOPCMPROPS pProps, AudioStreamBasicDescription *pASBD) +{ + AssertPtrReturnVoid(pProps); + AssertPtrReturnVoid(pASBD); + + RT_BZERO(pASBD, sizeof(AudioStreamBasicDescription)); + + pASBD->mFormatID = kAudioFormatLinearPCM; + pASBD->mFormatFlags = kAudioFormatFlagIsPacked; + if (pProps->fSigned) + pASBD->mFormatFlags |= kAudioFormatFlagIsSignedInteger; + if (PDMAudioPropsIsBigEndian(pProps)) + pASBD->mFormatFlags |= kAudioFormatFlagIsBigEndian; + pASBD->mSampleRate = PDMAudioPropsHz(pProps); + pASBD->mChannelsPerFrame = PDMAudioPropsChannels(pProps); + pASBD->mBitsPerChannel = PDMAudioPropsSampleBits(pProps); + pASBD->mBytesPerFrame = PDMAudioPropsFrameSize(pProps); + pASBD->mFramesPerPacket = 1; /* For uncompressed audio, set this to 1. */ + pASBD->mBytesPerPacket = PDMAudioPropsFrameSize(pProps) * pASBD->mFramesPerPacket; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamCreate} + */ +static DECLCALLBACK(int) drvHstAudCaHA_StreamCreate(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + PCPDMAUDIOSTREAMCFG pCfgReq, PPDMAUDIOSTREAMCFG pCfgAcq) +{ + PDRVHOSTCOREAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVHOSTCOREAUDIO, IHostAudio); + PCOREAUDIOSTREAM pStreamCA = (PCOREAUDIOSTREAM)pStream; + AssertPtrReturn(pStreamCA, 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); + int rc; + + /** @todo This takes too long. Stats indicates it may take up to 200 ms. + * Knoppix guest resets the stream and we hear nada because the + * draining is aborted when the stream is destroyed. Should try use + * async init for parts (much) of this. */ + + /* + * Permission check for input devices before we start. + */ + if (pCfgReq->enmDir == PDMAUDIODIR_IN) + { + rc = coreAudioInputPermissionCheck(); + if (RT_FAILURE(rc)) + return rc; + } + + /* + * Do we have a device for the requested stream direction? + */ + RTCritSectEnter(&pThis->CritSect); + CFStringRef hDevUidStr = pCfgReq->enmDir == PDMAUDIODIR_IN ? pThis->InputDevice.hStrUid : pThis->OutputDevice.hStrUid; + if (hDevUidStr) + CFRetain(hDevUidStr); + RTCritSectLeave(&pThis->CritSect); + +#ifdef LOG_ENABLED + char szTmp[PDMAUDIOSTRMCFGTOSTRING_MAX]; +#endif + LogFunc(("hDevUidStr=%p *pCfgReq: %s\n", hDevUidStr, PDMAudioStrmCfgToString(pCfgReq, szTmp, sizeof(szTmp)) )); + if (hDevUidStr) + { + /* + * Basic structure init. + */ + pStreamCA->fEnabled = false; + pStreamCA->fStarted = false; + pStreamCA->fDraining = false; + pStreamCA->fRestartOnResume = false; + pStreamCA->offInternal = 0; + pStreamCA->idxBuffer = 0; + pStreamCA->enmInitState = COREAUDIOINITSTATE_IN_INIT; + + rc = RTCritSectInit(&pStreamCA->CritSect); + if (RT_SUCCESS(rc)) + { + /* + * Do format conversion and create the circular buffer we use to shuffle + * data to/from the queue thread. + */ + PDMAudioStrmCfgCopy(&pStreamCA->Cfg, pCfgReq); + drvHstAudCaPropsToAsbd(&pCfgReq->Props, &pStreamCA->BasicStreamDesc); + /** @todo Do some validation? */ + drvHstAudCaLogAsbd(pCfgReq->enmDir == PDMAUDIODIR_IN ? "Capturing queue format" : "Playback queue format", + &pStreamCA->BasicStreamDesc); + /* + * Create audio queue. + * + * Documentation says the callbacks will be run on some core audio + * related thread if we don't specify a runloop here. That's simpler. + */ +#ifdef CORE_AUDIO_WITH_WORKER_THREAD + CFRunLoopRef const hRunLoop = pThis->hThreadRunLoop; + CFStringRef const hRunLoopMode = kCFRunLoopDefaultMode; +#else + CFRunLoopRef const hRunLoop = NULL; + CFStringRef const hRunLoopMode = NULL; +#endif + OSStatus orc; + if (pCfgReq->enmDir == PDMAUDIODIR_OUT) + orc = AudioQueueNewOutput(&pStreamCA->BasicStreamDesc, drvHstAudCaOutputQueueBufferCallback, pStreamCA, + hRunLoop, hRunLoopMode, 0 /*fFlags - MBZ*/, &pStreamCA->hAudioQueue); + else + orc = AudioQueueNewInput(&pStreamCA->BasicStreamDesc, drvHstAudCaInputQueueBufferCallback, pStreamCA, + hRunLoop, hRunLoopMode, 0 /*fFlags - MBZ*/, &pStreamCA->hAudioQueue); + LogFlowFunc(("AudioQueueNew%s -> %#x\n", pCfgReq->enmDir == PDMAUDIODIR_OUT ? "Output" : "Input", orc)); + if (orc == noErr) + { + /* + * Assign device to the queue. + */ + UInt32 uSize = sizeof(hDevUidStr); + orc = AudioQueueSetProperty(pStreamCA->hAudioQueue, kAudioQueueProperty_CurrentDevice, &hDevUidStr, uSize); + LogFlowFunc(("AudioQueueSetProperty -> %#x\n", orc)); + if (orc == noErr) + { + /* + * Sanity-adjust the requested buffer size. + */ + uint32_t cFramesBufferSizeMax = PDMAudioPropsMilliToFrames(&pStreamCA->Cfg.Props, 2 * RT_MS_1SEC); + uint32_t cFramesBufferSize = PDMAudioPropsMilliToFrames(&pStreamCA->Cfg.Props, 32 /*ms*/); + cFramesBufferSize = RT_MAX(cFramesBufferSize, pCfgReq->Backend.cFramesBufferSize); + cFramesBufferSize = RT_MIN(cFramesBufferSize, cFramesBufferSizeMax); + + /* + * The queue buffers size is based on cMsSchedulingHint so that we're likely to + * have a new one ready/done after each guest DMA transfer. We must however + * make sure we don't end up with too may or too few. + */ + Assert(pCfgReq->Device.cMsSchedulingHint > 0); + uint32_t cFramesQueueBuffer = PDMAudioPropsMilliToFrames(&pStreamCA->Cfg.Props, + pCfgReq->Device.cMsSchedulingHint > 0 + ? pCfgReq->Device.cMsSchedulingHint : 10); + uint32_t cQueueBuffers; + if (cFramesQueueBuffer * COREAUDIO_MIN_BUFFERS <= cFramesBufferSize) + { + cQueueBuffers = cFramesBufferSize / cFramesQueueBuffer; + if (cQueueBuffers > COREAUDIO_MAX_BUFFERS) + { + cQueueBuffers = COREAUDIO_MAX_BUFFERS; + cFramesQueueBuffer = cFramesBufferSize / COREAUDIO_MAX_BUFFERS; + } + } + else + { + cQueueBuffers = COREAUDIO_MIN_BUFFERS; + cFramesQueueBuffer = cFramesBufferSize / COREAUDIO_MIN_BUFFERS; + } + + cFramesBufferSize = cQueueBuffers * cFramesBufferSize; + + /* + * Allocate the audio queue buffers. + */ + pStreamCA->paBuffers = (PCOREAUDIOBUF)RTMemAllocZ(sizeof(pStreamCA->paBuffers[0]) * cQueueBuffers); + if (pStreamCA->paBuffers != NULL) + { + pStreamCA->cBuffers = cQueueBuffers; + + const size_t cbQueueBuffer = PDMAudioPropsFramesToBytes(&pStreamCA->Cfg.Props, cFramesQueueBuffer); + LogFlowFunc(("Allocating %u, each %#x bytes / %u frames\n", cQueueBuffers, cbQueueBuffer, cFramesQueueBuffer)); + cFramesBufferSize = 0; + for (uint32_t iBuf = 0; iBuf < cQueueBuffers; iBuf++) + { + AudioQueueBufferRef pBuf = NULL; + orc = AudioQueueAllocateBuffer(pStreamCA->hAudioQueue, cbQueueBuffer, &pBuf); + if (RT_LIKELY(orc == noErr)) + { + pBuf->mUserData = (void *)(uintptr_t)(iBuf << 8); /* bit zero is the queued-indicator. */ + pStreamCA->paBuffers[iBuf].pBuf = pBuf; + cFramesBufferSize += PDMAudioPropsBytesToFrames(&pStreamCA->Cfg.Props, + pBuf->mAudioDataBytesCapacity); + Assert(PDMAudioPropsIsSizeAligned(&pStreamCA->Cfg.Props, pBuf->mAudioDataBytesCapacity)); + } + else + { + LogRel(("CoreAudio: Out of memory (buffer %#x out of %#x, %#x bytes)\n", + iBuf, cQueueBuffers, cbQueueBuffer)); + while (iBuf-- > 0) + { + AudioQueueFreeBuffer(pStreamCA->hAudioQueue, pStreamCA->paBuffers[iBuf].pBuf); + pStreamCA->paBuffers[iBuf].pBuf = NULL; + } + break; + } + } + if (orc == noErr) + { + /* + * Update the stream config. + */ + pStreamCA->Cfg.Backend.cFramesBufferSize = cFramesBufferSize; + pStreamCA->Cfg.Backend.cFramesPeriod = cFramesQueueBuffer; /* whatever */ + pStreamCA->Cfg.Backend.cFramesPreBuffering = pStreamCA->Cfg.Backend.cFramesPreBuffering + * pStreamCA->Cfg.Backend.cFramesBufferSize + / RT_MAX(pCfgReq->Backend.cFramesBufferSize, 1); + + PDMAudioStrmCfgCopy(pCfgAcq, &pStreamCA->Cfg); + + ASMAtomicWriteU32(&pStreamCA->enmInitState, COREAUDIOINITSTATE_INIT); + + LogFunc(("returns VINF_SUCCESS\n")); + CFRelease(hDevUidStr); + return VINF_SUCCESS; + } + + RTMemFree(pStreamCA->paBuffers); + } + else + rc = VERR_NO_MEMORY; + } + else + LogRelMax(64, ("CoreAudio: Failed to associate device with queue: %#x (%d)\n", orc, orc)); + AudioQueueDispose(pStreamCA->hAudioQueue, TRUE /*inImmediate*/); + } + else + LogRelMax(64, ("CoreAudio: Failed to create audio queue: %#x (%d)\n", orc, orc)); + RTCritSectDelete(&pStreamCA->CritSect); + } + else + LogRel(("CoreAudio: Failed to initialize critical section for stream: %Rrc\n", rc)); + CFRelease(hDevUidStr); + } + else + { + LogRelMax(64, ("CoreAudio: No device for %s stream.\n", PDMAudioDirGetName(pCfgReq->enmDir))); + rc = VERR_AUDIO_STREAM_COULD_NOT_CREATE; + } + + LogFunc(("returns %Rrc\n", rc)); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDestroy} + */ +static DECLCALLBACK(int) drvHstAudCaHA_StreamDestroy(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, bool fImmediate) +{ + RT_NOREF(pInterface); + PCOREAUDIOSTREAM pStreamCA = (PCOREAUDIOSTREAM)pStream; + AssertPtrReturn(pStreamCA, VERR_INVALID_POINTER); + LogFunc(("%p: %s fImmediate=%RTbool\n", pStreamCA, pStreamCA->Cfg.szName, fImmediate)); +#ifdef LOG_ENABLED + uint64_t const nsStart = RTTimeNanoTS(); +#endif + + /* + * Never mind if the status isn't INIT (it should always be, though). + */ + COREAUDIOINITSTATE const enmInitState = (COREAUDIOINITSTATE)ASMAtomicReadU32(&pStreamCA->enmInitState); + AssertMsg(enmInitState == COREAUDIOINITSTATE_INIT, ("%d\n", enmInitState)); + if (enmInitState == COREAUDIOINITSTATE_INIT) + { + Assert(RTCritSectIsInitialized(&pStreamCA->CritSect)); + + /* + * Change the stream state and stop the stream (just to be sure). + */ + OSStatus orc; + ASMAtomicWriteU32(&pStreamCA->enmInitState, COREAUDIOINITSTATE_IN_UNINIT); + if (pStreamCA->hAudioQueue) + { + orc = AudioQueueStop(pStreamCA->hAudioQueue, fImmediate ? TRUE : FALSE /*inImmediate/synchronously*/); + LogFlowFunc(("AudioQueueStop -> %#x\n", orc)); + } + + /* + * Enter and leave the critsect afterwards for paranoid reasons. + */ + RTCritSectEnter(&pStreamCA->CritSect); + RTCritSectLeave(&pStreamCA->CritSect); + + /* + * Free the queue buffers and the queue. + * + * This may take a while. The AudioQueueReset call seems to helps + * reducing time stuck in AudioQueueDispose. + */ +#ifdef CORE_AUDIO_WITH_BREAKPOINT_TIMER + LogRel(("Queue-destruction timer starting...\n")); + PDRVHOSTCOREAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVHOSTCOREAUDIO, IHostAudio); + RTTimerLRStart(pThis->hBreakpointTimer, RT_NS_100MS); + uint64_t nsStart = RTTimeNanoTS(); +#endif + +#if 0 /* This seems to work even when doing a non-immediate stop&dispose. However, it doesn't make sense conceptually. */ + if (pStreamCA->hAudioQueue /*&& fImmediate*/) + { + LogFlowFunc(("Calling AudioQueueReset ...\n")); + orc = AudioQueueReset(pStreamCA->hAudioQueue); + LogFlowFunc(("AudioQueueReset -> %#x\n", orc)); + } +#endif + + if (pStreamCA->paBuffers && fImmediate) + { + LogFlowFunc(("Freeing %u buffers ...\n", pStreamCA->cBuffers)); + for (uint32_t iBuf = 0; iBuf < pStreamCA->cBuffers; iBuf++) + { + orc = AudioQueueFreeBuffer(pStreamCA->hAudioQueue, pStreamCA->paBuffers[iBuf].pBuf); + AssertMsg(orc == noErr, ("AudioQueueFreeBuffer(#%u) -> orc=%#x\n", iBuf, orc)); + pStreamCA->paBuffers[iBuf].pBuf = NULL; + } + } + + if (pStreamCA->hAudioQueue) + { + LogFlowFunc(("Disposing of the queue ...\n")); + orc = AudioQueueDispose(pStreamCA->hAudioQueue, fImmediate ? TRUE : FALSE /*inImmediate/synchronously*/); /* may take some time */ + LogFlowFunc(("AudioQueueDispose -> %#x (%d)\n", orc, orc)); + AssertMsg(orc == noErr, ("AudioQueueDispose -> orc=%#x\n", orc)); + pStreamCA->hAudioQueue = NULL; + } + + /* We should get no further buffer callbacks at this point according to the docs. */ + if (pStreamCA->paBuffers) + { + RTMemFree(pStreamCA->paBuffers); + pStreamCA->paBuffers = NULL; + } + pStreamCA->cBuffers = 0; + +#ifdef CORE_AUDIO_WITH_BREAKPOINT_TIMER + RTTimerLRStop(pThis->hBreakpointTimer); + LogRel(("Queue-destruction: %'RU64\n", RTTimeNanoTS() - nsStart)); +#endif + + /* + * Delete the critsect and we're done. + */ + RTCritSectDelete(&pStreamCA->CritSect); + + ASMAtomicWriteU32(&pStreamCA->enmInitState, COREAUDIOINITSTATE_UNINIT); + } + else + LogFunc(("Wrong stream init state for %p: %d - leaking it\n", pStream, enmInitState)); + + LogFunc(("returns (took %'RU64 ns)\n", RTTimeNanoTS() - nsStart)); + return VINF_SUCCESS; +} + + +#ifdef CORE_AUDIO_WITH_BREAKPOINT_TIMER +/** @callback_method_impl{FNRTTIMERLR, For debugging things that takes too long.} */ +static DECLCALLBACK(void) drvHstAudCaBreakpointTimer(RTTIMERLR hTimer, void *pvUser, uint64_t iTick) +{ + LogFlowFunc(("Queue-destruction timeout! iTick=%RU64\n", iTick)); + RT_NOREF(hTimer, pvUser, iTick); + RTLogFlush(NULL); + RT_BREAKPOINT(); +} +#endif + + +/** + * @ interface_method_impl{PDMIHOSTAUDIO,pfnStreamEnable} + */ +static DECLCALLBACK(int) drvHstAudCaHA_StreamEnable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PCOREAUDIOSTREAM pStreamCA = (PCOREAUDIOSTREAM)pStream; + LogFlowFunc(("Stream '%s' {%s}\n", pStreamCA->Cfg.szName, drvHstAudCaStreamStatusString(pStreamCA))); + AssertReturn(pStreamCA->enmInitState == COREAUDIOINITSTATE_INIT, VERR_AUDIO_STREAM_NOT_READY); + RTCritSectEnter(&pStreamCA->CritSect); + + Assert(!pStreamCA->fEnabled); + Assert(!pStreamCA->fStarted); + + /* + * We always reset the buffer before enabling the stream (normally never necessary). + */ + OSStatus orc = AudioQueueReset(pStreamCA->hAudioQueue); + if (orc != noErr) + LogRelMax(64, ("CoreAudio: Stream reset failed when enabling '%s': %#x (%d)\n", pStreamCA->Cfg.szName, orc, orc)); + Assert(orc == noErr); + for (uint32_t iBuf = 0; iBuf < pStreamCA->cBuffers; iBuf++) + Assert(!drvHstAudCaIsBufferQueued(pStreamCA->paBuffers[iBuf].pBuf)); + + pStreamCA->offInternal = 0; + pStreamCA->fDraining = false; + pStreamCA->fEnabled = true; + pStreamCA->fRestartOnResume = false; + pStreamCA->idxBuffer = 0; + + /* + * Input streams will start capturing, while output streams will only start + * playing once we get some audio data to play (see drvHstAudCaHA_StreamPlay). + */ + int rc = VINF_SUCCESS; + if (pStreamCA->Cfg.enmDir == PDMAUDIODIR_IN) + { + /* Zero (probably not needed) and submit all the buffers first. */ + for (uint32_t iBuf = 0; iBuf < pStreamCA->cBuffers; iBuf++) + { + AudioQueueBufferRef pBuf = pStreamCA->paBuffers[iBuf].pBuf; + + RT_BZERO(pBuf->mAudioData, pBuf->mAudioDataBytesCapacity); + pBuf->mAudioDataByteSize = 0; + drvHstAudCaSetBufferQueued(pBuf, true /*fQueued*/); + + orc = AudioQueueEnqueueBuffer(pStreamCA->hAudioQueue, pBuf, 0 /*inNumPacketDescs*/, NULL /*inPacketDescs*/); + AssertLogRelMsgBreakStmt(orc == noErr, ("CoreAudio: AudioQueueEnqueueBuffer(#%u) -> %#x (%d) - stream '%s'\n", + iBuf, orc, orc, pStreamCA->Cfg.szName), + drvHstAudCaSetBufferQueued(pBuf, false /*fQueued*/)); + } + + /* Start the stream. */ + if (orc == noErr) + { + LogFlowFunc(("Start input stream '%s'...\n", pStreamCA->Cfg.szName)); + orc = AudioQueueStart(pStreamCA->hAudioQueue, NULL /*inStartTime*/); + AssertLogRelMsgStmt(orc == noErr, ("CoreAudio: AudioQueueStart(%s) -> %#x (%d) \n", pStreamCA->Cfg.szName, orc, orc), + rc = VERR_AUDIO_STREAM_NOT_READY); + pStreamCA->fStarted = orc == noErr; + } + else + rc = VERR_AUDIO_STREAM_NOT_READY; + } + else + Assert(pStreamCA->Cfg.enmDir == PDMAUDIODIR_OUT); + + RTCritSectLeave(&pStreamCA->CritSect); + LogFlowFunc(("returns %Rrc\n", rc)); + return rc; +} + + +/** + * @ interface_method_impl{PDMIHOSTAUDIO,pfnStreamDisable} + */ +static DECLCALLBACK(int) drvHstAudCaHA_StreamDisable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PCOREAUDIOSTREAM pStreamCA = (PCOREAUDIOSTREAM)pStream; + LogFlowFunc(("cMsLastTransfer=%RI64 ms, stream '%s' {%s} \n", + pStreamCA->msLastTransfer ? RTTimeMilliTS() - pStreamCA->msLastTransfer : -1, + pStreamCA->Cfg.szName, drvHstAudCaStreamStatusString(pStreamCA) )); + AssertReturn(pStreamCA->enmInitState == COREAUDIOINITSTATE_INIT, VERR_AUDIO_STREAM_NOT_READY); + RTCritSectEnter(&pStreamCA->CritSect); + + /* + * Always stop it (draining or no). + */ + pStreamCA->fEnabled = false; + pStreamCA->fRestartOnResume = false; + Assert(!pStreamCA->fDraining || pStreamCA->Cfg.enmDir == PDMAUDIODIR_OUT); + + int rc = VINF_SUCCESS; + if (pStreamCA->fStarted) + { +#if 0 + OSStatus orc2 = AudioQueueReset(pStreamCA->hAudioQueue); + LogFlowFunc(("AudioQueueReset(%s) returns %#x (%d)\n", pStreamCA->Cfg.szName, orc2, orc2)); RT_NOREF(orc2); + orc2 = AudioQueueFlush(pStreamCA->hAudioQueue); + LogFlowFunc(("AudioQueueFlush(%s) returns %#x (%d)\n", pStreamCA->Cfg.szName, orc2, orc2)); RT_NOREF(orc2); +#endif + + OSStatus orc = AudioQueueStop(pStreamCA->hAudioQueue, TRUE /*inImmediate*/); + LogFlowFunc(("AudioQueueStop(%s,TRUE) returns %#x (%d)\n", pStreamCA->Cfg.szName, orc, orc)); + if (orc != noErr) + { + LogRelMax(64, ("CoreAudio: Stopping '%s' failed (disable): %#x (%d)\n", pStreamCA->Cfg.szName, orc, orc)); + rc = VERR_GENERAL_FAILURE; + } + pStreamCA->fStarted = false; + pStreamCA->fDraining = false; + } + + RTCritSectLeave(&pStreamCA->CritSect); + LogFlowFunc(("returns %Rrc {%s}\n", rc, drvHstAudCaStreamStatusString(pStreamCA))); + return rc; +} + + +/** + * @ interface_method_impl{PDMIHOSTAUDIO,pfnStreamPause} + */ +static DECLCALLBACK(int) drvHstAudCaHA_StreamPause(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PCOREAUDIOSTREAM pStreamCA = (PCOREAUDIOSTREAM)pStream; + LogFlowFunc(("cMsLastTransfer=%RI64 ms, stream '%s' {%s} \n", + pStreamCA->msLastTransfer ? RTTimeMilliTS() - pStreamCA->msLastTransfer : -1, + pStreamCA->Cfg.szName, drvHstAudCaStreamStatusString(pStreamCA) )); + AssertReturn(pStreamCA->enmInitState == COREAUDIOINITSTATE_INIT, VERR_AUDIO_STREAM_NOT_READY); + RTCritSectEnter(&pStreamCA->CritSect); + + /* + * Unless we're draining the stream, pause it if it has started. + */ + int rc = VINF_SUCCESS; + if (pStreamCA->fStarted && !pStreamCA->fDraining) + { + pStreamCA->fRestartOnResume = true; + + OSStatus orc = AudioQueuePause(pStreamCA->hAudioQueue); + LogFlowFunc(("AudioQueuePause(%s) returns %#x (%d)\n", pStreamCA->Cfg.szName, orc, orc)); + if (orc != noErr) + { + LogRelMax(64, ("CoreAudio: Pausing '%s' failed: %#x (%d)\n", pStreamCA->Cfg.szName, orc, orc)); + rc = VERR_GENERAL_FAILURE; + } + pStreamCA->fStarted = false; + } + else + { + pStreamCA->fRestartOnResume = false; + if (pStreamCA->fDraining) + { + LogFunc(("Stream '%s' is draining\n", pStreamCA->Cfg.szName)); + Assert(pStreamCA->fStarted); + } + } + + RTCritSectLeave(&pStreamCA->CritSect); + LogFlowFunc(("returns %Rrc {%s}\n", rc, drvHstAudCaStreamStatusString(pStreamCA))); + return rc; +} + + +/** + * @ interface_method_impl{PDMIHOSTAUDIO,pfnStreamResume} + */ +static DECLCALLBACK(int) drvHstAudCaHA_StreamResume(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PCOREAUDIOSTREAM pStreamCA = (PCOREAUDIOSTREAM)pStream; + LogFlowFunc(("Stream '%s' {%s}\n", pStreamCA->Cfg.szName, drvHstAudCaStreamStatusString(pStreamCA))); + AssertReturn(pStreamCA->enmInitState == COREAUDIOINITSTATE_INIT, VERR_AUDIO_STREAM_NOT_READY); + RTCritSectEnter(&pStreamCA->CritSect); + + /* + * Resume according to state saved by drvHstAudCaHA_StreamPause. + */ + int rc = VINF_SUCCESS; + if (pStreamCA->fRestartOnResume) + { + OSStatus orc = AudioQueueStart(pStreamCA->hAudioQueue, NULL /*inStartTime*/); + LogFlowFunc(("AudioQueueStart(%s, NULL) returns %#x (%d)\n", pStreamCA->Cfg.szName, orc, orc)); + if (orc != noErr) + { + LogRelMax(64, ("CoreAudio: Pausing '%s' failed: %#x (%d)\n", pStreamCA->Cfg.szName, orc, orc)); + rc = VERR_AUDIO_STREAM_NOT_READY; + } + } + pStreamCA->fRestartOnResume = false; + + RTCritSectLeave(&pStreamCA->CritSect); + LogFlowFunc(("returns %Rrc {%s}\n", rc, drvHstAudCaStreamStatusString(pStreamCA))); + return rc; +} + + +/** + * @ interface_method_impl{PDMIHOSTAUDIO,pfnStreamDrain} + */ +static DECLCALLBACK(int) drvHstAudCaHA_StreamDrain(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PCOREAUDIOSTREAM pStreamCA = (PCOREAUDIOSTREAM)pStream; + AssertReturn(pStreamCA->Cfg.enmDir == PDMAUDIODIR_OUT, VERR_INVALID_PARAMETER); + LogFlowFunc(("cMsLastTransfer=%RI64 ms, stream '%s' {%s} \n", + pStreamCA->msLastTransfer ? RTTimeMilliTS() - pStreamCA->msLastTransfer : -1, + pStreamCA->Cfg.szName, drvHstAudCaStreamStatusString(pStreamCA) )); + AssertReturn(pStreamCA->enmInitState == COREAUDIOINITSTATE_INIT, VERR_AUDIO_STREAM_NOT_READY); + RTCritSectEnter(&pStreamCA->CritSect); + + /* + * The AudioQueueStop function has both an immediate and a drain mode, + * so we'll obviously use the latter here. For checking draining progress, + * we will just check if all buffers have been returned or not. + */ + int rc = VINF_SUCCESS; + if (pStreamCA->fStarted) + { + if (!pStreamCA->fDraining) + { + OSStatus orc = AudioQueueStop(pStreamCA->hAudioQueue, FALSE /*inImmediate*/); + LogFlowFunc(("AudioQueueStop(%s, FALSE) returns %#x (%d)\n", pStreamCA->Cfg.szName, orc, orc)); + if (orc == noErr) + pStreamCA->fDraining = true; + else + { + LogRelMax(64, ("CoreAudio: Stopping '%s' failed (drain): %#x (%d)\n", pStreamCA->Cfg.szName, orc, orc)); + rc = VERR_GENERAL_FAILURE; + } + } + else + LogFlowFunc(("Already draining '%s' ...\n", pStreamCA->Cfg.szName)); + } + else + { + LogFlowFunc(("Drain requested for '%s', but not started playback...\n", pStreamCA->Cfg.szName)); + AssertStmt(!pStreamCA->fDraining, pStreamCA->fDraining = false); + } + + RTCritSectLeave(&pStreamCA->CritSect); + LogFlowFunc(("returns %Rrc {%s}\n", rc, drvHstAudCaStreamStatusString(pStreamCA))); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetReadable} + */ +static DECLCALLBACK(uint32_t) drvHstAudCaHA_StreamGetReadable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PCOREAUDIOSTREAM pStreamCA = (PCOREAUDIOSTREAM)pStream; + AssertPtrReturn(pStreamCA, VERR_INVALID_POINTER); + AssertReturn(pStreamCA->enmInitState == COREAUDIOINITSTATE_INIT, 0); + + uint32_t cbReadable = 0; + if (pStreamCA->Cfg.enmDir == PDMAUDIODIR_IN) + { + RTCritSectEnter(&pStreamCA->CritSect); + PCOREAUDIOBUF const paBuffers = pStreamCA->paBuffers; + uint32_t const cBuffers = pStreamCA->cBuffers; + uint32_t const idxStart = pStreamCA->idxBuffer; + uint32_t idxBuffer = idxStart; + AudioQueueBufferRef pBuf; + + if ( cBuffers > 0 + && !drvHstAudCaIsBufferQueued(pBuf = paBuffers[idxBuffer].pBuf)) + { + do + { + uint32_t const cbTotal = pBuf->mAudioDataBytesCapacity; + uint32_t cbFill = pBuf->mAudioDataByteSize; + AssertStmt(cbFill <= cbTotal, cbFill = cbTotal); + uint32_t off = paBuffers[idxBuffer].offRead; + AssertStmt(off < cbFill, off = cbFill); + + cbReadable += cbFill - off; + + /* Advance. */ + idxBuffer++; + if (idxBuffer < cBuffers) + { /* likely */ } + else + idxBuffer = 0; + } while (idxBuffer != idxStart && !drvHstAudCaIsBufferQueued(pBuf = paBuffers[idxBuffer].pBuf)); + } + + RTCritSectLeave(&pStreamCA->CritSect); + } + Log2Func(("returns %#x for '%s'\n", cbReadable, pStreamCA->Cfg.szName)); + return cbReadable; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetWritable} + */ +static DECLCALLBACK(uint32_t) drvHstAudCaHA_StreamGetWritable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PCOREAUDIOSTREAM pStreamCA = (PCOREAUDIOSTREAM)pStream; + AssertPtrReturn(pStreamCA, VERR_INVALID_POINTER); + AssertReturn(pStreamCA->enmInitState == COREAUDIOINITSTATE_INIT, 0); + + uint32_t cbWritable = 0; + if (pStreamCA->Cfg.enmDir == PDMAUDIODIR_OUT) + { + RTCritSectEnter(&pStreamCA->CritSect); + PCOREAUDIOBUF const paBuffers = pStreamCA->paBuffers; + uint32_t const cBuffers = pStreamCA->cBuffers; + uint32_t const idxStart = pStreamCA->idxBuffer; + uint32_t idxBuffer = idxStart; + AudioQueueBufferRef pBuf; + + if ( cBuffers > 0 + && !drvHstAudCaIsBufferQueued(pBuf = paBuffers[idxBuffer].pBuf)) + { + do + { + uint32_t const cbTotal = pBuf->mAudioDataBytesCapacity; + uint32_t cbUsed = pBuf->mAudioDataByteSize; + AssertStmt(cbUsed <= cbTotal, paBuffers[idxBuffer].pBuf->mAudioDataByteSize = cbUsed = cbTotal); + + cbWritable += cbTotal - cbUsed; + + /* Advance. */ + idxBuffer++; + if (idxBuffer < cBuffers) + { /* likely */ } + else + idxBuffer = 0; + } while (idxBuffer != idxStart && !drvHstAudCaIsBufferQueued(pBuf = paBuffers[idxBuffer].pBuf)); + } + + RTCritSectLeave(&pStreamCA->CritSect); + } + Log2Func(("returns %#x for '%s'\n", cbWritable, pStreamCA->Cfg.szName)); + return cbWritable; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetState} + */ +static DECLCALLBACK(PDMHOSTAUDIOSTREAMSTATE) drvHstAudCaHA_StreamGetState(PPDMIHOSTAUDIO pInterface, + PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PCOREAUDIOSTREAM pStreamCA = (PCOREAUDIOSTREAM)pStream; + AssertPtrReturn(pStreamCA, PDMHOSTAUDIOSTREAMSTATE_INVALID); + + if (ASMAtomicReadU32(&pStreamCA->enmInitState) == COREAUDIOINITSTATE_INIT) + { + if (!pStreamCA->fDraining) + { /* likely */ } + else + { + /* + * If we're draining, we're done when we've got all the buffers back. + */ + RTCritSectEnter(&pStreamCA->CritSect); + PCOREAUDIOBUF const paBuffers = pStreamCA->paBuffers; + uintptr_t idxBuffer = pStreamCA->cBuffers; + while (idxBuffer-- > 0) + if (!drvHstAudCaIsBufferQueued(paBuffers[idxBuffer].pBuf)) + { /* likely */ } + else + { +#ifdef LOG_ENABLED + uint32_t cQueued = 1; + while (idxBuffer-- > 0) + cQueued += drvHstAudCaIsBufferQueued(paBuffers[idxBuffer].pBuf); + LogFunc(("Still done draining '%s': %u queued buffers\n", pStreamCA->Cfg.szName, cQueued)); +#endif + RTCritSectLeave(&pStreamCA->CritSect); + return PDMHOSTAUDIOSTREAMSTATE_DRAINING; + } + + LogFunc(("Done draining '%s'\n", pStreamCA->Cfg.szName)); + pStreamCA->fDraining = false; + pStreamCA->fEnabled = false; + pStreamCA->fStarted = false; + RTCritSectLeave(&pStreamCA->CritSect); + } + + return PDMHOSTAUDIOSTREAMSTATE_OKAY; + } + return PDMHOSTAUDIOSTREAMSTATE_NOT_WORKING; /** @todo ?? */ +} + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamPlay} + */ +static DECLCALLBACK(int) drvHstAudCaHA_StreamPlay(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + const void *pvBuf, uint32_t cbBuf, uint32_t *pcbWritten) +{ + RT_NOREF(pInterface); + PCOREAUDIOSTREAM pStreamCA = (PCOREAUDIOSTREAM)pStream; + AssertPtrReturn(pStreamCA, VERR_INVALID_POINTER); + AssertPtrReturn(pcbWritten, VERR_INVALID_POINTER); + if (cbBuf) + AssertPtrReturn(pvBuf, VERR_INVALID_POINTER); + Assert(PDMAudioPropsIsSizeAligned(&pStreamCA->Cfg.Props, cbBuf)); + AssertReturnStmt(pStreamCA->enmInitState == COREAUDIOINITSTATE_INIT, *pcbWritten = 0, VERR_AUDIO_STREAM_NOT_READY); + + RTCritSectEnter(&pStreamCA->CritSect); + if (pStreamCA->fEnabled) + { /* likely */ } + else + { + RTCritSectLeave(&pStreamCA->CritSect); + *pcbWritten = 0; + LogFunc(("Skipping %#x byte write to disabled stream {%s}\n", cbBuf, drvHstAudCaStreamStatusString(pStreamCA) )); + return VINF_SUCCESS; + } + Log4Func(("cbBuf=%#x stream '%s' {%s}\n", cbBuf, pStreamCA->Cfg.szName, drvHstAudCaStreamStatusString(pStreamCA) )); + + /* + * Transfer loop. + */ + PCOREAUDIOBUF const paBuffers = pStreamCA->paBuffers; + uint32_t const cBuffers = pStreamCA->cBuffers; + AssertMsgReturnStmt(cBuffers >= COREAUDIO_MIN_BUFFERS && cBuffers < COREAUDIO_MAX_BUFFERS, ("%u\n", cBuffers), + RTCritSectLeave(&pStreamCA->CritSect), VERR_AUDIO_STREAM_NOT_READY); + + uint32_t idxBuffer = pStreamCA->idxBuffer; + AssertStmt(idxBuffer < cBuffers, idxBuffer %= cBuffers); + + int rc = VINF_SUCCESS; + uint32_t cbWritten = 0; + while (cbBuf > 0) + { + AssertBreakStmt(pStreamCA->hAudioQueue, rc = VERR_AUDIO_STREAM_NOT_READY); + + /* + * Check out how much we can put into the current buffer. + */ + AudioQueueBufferRef const pBuf = paBuffers[idxBuffer].pBuf; + if (!drvHstAudCaIsBufferQueued(pBuf)) + { /* likely */ } + else + { + LogFunc(("@%#RX64: Warning! Out of buffer space! (%#x bytes unwritten)\n", pStreamCA->offInternal, cbBuf)); + /** @todo stats */ + break; + } + + AssertPtrBreakStmt(pBuf, rc = VERR_INTERNAL_ERROR_2); + uint32_t const cbTotal = pBuf->mAudioDataBytesCapacity; + uint32_t cbUsed = pBuf->mAudioDataByteSize; + AssertStmt(cbUsed < cbTotal, cbUsed = cbTotal); + uint32_t const cbAvail = cbTotal - cbUsed; + + /* + * Copy over the data. + */ + if (cbBuf < cbAvail) + { + Log3Func(("@%#RX64: buffer #%u/%u: %#x bytes, have %#x only - leaving unqueued {%s}\n", + pStreamCA->offInternal, idxBuffer, cBuffers, cbAvail, cbBuf, drvHstAudCaStreamStatusString(pStreamCA) )); + memcpy((uint8_t *)pBuf->mAudioData + cbUsed, pvBuf, cbBuf); + pBuf->mAudioDataByteSize = cbUsed + cbBuf; + cbWritten += cbBuf; + pStreamCA->offInternal += cbBuf; + /** @todo Maybe queue it anyway if it's almost full or we haven't got a lot of + * buffers queued. */ + break; + } + + Log3Func(("@%#RX64: buffer #%u/%u: %#x bytes, have %#x - will queue {%s}\n", + pStreamCA->offInternal, idxBuffer, cBuffers, cbAvail, cbBuf, drvHstAudCaStreamStatusString(pStreamCA) )); + memcpy((uint8_t *)pBuf->mAudioData + cbUsed, pvBuf, cbAvail); + pBuf->mAudioDataByteSize = cbTotal; + cbWritten += cbAvail; + pStreamCA->offInternal += cbAvail; + drvHstAudCaSetBufferQueued(pBuf, true /*fQueued*/); + + OSStatus orc = AudioQueueEnqueueBuffer(pStreamCA->hAudioQueue, pBuf, 0 /*inNumPacketDesc*/, NULL /*inPacketDescs*/); + if (orc == noErr) + { /* likely */ } + else + { + LogRelMax(256, ("CoreAudio: AudioQueueEnqueueBuffer('%s', #%u) failed: %#x (%d)\n", + pStreamCA->Cfg.szName, idxBuffer, orc, orc)); + drvHstAudCaSetBufferQueued(pBuf, false /*fQueued*/); + pBuf->mAudioDataByteSize -= PDMAudioPropsFramesToBytes(&pStreamCA->Cfg.Props, 1); /* avoid assertions above */ + rc = VERR_AUDIO_STREAM_NOT_READY; + break; + } + + /* + * Advance. + */ + idxBuffer += 1; + if (idxBuffer < cBuffers) + { /* likely */ } + else + idxBuffer = 0; + pStreamCA->idxBuffer = idxBuffer; + + pvBuf = (const uint8_t *)pvBuf + cbAvail; + cbBuf -= cbAvail; + } + + /* + * Start the stream if we haven't do so yet. + */ + if ( pStreamCA->fStarted + || cbWritten == 0 + || RT_FAILURE_NP(rc)) + { /* likely */ } + else + { + UInt32 cFramesPrepared = 0; +#if 0 /* taking too long? */ + OSStatus orc = AudioQueuePrime(pStreamCA->hAudioQueue, 0 /*inNumberOfFramesToPrepare*/, &cFramesPrepared); + LogFlowFunc(("AudioQueuePrime(%s, 0,) returns %#x (%d) and cFramesPrepared=%u (offInternal=%#RX64)\n", + pStreamCA->Cfg.szName, orc, orc, cFramesPrepared, pStreamCA->offInternal)); + AssertMsg(orc == noErr, ("%#x (%d)\n", orc, orc)); +#else + OSStatus orc; +#endif + orc = AudioQueueStart(pStreamCA->hAudioQueue, NULL /*inStartTime*/); + LogFunc(("AudioQueueStart(%s, NULL) returns %#x (%d)\n", pStreamCA->Cfg.szName, orc, orc)); + if (orc == noErr) + pStreamCA->fStarted = true; + else + { + LogRelMax(128, ("CoreAudio: Starting '%s' failed: %#x (%d) - %u frames primed, %#x bytes queued\n", + pStreamCA->Cfg.szName, orc, orc, cFramesPrepared, pStreamCA->offInternal)); + rc = VERR_AUDIO_STREAM_NOT_READY; + } + } + + /* + * Done. + */ +#ifdef LOG_ENABLED + uint64_t const msPrev = pStreamCA->msLastTransfer; +#endif + uint64_t const msNow = RTTimeMilliTS(); + if (cbWritten) + pStreamCA->msLastTransfer = msNow; + + RTCritSectLeave(&pStreamCA->CritSect); + + *pcbWritten = cbWritten; + if (RT_SUCCESS(rc) || !cbWritten) + { } + else + { + LogFlowFunc(("Suppressing %Rrc to report %#x bytes written\n", rc, cbWritten)); + rc = VINF_SUCCESS; + } + LogFlowFunc(("@%#RX64: rc=%Rrc cbWritten=%RU32 cMsDelta=%RU64 (%RU64 -> %RU64) {%s}\n", pStreamCA->offInternal, rc, cbWritten, + msPrev ? msNow - msPrev : 0, msPrev, pStreamCA->msLastTransfer, drvHstAudCaStreamStatusString(pStreamCA) )); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamCapture} + */ +static DECLCALLBACK(int) drvHstAudCaHA_StreamCapture(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + void *pvBuf, uint32_t cbBuf, uint32_t *pcbRead) +{ + RT_NOREF(pInterface); + PCOREAUDIOSTREAM pStreamCA = (PCOREAUDIOSTREAM)pStream; + AssertPtrReturn(pStreamCA, 0); + AssertPtrReturn(pvBuf, VERR_INVALID_POINTER); + AssertReturn(cbBuf, VERR_INVALID_PARAMETER); + AssertPtrReturn(pcbRead, VERR_INVALID_POINTER); + Assert(PDMAudioPropsIsSizeAligned(&pStreamCA->Cfg.Props, cbBuf)); + AssertReturnStmt(pStreamCA->enmInitState == COREAUDIOINITSTATE_INIT, *pcbRead = 0, VERR_AUDIO_STREAM_NOT_READY); + + RTCritSectEnter(&pStreamCA->CritSect); + if (pStreamCA->fEnabled) + { /* likely */ } + else + { + RTCritSectLeave(&pStreamCA->CritSect); + *pcbRead = 0; + LogFunc(("Skipping %#x byte read from disabled stream {%s}\n", cbBuf, drvHstAudCaStreamStatusString(pStreamCA))); + return VINF_SUCCESS; + } + Log4Func(("cbBuf=%#x stream '%s' {%s}\n", cbBuf, pStreamCA->Cfg.szName, drvHstAudCaStreamStatusString(pStreamCA) )); + + + /* + * Transfer loop. + */ + uint32_t const cbFrame = PDMAudioPropsFrameSize(&pStreamCA->Cfg.Props); + PCOREAUDIOBUF const paBuffers = pStreamCA->paBuffers; + uint32_t const cBuffers = pStreamCA->cBuffers; + AssertMsgReturnStmt(cBuffers >= COREAUDIO_MIN_BUFFERS && cBuffers < COREAUDIO_MAX_BUFFERS, ("%u\n", cBuffers), + RTCritSectLeave(&pStreamCA->CritSect), VERR_AUDIO_STREAM_NOT_READY); + + uint32_t idxBuffer = pStreamCA->idxBuffer; + AssertStmt(idxBuffer < cBuffers, idxBuffer %= cBuffers); + + int rc = VINF_SUCCESS; + uint32_t cbRead = 0; + while (cbBuf > cbFrame) + { + AssertBreakStmt(pStreamCA->hAudioQueue, rc = VERR_AUDIO_STREAM_NOT_READY); + + /* + * Check out how much we can read from the current buffer (if anything at all). + */ + AudioQueueBufferRef const pBuf = paBuffers[idxBuffer].pBuf; + if (!drvHstAudCaIsBufferQueued(pBuf)) + { /* likely */ } + else + { + LogFunc(("@%#RX64: Warning! Underrun! (%#x bytes unread)\n", pStreamCA->offInternal, cbBuf)); + /** @todo stats */ + break; + } + + AssertPtrBreakStmt(pBuf, rc = VERR_INTERNAL_ERROR_2); + uint32_t const cbTotal = pBuf->mAudioDataBytesCapacity; + uint32_t cbValid = pBuf->mAudioDataByteSize; + AssertStmt(cbValid < cbTotal, cbValid = cbTotal); + uint32_t offRead = paBuffers[idxBuffer].offRead; + uint32_t const cbLeft = cbValid - offRead; + + /* + * Copy over the data. + */ + if (cbBuf < cbLeft) + { + Log3Func(("@%#RX64: buffer #%u/%u: %#x bytes, want %#x - leaving unqueued {%s}\n", + pStreamCA->offInternal, idxBuffer, cBuffers, cbLeft, cbBuf, drvHstAudCaStreamStatusString(pStreamCA) )); + memcpy(pvBuf, (uint8_t const *)pBuf->mAudioData + offRead, cbBuf); + paBuffers[idxBuffer].offRead = offRead + cbBuf; + cbRead += cbBuf; + pStreamCA->offInternal += cbBuf; + break; + } + + Log3Func(("@%#RX64: buffer #%u/%u: %#x bytes, want all (%#x) - will queue {%s}\n", + pStreamCA->offInternal, idxBuffer, cBuffers, cbLeft, cbBuf, drvHstAudCaStreamStatusString(pStreamCA) )); + memcpy(pvBuf, (uint8_t const *)pBuf->mAudioData + offRead, cbLeft); + cbRead += cbLeft; + pStreamCA->offInternal += cbLeft; + + RT_BZERO(pBuf->mAudioData, cbTotal); /* paranoia */ + paBuffers[idxBuffer].offRead = 0; + pBuf->mAudioDataByteSize = 0; + drvHstAudCaSetBufferQueued(pBuf, true /*fQueued*/); + + OSStatus orc = AudioQueueEnqueueBuffer(pStreamCA->hAudioQueue, pBuf, 0 /*inNumPacketDesc*/, NULL /*inPacketDescs*/); + if (orc == noErr) + { /* likely */ } + else + { + LogRelMax(256, ("CoreAudio: AudioQueueEnqueueBuffer('%s', #%u) failed: %#x (%d)\n", + pStreamCA->Cfg.szName, idxBuffer, orc, orc)); + drvHstAudCaSetBufferQueued(pBuf, false /*fQueued*/); + rc = VERR_AUDIO_STREAM_NOT_READY; + break; + } + + /* + * Advance. + */ + idxBuffer += 1; + if (idxBuffer < cBuffers) + { /* likely */ } + else + idxBuffer = 0; + pStreamCA->idxBuffer = idxBuffer; + + pvBuf = (uint8_t *)pvBuf + cbLeft; + cbBuf -= cbLeft; + } + + /* + * Done. + */ +#ifdef LOG_ENABLED + uint64_t const msPrev = pStreamCA->msLastTransfer; +#endif + uint64_t const msNow = RTTimeMilliTS(); + if (cbRead) + pStreamCA->msLastTransfer = msNow; + + RTCritSectLeave(&pStreamCA->CritSect); + + *pcbRead = cbRead; + if (RT_SUCCESS(rc) || !cbRead) + { } + else + { + LogFlowFunc(("Suppressing %Rrc to report %#x bytes read\n", rc, cbRead)); + rc = VINF_SUCCESS; + } + LogFlowFunc(("@%#RX64: rc=%Rrc cbRead=%RU32 cMsDelta=%RU64 (%RU64 -> %RU64) {%s}\n", pStreamCA->offInternal, rc, cbRead, + msPrev ? msNow - msPrev : 0, msPrev, pStreamCA->msLastTransfer, drvHstAudCaStreamStatusString(pStreamCA) )); + return rc; +} + + +/********************************************************************************************************************************* +* PDMIBASE * +*********************************************************************************************************************************/ + +/** + * @interface_method_impl{PDMIBASE,pfnQueryInterface} + */ +static DECLCALLBACK(void *) drvHstAudCaQueryInterface(PPDMIBASE pInterface, const char *pszIID) +{ + PPDMDRVINS pDrvIns = PDMIBASE_2_PDMDRV(pInterface); + PDRVHOSTCOREAUDIO pThis = PDMINS_2_DATA(pDrvIns, PDRVHOSTCOREAUDIO); + + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIBASE, &pDrvIns->IBase); + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIHOSTAUDIO, &pThis->IHostAudio); + + return NULL; +} + + +/********************************************************************************************************************************* +* PDMDRVREG * +*********************************************************************************************************************************/ + +/** + * Worker for the power off and destructor callbacks. + */ +static void drvHstAudCaRemoveDefaultDeviceListners(PDRVHOSTCOREAUDIO pThis) +{ + /* + * Unregister system callbacks. + */ + AudioObjectPropertyAddress PropAddr = + { + kAudioHardwarePropertyDefaultInputDevice, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + + OSStatus orc; + if (pThis->fRegisteredDefaultInputListener) + { + orc = AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &PropAddr, + drvHstAudCaDefaultDeviceChangedCallback, pThis); + if ( orc != noErr + && orc != kAudioHardwareBadObjectError) + LogRel(("CoreAudio: Failed to remove the default input device changed listener: %d (%#x))\n", orc, orc)); + pThis->fRegisteredDefaultInputListener = false; + } + + if (pThis->fRegisteredDefaultOutputListener) + { + PropAddr.mSelector = kAudioHardwarePropertyDefaultOutputDevice; + orc = AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &PropAddr, + drvHstAudCaDefaultDeviceChangedCallback, pThis); + if ( orc != noErr + && orc != kAudioHardwareBadObjectError) + LogRel(("CoreAudio: Failed to remove the default output device changed listener: %d (%#x))\n", orc, orc)); + pThis->fRegisteredDefaultOutputListener = false; + } + + /* + * Unregister device callbacks. + */ + RTCritSectEnter(&pThis->CritSect); + + drvHstAudCaDeviceUnregisterCallbacks(pThis, pThis->InputDevice.idDevice); + pThis->InputDevice.idDevice = kAudioDeviceUnknown; + + drvHstAudCaDeviceUnregisterCallbacks(pThis, pThis->OutputDevice.idDevice); + pThis->OutputDevice.idDevice = kAudioDeviceUnknown; + + RTCritSectLeave(&pThis->CritSect); + + LogFlowFuncEnter(); +} + + +/** + * @interface_method_impl{PDMDRVREG,pfnPowerOff} + */ +static DECLCALLBACK(void) drvHstAudCaPowerOff(PPDMDRVINS pDrvIns) +{ + PDRVHOSTCOREAUDIO pThis = PDMINS_2_DATA(pDrvIns, PDRVHOSTCOREAUDIO); + drvHstAudCaRemoveDefaultDeviceListners(pThis); +} + + +/** + * @callback_method_impl{FNPDMDRVDESTRUCT} + */ +static DECLCALLBACK(void) drvHstAudCaDestruct(PPDMDRVINS pDrvIns) +{ + PDMDRV_CHECK_VERSIONS_RETURN_VOID(pDrvIns); + PDRVHOSTCOREAUDIO pThis = PDMINS_2_DATA(pDrvIns, PDRVHOSTCOREAUDIO); + + if (RTCritSectIsInitialized(&pThis->CritSect)) + drvHstAudCaRemoveDefaultDeviceListners(pThis); + +#ifdef CORE_AUDIO_WITH_WORKER_THREAD + if (pThis->hThread != NIL_RTTHREAD) + { + for (unsigned iLoop = 0; iLoop < 60; iLoop++) + { + if (pThis->hThreadRunLoop) + CFRunLoopStop(pThis->hThreadRunLoop); + if (iLoop > 10) + RTThreadPoke(pThis->hThread); + int rc = RTThreadWait(pThis->hThread, 500 /*ms*/, NULL /*prcThread*/); + if (RT_SUCCESS(rc)) + break; + AssertMsgBreak(rc == VERR_TIMEOUT, ("RTThreadWait -> %Rrc\n",rc)); + } + pThis->hThread = NIL_RTTHREAD; + } + if (pThis->hThreadPortSrc) + { + CFRelease(pThis->hThreadPortSrc); + pThis->hThreadPortSrc = NULL; + } + if (pThis->hThreadPort) + { + CFMachPortInvalidate(pThis->hThreadPort); + CFRelease(pThis->hThreadPort); + pThis->hThreadPort = NULL; + } + if (pThis->hThreadRunLoop) + { + CFRelease(pThis->hThreadRunLoop); + pThis->hThreadRunLoop = NULL; + } +#endif + +#ifdef CORE_AUDIO_WITH_BREAKPOINT_TIMER + if (pThis->hBreakpointTimer != NIL_RTTIMERLR) + { + RTTimerLRDestroy(pThis->hBreakpointTimer); + pThis->hBreakpointTimer = NIL_RTTIMERLR; + } +#endif + + if (RTCritSectIsInitialized(&pThis->CritSect)) + { + int rc2 = RTCritSectDelete(&pThis->CritSect); + AssertRC(rc2); + } + + LogFlowFuncLeave(); +} + + +/** + * @callback_method_impl{FNPDMDRVCONSTRUCT, + * Construct a Core Audio driver instance.} + */ +static DECLCALLBACK(int) drvHstAudCaConstruct(PPDMDRVINS pDrvIns, PCFGMNODE pCfg, uint32_t fFlags) +{ + RT_NOREF(pCfg, fFlags); + PDMDRV_CHECK_VERSIONS_RETURN(pDrvIns); + PDRVHOSTCOREAUDIO pThis = PDMINS_2_DATA(pDrvIns, PDRVHOSTCOREAUDIO); + PCPDMDRVHLPR3 pHlp = pDrvIns->pHlpR3; + LogRel(("Audio: Initializing Core Audio driver\n")); + + /* + * Init the static parts. + */ + pThis->pDrvIns = pDrvIns; +#ifdef CORE_AUDIO_WITH_WORKER_THREAD + pThis->hThread = NIL_RTTHREAD; +#endif +#ifdef CORE_AUDIO_WITH_BREAKPOINT_TIMER + pThis->hBreakpointTimer = NIL_RTTIMERLR; +#endif + /* IBase */ + pDrvIns->IBase.pfnQueryInterface = drvHstAudCaQueryInterface; + /* IHostAudio */ + pThis->IHostAudio.pfnGetConfig = drvHstAudCaHA_GetConfig; + pThis->IHostAudio.pfnGetDevices = drvHstAudCaHA_GetDevices; + pThis->IHostAudio.pfnSetDevice = drvHstAudCaHA_SetDevice; + pThis->IHostAudio.pfnGetStatus = drvHstAudCaHA_GetStatus; + pThis->IHostAudio.pfnDoOnWorkerThread = NULL; + pThis->IHostAudio.pfnStreamConfigHint = NULL; + pThis->IHostAudio.pfnStreamCreate = drvHstAudCaHA_StreamCreate; + pThis->IHostAudio.pfnStreamInitAsync = NULL; + pThis->IHostAudio.pfnStreamDestroy = drvHstAudCaHA_StreamDestroy; + pThis->IHostAudio.pfnStreamNotifyDeviceChanged = NULL; + pThis->IHostAudio.pfnStreamEnable = drvHstAudCaHA_StreamEnable; + pThis->IHostAudio.pfnStreamDisable = drvHstAudCaHA_StreamDisable; + pThis->IHostAudio.pfnStreamPause = drvHstAudCaHA_StreamPause; + pThis->IHostAudio.pfnStreamResume = drvHstAudCaHA_StreamResume; + pThis->IHostAudio.pfnStreamDrain = drvHstAudCaHA_StreamDrain; + pThis->IHostAudio.pfnStreamGetReadable = drvHstAudCaHA_StreamGetReadable; + pThis->IHostAudio.pfnStreamGetWritable = drvHstAudCaHA_StreamGetWritable; + pThis->IHostAudio.pfnStreamGetPending = NULL; + pThis->IHostAudio.pfnStreamGetState = drvHstAudCaHA_StreamGetState; + pThis->IHostAudio.pfnStreamPlay = drvHstAudCaHA_StreamPlay; + pThis->IHostAudio.pfnStreamCapture = drvHstAudCaHA_StreamCapture; + + int rc = RTCritSectInit(&pThis->CritSect); + AssertRCReturn(rc, rc); + + /* + * Validate and read configuration. + */ + PDMDRV_VALIDATE_CONFIG_RETURN(pDrvIns, "InputDeviceID|OutputDeviceID", ""); + + char *pszTmp = NULL; + rc = pHlp->pfnCFGMQueryStringAlloc(pCfg, "InputDeviceID", &pszTmp); + if (RT_SUCCESS(rc)) + { + rc = drvHstAudCaSetDevice(pThis, &pThis->InputDevice, true /*fInput*/, false /*fNotify*/, pszTmp); + PDMDrvHlpMMHeapFree(pDrvIns, pszTmp); + } + else if (rc != VERR_CFGM_VALUE_NOT_FOUND && rc != VERR_CFGM_NO_PARENT) + return PDMDRV_SET_ERROR(pDrvIns, rc, "Failed to query 'InputDeviceID'"); + + rc = pHlp->pfnCFGMQueryStringAlloc(pCfg, "OutputDeviceID", &pszTmp); + if (RT_SUCCESS(rc)) + { + rc = drvHstAudCaSetDevice(pThis, &pThis->OutputDevice, false /*fInput*/, false /*fNotify*/, pszTmp); + PDMDrvHlpMMHeapFree(pDrvIns, pszTmp); + } + else if (rc != VERR_CFGM_VALUE_NOT_FOUND && rc != VERR_CFGM_NO_PARENT) + return PDMDRV_SET_ERROR(pDrvIns, rc, "Failed to query 'OutputDeviceID'"); + + /* + * 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); + +#ifdef CORE_AUDIO_WITH_WORKER_THREAD + /* + * Create worker thread for running callbacks on. + */ + CFMachPortContext PortCtx; + PortCtx.version = 0; + PortCtx.info = pThis; + PortCtx.retain = NULL; + PortCtx.release = NULL; + PortCtx.copyDescription = NULL; + pThis->hThreadPort = CFMachPortCreate(NULL /*allocator*/, drvHstAudCaThreadPortCallback, &PortCtx, NULL); + AssertLogRelReturn(pThis->hThreadPort != NULL, VERR_NO_MEMORY); + + pThis->hThreadPortSrc = CFMachPortCreateRunLoopSource(NULL, pThis->hThreadPort, 0 /*order*/); + AssertLogRelReturn(pThis->hThreadPortSrc != NULL, VERR_NO_MEMORY); + + rc = RTThreadCreateF(&pThis->hThread, drvHstAudCaThread, pThis, 0, RTTHREADTYPE_IO, + RTTHREADFLAGS_WAITABLE, "CaAud-%u", pDrvIns->iInstance); + AssertLogRelMsgReturn(RT_SUCCESS(rc), ("RTThreadCreateF failed: %Rrc\n", rc), rc); + + RTThreadUserWait(pThis->hThread, RT_MS_10SEC); + AssertLogRel(pThis->hThreadRunLoop); +#endif + +#ifdef CORE_AUDIO_WITH_BREAKPOINT_TIMER + /* + * Create a IPRT timer. The TM timers won't necessarily work as EMT is probably busy. + */ + rc = RTTimerLRCreateEx(&pThis->hBreakpointTimer, 0 /*no interval*/, 0, drvHstAudCaBreakpointTimer, pThis); + AssertRCReturn(rc, rc); +#endif + + /* + * Determin the default devices. + */ + drvHstAudCaUpdateOneDefaultDevice(pThis, &pThis->OutputDevice, false /*fInput*/, false /*fNotifty*/); + drvHstAudCaUpdateOneDefaultDevice(pThis, &pThis->InputDevice, true /*fInput*/, false /*fNotifty*/); + + /* + * Register callbacks for default device input and output changes. + * (We just ignore failures here as there isn't much we can do about it, + * and it isn't 100% critical.) + */ + AudioObjectPropertyAddress PropAddr = + { + /* .mSelector = */ kAudioHardwarePropertyDefaultInputDevice, + /* .mScope = */ kAudioObjectPropertyScopeGlobal, + /* .mElement = */ kAudioObjectPropertyElementMaster + }; + + OSStatus orc; + orc = AudioObjectAddPropertyListener(kAudioObjectSystemObject, &PropAddr, drvHstAudCaDefaultDeviceChangedCallback, pThis); + pThis->fRegisteredDefaultInputListener = orc == noErr; + if ( orc != noErr + && orc != kAudioHardwareIllegalOperationError) + LogRel(("CoreAudio: Failed to add the input default device changed listener: %d (%#x)\n", orc, orc)); + + PropAddr.mSelector = kAudioHardwarePropertyDefaultOutputDevice; + orc = AudioObjectAddPropertyListener(kAudioObjectSystemObject, &PropAddr, drvHstAudCaDefaultDeviceChangedCallback, pThis); + pThis->fRegisteredDefaultOutputListener = orc == noErr; + if ( orc != noErr + && orc != kAudioHardwareIllegalOperationError) + LogRel(("CoreAudio: Failed to add the output default device changed listener: %d (%#x)\n", orc, orc)); + + LogFlowFuncLeaveRC(rc); + return rc; +} + + +/** + * Char driver registration record. + */ +const PDMDRVREG g_DrvHostCoreAudio = +{ + /* u32Version */ + PDM_DRVREG_VERSION, + /* szName */ + "CoreAudio", + /* szRCMod */ + "", + /* szR0Mod */ + "", + /* pszDescription */ + "Core Audio host driver", + /* fFlags */ + PDM_DRVREG_FLAGS_HOST_BITS_DEFAULT, + /* fClass. */ + PDM_DRVREG_CLASS_AUDIO, + /* cMaxInstances */ + ~0U, + /* cbInstance */ + sizeof(DRVHOSTCOREAUDIO), + /* pfnConstruct */ + drvHstAudCaConstruct, + /* pfnDestruct */ + drvHstAudCaDestruct, + /* pfnRelocate */ + NULL, + /* pfnIOCtl */ + NULL, + /* pfnPowerOn */ + NULL, + /* pfnReset */ + NULL, + /* pfnSuspend */ + NULL, + /* pfnResume */ + NULL, + /* pfnAttach */ + NULL, + /* pfnDetach */ + NULL, + /* pfnPowerOff */ + drvHstAudCaPowerOff, + /* pfnSoftReset */ + NULL, + /* u32EndVersion */ + PDM_DRVREG_VERSION +}; diff --git a/src/VBox/Devices/Audio/DrvHostAudioCoreAudioAuth.mm b/src/VBox/Devices/Audio/DrvHostAudioCoreAudioAuth.mm new file mode 100644 index 00000000..adc8889d --- /dev/null +++ b/src/VBox/Devices/Audio/DrvHostAudioCoreAudioAuth.mm @@ -0,0 +1,150 @@ +/* $Id: DrvHostAudioCoreAudioAuth.mm $ */ +/** @file + * Host audio driver - Mac OS X CoreAudio, authorization helpers for Mojave+. + */ + +/* + * Copyright (C) 2020-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 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_DRV_HOST_AUDIO +#include <VBox/log.h> + +#include <iprt/errcore.h> +#include <iprt/semaphore.h> + +#if MAC_OS_X_VERSION_MIN_REQUIRED >= 1070 +# import <AVFoundation/AVFoundation.h> +# import <AVFoundation/AVMediaFormat.h> +#endif +#import <Foundation/NSException.h> + + +#if MAC_OS_X_VERSION_MIN_REQUIRED < 101400 + +/* HACK ALERT! It's there in the 10.13 SDK, but only for iOS 7.0+. Deploying CPP trickery to shut up warnings/errors. */ +# if MAC_OS_X_VERSION_MIN_REQUIRED >= 101300 +# define AVAuthorizationStatus OurAVAuthorizationStatus +# define AVAuthorizationStatusNotDetermined OurAVAuthorizationStatusNotDetermined +# define AVAuthorizationStatusRestricted OurAVAuthorizationStatusRestricted +# define AVAuthorizationStatusDenied OurAVAuthorizationStatusDenied +# define AVAuthorizationStatusAuthorized OurAVAuthorizationStatusAuthorized +# endif + +/** + * The authorization status enum. + * + * Starting macOS 10.14 we need to request permissions in order to use any audio input device + * but as we build against an older SDK where this is not available we have to duplicate + * AVAuthorizationStatus and do everything dynmically during runtime, sigh... + */ +typedef enum AVAuthorizationStatus +# if RT_CPLUSPLUS_PREREQ(201100) + : NSInteger +# endif +{ + AVAuthorizationStatusNotDetermined = 0, + AVAuthorizationStatusRestricted = 1, + AVAuthorizationStatusDenied = 2, + AVAuthorizationStatusAuthorized = 3 +} AVAuthorizationStatus; + +#endif + + +#if MAC_OS_X_VERSION_MIN_REQUIRED >= 1070 /** @todo need some better fix/whatever for AudioTest */ + +/** + * Requests camera permissions for Mojave and onwards. + * + * @returns VBox status code. + */ +static int coreAudioInputPermissionRequest(void) +{ + __block RTSEMEVENT hEvt = NIL_RTSEMEVENT; + __block int rc = RTSemEventCreate(&hEvt); + if (RT_SUCCESS(rc)) + { + /* Perform auth request. */ + [AVCaptureDevice performSelector: @selector(requestAccessForMediaType: completionHandler:) + withObject: (id)AVMediaTypeAudio + withObject: (id)^(BOOL granted) + { + if (!granted) + { + LogRel(("CoreAudio: Access denied!\n")); + rc = VERR_ACCESS_DENIED; + } + RTSemEventSignal(hEvt); + }]; + + rc = RTSemEventWait(hEvt, RT_MS_10SEC); + RTSemEventDestroy(hEvt); + } + + return rc; +} + +#endif + +/** + * Checks permission for capturing devices on Mojave and onwards. + * + * @returns VBox status code. + */ +DECLHIDDEN(int) coreAudioInputPermissionCheck(void) +{ + int rc = VINF_SUCCESS; + + if (NSFoundationVersionNumber >= 10.14) + { + /* + * Because we build with an older SDK where the authorization APIs are not available + * (introduced with Mojave 10.14) we have to resort to resolving the APIs dynamically. + */ +#if MAC_OS_X_VERSION_MIN_REQUIRED >= 1070 /** @todo need some better fix/whatever for AudioTest */ + LogRel(("CoreAudio: macOS 10.14+ detected, checking audio input permissions\n")); + + if ([AVCaptureDevice respondsToSelector:@selector(authorizationStatusForMediaType:)]) + { + AVAuthorizationStatus enmAuthSts + = (AVAuthorizationStatus)(NSInteger)[AVCaptureDevice performSelector: @selector(authorizationStatusForMediaType:) + withObject: (id)AVMediaTypeAudio]; + if (enmAuthSts == AVAuthorizationStatusNotDetermined) + rc = coreAudioInputPermissionRequest(); + else if ( enmAuthSts == AVAuthorizationStatusRestricted + || enmAuthSts == AVAuthorizationStatusDenied) + { + LogRel(("CoreAudio: Access denied!\n")); + rc = VERR_ACCESS_DENIED; + } + } +#else + LogRel(("CoreAudio: WARNING! macOS 10.14+ detected. Audio input probably wont work as this app was compiled using a too old SDK.\n")); +#endif + } + + return rc; +} diff --git a/src/VBox/Devices/Audio/DrvHostAudioDSound.cpp b/src/VBox/Devices/Audio/DrvHostAudioDSound.cpp new file mode 100644 index 00000000..10a81ad2 --- /dev/null +++ b/src/VBox/Devices/Audio/DrvHostAudioDSound.cpp @@ -0,0 +1,2881 @@ +/* $Id: DrvHostAudioDSound.cpp $ */ +/** @file + * Host audio driver - DirectSound (Windows). + */ + +/* + * Copyright (C) 2006-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 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_DRV_HOST_AUDIO +#define INITGUID +#include <VBox/log.h> +#include <iprt/win/windows.h> +#include <dsound.h> +#include <mmdeviceapi.h> +#include <functiondiscoverykeys_devpkey.h> +#include <iprt/win/mmreg.h> /* WAVEFORMATEXTENSIBLE */ + +#include <iprt/alloc.h> +#include <iprt/system.h> +#include <iprt/uuid.h> +#include <iprt/utf16.h> + +#include <VBox/vmm/pdmaudioinline.h> +#include <VBox/vmm/pdmaudiohostenuminline.h> + +#include "VBoxDD.h" + +#ifdef VBOX_WITH_AUDIO_MMNOTIFICATION_CLIENT +# include <new> /* For bad_alloc. */ +# include "DrvHostAudioDSoundMMNotifClient.h" +#endif + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +/* + * Optional release logging, which a user can turn on with the + * 'VBoxManage debugvm' command. + * Debug logging still uses the common Log* macros from VBox. + * Messages which always should go to the release log use LogRel. + * + * @deprecated Use LogRelMax, LogRel2 and LogRel3 directly. + */ +/** General code behavior. */ +#define DSLOG(a) do { LogRel2(a); } while(0) +/** Something which produce a lot of logging during playback/recording. */ +#define DSLOGF(a) do { LogRel3(a); } while(0) +/** Important messages like errors. Limited in the default release log to avoid log flood. */ +#define DSLOGREL(a) \ + do { \ + static int8_t s_cLogged = 0; \ + if (s_cLogged < 8) { \ + ++s_cLogged; \ + LogRel(a); \ + } else DSLOG(a); \ + } while (0) + +/** Maximum number of attempts to restore the sound buffer before giving up. */ +#define DRV_DSOUND_RESTORE_ATTEMPTS_MAX 3 +#if 0 /** @todo r=bird: What are these for? Nobody is using them... */ +/** Default input latency (in ms). */ +#define DRV_DSOUND_DEFAULT_LATENCY_MS_IN 50 +/** Default output latency (in ms). */ +#define DRV_DSOUND_DEFAULT_LATENCY_MS_OUT 50 +#endif + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +/* Dynamically load dsound.dll. */ +typedef HRESULT WINAPI FNDIRECTSOUNDENUMERATEW(LPDSENUMCALLBACKW pDSEnumCallback, PVOID pContext); +typedef FNDIRECTSOUNDENUMERATEW *PFNDIRECTSOUNDENUMERATEW; +typedef HRESULT WINAPI FNDIRECTSOUNDCAPTUREENUMERATEW(LPDSENUMCALLBACKW pDSEnumCallback, PVOID pContext); +typedef FNDIRECTSOUNDCAPTUREENUMERATEW *PFNDIRECTSOUNDCAPTUREENUMERATEW; +typedef HRESULT WINAPI FNDIRECTSOUNDCAPTURECREATE8(LPCGUID lpcGUID, LPDIRECTSOUNDCAPTURE8 *lplpDSC, LPUNKNOWN pUnkOuter); +typedef FNDIRECTSOUNDCAPTURECREATE8 *PFNDIRECTSOUNDCAPTURECREATE8; + +#define VBOX_DSOUND_MAX_EVENTS 3 + +typedef enum DSOUNDEVENT +{ + DSOUNDEVENT_NOTIFY = 0, + DSOUNDEVENT_INPUT, + DSOUNDEVENT_OUTPUT, +} DSOUNDEVENT; + +typedef struct DSOUNDHOSTCFG +{ + RTUUID uuidPlay; + LPCGUID pGuidPlay; + RTUUID uuidCapture; + LPCGUID pGuidCapture; +} DSOUNDHOSTCFG, *PDSOUNDHOSTCFG; + +typedef struct DSOUNDSTREAM +{ + /** Common part. */ + PDMAUDIOBACKENDSTREAM Core; + /** Entry in DRVHOSTDSOUND::HeadStreams. */ + RTLISTNODE ListEntry; + /** The stream's acquired configuration. */ + PDMAUDIOSTREAMCFG Cfg; + /** Buffer alignment. */ + uint8_t uAlign; + /** Whether this stream is in an enable state on the DirectSound side. */ + bool fEnabled; + bool afPadding[2]; + /** Size (in bytes) of the DirectSound buffer. */ + DWORD cbBufSize; + union + { + struct + { + /** The actual DirectSound Buffer (DSB) used for the capturing. + * This is a secondary buffer and is used as a streaming buffer. */ + LPDIRECTSOUNDCAPTUREBUFFER8 pDSCB; + /** Current read offset (in bytes) within the DSB. */ + DWORD offReadPos; + /** Number of buffer overruns happened. Used for logging. */ + uint8_t cOverruns; + } In; + struct + { + /** The actual DirectSound Buffer (DSB) used for playback. + * This is a secondary buffer and is used as a streaming buffer. */ + LPDIRECTSOUNDBUFFER8 pDSB; + /** Current write offset (in bytes) within the DSB. + * @note This is needed as the current write position as kept by direct sound + * will move ahead if we're too late. */ + DWORD offWritePos; + /** Offset of last play cursor within the DSB when checked for pending. */ + DWORD offPlayCursorLastPending; + /** Offset of last play cursor within the DSB when last played. */ + DWORD offPlayCursorLastPlayed; + /** Total amount (in bytes) written to our internal ring buffer. */ + uint64_t cbWritten; + /** Total amount (in bytes) played (to the DirectSound buffer). */ + uint64_t cbTransferred; + /** Flag indicating whether playback was just (re)started. */ + bool fFirstTransfer; + /** Flag indicating whether this stream is in draining mode, e.g. no new + * data is being written to it but DirectSound still needs to be able to + * play its remaining (buffered) data. */ + bool fDrain; + /** How much (in bytes) the last transfer from the internal buffer + * to the DirectSound buffer was. */ + uint32_t cbLastTransferred; + /** The RTTimeMilliTS() deadline for the draining of this stream. */ + uint64_t msDrainDeadline; + } Out; + }; + /** Timestamp (in ms) of the last transfer from the internal buffer to/from the + * DirectSound buffer. */ + uint64_t msLastTransfer; + /** The stream's critical section for synchronizing access. */ + RTCRITSECT CritSect; + /** Used for formatting the current DSound status. */ + char szStatus[127]; + /** Fixed zero terminator. */ + char const chStateZero; +} DSOUNDSTREAM, *PDSOUNDSTREAM; + +/** + * DirectSound-specific device entry. + */ +typedef struct DSOUNDDEV +{ + PDMAUDIOHOSTDEV Core; + /** The GUID if handy. */ + GUID Guid; + /** The GUID as a string (empty if default). */ + char szGuid[RTUUID_STR_LENGTH]; +} DSOUNDDEV; +/** Pointer to a DirectSound device entry. */ +typedef DSOUNDDEV *PDSOUNDDEV; + +/** + * Structure for holding a device enumeration context. + */ +typedef struct DSOUNDENUMCBCTX +{ + /** Enumeration flags. */ + uint32_t fFlags; + /** Pointer to device list to populate. */ + PPDMAUDIOHOSTENUM pDevEnm; +} DSOUNDENUMCBCTX, *PDSOUNDENUMCBCTX; + +typedef struct DRVHOSTDSOUND +{ + /** Pointer to the driver instance structure. */ + PPDMDRVINS pDrvIns; + /** Our audio host audio interface. */ + PDMIHOSTAUDIO IHostAudio; + /** Critical section to serialize access. */ + RTCRITSECT CritSect; + /** DirectSound configuration options. */ + DSOUNDHOSTCFG Cfg; + /** List of devices of last enumeration. */ + PDMAUDIOHOSTENUM DeviceEnum; + /** Whether this backend supports any audio input. + * @todo r=bird: This is not actually used for anything. */ + bool fEnabledIn; + /** Whether this backend supports any audio output. + * @todo r=bird: This is not actually used for anything. */ + bool fEnabledOut; + /** The Direct Sound playback interface. */ + LPDIRECTSOUND8 pDS; + /** The Direct Sound capturing interface. */ + LPDIRECTSOUNDCAPTURE8 pDSC; + /** List of streams (DSOUNDSTREAM). + * Requires CritSect ownership. */ + RTLISTANCHOR HeadStreams; + +#ifdef VBOX_WITH_AUDIO_MMNOTIFICATION_CLIENT + DrvHostAudioDSoundMMNotifClient *m_pNotificationClient; +#endif +} DRVHOSTDSOUND, *PDRVHOSTDSOUND; + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ +static HRESULT directSoundPlayRestore(PDRVHOSTDSOUND pThis, LPDIRECTSOUNDBUFFER8 pDSB); +static int drvHostDSoundStreamStopPlayback(PDRVHOSTDSOUND pThis, PDSOUNDSTREAM pStreamDS, bool fReset); + +static int dsoundDevicesEnumerate(PDRVHOSTDSOUND pThis, PDMAUDIOHOSTENUM pDevEnm, uint32_t fEnum); + +static void dsoundUpdateStatusInternal(PDRVHOSTDSOUND pThis); + + +#if defined(LOG_ENABLED) || defined(RTLOG_REL_ENABLED) +/** + * Gets the stream status as a string for logging purposes. + * + * @returns Status string (pStreamDS->szStatus). + * @param pStreamDS The stream to get the status for. + */ +static const char *drvHostDSoundStreamStatusString(PDSOUNDSTREAM pStreamDS) +{ + /* + * Out internal stream status first. + */ + size_t off; + if (pStreamDS->fEnabled) + { + memcpy(pStreamDS->szStatus, RT_STR_TUPLE("ENABLED ")); + off = sizeof("ENABLED ") - 1; + } + else + { + memcpy(pStreamDS->szStatus, RT_STR_TUPLE("DISABLED")); + off = sizeof("DISABLED") - 1; + } + + /* + * Direction specific stuff, returning with a status DWORD and string mappings for it. + */ + typedef struct DRVHOSTDSOUNDSFLAGS2STR + { + const char *pszMnemonic; + uint32_t cchMnemonic; + uint32_t fFlag; + } DRVHOSTDSOUNDSFLAGS2STR; + static const DRVHOSTDSOUNDSFLAGS2STR s_aCaptureFlags[] = + { + { RT_STR_TUPLE(" CAPTURING"), DSCBSTATUS_CAPTURING }, + { RT_STR_TUPLE(" LOOPING"), DSCBSTATUS_LOOPING }, + }; + static const DRVHOSTDSOUNDSFLAGS2STR s_aPlaybackFlags[] = + { + { RT_STR_TUPLE(" PLAYING"), DSBSTATUS_PLAYING }, + { RT_STR_TUPLE(" BUFFERLOST"), DSBSTATUS_BUFFERLOST }, + { RT_STR_TUPLE(" LOOPING"), DSBSTATUS_LOOPING }, + { RT_STR_TUPLE(" LOCHARDWARE"), DSBSTATUS_LOCHARDWARE }, + { RT_STR_TUPLE(" LOCSOFTWARE"), DSBSTATUS_LOCSOFTWARE }, + { RT_STR_TUPLE(" TERMINATED"), DSBSTATUS_TERMINATED }, + }; + DRVHOSTDSOUNDSFLAGS2STR const *paMappings = NULL; + size_t cMappings = 0; + DWORD fStatus = 0; + if (pStreamDS->Cfg.enmDir == PDMAUDIODIR_IN) + { + if (pStreamDS->In.pDSCB) + { + HRESULT hrc = pStreamDS->In.pDSCB->GetStatus(&fStatus); + if (SUCCEEDED(hrc)) + { + paMappings = s_aCaptureFlags; + cMappings = RT_ELEMENTS(s_aCaptureFlags); + } + else + RTStrPrintf(&pStreamDS->szStatus[off], sizeof(pStreamDS->szStatus) - off, "GetStatus->%Rhrc", hrc); + } + else + RTStrCopy(&pStreamDS->szStatus[off], sizeof(pStreamDS->szStatus) - off, "NO-DSCB"); + } + else if (pStreamDS->Cfg.enmDir == PDMAUDIODIR_OUT) + { + if (pStreamDS->Out.fDrain) + { + memcpy(&pStreamDS->szStatus[off], RT_STR_TUPLE(" DRAINING")); + off += sizeof(" DRAINING") - 1; + } + + if (pStreamDS->Out.fFirstTransfer) + { + memcpy(&pStreamDS->szStatus[off], RT_STR_TUPLE(" NOXFER")); + off += sizeof(" NOXFER") - 1; + } + + if (pStreamDS->Out.pDSB) + { + HRESULT hrc = pStreamDS->Out.pDSB->GetStatus(&fStatus); + if (SUCCEEDED(hrc)) + { + paMappings = s_aPlaybackFlags; + cMappings = RT_ELEMENTS(s_aPlaybackFlags); + } + else + RTStrPrintf(&pStreamDS->szStatus[off], sizeof(pStreamDS->szStatus) - off, "GetStatus->%Rhrc", hrc); + } + else + RTStrCopy(&pStreamDS->szStatus[off], sizeof(pStreamDS->szStatus) - off, "NO-DSB"); + } + else + RTStrCopy(&pStreamDS->szStatus[off], sizeof(pStreamDS->szStatus) - off, "BAD-DIR"); + + /* Format flags. */ + if (paMappings) + { + if (fStatus == 0) + RTStrCopy(&pStreamDS->szStatus[off], sizeof(pStreamDS->szStatus) - off, " 0"); + else + { + for (size_t i = 0; i < cMappings; i++) + if (fStatus & paMappings[i].fFlag) + { + memcpy(&pStreamDS->szStatus[off], paMappings[i].pszMnemonic, paMappings[i].cchMnemonic); + off += paMappings[i].cchMnemonic; + + fStatus &= ~paMappings[i].fFlag; + if (!fStatus) + break; + } + if (fStatus != 0) + off += RTStrPrintf(&pStreamDS->szStatus[off], sizeof(pStreamDS->szStatus) - off, " %#x", fStatus); + } + } + + /* + * Finally, terminate the string. By postponing it this long, it won't be + * a big deal if two threads go thru here at the same time as long as the + * status is the same. + */ + Assert(off < sizeof(pStreamDS->szStatus)); + pStreamDS->szStatus[off] = '\0'; + + return pStreamDS->szStatus; +} +#endif /* LOG_ENABLED || RTLOG_REL_ENABLED */ + + +static DWORD dsoundRingDistance(DWORD offEnd, DWORD offBegin, DWORD cSize) +{ + AssertReturn(offEnd <= cSize, 0); + AssertReturn(offBegin <= cSize, 0); + + return offEnd >= offBegin ? offEnd - offBegin : cSize - offBegin + offEnd; +} + + +static char *dsoundGUIDToUtf8StrA(LPCGUID pGUID) +{ + if (pGUID) + { + LPOLESTR lpOLEStr; + HRESULT hr = StringFromCLSID(*pGUID, &lpOLEStr); + if (SUCCEEDED(hr)) + { + char *pszGUID; + int rc = RTUtf16ToUtf8(lpOLEStr, &pszGUID); + CoTaskMemFree(lpOLEStr); + + return RT_SUCCESS(rc) ? pszGUID : NULL; + } + } + + return RTStrDup("{Default device}"); +} + + +static HRESULT directSoundPlayRestore(PDRVHOSTDSOUND pThis, LPDIRECTSOUNDBUFFER8 pDSB) +{ + RT_NOREF(pThis); + HRESULT hr = IDirectSoundBuffer8_Restore(pDSB); + if (FAILED(hr)) + DSLOG(("DSound: Restoring playback buffer\n")); + else + DSLOGREL(("DSound: Restoring playback buffer failed with %Rhrc\n", hr)); + + return hr; +} + + +static HRESULT directSoundPlayUnlock(PDRVHOSTDSOUND pThis, LPDIRECTSOUNDBUFFER8 pDSB, + PVOID pv1, PVOID pv2, + DWORD cb1, DWORD cb2) +{ + RT_NOREF(pThis); + HRESULT hr = IDirectSoundBuffer8_Unlock(pDSB, pv1, cb1, pv2, cb2); + if (FAILED(hr)) + DSLOGREL(("DSound: Unlocking playback buffer failed with %Rhrc\n", hr)); + return hr; +} + + +static HRESULT directSoundPlayLock(PDRVHOSTDSOUND pThis, PDSOUNDSTREAM pStreamDS, + DWORD dwOffset, DWORD dwBytes, + PVOID *ppv1, PVOID *ppv2, + DWORD *pcb1, DWORD *pcb2, + DWORD dwFlags) +{ + AssertReturn(dwBytes, VERR_INVALID_PARAMETER); + + HRESULT hr = E_FAIL; + AssertCompile(DRV_DSOUND_RESTORE_ATTEMPTS_MAX > 0); + for (unsigned i = 0; i < DRV_DSOUND_RESTORE_ATTEMPTS_MAX; i++) + { + PVOID pv1, pv2; + DWORD cb1, cb2; + hr = IDirectSoundBuffer8_Lock(pStreamDS->Out.pDSB, dwOffset, dwBytes, &pv1, &cb1, &pv2, &cb2, dwFlags); + if (SUCCEEDED(hr)) + { + if ( (!pv1 || !(cb1 & pStreamDS->uAlign)) + && (!pv2 || !(cb2 & pStreamDS->uAlign))) + { + if (ppv1) + *ppv1 = pv1; + if (ppv2) + *ppv2 = pv2; + if (pcb1) + *pcb1 = cb1; + if (pcb2) + *pcb2 = cb2; + return S_OK; + } + DSLOGREL(("DSound: Locking playback buffer returned misaligned buffer: cb1=%#RX32, cb2=%#RX32 (alignment: %#RX32)\n", + *pcb1, *pcb2, pStreamDS->uAlign)); + directSoundPlayUnlock(pThis, pStreamDS->Out.pDSB, pv1, pv2, cb1, cb2); + return E_FAIL; + } + + if (hr != DSERR_BUFFERLOST) + break; + + LogFlowFunc(("Locking failed due to lost buffer, restoring ...\n")); + directSoundPlayRestore(pThis, pStreamDS->Out.pDSB); + } + + DSLOGREL(("DSound: Locking playback buffer failed with %Rhrc (dwOff=%ld, dwBytes=%ld)\n", hr, dwOffset, dwBytes)); + return hr; +} + + +static HRESULT directSoundCaptureUnlock(LPDIRECTSOUNDCAPTUREBUFFER8 pDSCB, + PVOID pv1, PVOID pv2, + DWORD cb1, DWORD cb2) +{ + HRESULT hr = IDirectSoundCaptureBuffer8_Unlock(pDSCB, pv1, cb1, pv2, cb2); + if (FAILED(hr)) + DSLOGREL(("DSound: Unlocking capture buffer failed with %Rhrc\n", hr)); + return hr; +} + + +static HRESULT directSoundCaptureLock(PDSOUNDSTREAM pStreamDS, + DWORD dwOffset, DWORD dwBytes, + PVOID *ppv1, PVOID *ppv2, + DWORD *pcb1, DWORD *pcb2, + DWORD dwFlags) +{ + PVOID pv1 = NULL; + PVOID pv2 = NULL; + DWORD cb1 = 0; + DWORD cb2 = 0; + + HRESULT hr = IDirectSoundCaptureBuffer8_Lock(pStreamDS->In.pDSCB, dwOffset, dwBytes, + &pv1, &cb1, &pv2, &cb2, dwFlags); + if (FAILED(hr)) + { + DSLOGREL(("DSound: Locking capture buffer failed with %Rhrc\n", hr)); + return hr; + } + + if ( (pv1 && (cb1 & pStreamDS->uAlign)) + || (pv2 && (cb2 & pStreamDS->uAlign))) + { + DSLOGREL(("DSound: Locking capture buffer returned misaligned buffer: cb1=%RI32, cb2=%RI32 (alignment: %RU32)\n", + cb1, cb2, pStreamDS->uAlign)); + directSoundCaptureUnlock(pStreamDS->In.pDSCB, pv1, pv2, cb1, cb2); + return E_FAIL; + } + + *ppv1 = pv1; + *ppv2 = pv2; + *pcb1 = cb1; + *pcb2 = cb2; + + return S_OK; +} + + +/* + * DirectSound playback + */ + +/** + * Creates a DirectSound playback instance. + * + * @return HRESULT + * @param pGUID GUID of device to create the playback interface for. NULL + * for the default device. + * @param ppDS Where to return the interface to the created instance. + */ +static HRESULT drvHostDSoundCreateDSPlaybackInstance(LPCGUID pGUID, LPDIRECTSOUND8 *ppDS) +{ + LogFlowFuncEnter(); + + LPDIRECTSOUND8 pDS = NULL; + HRESULT hrc = CoCreateInstance(CLSID_DirectSound8, NULL, CLSCTX_ALL, IID_IDirectSound8, (void **)&pDS); + if (SUCCEEDED(hrc)) + { + hrc = IDirectSound8_Initialize(pDS, pGUID); + if (SUCCEEDED(hrc)) + { + HWND hWnd = GetDesktopWindow(); + hrc = IDirectSound8_SetCooperativeLevel(pDS, hWnd, DSSCL_PRIORITY); + if (SUCCEEDED(hrc)) + { + *ppDS = pDS; + LogFlowFunc(("LEAVE S_OK\n")); + return S_OK; + } + LogRelMax(64, ("DSound: Setting cooperative level for (hWnd=%p) failed: %Rhrc\n", hWnd, hrc)); + } + else if (hrc == DSERR_NODRIVER) /* Usually means that no playback devices are attached. */ + LogRelMax(64, ("DSound: DirectSound playback is currently unavailable\n")); + else + LogRelMax(64, ("DSound: DirectSound playback initialization failed: %Rhrc\n", hrc)); + + IDirectSound8_Release(pDS); + } + else + LogRelMax(64, ("DSound: Creating playback instance failed: %Rhrc\n", hrc)); + + LogFlowFunc(("LEAVE %Rhrc\n", hrc)); + return hrc; +} + + +#if 0 /* not used */ +static HRESULT directSoundPlayGetStatus(PDRVHOSTDSOUND pThis, LPDIRECTSOUNDBUFFER8 pDSB, DWORD *pdwStatus) +{ + AssertPtrReturn(pThis, E_POINTER); + AssertPtrReturn(pDSB, E_POINTER); + + AssertPtrNull(pdwStatus); + + DWORD dwStatus = 0; + HRESULT hr = E_FAIL; + for (unsigned i = 0; i < DRV_DSOUND_RESTORE_ATTEMPTS_MAX; i++) + { + hr = IDirectSoundBuffer8_GetStatus(pDSB, &dwStatus); + if ( hr == DSERR_BUFFERLOST + || ( SUCCEEDED(hr) + && (dwStatus & DSBSTATUS_BUFFERLOST))) + { + LogFlowFunc(("Getting status failed due to lost buffer, restoring ...\n")); + directSoundPlayRestore(pThis, pDSB); + } + else + break; + } + + if (SUCCEEDED(hr)) + { + if (pdwStatus) + *pdwStatus = dwStatus; + } + else + DSLOGREL(("DSound: Retrieving playback status failed with %Rhrc\n", hr)); + + return hr; +} +#endif + + +/* + * DirectSoundCapture + */ + +#if 0 /* unused */ +static LPCGUID dsoundCaptureSelectDevice(PDRVHOSTDSOUND pThis, PPDMAUDIOSTREAMCFG pCfg) +{ + AssertPtrReturn(pThis, NULL); + AssertPtrReturn(pCfg, NULL); + + int rc = VINF_SUCCESS; + + LPCGUID pGUID = pThis->Cfg.pGuidCapture; + if (!pGUID) + { + PDSOUNDDEV pDev = NULL; + switch (pCfg->enmPath) + { + case PDMAUDIOPATH_IN_LINE: + /* + * At the moment we're only supporting line-in in the HDA emulation, + * and line-in + mic-in in the AC'97 emulation both are expected + * to use the host's mic-in as well. + * + * So the fall through here is intentional for now. + */ + case PDMAUDIOPATH_IN_MIC: + pDev = (PDSOUNDDEV)DrvAudioHlpDeviceEnumGetDefaultDevice(&pThis->DeviceEnum, PDMAUDIODIR_IN); + break; + + default: + AssertFailedStmt(rc = VERR_NOT_SUPPORTED); + break; + } + + if ( RT_SUCCESS(rc) + && pDev) + { + DSLOG(("DSound: Guest source '%s' is using host recording device '%s'\n", + PDMAudioPathGetName(pCfg->enmPath), pDev->Core.szName)); + pGUID = &pDev->Guid; + } + if (RT_FAILURE(rc)) + { + LogRel(("DSound: Selecting recording device failed with %Rrc\n", rc)); + return NULL; + } + } + + /* This always has to be in the release log. */ + char *pszGUID = dsoundGUIDToUtf8StrA(pGUID); + LogRel(("DSound: Guest source '%s' is using host recording device with GUID '%s'\n", + PDMAudioPathGetName(pCfg->enmPath), pszGUID ? pszGUID: "{?}")); + RTStrFree(pszGUID); + + return pGUID; +} +#endif + + +/** + * Creates a DirectSound capture instance. + * + * @returns HRESULT + * @param pGUID GUID of device to create the capture interface for. NULL + * for default. + * @param ppDSC Where to return the interface to the created instance. + */ +static HRESULT drvHostDSoundCreateDSCaptureInstance(LPCGUID pGUID, LPDIRECTSOUNDCAPTURE8 *ppDSC) +{ + LogFlowFuncEnter(); + + LPDIRECTSOUNDCAPTURE8 pDSC = NULL; + HRESULT hrc = CoCreateInstance(CLSID_DirectSoundCapture8, NULL, CLSCTX_ALL, IID_IDirectSoundCapture8, (void **)&pDSC); + if (SUCCEEDED(hrc)) + { + hrc = IDirectSoundCapture_Initialize(pDSC, pGUID); + if (SUCCEEDED(hrc)) + { + *ppDSC = pDSC; + LogFlowFunc(("LEAVE S_OK\n")); + return S_OK; + } + if (hrc == DSERR_NODRIVER) /* Usually means that no capture devices are attached. */ + LogRelMax(64, ("DSound: Capture device currently is unavailable\n")); + else + LogRelMax(64, ("DSound: Initializing capturing device failed: %Rhrc\n", hrc)); + IDirectSoundCapture_Release(pDSC); + } + else + LogRelMax(64, ("DSound: Creating capture instance failed: %Rhrc\n", hrc)); + + LogFlowFunc(("LEAVE %Rhrc\n", hrc)); + return hrc; +} + + +/** + * Updates this host driver's internal status, according to the global, overall input/output + * state and all connected (native) audio streams. + * + * @todo r=bird: This is a 'ing waste of 'ing time! We're doing this everytime + * an 'ing stream is created and we doesn't 'ing use the information here + * for any darn thing! Given the reported slowness of enumeration and + * issues with the 'ing code the only appropriate response is: + * AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARG!!!!!!! + * + * @param pThis Host audio driver instance. + */ +static void dsoundUpdateStatusInternal(PDRVHOSTDSOUND pThis) +{ +#if 0 /** @todo r=bird: This isn't doing *ANYTHING* useful. So, I've just disabled it. */ + AssertPtrReturnVoid(pThis); + LogFlowFuncEnter(); + + PDMAudioHostEnumDelete(&pThis->DeviceEnum); + int rc = dsoundDevicesEnumerate(pThis, &pThis->DeviceEnum); + if (RT_SUCCESS(rc)) + { +#if 0 + if ( pThis->fEnabledOut != RT_BOOL(cbCtx.cDevOut) + || pThis->fEnabledIn != RT_BOOL(cbCtx.cDevIn)) + { + /** @todo Use a registered callback to the audio connector (e.g "OnConfigurationChanged") to + * let the connector know that something has changed within the host backend. */ + } +#endif + pThis->fEnabledIn = PDMAudioHostEnumCountMatching(&pThis->DeviceEnum, PDMAUDIODIR_IN) != 0; + pThis->fEnabledOut = PDMAudioHostEnumCountMatching(&pThis->DeviceEnum, PDMAUDIODIR_OUT) != 0; + } + + LogFlowFuncLeaveRC(rc); +#else + RT_NOREF(pThis); +#endif +} + + +/********************************************************************************************************************************* +* PDMIHOSTAUDIO * +*********************************************************************************************************************************/ + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnGetConfig} + */ +static DECLCALLBACK(int) drvHostDSoundHA_GetConfig(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDCFG pBackendCfg) +{ + RT_NOREF(pInterface); + AssertPtrReturn(pInterface, VERR_INVALID_POINTER); + AssertPtrReturn(pBackendCfg, VERR_INVALID_POINTER); + + + /* + * Fill in the config structure. + */ + RTStrCopy(pBackendCfg->szName, sizeof(pBackendCfg->szName), "DirectSound"); + pBackendCfg->cbStream = sizeof(DSOUNDSTREAM); + pBackendCfg->fFlags = 0; + pBackendCfg->cMaxStreamsIn = UINT32_MAX; + pBackendCfg->cMaxStreamsOut = UINT32_MAX; + + return VINF_SUCCESS; +} + + +/** + * Callback for the playback device enumeration. + * + * @return TRUE if continuing enumeration, FALSE if not. + * @param pGUID Pointer to GUID of enumerated device. Can be NULL. + * @param pwszDescription Pointer to (friendly) description of enumerated device. + * @param pwszModule Pointer to module name of enumerated device. + * @param lpContext Pointer to PDSOUNDENUMCBCTX context for storing the enumerated information. + * + * @note Carbon copy of drvHostDSoundEnumOldStyleCaptureCallback with OUT direction. + */ +static BOOL CALLBACK drvHostDSoundEnumOldStylePlaybackCallback(LPGUID pGUID, LPCWSTR pwszDescription, + LPCWSTR pwszModule, PVOID lpContext) +{ + PDSOUNDENUMCBCTX pEnumCtx = (PDSOUNDENUMCBCTX)lpContext; + AssertPtrReturn(pEnumCtx, FALSE); + + PPDMAUDIOHOSTENUM pDevEnm = pEnumCtx->pDevEnm; + AssertPtrReturn(pDevEnm, FALSE); + + AssertPtrNullReturn(pGUID, FALSE); /* pGUID can be NULL for default device(s). */ + AssertPtrReturn(pwszDescription, FALSE); + RT_NOREF(pwszModule); /* Do not care about pwszModule. */ + + int rc; + size_t const cbName = RTUtf16CalcUtf8Len(pwszDescription) + 1; + PDSOUNDDEV pDev = (PDSOUNDDEV)PDMAudioHostDevAlloc(sizeof(DSOUNDDEV), cbName, 0); + if (pDev) + { + pDev->Core.enmUsage = PDMAUDIODIR_OUT; + pDev->Core.enmType = PDMAUDIODEVICETYPE_BUILTIN; + + if (pGUID == NULL) + pDev->Core.fFlags = PDMAUDIOHOSTDEV_F_DEFAULT_OUT; + + rc = RTUtf16ToUtf8Ex(pwszDescription, RTSTR_MAX, &pDev->Core.pszName, cbName, NULL); + if (RT_SUCCESS(rc)) + { + if (!pGUID) + pDev->Core.fFlags |= PDMAUDIOHOSTDEV_F_DEFAULT_OUT; + else + { + memcpy(&pDev->Guid, pGUID, sizeof(pDev->Guid)); + rc = RTUuidToStr((PCRTUUID)pGUID, pDev->szGuid, sizeof(pDev->szGuid)); + AssertRC(rc); + } + pDev->Core.pszId = &pDev->szGuid[0]; + + PDMAudioHostEnumAppend(pDevEnm, &pDev->Core); + + /* Note: Querying the actual device information will be done at some + * later point in time outside this enumeration callback to prevent + * DSound hangs. */ + return TRUE; + } + PDMAudioHostDevFree(&pDev->Core); + } + else + rc = VERR_NO_MEMORY; + + LogRel(("DSound: Error enumeration playback device '%ls': rc=%Rrc\n", pwszDescription, rc)); + return FALSE; /* Abort enumeration. */ +} + + +/** + * Callback for the capture device enumeration. + * + * @return TRUE if continuing enumeration, FALSE if not. + * @param pGUID Pointer to GUID of enumerated device. Can be NULL. + * @param pwszDescription Pointer to (friendly) description of enumerated device. + * @param pwszModule Pointer to module name of enumerated device. + * @param lpContext Pointer to PDSOUNDENUMCBCTX context for storing the enumerated information. + * + * @note Carbon copy of drvHostDSoundEnumOldStylePlaybackCallback with IN direction. + */ +static BOOL CALLBACK drvHostDSoundEnumOldStyleCaptureCallback(LPGUID pGUID, LPCWSTR pwszDescription, + LPCWSTR pwszModule, PVOID lpContext) +{ + PDSOUNDENUMCBCTX pEnumCtx = (PDSOUNDENUMCBCTX )lpContext; + AssertPtrReturn(pEnumCtx, FALSE); + + PPDMAUDIOHOSTENUM pDevEnm = pEnumCtx->pDevEnm; + AssertPtrReturn(pDevEnm, FALSE); + + AssertPtrNullReturn(pGUID, FALSE); /* pGUID can be NULL for default device(s). */ + AssertPtrReturn(pwszDescription, FALSE); + RT_NOREF(pwszModule); /* Do not care about pwszModule. */ + + int rc; + size_t const cbName = RTUtf16CalcUtf8Len(pwszDescription) + 1; + PDSOUNDDEV pDev = (PDSOUNDDEV)PDMAudioHostDevAlloc(sizeof(DSOUNDDEV), cbName, 0); + if (pDev) + { + pDev->Core.enmUsage = PDMAUDIODIR_IN; + pDev->Core.enmType = PDMAUDIODEVICETYPE_BUILTIN; + + rc = RTUtf16ToUtf8Ex(pwszDescription, RTSTR_MAX, &pDev->Core.pszName, cbName, NULL); + if (RT_SUCCESS(rc)) + { + if (!pGUID) + pDev->Core.fFlags |= PDMAUDIOHOSTDEV_F_DEFAULT_IN; + else + { + memcpy(&pDev->Guid, pGUID, sizeof(pDev->Guid)); + rc = RTUuidToStr((PCRTUUID)pGUID, pDev->szGuid, sizeof(pDev->szGuid)); + AssertRC(rc); + } + pDev->Core.pszId = &pDev->szGuid[0]; + + PDMAudioHostEnumAppend(pDevEnm, &pDev->Core); + + /* Note: Querying the actual device information will be done at some + * later point in time outside this enumeration callback to prevent + * DSound hangs. */ + return TRUE; + } + PDMAudioHostDevFree(&pDev->Core); + } + else + rc = VERR_NO_MEMORY; + + LogRel(("DSound: Error enumeration capture device '%ls', rc=%Rrc\n", pwszDescription, rc)); + return FALSE; /* Abort enumeration. */ +} + + +/** + * Queries information for a given (DirectSound) device. + * + * @returns VBox status code. + * @param pDev Audio device to query information for. + */ +static int drvHostDSoundEnumOldStyleQueryDeviceInfo(PDSOUNDDEV pDev) +{ + AssertPtr(pDev); + int rc; + + if (pDev->Core.enmUsage == PDMAUDIODIR_OUT) + { + LPDIRECTSOUND8 pDS; + HRESULT hr = drvHostDSoundCreateDSPlaybackInstance(&pDev->Guid, &pDS); + if (SUCCEEDED(hr)) + { + DSCAPS DSCaps; + RT_ZERO(DSCaps); + DSCaps.dwSize = sizeof(DSCAPS); + hr = IDirectSound_GetCaps(pDS, &DSCaps); + if (SUCCEEDED(hr)) + { + pDev->Core.cMaxOutputChannels = DSCaps.dwFlags & DSCAPS_PRIMARYSTEREO ? 2 : 1; + + DWORD dwSpeakerCfg; + hr = IDirectSound_GetSpeakerConfig(pDS, &dwSpeakerCfg); + if (SUCCEEDED(hr)) + { + unsigned uSpeakerCount = 0; + switch (DSSPEAKER_CONFIG(dwSpeakerCfg)) + { + case DSSPEAKER_MONO: uSpeakerCount = 1; break; + case DSSPEAKER_HEADPHONE: uSpeakerCount = 2; break; + case DSSPEAKER_STEREO: uSpeakerCount = 2; break; + case DSSPEAKER_QUAD: uSpeakerCount = 4; break; + case DSSPEAKER_SURROUND: uSpeakerCount = 4; break; + case DSSPEAKER_5POINT1: uSpeakerCount = 6; break; + case DSSPEAKER_5POINT1_SURROUND: uSpeakerCount = 6; break; + case DSSPEAKER_7POINT1: uSpeakerCount = 8; break; + case DSSPEAKER_7POINT1_SURROUND: uSpeakerCount = 8; break; + default: break; + } + + if (uSpeakerCount) /* Do we need to update the channel count? */ + pDev->Core.cMaxOutputChannels = uSpeakerCount; + + rc = VINF_SUCCESS; + } + else + { + LogRel(("DSound: Error retrieving playback device speaker config, hr=%Rhrc\n", hr)); + rc = VERR_ACCESS_DENIED; /** @todo Fudge! */ + } + } + else + { + LogRel(("DSound: Error retrieving playback device capabilities, hr=%Rhrc\n", hr)); + rc = VERR_ACCESS_DENIED; /** @todo Fudge! */ + } + + IDirectSound8_Release(pDS); + } + else + rc = VERR_GENERAL_FAILURE; + } + else if (pDev->Core.enmUsage == PDMAUDIODIR_IN) + { + LPDIRECTSOUNDCAPTURE8 pDSC; + HRESULT hr = drvHostDSoundCreateDSCaptureInstance(&pDev->Guid, &pDSC); + if (SUCCEEDED(hr)) + { + DSCCAPS DSCCaps; + RT_ZERO(DSCCaps); + DSCCaps.dwSize = sizeof(DSCCAPS); + hr = IDirectSoundCapture_GetCaps(pDSC, &DSCCaps); + if (SUCCEEDED(hr)) + { + pDev->Core.cMaxInputChannels = DSCCaps.dwChannels; + rc = VINF_SUCCESS; + } + else + { + LogRel(("DSound: Error retrieving capture device capabilities, hr=%Rhrc\n", hr)); + rc = VERR_ACCESS_DENIED; /** @todo Fudge! */ + } + + IDirectSoundCapture_Release(pDSC); + } + else + rc = VERR_GENERAL_FAILURE; + } + else + AssertFailedStmt(rc = VERR_NOT_SUPPORTED); + + return rc; +} + + +/** + * Queries information for @a pDevice and adds an entry to the enumeration. + * + * @returns VBox status code. + * @param pDevEnm The enumeration to add the device to. + * @param pDevice The device. + * @param enmType The type of device. + * @param fDefault Whether it's the default device. + */ +static int drvHostDSoundEnumNewStyleAdd(PPDMAUDIOHOSTENUM pDevEnm, IMMDevice *pDevice, EDataFlow enmType, bool fDefault) +{ + int rc = VINF_SUCCESS; /* ignore most errors */ + + /* + * Gather the necessary properties. + */ + IPropertyStore *pProperties = NULL; + HRESULT hrc = pDevice->OpenPropertyStore(STGM_READ, &pProperties); + if (SUCCEEDED(hrc)) + { + /* Get the friendly name. */ + PROPVARIANT VarName; + PropVariantInit(&VarName); + hrc = pProperties->GetValue(PKEY_Device_FriendlyName, &VarName); + if (SUCCEEDED(hrc)) + { + /* Get the DirectSound GUID. */ + PROPVARIANT VarGUID; + PropVariantInit(&VarGUID); + hrc = pProperties->GetValue(PKEY_AudioEndpoint_GUID, &VarGUID); + if (SUCCEEDED(hrc)) + { + /* Get the device format. */ + PROPVARIANT VarFormat; + PropVariantInit(&VarFormat); + hrc = pProperties->GetValue(PKEY_AudioEngine_DeviceFormat, &VarFormat); + if (SUCCEEDED(hrc)) + { + WAVEFORMATEX const * const pFormat = (WAVEFORMATEX const *)VarFormat.blob.pBlobData; + AssertPtr(pFormat); + + /* + * Create a enumeration entry for it. + */ + size_t const cbName = RTUtf16CalcUtf8Len(VarName.pwszVal) + 1; + PDSOUNDDEV pDev = (PDSOUNDDEV)PDMAudioHostDevAlloc(sizeof(DSOUNDDEV), cbName, 0); + if (pDev) + { + pDev->Core.enmUsage = enmType == eRender ? PDMAUDIODIR_OUT : PDMAUDIODIR_IN; + pDev->Core.enmType = PDMAUDIODEVICETYPE_BUILTIN; + if (fDefault) + pDev->Core.fFlags |= enmType == eRender + ? PDMAUDIOHOSTDEV_F_DEFAULT_OUT : PDMAUDIOHOSTDEV_F_DEFAULT_IN; + if (enmType == eRender) + pDev->Core.cMaxOutputChannels = pFormat->nChannels; + else + pDev->Core.cMaxInputChannels = pFormat->nChannels; + + //if (fDefault) + rc = RTUuidFromUtf16((PRTUUID)&pDev->Guid, VarGUID.pwszVal); + if (RT_SUCCESS(rc)) + { + rc = RTUuidToStr((PCRTUUID)&pDev->Guid, pDev->szGuid, sizeof(pDev->szGuid)); + AssertRC(rc); + pDev->Core.pszId = &pDev->szGuid[0]; + + rc = RTUtf16ToUtf8Ex(VarName.pwszVal, RTSTR_MAX, &pDev->Core.pszName, cbName, NULL); + if (RT_SUCCESS(rc)) + PDMAudioHostEnumAppend(pDevEnm, &pDev->Core); + else + PDMAudioHostDevFree(&pDev->Core); + } + else + { + LogFunc(("RTUuidFromUtf16(%ls): %Rrc\n", VarGUID.pwszVal, rc)); + PDMAudioHostDevFree(&pDev->Core); + } + } + else + rc = VERR_NO_MEMORY; + PropVariantClear(&VarFormat); + } + else + LogFunc(("Failed to get PKEY_AudioEngine_DeviceFormat: %Rhrc\n", hrc)); + PropVariantClear(&VarGUID); + } + else + LogFunc(("Failed to get PKEY_AudioEndpoint_GUID: %Rhrc\n", hrc)); + PropVariantClear(&VarName); + } + else + LogFunc(("Failed to get PKEY_Device_FriendlyName: %Rhrc\n", hrc)); + pProperties->Release(); + } + else + LogFunc(("OpenPropertyStore failed: %Rhrc\n", hrc)); + + if (hrc == E_OUTOFMEMORY && RT_SUCCESS_NP(rc)) + rc = VERR_NO_MEMORY; + return rc; +} + + +/** + * Does a (Re-)enumeration of the host's playback + capturing devices. + * + * @return VBox status code. + * @param pDevEnm Where to store the enumerated devices. + */ +static int drvHostDSoundEnumerateDevices(PPDMAUDIOHOSTENUM pDevEnm) +{ + DSLOG(("DSound: Enumerating devices ...\n")); + + /* + * Use the Vista+ API. + */ + IMMDeviceEnumerator *pEnumerator; + HRESULT hrc = CoCreateInstance(__uuidof(MMDeviceEnumerator), 0, CLSCTX_ALL, + __uuidof(IMMDeviceEnumerator), (void **)&pEnumerator); + if (SUCCEEDED(hrc)) + { + int rc = VINF_SUCCESS; + for (unsigned idxPass = 0; idxPass < 2 && RT_SUCCESS(rc); idxPass++) + { + EDataFlow const enmType = idxPass == 0 ? EDataFlow::eRender : EDataFlow::eCapture; + + /* Get the default device first. */ + IMMDevice *pDefaultDevice = NULL; + hrc = pEnumerator->GetDefaultAudioEndpoint(enmType, eMultimedia, &pDefaultDevice); + if (SUCCEEDED(hrc)) + rc = drvHostDSoundEnumNewStyleAdd(pDevEnm, pDefaultDevice, enmType, true); + else + pDefaultDevice = NULL; + + /* Enumerate the devices. */ + IMMDeviceCollection *pCollection = NULL; + hrc = pEnumerator->EnumAudioEndpoints(enmType, DEVICE_STATE_ACTIVE /*| DEVICE_STATE_UNPLUGGED?*/, &pCollection); + if (SUCCEEDED(hrc) && pCollection != NULL) + { + UINT cDevices = 0; + hrc = pCollection->GetCount(&cDevices); + if (SUCCEEDED(hrc)) + { + for (UINT idxDevice = 0; idxDevice < cDevices && RT_SUCCESS(rc); idxDevice++) + { + IMMDevice *pDevice = NULL; + hrc = pCollection->Item(idxDevice, &pDevice); + if (SUCCEEDED(hrc) && pDevice) + { + if (pDevice != pDefaultDevice) + rc = drvHostDSoundEnumNewStyleAdd(pDevEnm, pDevice, enmType, false); + pDevice->Release(); + } + } + } + pCollection->Release(); + } + else + LogRelMax(10, ("EnumAudioEndpoints(%s) failed: %Rhrc\n", idxPass == 0 ? "output" : "input", hrc)); + + if (pDefaultDevice) + pDefaultDevice->Release(); + } + pEnumerator->Release(); + if (pDevEnm->cDevices > 0 || RT_FAILURE(rc)) + { + DSLOG(("DSound: Enumerating devices done - %u device (%Rrc)\n", pDevEnm->cDevices, rc)); + return rc; + } + } + + /* + * Fall back to dsound. + */ + /* Resolve symbols once. */ + static PFNDIRECTSOUNDENUMERATEW volatile s_pfnDirectSoundEnumerateW = NULL; + static PFNDIRECTSOUNDCAPTUREENUMERATEW volatile s_pfnDirectSoundCaptureEnumerateW = NULL; + + PFNDIRECTSOUNDENUMERATEW pfnDirectSoundEnumerateW = s_pfnDirectSoundEnumerateW; + PFNDIRECTSOUNDCAPTUREENUMERATEW pfnDirectSoundCaptureEnumerateW = s_pfnDirectSoundCaptureEnumerateW; + if (!pfnDirectSoundEnumerateW || !pfnDirectSoundCaptureEnumerateW) + { + RTLDRMOD hModDSound = NIL_RTLDRMOD; + int rc = RTLdrLoadSystem("dsound.dll", true /*fNoUnload*/, &hModDSound); + if (RT_SUCCESS(rc)) + { + rc = RTLdrGetSymbol(hModDSound, "DirectSoundEnumerateW", (void **)&pfnDirectSoundEnumerateW); + if (RT_SUCCESS(rc)) + s_pfnDirectSoundEnumerateW = pfnDirectSoundEnumerateW; + else + LogRel(("DSound: Failed to get dsound.dll export DirectSoundEnumerateW: %Rrc\n", rc)); + + rc = RTLdrGetSymbol(hModDSound, "DirectSoundCaptureEnumerateW", (void **)&pfnDirectSoundCaptureEnumerateW); + if (RT_SUCCESS(rc)) + s_pfnDirectSoundCaptureEnumerateW = pfnDirectSoundCaptureEnumerateW; + else + LogRel(("DSound: Failed to get dsound.dll export DirectSoundCaptureEnumerateW: %Rrc\n", rc)); + RTLdrClose(hModDSound); + } + else + LogRel(("DSound: Unable to load dsound.dll for enumerating devices: %Rrc\n", rc)); + if (!pfnDirectSoundEnumerateW && !pfnDirectSoundCaptureEnumerateW) + return rc; + } + + /* Common callback context for both playback and capture enumerations: */ + DSOUNDENUMCBCTX EnumCtx; + EnumCtx.fFlags = 0; + EnumCtx.pDevEnm = pDevEnm; + + /* Enumerate playback devices. */ + if (pfnDirectSoundEnumerateW) + { + DSLOG(("DSound: Enumerating playback devices ...\n")); + HRESULT hr = pfnDirectSoundEnumerateW(&drvHostDSoundEnumOldStylePlaybackCallback, &EnumCtx); + if (FAILED(hr)) + LogRel(("DSound: Error enumerating host playback devices: %Rhrc\n", hr)); + } + + /* Enumerate capture devices. */ + if (pfnDirectSoundCaptureEnumerateW) + { + DSLOG(("DSound: Enumerating capture devices ...\n")); + HRESULT hr = pfnDirectSoundCaptureEnumerateW(&drvHostDSoundEnumOldStyleCaptureCallback, &EnumCtx); + if (FAILED(hr)) + LogRel(("DSound: Error enumerating host capture devices: %Rhrc\n", hr)); + } + + /* + * Query Information for all enumerated devices. + * Note! This is problematic to do from the enumeration callbacks. + */ + PDSOUNDDEV pDev; + RTListForEach(&pDevEnm->LstDevices, pDev, DSOUNDDEV, Core.ListEntry) + { + drvHostDSoundEnumOldStyleQueryDeviceInfo(pDev); /* ignore rc */ + } + + DSLOG(("DSound: Enumerating devices done\n")); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnGetDevices} + */ +static DECLCALLBACK(int) drvHostDSoundHA_GetDevices(PPDMIHOSTAUDIO pInterface, PPDMAUDIOHOSTENUM pDeviceEnum) +{ + RT_NOREF(pInterface); + AssertPtrReturn(pDeviceEnum, VERR_INVALID_POINTER); + + PDMAudioHostEnumInit(pDeviceEnum); + int rc = drvHostDSoundEnumerateDevices(pDeviceEnum); + if (RT_FAILURE(rc)) + PDMAudioHostEnumDelete(pDeviceEnum); + + LogFlowFunc(("Returning %Rrc\n", rc)); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnGetStatus} + */ +static DECLCALLBACK(PDMAUDIOBACKENDSTS) drvHostDSoundHA_GetStatus(PPDMIHOSTAUDIO pInterface, PDMAUDIODIR enmDir) +{ + RT_NOREF(pInterface, enmDir); + return PDMAUDIOBACKENDSTS_RUNNING; +} + + +/** + * Converts from PDM stream config to windows WAVEFORMATEXTENSIBLE struct. + * + * @param pCfg The PDM audio stream config to convert from. + * @param pFmt The windows structure to initialize. + */ +static void dsoundWaveFmtFromCfg(PCPDMAUDIOSTREAMCFG pCfg, PWAVEFORMATEXTENSIBLE pFmt) +{ + RT_ZERO(*pFmt); + pFmt->Format.wFormatTag = WAVE_FORMAT_PCM; + pFmt->Format.nChannels = PDMAudioPropsChannels(&pCfg->Props); + pFmt->Format.wBitsPerSample = PDMAudioPropsSampleBits(&pCfg->Props); + pFmt->Format.nSamplesPerSec = PDMAudioPropsHz(&pCfg->Props); + pFmt->Format.nBlockAlign = PDMAudioPropsFrameSize(&pCfg->Props); + pFmt->Format.nAvgBytesPerSec = PDMAudioPropsFramesToBytes(&pCfg->Props, PDMAudioPropsHz(&pCfg->Props)); + pFmt->Format.cbSize = 0; /* No extra data specified. */ + + /* + * We need to use the extensible structure if there are more than two channels + * or if the channels have non-standard assignments. + */ + if ( pFmt->Format.nChannels > 2 + || ( pFmt->Format.nChannels == 1 + ? pCfg->Props.aidChannels[0] != PDMAUDIOCHANNELID_MONO + : pCfg->Props.aidChannels[0] != PDMAUDIOCHANNELID_FRONT_LEFT + || pCfg->Props.aidChannels[1] != PDMAUDIOCHANNELID_FRONT_RIGHT)) + { + pFmt->Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; + pFmt->Format.cbSize = sizeof(*pFmt) - sizeof(pFmt->Format); + pFmt->Samples.wValidBitsPerSample = PDMAudioPropsSampleBits(&pCfg->Props); + pFmt->SubFormat = KSDATAFORMAT_SUBTYPE_PCM; + pFmt->dwChannelMask = 0; + unsigned const cSrcChannels = pFmt->Format.nChannels; + for (unsigned i = 0; i < cSrcChannels; i++) + if ( pCfg->Props.aidChannels[i] >= PDMAUDIOCHANNELID_FIRST_STANDARD + && pCfg->Props.aidChannels[i] < PDMAUDIOCHANNELID_END_STANDARD) + pFmt->dwChannelMask |= RT_BIT_32(pCfg->Props.aidChannels[i] - PDMAUDIOCHANNELID_FIRST_STANDARD); + else + pFmt->Format.nChannels -= 1; + } +} + + +/** + * Resets the state of a DirectSound stream, clearing the buffer content. + * + * @param pThis Host audio driver instance. + * @param pStreamDS Stream to reset state for. + */ +static void drvHostDSoundStreamReset(PDRVHOSTDSOUND pThis, PDSOUNDSTREAM pStreamDS) +{ + RT_NOREF(pThis); + LogFunc(("Resetting %s\n", pStreamDS->Cfg.enmDir == PDMAUDIODIR_IN ? "capture" : "playback")); + + if (pStreamDS->Cfg.enmDir == PDMAUDIODIR_IN) + { + /* + * Input streams. + */ + LogFunc(("Resetting capture stream '%s'\n", pStreamDS->Cfg.szName)); + + /* Reset the state: */ + pStreamDS->msLastTransfer = 0; +/** @todo r=bird: We set the read position to zero here, but shouldn't we query it + * from the buffer instead given that there isn't any interface for repositioning + * to the start of the buffer as with playback buffers? */ + pStreamDS->In.offReadPos = 0; + pStreamDS->In.cOverruns = 0; + + /* Clear the buffer content: */ + AssertPtr(pStreamDS->In.pDSCB); + if (pStreamDS->In.pDSCB) + { + PVOID pv1 = NULL; + DWORD cb1 = 0; + PVOID pv2 = NULL; + DWORD cb2 = 0; + HRESULT hrc = IDirectSoundCaptureBuffer8_Lock(pStreamDS->In.pDSCB, 0, pStreamDS->cbBufSize, + &pv1, &cb1, &pv2, &cb2, 0 /*fFlags*/); + if (SUCCEEDED(hrc)) + { + PDMAudioPropsClearBuffer(&pStreamDS->Cfg.Props, pv1, cb1, PDMAUDIOPCMPROPS_B2F(&pStreamDS->Cfg.Props, cb1)); + if (pv2 && cb2) + PDMAudioPropsClearBuffer(&pStreamDS->Cfg.Props, pv2, cb2, PDMAUDIOPCMPROPS_B2F(&pStreamDS->Cfg.Props, cb2)); + hrc = IDirectSoundCaptureBuffer8_Unlock(pStreamDS->In.pDSCB, pv1, cb1, pv2, cb2); + if (FAILED(hrc)) + LogRelMaxFunc(64, ("DSound: Unlocking capture buffer '%s' after reset failed: %Rhrc\n", + pStreamDS->Cfg.szName, hrc)); + } + else + LogRelMaxFunc(64, ("DSound: Locking capture buffer '%s' for reset failed: %Rhrc\n", + pStreamDS->Cfg.szName, hrc)); + } + } + else + { + /* + * Output streams. + */ + Assert(pStreamDS->Cfg.enmDir == PDMAUDIODIR_OUT); + LogFunc(("Resetting playback stream '%s'\n", pStreamDS->Cfg.szName)); + + /* If draining was enagaged, make sure dsound has stopped playing: */ + if (pStreamDS->Out.fDrain && pStreamDS->Out.pDSB) + pStreamDS->Out.pDSB->Stop(); + + /* Reset the internal state: */ + pStreamDS->msLastTransfer = 0; + pStreamDS->Out.fFirstTransfer = true; + pStreamDS->Out.fDrain = false; + pStreamDS->Out.cbLastTransferred = 0; + pStreamDS->Out.cbTransferred = 0; + pStreamDS->Out.cbWritten = 0; + pStreamDS->Out.offWritePos = 0; + pStreamDS->Out.offPlayCursorLastPending = 0; + pStreamDS->Out.offPlayCursorLastPlayed = 0; + + /* Reset the buffer content and repositioning the buffer to the start of the buffer. */ + AssertPtr(pStreamDS->Out.pDSB); + if (pStreamDS->Out.pDSB) + { + HRESULT hrc = IDirectSoundBuffer8_SetCurrentPosition(pStreamDS->Out.pDSB, 0); + if (FAILED(hrc)) + LogRelMaxFunc(64, ("DSound: Failed to set buffer position for '%s': %Rhrc\n", pStreamDS->Cfg.szName, hrc)); + + PVOID pv1 = NULL; + DWORD cb1 = 0; + PVOID pv2 = NULL; + DWORD cb2 = 0; + hrc = IDirectSoundBuffer8_Lock(pStreamDS->Out.pDSB, 0, pStreamDS->cbBufSize, &pv1, &cb1, &pv2, &cb2, 0 /*fFlags*/); + if (hrc == DSERR_BUFFERLOST) + { + directSoundPlayRestore(pThis, pStreamDS->Out.pDSB); + hrc = IDirectSoundBuffer8_Lock(pStreamDS->Out.pDSB, 0, pStreamDS->cbBufSize, &pv1, &cb1, &pv2, &cb2, 0 /*fFlags*/); + } + if (SUCCEEDED(hrc)) + { + PDMAudioPropsClearBuffer(&pStreamDS->Cfg.Props, pv1, cb1, PDMAUDIOPCMPROPS_B2F(&pStreamDS->Cfg.Props, cb1)); + if (pv2 && cb2) + PDMAudioPropsClearBuffer(&pStreamDS->Cfg.Props, pv2, cb2, PDMAUDIOPCMPROPS_B2F(&pStreamDS->Cfg.Props, cb2)); + + hrc = IDirectSoundBuffer8_Unlock(pStreamDS->Out.pDSB, pv1, cb1, pv2, cb2); + if (FAILED(hrc)) + LogRelMaxFunc(64, ("DSound: Unlocking playback buffer '%s' after reset failed: %Rhrc\n", + pStreamDS->Cfg.szName, hrc)); + } + else + LogRelMaxFunc(64, ("DSound: Locking playback buffer '%s' for reset failed: %Rhrc\n", pStreamDS->Cfg.szName, hrc)); + } + } +} + + +/** + * Worker for drvHostDSoundHA_StreamCreate that creates caputre stream. + * + * @returns Windows COM status code. + * @param pThis The DSound instance data. + * @param pStreamDS The stream instance data. + * @param pCfgReq The requested stream config (input). + * @param pCfgAcq Where to return the actual stream config. This is a + * copy of @a *pCfgReq when called. + * @param pWaveFmtExt On input the requested stream format. Updated to the + * actual stream format on successful return. + */ +static HRESULT drvHostDSoundStreamCreateCapture(PDRVHOSTDSOUND pThis, PDSOUNDSTREAM pStreamDS, PCPDMAUDIOSTREAMCFG pCfgReq, + PPDMAUDIOSTREAMCFG pCfgAcq, WAVEFORMATEXTENSIBLE *pWaveFmtExt) +{ + Assert(pStreamDS->In.pDSCB == NULL); + HRESULT hrc; + + /* + * Create, initialize and set up a IDirectSoundCapture instance the first time + * we go thru here. + */ + /** @todo bird: Or should we rather just throw this away after we've gotten the + * capture buffer? Old code would just leak it... */ + if (pThis->pDSC == NULL) + { + hrc = drvHostDSoundCreateDSCaptureInstance(pThis->Cfg.pGuidCapture, &pThis->pDSC); + if (FAILED(hrc)) + return hrc; /* The worker has complained to the release log already. */ + } + + /* + * Create the capture buffer. + */ + DSCBUFFERDESC BufferDesc = + { + /*.dwSize = */ sizeof(BufferDesc), + /*.dwFlags = */ 0, + /*.dwBufferBytes =*/ PDMAudioPropsFramesToBytes(&pCfgReq->Props, pCfgReq->Backend.cFramesBufferSize), + /*.dwReserved = */ 0, + /*.lpwfxFormat = */ &pWaveFmtExt->Format, + /*.dwFXCount = */ 0, + /*.lpDSCFXDesc = */ NULL + }; + + LogRel2(("DSound: Requested capture buffer is %#x B / %u B / %RU64 ms\n", BufferDesc.dwBufferBytes, BufferDesc.dwBufferBytes, + PDMAudioPropsBytesToMilli(&pCfgReq->Props, BufferDesc.dwBufferBytes))); + + LPDIRECTSOUNDCAPTUREBUFFER pLegacyDSCB = NULL; + hrc = IDirectSoundCapture_CreateCaptureBuffer(pThis->pDSC, &BufferDesc, &pLegacyDSCB, NULL); + if (FAILED(hrc)) + { + LogRelMax(64, ("DSound: Creating capture buffer for '%s' failed: %Rhrc\n", pCfgReq->szName, hrc)); + return hrc; + } + + /* Get the IDirectSoundCaptureBuffer8 version of the interface. */ + hrc = IDirectSoundCaptureBuffer_QueryInterface(pLegacyDSCB, IID_IDirectSoundCaptureBuffer8, (void **)&pStreamDS->In.pDSCB); + IDirectSoundCaptureBuffer_Release(pLegacyDSCB); + if (FAILED(hrc)) + { + LogRelMax(64, ("DSound: Querying IID_IDirectSoundCaptureBuffer8 for '%s' failed: %Rhrc\n", pCfgReq->szName, hrc)); + return hrc; + } + + /* + * Query the actual stream configuration. + */ +#if 0 /** @todo r=bird: WTF was this for? */ + DWORD offByteReadPos = 0; + hrc = IDirectSoundCaptureBuffer8_GetCurrentPosition(pStreamDS->In.pDSCB, NULL, &offByteReadPos); + if (FAILED(hrc)) + { + offByteReadPos = 0; + DSLOGREL(("DSound: Getting capture position failed with %Rhrc\n", hr)); + } +#endif + RT_ZERO(*pWaveFmtExt); + hrc = IDirectSoundCaptureBuffer8_GetFormat(pStreamDS->In.pDSCB, &pWaveFmtExt->Format, sizeof(*pWaveFmtExt), NULL); + if (SUCCEEDED(hrc)) + { + /** @todo r=bird: We aren't converting/checking the pWaveFmtX content... */ + + DSCBCAPS BufferCaps = { /*.dwSize = */ sizeof(BufferCaps), 0, 0, 0 }; + hrc = IDirectSoundCaptureBuffer8_GetCaps(pStreamDS->In.pDSCB, &BufferCaps); + if (SUCCEEDED(hrc)) + { + LogRel2(("DSound: Acquired capture buffer capabilities for '%s':\n" + "DSound: dwFlags = %#RX32\n" + "DSound: dwBufferBytes = %#RX32 B / %RU32 B / %RU64 ms\n" + "DSound: dwReserved = %#RX32\n", + pCfgReq->szName, BufferCaps.dwFlags, BufferCaps.dwBufferBytes, BufferCaps.dwBufferBytes, + PDMAudioPropsBytesToMilli(&pCfgReq->Props, BufferCaps.dwBufferBytes), BufferCaps.dwReserved )); + + /* Update buffer related stuff: */ + pStreamDS->In.offReadPos = 0; /** @todo shouldn't we use offBytReadPos here to "read at the initial capture position"? */ + pStreamDS->cbBufSize = BufferCaps.dwBufferBytes; + pCfgAcq->Backend.cFramesBufferSize = PDMAudioPropsBytesToFrames(&pCfgAcq->Props, BufferCaps.dwBufferBytes); + +#if 0 /** @todo r=bird: uAlign isn't set anywhere, so this hasn't been checking anything for a while... */ + if (bc.dwBufferBytes & pStreamDS->uAlign) + DSLOGREL(("DSound: Capture GetCaps returned misaligned buffer: size %RU32, alignment %RU32\n", + bc.dwBufferBytes, pStreamDS->uAlign + 1)); +#endif + LogFlow(("returns S_OK\n")); + return S_OK; + } + LogRelMax(64, ("DSound: Getting capture buffer capabilities for '%s' failed: %Rhrc\n", pCfgReq->szName, hrc)); + } + else + LogRelMax(64, ("DSound: Getting capture format for '%s' failed: %Rhrc\n", pCfgReq->szName, hrc)); + + IDirectSoundCaptureBuffer8_Release(pStreamDS->In.pDSCB); + pStreamDS->In.pDSCB = NULL; + LogFlowFunc(("returns %Rhrc\n", hrc)); + return hrc; +} + + +/** + * Worker for drvHostDSoundHA_StreamCreate that creates playback stream. + * + * @returns Windows COM status code. + * @param pThis The DSound instance data. + * @param pStreamDS The stream instance data. + * @param pCfgReq The requested stream config (input). + * @param pCfgAcq Where to return the actual stream config. This is a + * copy of @a *pCfgReq when called. + * @param pWaveFmtExt On input the requested stream format. + * Updated to the actual stream format on successful + * return. + */ +static HRESULT drvHostDSoundStreamCreatePlayback(PDRVHOSTDSOUND pThis, PDSOUNDSTREAM pStreamDS, PCPDMAUDIOSTREAMCFG pCfgReq, + PPDMAUDIOSTREAMCFG pCfgAcq, WAVEFORMATEXTENSIBLE *pWaveFmtExt) +{ + Assert(pStreamDS->Out.pDSB == NULL); + HRESULT hrc; + + /* + * Create, initialize and set up a DirectSound8 instance the first time + * we go thru here. + */ + /** @todo bird: Or should we rather just throw this away after we've gotten the + * sound buffer? Old code would just leak it... */ + if (pThis->pDS == NULL) + { + hrc = drvHostDSoundCreateDSPlaybackInstance(pThis->Cfg.pGuidPlay, &pThis->pDS); + if (FAILED(hrc)) + return hrc; /* The worker has complained to the release log already. */ + } + + /* + * As we reuse our (secondary) buffer for playing out data as it comes in, + * we're using this buffer as a so-called streaming buffer. + * + * See https://msdn.microsoft.com/en-us/library/windows/desktop/ee419014(v=vs.85).aspx + * + * However, as we do not want to use memory on the sound device directly + * (as most modern audio hardware on the host doesn't have this anyway), + * we're *not* going to use DSBCAPS_STATIC for that. + * + * Instead we're specifying DSBCAPS_LOCSOFTWARE, as this fits the bill + * of copying own buffer data to our secondary's Direct Sound buffer. + */ + DSBUFFERDESC BufferDesc = + { + /*.dwSize = */ sizeof(BufferDesc), + /*.dwFlags = */ DSBCAPS_GLOBALFOCUS | DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_LOCSOFTWARE, + /*.dwBufferBytes = */ PDMAudioPropsFramesToBytes(&pCfgReq->Props, pCfgReq->Backend.cFramesBufferSize), + /*.dwReserved = */ 0, + /*.lpwfxFormat = */ &pWaveFmtExt->Format, + /*.guid3DAlgorithm = {0, 0, 0, {0,0,0,0, 0,0,0,0}} */ + }; + LogRel2(("DSound: Requested playback buffer is %#x B / %u B / %RU64 ms\n", BufferDesc.dwBufferBytes, BufferDesc.dwBufferBytes, + PDMAudioPropsBytesToMilli(&pCfgReq->Props, BufferDesc.dwBufferBytes))); + + LPDIRECTSOUNDBUFFER pLegacyDSB = NULL; + hrc = IDirectSound8_CreateSoundBuffer(pThis->pDS, &BufferDesc, &pLegacyDSB, NULL); + if (FAILED(hrc)) + { + LogRelMax(64, ("DSound: Creating playback sound buffer for '%s' failed: %Rhrc\n", pCfgReq->szName, hrc)); + return hrc; + } + + /* Get the IDirectSoundBuffer8 version of the interface. */ + hrc = IDirectSoundBuffer_QueryInterface(pLegacyDSB, IID_IDirectSoundBuffer8, (PVOID *)&pStreamDS->Out.pDSB); + IDirectSoundBuffer_Release(pLegacyDSB); + if (FAILED(hrc)) + { + LogRelMax(64, ("DSound: Querying IID_IDirectSoundBuffer8 for '%s' failed: %Rhrc\n", pCfgReq->szName, hrc)); + return hrc; + } + + /* + * Query the actual stream parameters, they may differ from what we requested. + */ + RT_ZERO(*pWaveFmtExt); + hrc = IDirectSoundBuffer8_GetFormat(pStreamDS->Out.pDSB, &pWaveFmtExt->Format, sizeof(*pWaveFmtExt), NULL); + if (SUCCEEDED(hrc)) + { + /** @todo r=bird: We aren't converting/checking the pWaveFmtX content... */ + + DSBCAPS BufferCaps = { /*.dwSize = */ sizeof(BufferCaps), 0, 0, 0, 0 }; + hrc = IDirectSoundBuffer8_GetCaps(pStreamDS->Out.pDSB, &BufferCaps); + if (SUCCEEDED(hrc)) + { + LogRel2(("DSound: Acquired playback buffer capabilities for '%s':\n" + "DSound: dwFlags = %#RX32\n" + "DSound: dwBufferBytes = %#RX32 B / %RU32 B / %RU64 ms\n" + "DSound: dwUnlockTransferRate = %RU32 KB/s\n" + "DSound: dwPlayCpuOverhead = %RU32%%\n", + pCfgReq->szName, BufferCaps.dwFlags, BufferCaps.dwBufferBytes, BufferCaps.dwBufferBytes, + PDMAudioPropsBytesToMilli(&pCfgReq->Props, BufferCaps.dwBufferBytes), + BufferCaps.dwUnlockTransferRate, BufferCaps.dwPlayCpuOverhead)); + + /* Update buffer related stuff: */ + pStreamDS->cbBufSize = BufferCaps.dwBufferBytes; + pCfgAcq->Backend.cFramesBufferSize = PDMAudioPropsBytesToFrames(&pCfgAcq->Props, BufferCaps.dwBufferBytes); + pCfgAcq->Backend.cFramesPeriod = pCfgAcq->Backend.cFramesBufferSize / 4; /* total fiction */ + pCfgAcq->Backend.cFramesPreBuffering = pCfgReq->Backend.cFramesPreBuffering * pCfgAcq->Backend.cFramesBufferSize + / RT_MAX(pCfgReq->Backend.cFramesBufferSize, 1); + +#if 0 /** @todo r=bird: uAlign isn't set anywhere, so this hasn't been checking anything for a while... */ + if (bc.dwBufferBytes & pStreamDS->uAlign) + DSLOGREL(("DSound: Playback capabilities returned misaligned buffer: size %RU32, alignment %RU32\n", + bc.dwBufferBytes, pStreamDS->uAlign + 1)); +#endif + LogFlow(("returns S_OK\n")); + return S_OK; + } + LogRelMax(64, ("DSound: Getting playback buffer capabilities for '%s' failed: %Rhrc\n", pCfgReq->szName, hrc)); + } + else + LogRelMax(64, ("DSound: Getting playback format for '%s' failed: %Rhrc\n", pCfgReq->szName, hrc)); + + IDirectSoundBuffer8_Release(pStreamDS->Out.pDSB); + pStreamDS->Out.pDSB = NULL; + LogFlowFunc(("returns %Rhrc\n", hrc)); + return hrc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamCreate} + */ +static DECLCALLBACK(int) drvHostDSoundHA_StreamCreate(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + PCPDMAUDIOSTREAMCFG pCfgReq, PPDMAUDIOSTREAMCFG pCfgAcq) +{ + PDRVHOSTDSOUND pThis = RT_FROM_MEMBER(pInterface, DRVHOSTDSOUND, IHostAudio); + PDSOUNDSTREAM pStreamDS = (PDSOUNDSTREAM)pStream; + AssertPtrReturn(pStreamDS, 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)); + + const char * const pszStreamType = pCfgReq->enmDir == PDMAUDIODIR_IN ? "capture" : "playback"; RT_NOREF(pszStreamType); + LogFlowFunc(("enmPath=%s '%s'\n", PDMAudioPathGetName(pCfgReq->enmPath), pCfgReq->szName)); + RTListInit(&pStreamDS->ListEntry); /* paranoia */ + + /* For whatever reason: */ + dsoundUpdateStatusInternal(pThis); + + /* + * DSound has different COM interfaces for working with input and output + * streams, so we'll quickly part ways here after some common format + * specification setup and logging. + */ +#if defined(RTLOG_REL_ENABLED) || defined(LOG_ENABLED) + char szTmp[64]; +#endif + LogRel2(("DSound: Opening %s stream '%s' (%s)\n", pCfgReq->szName, pszStreamType, + PDMAudioPropsToString(&pCfgReq->Props, szTmp, sizeof(szTmp)))); + + WAVEFORMATEXTENSIBLE WaveFmtExt; + dsoundWaveFmtFromCfg(pCfgReq, &WaveFmtExt); + LogRel2(("DSound: Requested %s format for '%s':\n" + "DSound: wFormatTag = %RU16\n" + "DSound: nChannels = %RU16\n" + "DSound: nSamplesPerSec = %RU32\n" + "DSound: nAvgBytesPerSec = %RU32\n" + "DSound: nBlockAlign = %RU16\n" + "DSound: wBitsPerSample = %RU16\n" + "DSound: cbSize = %RU16\n", + pszStreamType, pCfgReq->szName, WaveFmtExt.Format.wFormatTag, WaveFmtExt.Format.nChannels, + WaveFmtExt.Format.nSamplesPerSec, WaveFmtExt.Format.nAvgBytesPerSec, WaveFmtExt.Format.nBlockAlign, + WaveFmtExt.Format.wBitsPerSample, WaveFmtExt.Format.cbSize)); + if (WaveFmtExt.Format.cbSize != 0) + LogRel2(("DSound: dwChannelMask = %#RX32\n" + "DSound: wValidBitsPerSample = %RU16\n", + WaveFmtExt.dwChannelMask, WaveFmtExt.Samples.wValidBitsPerSample)); + + HRESULT hrc; + if (pCfgReq->enmDir == PDMAUDIODIR_IN) + hrc = drvHostDSoundStreamCreateCapture(pThis, pStreamDS, pCfgReq, pCfgAcq, &WaveFmtExt); + else + hrc = drvHostDSoundStreamCreatePlayback(pThis, pStreamDS, pCfgReq, pCfgAcq, &WaveFmtExt); + int rc; + if (SUCCEEDED(hrc)) + { + LogRel2(("DSound: Acquired %s format for '%s':\n" + "DSound: wFormatTag = %RU16\n" + "DSound: nChannels = %RU16\n" + "DSound: nSamplesPerSec = %RU32\n" + "DSound: nAvgBytesPerSec = %RU32\n" + "DSound: nBlockAlign = %RU16\n" + "DSound: wBitsPerSample = %RU16\n" + "DSound: cbSize = %RU16\n", + pszStreamType, pCfgReq->szName, WaveFmtExt.Format.wFormatTag, WaveFmtExt.Format.nChannels, + WaveFmtExt.Format.nSamplesPerSec, WaveFmtExt.Format.nAvgBytesPerSec, WaveFmtExt.Format.nBlockAlign, + WaveFmtExt.Format.wBitsPerSample, WaveFmtExt.Format.cbSize)); + if (WaveFmtExt.Format.cbSize != 0) + { + LogRel2(("DSound: dwChannelMask = %#RX32\n" + "DSound: wValidBitsPerSample = %RU16\n", + WaveFmtExt.dwChannelMask, WaveFmtExt.Samples.wValidBitsPerSample)); + + /* Update the channel count and map here. */ + PDMAudioPropsSetChannels(&pCfgAcq->Props, WaveFmtExt.Format.nChannels); + uint8_t idCh = 0; + for (unsigned iBit = 0; iBit < 32 && idCh < WaveFmtExt.Format.nChannels; iBit++) + if (WaveFmtExt.dwChannelMask & RT_BIT_32(iBit)) + { + pCfgAcq->Props.aidChannels[idCh] = (unsigned)PDMAUDIOCHANNELID_FIRST_STANDARD + iBit; + idCh++; + } + Assert(idCh == WaveFmtExt.Format.nChannels); + } + + /* + * Copy the acquired config and reset the stream (clears the buffer). + */ + PDMAudioStrmCfgCopy(&pStreamDS->Cfg, pCfgAcq); + drvHostDSoundStreamReset(pThis, pStreamDS); + + RTCritSectEnter(&pThis->CritSect); + RTListAppend(&pThis->HeadStreams, &pStreamDS->ListEntry); + RTCritSectLeave(&pThis->CritSect); + + rc = VINF_SUCCESS; + } + else + rc = VERR_AUDIO_STREAM_COULD_NOT_CREATE; + + LogFlowFunc(("returns %Rrc\n", rc)); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDestroy} + */ +static DECLCALLBACK(int) drvHostDSoundHA_StreamDestroy(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + bool fImmediate) +{ + PDRVHOSTDSOUND pThis = RT_FROM_MEMBER(pInterface, DRVHOSTDSOUND, IHostAudio); + PDSOUNDSTREAM pStreamDS = (PDSOUNDSTREAM)pStream; + AssertPtrReturn(pStreamDS, VERR_INVALID_POINTER); + LogFlowFunc(("Stream '%s' {%s}\n", pStreamDS->Cfg.szName, drvHostDSoundStreamStatusString(pStreamDS))); + RT_NOREF(fImmediate); + + RTCritSectEnter(&pThis->CritSect); + RTListNodeRemove(&pStreamDS->ListEntry); + RTCritSectLeave(&pThis->CritSect); + + if (pStreamDS->Cfg.enmDir == PDMAUDIODIR_IN) + { + /* + * Input. + */ + if (pStreamDS->In.pDSCB) + { + HRESULT hrc = IDirectSoundCaptureBuffer_Stop(pStreamDS->In.pDSCB); + if (FAILED(hrc)) + LogFunc(("IDirectSoundCaptureBuffer_Stop failed: %Rhrc\n", hrc)); + + drvHostDSoundStreamReset(pThis, pStreamDS); + + IDirectSoundCaptureBuffer8_Release(pStreamDS->In.pDSCB); + pStreamDS->In.pDSCB = NULL; + } + } + else + { + /* + * Output. + */ + if (pStreamDS->Out.pDSB) + { + drvHostDSoundStreamStopPlayback(pThis, pStreamDS, true /*fReset*/); + + IDirectSoundBuffer8_Release(pStreamDS->Out.pDSB); + pStreamDS->Out.pDSB = NULL; + } + } + + if (RTCritSectIsInitialized(&pStreamDS->CritSect)) + RTCritSectDelete(&pStreamDS->CritSect); + + return VINF_SUCCESS; +} + + +/** + * Worker for drvHostDSoundHA_StreamEnable and drvHostDSoundHA_StreamResume. + * + * This will try re-open the capture device if we're having trouble starting it. + * + * @returns VBox status code. + * @param pThis The DSound host audio driver instance data. + * @param pStreamDS The stream instance data. + */ +static int drvHostDSoundStreamCaptureStart(PDRVHOSTDSOUND pThis, PDSOUNDSTREAM pStreamDS) +{ + /* + * Check the stream status first. + */ + int rc = VERR_AUDIO_STREAM_NOT_READY; + if (pStreamDS->In.pDSCB) + { + DWORD fStatus = 0; + HRESULT hrc = IDirectSoundCaptureBuffer8_GetStatus(pStreamDS->In.pDSCB, &fStatus); + if (SUCCEEDED(hrc)) + { + /* + * Try start capturing if it's not already doing so. + */ + if (!(fStatus & DSCBSTATUS_CAPTURING)) + { + LogRel2(("DSound: Starting capture on '%s' ... \n", pStreamDS->Cfg.szName)); + hrc = IDirectSoundCaptureBuffer8_Start(pStreamDS->In.pDSCB, DSCBSTART_LOOPING); + if (SUCCEEDED(hrc)) + rc = VINF_SUCCESS; + else + { + /* + * Failed to start, try re-create the capture buffer. + */ + LogRelMax(64, ("DSound: Starting to capture on '%s' failed: %Rhrc - will try re-open it ...\n", + pStreamDS->Cfg.szName, hrc)); + + IDirectSoundCaptureBuffer8_Release(pStreamDS->In.pDSCB); + pStreamDS->In.pDSCB = NULL; + + PDMAUDIOSTREAMCFG CfgReq = pStreamDS->Cfg; + PDMAUDIOSTREAMCFG CfgAcq = pStreamDS->Cfg; + WAVEFORMATEXTENSIBLE WaveFmtExt; + dsoundWaveFmtFromCfg(&pStreamDS->Cfg, &WaveFmtExt); + hrc = drvHostDSoundStreamCreateCapture(pThis, pStreamDS, &CfgReq, &CfgAcq, &WaveFmtExt); + if (SUCCEEDED(hrc)) + { + PDMAudioStrmCfgCopy(&pStreamDS->Cfg, &CfgAcq); + + /* + * Try starting capture again. + */ + LogRel2(("DSound: Starting capture on re-opened '%s' ... \n", pStreamDS->Cfg.szName)); + hrc = IDirectSoundCaptureBuffer8_Start(pStreamDS->In.pDSCB, DSCBSTART_LOOPING); + if (SUCCEEDED(hrc)) + rc = VINF_SUCCESS; + else + LogRelMax(64, ("DSound: Starting to capture on re-opened '%s' failed: %Rhrc\n", + pStreamDS->Cfg.szName, hrc)); + } + else + LogRelMax(64, ("DSound: Re-opening '%s' failed: %Rhrc\n", pStreamDS->Cfg.szName, hrc)); + } + } + else + { + LogRel2(("DSound: Already capturing (%#x)\n", fStatus)); + AssertFailed(); + } + } + else + LogRelMax(64, ("DSound: Retrieving capture status for '%s' failed: %Rhrc\n", pStreamDS->Cfg.szName, hrc)); + } + LogFlowFunc(("returns %Rrc\n", rc)); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamEnable} + */ +static DECLCALLBACK(int) drvHostDSoundHA_StreamEnable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + PDRVHOSTDSOUND pThis = RT_FROM_MEMBER(pInterface, DRVHOSTDSOUND, IHostAudio); + PDSOUNDSTREAM pStreamDS = (PDSOUNDSTREAM)pStream; + LogFlowFunc(("Stream '%s' {%s}\n", pStreamDS->Cfg.szName, drvHostDSoundStreamStatusString(pStreamDS))); + + /* + * We always reset the buffer before enabling the stream (normally never necessary). + */ + drvHostDSoundStreamReset(pThis, pStreamDS); + pStreamDS->fEnabled = true; + + /* + * Input streams will start capturing, while output streams will only start + * playing once we get some audio data to play. + */ + int rc = VINF_SUCCESS; + if (pStreamDS->Cfg.enmDir == PDMAUDIODIR_IN) + rc = drvHostDSoundStreamCaptureStart(pThis, pStreamDS); + else + Assert(pStreamDS->Cfg.enmDir == PDMAUDIODIR_OUT); + + LogFlowFunc(("returns %Rrc\n", rc)); + return rc; +} + + +/** + * Worker for drvHostDSoundHA_StreamDestroy, drvHostDSoundHA_StreamDisable and + * drvHostDSoundHA_StreamPause. + * + * @returns VBox status code. + * @param pThis The DSound host audio driver instance data. + * @param pStreamDS The stream instance data. + * @param fReset Whether to reset the buffer and state. + */ +static int drvHostDSoundStreamStopPlayback(PDRVHOSTDSOUND pThis, PDSOUNDSTREAM pStreamDS, bool fReset) +{ + if (!pStreamDS->Out.pDSB) + return VINF_SUCCESS; + + LogRel2(("DSound: Stopping playback of '%s'...\n", pStreamDS->Cfg.szName)); + HRESULT hrc = IDirectSoundBuffer8_Stop(pStreamDS->Out.pDSB); + if (FAILED(hrc)) + { + LogFunc(("IDirectSoundBuffer8_Stop -> %Rhrc; will attempt restoring the stream...\n", hrc)); + directSoundPlayRestore(pThis, pStreamDS->Out.pDSB); + hrc = IDirectSoundBuffer8_Stop(pStreamDS->Out.pDSB); + if (FAILED(hrc)) + LogRelMax(64, ("DSound: %s playback of '%s' failed: %Rhrc\n", fReset ? "Stopping" : "Pausing", + pStreamDS->Cfg.szName, hrc)); + } + LogRel2(("DSound: Stopped playback of '%s': %Rhrc\n", pStreamDS->Cfg.szName, hrc)); + + if (fReset) + drvHostDSoundStreamReset(pThis, pStreamDS); + return SUCCEEDED(hrc) ? VINF_SUCCESS : VERR_AUDIO_STREAM_NOT_READY; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDisable} + */ +static DECLCALLBACK(int) drvHostDSoundHA_StreamDisable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + PDRVHOSTDSOUND pThis = RT_FROM_MEMBER(pInterface, DRVHOSTDSOUND, IHostAudio); + PDSOUNDSTREAM pStreamDS = (PDSOUNDSTREAM)pStream; + LogFlowFunc(("cMsLastTransfer=%RI64 ms, stream '%s' {%s} \n", + pStreamDS->msLastTransfer ? RTTimeMilliTS() - pStreamDS->msLastTransfer : -1, + pStreamDS->Cfg.szName, drvHostDSoundStreamStatusString(pStreamDS) )); + + /* + * Change the state. + */ + pStreamDS->fEnabled = false; + + /* + * Stop the stream and maybe reset the buffer. + */ + int rc = VINF_SUCCESS; + if (pStreamDS->Cfg.enmDir == PDMAUDIODIR_IN) + { + if (pStreamDS->In.pDSCB) + { + HRESULT hrc = IDirectSoundCaptureBuffer_Stop(pStreamDS->In.pDSCB); + if (SUCCEEDED(hrc)) + LogRel3(("DSound: Stopped capture on '%s'.\n", pStreamDS->Cfg.szName)); + else + { + LogRelMax(64, ("DSound: Stopping capture on '%s' failed: %Rhrc\n", pStreamDS->Cfg.szName, hrc)); + /* Don't report errors up to the caller, as it might just be a capture device change. */ + } + + /* This isn't strictly speaking necessary since StreamEnable does it too... */ + drvHostDSoundStreamReset(pThis, pStreamDS); + } + } + else + { + Assert(pStreamDS->Cfg.enmDir == PDMAUDIODIR_OUT); + if (pStreamDS->Out.pDSB) + { + rc = drvHostDSoundStreamStopPlayback(pThis, pStreamDS, true /*fReset*/); + if (RT_SUCCESS(rc)) + LogRel3(("DSound: Stopped playback on '%s'.\n", pStreamDS->Cfg.szName)); + } + } + + LogFlowFunc(("returns %Rrc {%s}\n", rc, drvHostDSoundStreamStatusString(pStreamDS))); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamPause} + * + * @note Basically the same as drvHostDSoundHA_StreamDisable, just w/o the + * buffer resetting and fEnabled change. + */ +static DECLCALLBACK(int) drvHostDSoundHA_StreamPause(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + PDRVHOSTDSOUND pThis = RT_FROM_MEMBER(pInterface, DRVHOSTDSOUND, IHostAudio); + PDSOUNDSTREAM pStreamDS = (PDSOUNDSTREAM)pStream; + LogFlowFunc(("cMsLastTransfer=%RI64 ms, stream '%s' {%s} \n", + pStreamDS->msLastTransfer ? RTTimeMilliTS() - pStreamDS->msLastTransfer : -1, + pStreamDS->Cfg.szName, drvHostDSoundStreamStatusString(pStreamDS) )); + + /* + * Stop the stream and maybe reset the buffer. + */ + int rc = VINF_SUCCESS; + if (pStreamDS->Cfg.enmDir == PDMAUDIODIR_IN) + { + if (pStreamDS->In.pDSCB) + { + HRESULT hrc = IDirectSoundCaptureBuffer_Stop(pStreamDS->In.pDSCB); + if (SUCCEEDED(hrc)) + LogRel3(("DSound: Stopped capture on '%s'.\n", pStreamDS->Cfg.szName)); + else + { + LogRelMax(64, ("DSound: Stopping capture on '%s' failed: %Rhrc\n", pStreamDS->Cfg.szName, hrc)); + /* Don't report errors up to the caller, as it might just be a capture device change. */ + } + } + } + else + { + Assert(pStreamDS->Cfg.enmDir == PDMAUDIODIR_OUT); + if (pStreamDS->Out.pDSB) + { + /* Don't stop draining buffers, we won't be resuming them right. + They'll stop by themselves anyway. */ + if (pStreamDS->Out.fDrain) + LogFunc(("Stream '%s' is draining\n", pStreamDS->Cfg.szName)); + else + { + rc = drvHostDSoundStreamStopPlayback(pThis, pStreamDS, false /*fReset*/); + if (RT_SUCCESS(rc)) + LogRel3(("DSound: Stopped playback on '%s'.\n", pStreamDS->Cfg.szName)); + } + } + } + + LogFlowFunc(("returns %Rrc {%s}\n", rc, drvHostDSoundStreamStatusString(pStreamDS))); + return rc; +} + + +/** + * Worker for drvHostDSoundHA_StreamResume and drvHostDSoundHA_StreamPlay that + * starts playing the DirectSound Buffer. + * + * @returns VBox status code. + * @param pThis Host audio driver instance. + * @param pStreamDS Stream to start playing. + */ +static int directSoundPlayStart(PDRVHOSTDSOUND pThis, PDSOUNDSTREAM pStreamDS) +{ + if (!pStreamDS->Out.pDSB) + return VERR_AUDIO_STREAM_NOT_READY; + + LogRel2(("DSound: Starting playback of '%s' ...\n", pStreamDS->Cfg.szName)); + HRESULT hrc = IDirectSoundBuffer8_Play(pStreamDS->Out.pDSB, 0, 0, DSCBSTART_LOOPING); + if (SUCCEEDED(hrc)) + return VINF_SUCCESS; + + for (unsigned i = 0; hrc == DSERR_BUFFERLOST && i < DRV_DSOUND_RESTORE_ATTEMPTS_MAX; i++) + { + LogFunc(("Restarting playback failed due to lost buffer, restoring ...\n")); + directSoundPlayRestore(pThis, pStreamDS->Out.pDSB); + + hrc = IDirectSoundBuffer8_Play(pStreamDS->Out.pDSB, 0, 0, DSCBSTART_LOOPING); + if (SUCCEEDED(hrc)) + return VINF_SUCCESS; + } + + LogRelMax(64, ("DSound: Failed to start playback of '%s': %Rhrc\n", pStreamDS->Cfg.szName, hrc)); + return VERR_AUDIO_STREAM_NOT_READY; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamResume} + */ +static DECLCALLBACK(int) drvHostDSoundHA_StreamResume(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + PDRVHOSTDSOUND pThis = RT_FROM_MEMBER(pInterface, DRVHOSTDSOUND, IHostAudio); + PDSOUNDSTREAM pStreamDS = (PDSOUNDSTREAM)pStream; + LogFlowFunc(("Stream '%s' {%s}\n", pStreamDS->Cfg.szName, drvHostDSoundStreamStatusString(pStreamDS))); + + /* + * Input streams will start capturing, while output streams will only start + * playing if we're past the pre-buffering state. + */ + int rc = VINF_SUCCESS; + if (pStreamDS->Cfg.enmDir == PDMAUDIODIR_IN) + rc = drvHostDSoundStreamCaptureStart(pThis, pStreamDS); + else + { + Assert(pStreamDS->Cfg.enmDir == PDMAUDIODIR_OUT); + if (!pStreamDS->Out.fFirstTransfer) + rc = directSoundPlayStart(pThis, pStreamDS); + } + + LogFlowFunc(("returns %Rrc {%s}\n", rc, drvHostDSoundStreamStatusString(pStreamDS))); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDrain} + */ +static DECLCALLBACK(int) drvHostDSoundHA_StreamDrain(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + PDRVHOSTDSOUND pThis = RT_FROM_MEMBER(pInterface, DRVHOSTDSOUND, IHostAudio); + PDSOUNDSTREAM pStreamDS = (PDSOUNDSTREAM)pStream; + AssertReturn(pStreamDS->Cfg.enmDir == PDMAUDIODIR_OUT, VERR_INVALID_PARAMETER); + LogFlowFunc(("cMsLastTransfer=%RI64 ms, stream '%s' {%s} \n", + pStreamDS->msLastTransfer ? RTTimeMilliTS() - pStreamDS->msLastTransfer : -1, + pStreamDS->Cfg.szName, drvHostDSoundStreamStatusString(pStreamDS) )); + + /* + * We've started the buffer in looping mode, try switch to non-looping... + */ + int rc = VINF_SUCCESS; + if (pStreamDS->Out.pDSB && !pStreamDS->Out.fDrain) + { + LogRel2(("DSound: Switching playback stream '%s' to drain mode...\n", pStreamDS->Cfg.szName)); + HRESULT hrc = IDirectSoundBuffer8_Stop(pStreamDS->Out.pDSB); + if (SUCCEEDED(hrc)) + { + hrc = IDirectSoundBuffer8_Play(pStreamDS->Out.pDSB, 0, 0, 0); + if (SUCCEEDED(hrc)) + { + uint64_t const msNow = RTTimeMilliTS(); + pStreamDS->Out.msDrainDeadline = PDMAudioPropsBytesToMilli(&pStreamDS->Cfg.Props, pStreamDS->cbBufSize) + msNow; + pStreamDS->Out.fDrain = true; + } + else + LogRelMax(64, ("DSound: Failed to restart '%s' in drain mode: %Rhrc\n", pStreamDS->Cfg.szName, hrc)); + } + else + { + Log2Func(("drain: IDirectSoundBuffer8_Stop failed: %Rhrc\n", hrc)); + directSoundPlayRestore(pThis, pStreamDS->Out.pDSB); + + HRESULT hrc2 = IDirectSoundBuffer8_Stop(pStreamDS->Out.pDSB); + if (SUCCEEDED(hrc2)) + LogFunc(("Successfully stopped the stream after restoring it. (hrc=%Rhrc)\n", hrc)); + else + { + LogRelMax(64, ("DSound: Failed to stop playback stream '%s' for putting into drain mode: %Rhrc (initial), %Rhrc (after restore)\n", + pStreamDS->Cfg.szName, hrc, hrc2)); + rc = VERR_AUDIO_STREAM_NOT_READY; + } + } + } + LogFlowFunc(("returns %Rrc {%s}\n", rc, drvHostDSoundStreamStatusString(pStreamDS))); + return rc; +} + + +/** + * Retrieves the number of free bytes available for writing to a DirectSound output stream. + * + * @return VBox status code. VERR_NOT_AVAILABLE if unable to determine or the + * buffer was not recoverable. + * @param pThis Host audio driver instance. + * @param pStreamDS DirectSound output stream to retrieve number for. + * @param pdwFree Where to return the free amount on success. + * @param poffPlayCursor Where to return the play cursor offset. + */ +static int dsoundGetFreeOut(PDRVHOSTDSOUND pThis, PDSOUNDSTREAM pStreamDS, DWORD *pdwFree, DWORD *poffPlayCursor) +{ + AssertPtrReturn(pThis, VERR_INVALID_POINTER); + AssertPtrReturn(pStreamDS, VERR_INVALID_POINTER); + AssertPtrReturn(pdwFree, VERR_INVALID_POINTER); + AssertPtr(poffPlayCursor); + + Assert(pStreamDS->Cfg.enmDir == PDMAUDIODIR_OUT); /* Paranoia. */ + + LPDIRECTSOUNDBUFFER8 pDSB = pStreamDS->Out.pDSB; + AssertPtrReturn(pDSB, VERR_INVALID_POINTER); + + HRESULT hr = S_OK; + + /* Get the current play position which is used for calculating the free space in the buffer. */ + for (unsigned i = 0; i < DRV_DSOUND_RESTORE_ATTEMPTS_MAX; i++) + { + DWORD offPlayCursor = 0; + DWORD offWriteCursor = 0; + hr = IDirectSoundBuffer8_GetCurrentPosition(pDSB, &offPlayCursor, &offWriteCursor); + if (SUCCEEDED(hr)) + { + int32_t cbDiff = offWriteCursor - offPlayCursor; + if (cbDiff < 0) + cbDiff += pStreamDS->cbBufSize; + + int32_t cbFree = offPlayCursor - pStreamDS->Out.offWritePos; + if (cbFree < 0) + cbFree += pStreamDS->cbBufSize; + + if (cbFree > (int32_t)pStreamDS->cbBufSize - cbDiff) + { + /** @todo count/log these. */ + pStreamDS->Out.offWritePos = offWriteCursor; + cbFree = pStreamDS->cbBufSize - cbDiff; + } + + /* When starting to use a DirectSound buffer, offPlayCursor and offWriteCursor + * both point at position 0, so we won't be able to detect how many bytes + * are writable that way. + * + * So use our per-stream written indicator to see if we just started a stream. */ + if (pStreamDS->Out.cbWritten == 0) + cbFree = pStreamDS->cbBufSize; + + LogRel4(("DSound: offPlayCursor=%RU32, offWriteCursor=%RU32, offWritePos=%RU32 -> cbFree=%RI32\n", + offPlayCursor, offWriteCursor, pStreamDS->Out.offWritePos, cbFree)); + + *pdwFree = cbFree; + *poffPlayCursor = offPlayCursor; + return VINF_SUCCESS; + } + + if (hr != DSERR_BUFFERLOST) /** @todo MSDN doesn't state this error for GetCurrentPosition(). */ + break; + + LogFunc(("Getting playing position failed due to lost buffer, restoring ...\n")); + + directSoundPlayRestore(pThis, pDSB); + } + + if (hr != DSERR_BUFFERLOST) /* Avoid log flooding if the error is still there. */ + DSLOGREL(("DSound: Getting current playback position failed with %Rhrc\n", hr)); + + LogFunc(("Failed with %Rhrc\n", hr)); + + *poffPlayCursor = pStreamDS->cbBufSize; + return VERR_NOT_AVAILABLE; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetState} + */ +static DECLCALLBACK(PDMHOSTAUDIOSTREAMSTATE) drvHostDSoundHA_StreamGetState(PPDMIHOSTAUDIO pInterface, + PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PDSOUNDSTREAM pStreamDS = (PDSOUNDSTREAM)pStream; + AssertPtrReturn(pStreamDS, PDMHOSTAUDIOSTREAMSTATE_INVALID); + + if ( pStreamDS->Cfg.enmDir != PDMAUDIODIR_OUT + || !pStreamDS->Out.fDrain) + { + LogFlowFunc(("returns OKAY for '%s' {%s}\n", pStreamDS->Cfg.szName, drvHostDSoundStreamStatusString(pStreamDS))); + return PDMHOSTAUDIOSTREAMSTATE_OKAY; + } + LogFlowFunc(("returns DRAINING for '%s' {%s}\n", pStreamDS->Cfg.szName, drvHostDSoundStreamStatusString(pStreamDS))); + return PDMHOSTAUDIOSTREAMSTATE_DRAINING; +} + +#if 0 /* This isn't working as the write cursor is more a function of time than what we do. + Previously we only reported the pre-buffering status anyway, so no harm. */ +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetPending} + */ +static DECLCALLBACK(uint32_t) drvHostDSoundHA_StreamGetPending(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + /*PDRVHOSTDSOUND pThis = RT_FROM_MEMBER(pInterface, DRVHOSTDSOUND, IHostAudio); */ RT_NOREF(pInterface); + PDSOUNDSTREAM pStreamDS = (PDSOUNDSTREAM)pStream; + AssertPtrReturn(pStreamDS, 0); + LogFlowFunc(("Stream '%s' {%s}\n", pStreamDS->Cfg.szName, drvHostDSoundStreamStatusString(pStreamDS))); + + if (pStreamDS->Cfg.enmDir == PDMAUDIODIR_OUT) + { + /* This is a similar calculation as for StreamGetReadable, only for an output buffer. */ + AssertPtr(pStreamDS->In.pDSCB); + DWORD offPlayCursor = 0; + DWORD offWriteCursor = 0; + HRESULT hrc = IDirectSoundBuffer8_GetCurrentPosition(pStreamDS->Out.pDSB, &offPlayCursor, &offWriteCursor); + if (SUCCEEDED(hrc)) + { + uint32_t cbPending = dsoundRingDistance(offWriteCursor, offPlayCursor, pStreamDS->cbBufSize); + Log3Func(("cbPending=%RU32\n", cbPending)); + return cbPending; + } + AssertMsgFailed(("hrc=%Rhrc\n", hrc)); + } + /* else: For input streams we never have any pending data. */ + + return 0; +} +#endif + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetWritable} + */ +static DECLCALLBACK(uint32_t) drvHostDSoundHA_StreamGetWritable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + PDRVHOSTDSOUND pThis = RT_FROM_MEMBER(pInterface, DRVHOSTDSOUND, IHostAudio); + PDSOUNDSTREAM pStreamDS = (PDSOUNDSTREAM)pStream; + AssertPtrReturn(pStreamDS, 0); + LogFlowFunc(("Stream '%s' {%s}\n", pStreamDS->Cfg.szName, drvHostDSoundStreamStatusString(pStreamDS))); + + DWORD cbFree = 0; + DWORD offIgn = 0; + int rc = dsoundGetFreeOut(pThis, pStreamDS, &cbFree, &offIgn); + AssertRCReturn(rc, 0); + + return cbFree; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamPlay} + */ +static DECLCALLBACK(int) drvHostDSoundHA_StreamPlay(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + const void *pvBuf, uint32_t cbBuf, uint32_t *pcbWritten) +{ + PDRVHOSTDSOUND pThis = RT_FROM_MEMBER(pInterface, DRVHOSTDSOUND, IHostAudio); + PDSOUNDSTREAM pStreamDS = (PDSOUNDSTREAM)pStream; + AssertPtrReturn(pStreamDS, 0); + if (cbBuf) + AssertPtrReturn(pvBuf, VERR_INVALID_POINTER); + AssertPtrReturn(pcbWritten, VERR_INVALID_POINTER); + + if (pStreamDS->fEnabled) + AssertReturn(pStreamDS->cbBufSize, VERR_INTERNAL_ERROR_2); + else + { + Log2Func(("Skipping disabled stream {%s}\n", drvHostDSoundStreamStatusString(pStreamDS))); + return VINF_SUCCESS; + } + Log4Func(("cbBuf=%#x stream '%s' {%s}\n", cbBuf, pStreamDS->Cfg.szName, drvHostDSoundStreamStatusString(pStreamDS) )); + +/** @todo Any condition under which we should call dsoundUpdateStatusInternal(pThis) here? + * The old code thought it did so in case of failure, only it couldn't ever fails, so it never did. */ + + /* + * Transfer loop. + */ + uint32_t cbWritten = 0; + while (cbBuf > 0) + { + /* + * Figure out how much we can possibly write. + */ + DWORD offPlayCursor = 0; + DWORD cbWritable = 0; + int rc = dsoundGetFreeOut(pThis, pStreamDS, &cbWritable, &offPlayCursor); + AssertRCReturn(rc, rc); + if (cbWritable < pStreamDS->Cfg.Props.cbFrame) + break; + + uint32_t const cbToWrite = RT_MIN(cbWritable, cbBuf); + Log3Func(("offPlay=%#x offWritePos=%#x -> cbWritable=%#x cbToWrite=%#x {%s}\n", offPlayCursor, pStreamDS->Out.offWritePos, + cbWritable, cbToWrite, drvHostDSoundStreamStatusString(pStreamDS) )); + + /* + * Lock that amount of buffer. + */ + PVOID pv1 = NULL; + DWORD cb1 = 0; + PVOID pv2 = NULL; + DWORD cb2 = 0; + HRESULT hrc = directSoundPlayLock(pThis, pStreamDS, pStreamDS->Out.offWritePos, cbToWrite, + &pv1, &pv2, &cb1, &cb2, 0 /*dwFlags*/); + AssertMsgReturn(SUCCEEDED(hrc), ("%Rhrc\n", hrc), VERR_ACCESS_DENIED); /** @todo translate these status codes already! */ + //AssertMsg(cb1 + cb2 == cbToWrite, ("%#x + %#x vs %#x\n", cb1, cb2, cbToWrite)); + + /* + * Copy over the data. + */ + memcpy(pv1, pvBuf, cb1); + pvBuf = (uint8_t *)pvBuf + cb1; + cbBuf -= cb1; + cbWritten += cb1; + + if (pv2) + { + memcpy(pv2, pvBuf, cb2); + pvBuf = (uint8_t *)pvBuf + cb2; + cbBuf -= cb2; + cbWritten += cb2; + } + + /* + * Unlock and update the write position. + */ + directSoundPlayUnlock(pThis, pStreamDS->Out.pDSB, pv1, pv2, cb1, cb2); /** @todo r=bird: pThis + pDSB parameters here for Unlock, but only pThis for Lock. Why? */ + pStreamDS->Out.offWritePos = (pStreamDS->Out.offWritePos + cb1 + cb2) % pStreamDS->cbBufSize; + + /* + * If this was the first chunk, kick off playing. + */ + if (!pStreamDS->Out.fFirstTransfer) + { /* likely */ } + else + { + *pcbWritten = cbWritten; + rc = directSoundPlayStart(pThis, pStreamDS); + AssertRCReturn(rc, rc); + pStreamDS->Out.fFirstTransfer = false; + } + } + + /* + * Done. + */ + *pcbWritten = cbWritten; + + pStreamDS->Out.cbTransferred += cbWritten; + if (cbWritten) + { + uint64_t const msPrev = pStreamDS->msLastTransfer; RT_NOREF(msPrev); + pStreamDS->Out.cbLastTransferred = cbWritten; + pStreamDS->msLastTransfer = RTTimeMilliTS(); + LogFlowFunc(("cbLastTransferred=%RU32, msLastTransfer=%RU64 msNow=%RU64 cMsDelta=%RU64 {%s}\n", + cbWritten, msPrev, pStreamDS->msLastTransfer, msPrev ? pStreamDS->msLastTransfer - msPrev : 0, + drvHostDSoundStreamStatusString(pStreamDS) )); + } + else if ( pStreamDS->Out.fDrain + && RTTimeMilliTS() >= pStreamDS->Out.msDrainDeadline) + { + LogRel2(("DSound: Stopping draining of '%s' {%s} ...\n", pStreamDS->Cfg.szName, drvHostDSoundStreamStatusString(pStreamDS))); + if (pStreamDS->Out.pDSB) + { + HRESULT hrc = IDirectSoundBuffer8_Stop(pStreamDS->Out.pDSB); + if (FAILED(hrc)) + LogRelMax(64, ("DSound: Failed to stop draining stream '%s': %Rhrc\n", pStreamDS->Cfg.szName, hrc)); + } + pStreamDS->Out.fDrain = false; + pStreamDS->fEnabled = false; + } + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetReadable} + */ +static DECLCALLBACK(uint32_t) drvHostDSoundHA_StreamGetReadable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + /*PDRVHOSTDSOUND pThis = RT_FROM_MEMBER(pInterface, DRVHOSTDSOUND, IHostAudio); */ RT_NOREF(pInterface); + PDSOUNDSTREAM pStreamDS = (PDSOUNDSTREAM)pStream; + AssertPtrReturn(pStreamDS, 0); + Assert(pStreamDS->Cfg.enmDir == PDMAUDIODIR_IN); + + if (pStreamDS->fEnabled) + { + /* This is the same calculation as for StreamGetPending. */ + AssertPtr(pStreamDS->In.pDSCB); + DWORD offCaptureCursor = 0; + DWORD offReadCursor = 0; + HRESULT hrc = IDirectSoundCaptureBuffer_GetCurrentPosition(pStreamDS->In.pDSCB, &offCaptureCursor, &offReadCursor); + if (SUCCEEDED(hrc)) + { + uint32_t cbPending = dsoundRingDistance(offCaptureCursor, offReadCursor, pStreamDS->cbBufSize); + Log3Func(("cbPending=%RU32\n", cbPending)); + return cbPending; + } + AssertMsgFailed(("hrc=%Rhrc\n", hrc)); + } + + return 0; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamCapture} + */ +static DECLCALLBACK(int) drvHostDSoundHA_StreamCapture(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + void *pvBuf, uint32_t cbBuf, uint32_t *pcbRead) +{ + /*PDRVHOSTDSOUND pThis = RT_FROM_MEMBER(pInterface, DRVHOSTDSOUND, IHostAudio);*/ RT_NOREF(pInterface); + PDSOUNDSTREAM pStreamDS = (PDSOUNDSTREAM)pStream; + AssertPtrReturn(pStreamDS, 0); + AssertPtrReturn(pvBuf, VERR_INVALID_POINTER); + AssertReturn(cbBuf, VERR_INVALID_PARAMETER); + AssertPtrReturn(pcbRead, VERR_INVALID_POINTER); + +#if 0 /** @todo r=bird: shouldn't we do the same check as for output streams? */ + if (pStreamDS->fEnabled) + AssertReturn(pStreamDS->cbBufSize, VERR_INTERNAL_ERROR_2); + else + { + Log2Func(("Stream disabled, skipping\n")); + return VINF_SUCCESS; + } +#endif + Log4Func(("cbBuf=%#x stream '%s' {%s}\n", cbBuf, pStreamDS->Cfg.szName, drvHostDSoundStreamStatusString(pStreamDS) )); + + /* + * Read loop. + */ + uint32_t cbRead = 0; + while (cbBuf > 0) + { + /* + * Figure out how much we can read. + */ + DWORD offCaptureCursor = 0; + DWORD offReadCursor = 0; + HRESULT hrc = IDirectSoundCaptureBuffer_GetCurrentPosition(pStreamDS->In.pDSCB, &offCaptureCursor, &offReadCursor); + AssertMsgReturn(SUCCEEDED(hrc), ("%Rhrc\n", hrc), VERR_ACCESS_DENIED); /** @todo translate these status codes already! */ + //AssertMsg(offReadCursor == pStreamDS->In.offReadPos, ("%#x %#x\n", offReadCursor, pStreamDS->In.offReadPos)); + + uint32_t const cbReadable = dsoundRingDistance(offCaptureCursor, pStreamDS->In.offReadPos, pStreamDS->cbBufSize); + + if (cbReadable >= pStreamDS->Cfg.Props.cbFrame) + { /* likely */ } + else + { + if (cbRead > 0) + { /* likely */ } + else if (pStreamDS->In.cOverruns < 32) + { + pStreamDS->In.cOverruns++; + DSLOG(("DSound: Warning: Buffer full (size is %zu bytes), skipping to record data (overflow #%RU32)\n", + pStreamDS->cbBufSize, pStreamDS->In.cOverruns)); + } + break; + } + + uint32_t const cbToRead = RT_MIN(cbReadable, cbBuf); + Log3Func(("offCapture=%#x offRead=%#x/%#x -> cbWritable=%#x cbToWrite=%#x {%s}\n", offCaptureCursor, offReadCursor, + pStreamDS->In.offReadPos, cbReadable, cbToRead, drvHostDSoundStreamStatusString(pStreamDS))); + + /* + * Lock that amount of buffer. + */ + PVOID pv1 = NULL; + DWORD cb1 = 0; + PVOID pv2 = NULL; + DWORD cb2 = 0; + hrc = directSoundCaptureLock(pStreamDS, pStreamDS->In.offReadPos, cbToRead, &pv1, &pv2, &cb1, &cb2, 0 /*dwFlags*/); + AssertMsgReturn(SUCCEEDED(hrc), ("%Rhrc\n", hrc), VERR_ACCESS_DENIED); /** @todo translate these status codes already! */ + AssertMsg(cb1 + cb2 == cbToRead, ("%#x + %#x vs %#x\n", cb1, cb2, cbToRead)); + + /* + * Copy over the data. + */ + memcpy(pvBuf, pv1, cb1); + pvBuf = (uint8_t *)pvBuf + cb1; + cbBuf -= cb1; + cbRead += cb1; + + if (pv2) + { + memcpy(pvBuf, pv2, cb2); + pvBuf = (uint8_t *)pvBuf + cb2; + cbBuf -= cb2; + cbRead += cb2; + } + + /* + * Unlock and update the write position. + */ + directSoundCaptureUnlock(pStreamDS->In.pDSCB, pv1, pv2, cb1, cb2); /** @todo r=bird: pDSB parameter here for Unlock, but pStreamDS for Lock. Why? */ + pStreamDS->In.offReadPos = (pStreamDS->In.offReadPos + cb1 + cb2) % pStreamDS->cbBufSize; + } + + /* + * Done. + */ + *pcbRead = cbRead; + if (cbRead) + { + uint64_t const msPrev = pStreamDS->msLastTransfer; RT_NOREF(msPrev); + pStreamDS->msLastTransfer = RTTimeMilliTS(); + LogFlowFunc(("cbRead=%RU32, msLastTransfer=%RU64 msNow=%RU64 cMsDelta=%RU64 {%s}\n", + cbRead, msPrev, pStreamDS->msLastTransfer, msPrev ? pStreamDS->msLastTransfer - msPrev : 0, + drvHostDSoundStreamStatusString(pStreamDS) )); + } + + return VINF_SUCCESS; +} + + +/********************************************************************************************************************************* +* PDMDRVINS::IBase Interface * +*********************************************************************************************************************************/ + +/** + * @callback_method_impl{PDMIBASE,pfnQueryInterface} + */ +static DECLCALLBACK(void *) drvHostDSoundQueryInterface(PPDMIBASE pInterface, const char *pszIID) +{ + PPDMDRVINS pDrvIns = PDMIBASE_2_PDMDRV(pInterface); + PDRVHOSTDSOUND pThis = PDMINS_2_DATA(pDrvIns, PDRVHOSTDSOUND); + + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIBASE, &pDrvIns->IBase); + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIHOSTAUDIO, &pThis->IHostAudio); + return NULL; +} + + +/********************************************************************************************************************************* +* PDMDRVREG Interface * +*********************************************************************************************************************************/ + +/** + * @callback_method_impl{FNPDMDRVDESTRUCT, pfnDestruct} + */ +static DECLCALLBACK(void) drvHostDSoundDestruct(PPDMDRVINS pDrvIns) +{ + PDRVHOSTDSOUND pThis = PDMINS_2_DATA(pDrvIns, PDRVHOSTDSOUND); + PDMDRV_CHECK_VERSIONS_RETURN_VOID(pDrvIns); + + LogFlowFuncEnter(); + +#ifdef VBOX_WITH_AUDIO_MMNOTIFICATION_CLIENT + if (pThis->m_pNotificationClient) + { + pThis->m_pNotificationClient->Unregister(); + pThis->m_pNotificationClient->Release(); + + pThis->m_pNotificationClient = NULL; + } +#endif + + PDMAudioHostEnumDelete(&pThis->DeviceEnum); + + int rc2 = RTCritSectDelete(&pThis->CritSect); + AssertRC(rc2); + + LogFlowFuncLeave(); +} + + +static LPCGUID dsoundConfigQueryGUID(PPDMDRVINS pDrvIns, PCFGMNODE pCfg, const char *pszName, RTUUID *pUuid) +{ + PCPDMDRVHLPR3 pHlp = pDrvIns->pHlpR3; + LPCGUID pGuid = NULL; + + char *pszGuid = NULL; + int rc = pHlp->pfnCFGMQueryStringAlloc(pCfg, pszName, &pszGuid); + if (RT_SUCCESS(rc)) + { + rc = RTUuidFromStr(pUuid, pszGuid); + if (RT_SUCCESS(rc)) + pGuid = (LPCGUID)&pUuid; + else + DSLOGREL(("DSound: Error parsing device GUID for device '%s': %Rrc\n", pszName, rc)); + + RTStrFree(pszGuid); + } + + return pGuid; +} + + +static void dsoundConfigInit(PDRVHOSTDSOUND pThis, PCFGMNODE pCfg) +{ + pThis->Cfg.pGuidPlay = dsoundConfigQueryGUID(pThis->pDrvIns, pCfg, "DeviceGuidOut", &pThis->Cfg.uuidPlay); + pThis->Cfg.pGuidCapture = dsoundConfigQueryGUID(pThis->pDrvIns, pCfg, "DeviceGuidIn", &pThis->Cfg.uuidCapture); + + DSLOG(("DSound: Configuration: DeviceGuidOut {%RTuuid}, DeviceGuidIn {%RTuuid}\n", + &pThis->Cfg.uuidPlay, + &pThis->Cfg.uuidCapture)); +} + + +/** + * @callback_method_impl{FNPDMDRVCONSTRUCT, + * Construct a DirectSound Audio driver instance.} + */ +static DECLCALLBACK(int) drvHostDSoundConstruct(PPDMDRVINS pDrvIns, PCFGMNODE pCfg, uint32_t fFlags) +{ + PDMDRV_CHECK_VERSIONS_RETURN(pDrvIns); + PDRVHOSTDSOUND pThis = PDMINS_2_DATA(pDrvIns, PDRVHOSTDSOUND); + RT_NOREF(fFlags); + LogRel(("Audio: Initializing DirectSound audio driver\n")); + + /* + * Init basic data members and interfaces. + */ + RTListInit(&pThis->HeadStreams); + pThis->pDrvIns = pDrvIns; + /* IBase */ + pDrvIns->IBase.pfnQueryInterface = drvHostDSoundQueryInterface; + /* IHostAudio */ + pThis->IHostAudio.pfnGetConfig = drvHostDSoundHA_GetConfig; + pThis->IHostAudio.pfnGetDevices = drvHostDSoundHA_GetDevices; + pThis->IHostAudio.pfnSetDevice = NULL; + pThis->IHostAudio.pfnGetStatus = drvHostDSoundHA_GetStatus; + pThis->IHostAudio.pfnDoOnWorkerThread = NULL; + pThis->IHostAudio.pfnStreamConfigHint = NULL; + pThis->IHostAudio.pfnStreamCreate = drvHostDSoundHA_StreamCreate; + pThis->IHostAudio.pfnStreamInitAsync = NULL; + pThis->IHostAudio.pfnStreamDestroy = drvHostDSoundHA_StreamDestroy; + pThis->IHostAudio.pfnStreamNotifyDeviceChanged = NULL; + pThis->IHostAudio.pfnStreamEnable = drvHostDSoundHA_StreamEnable; + pThis->IHostAudio.pfnStreamDisable = drvHostDSoundHA_StreamDisable; + pThis->IHostAudio.pfnStreamPause = drvHostDSoundHA_StreamPause; + pThis->IHostAudio.pfnStreamResume = drvHostDSoundHA_StreamResume; + pThis->IHostAudio.pfnStreamDrain = drvHostDSoundHA_StreamDrain; + pThis->IHostAudio.pfnStreamGetState = drvHostDSoundHA_StreamGetState; + pThis->IHostAudio.pfnStreamGetPending = NULL; + pThis->IHostAudio.pfnStreamGetWritable = drvHostDSoundHA_StreamGetWritable; + pThis->IHostAudio.pfnStreamPlay = drvHostDSoundHA_StreamPlay; + pThis->IHostAudio.pfnStreamGetReadable = drvHostDSoundHA_StreamGetReadable; + pThis->IHostAudio.pfnStreamCapture = drvHostDSoundHA_StreamCapture; + + /* + * Init the static parts. + */ + PDMAudioHostEnumInit(&pThis->DeviceEnum); + + pThis->fEnabledIn = false; + pThis->fEnabledOut = false; + + /* + * Verify that IDirectSound is available. + */ + LPDIRECTSOUND pDirectSound = NULL; + HRESULT hrc = CoCreateInstance(CLSID_DirectSound, NULL, CLSCTX_ALL, IID_IDirectSound, (void **)&pDirectSound); + if (SUCCEEDED(hrc)) + IDirectSound_Release(pDirectSound); + else + { + LogRel(("DSound: DirectSound not available: %Rhrc\n", hrc)); + return VERR_AUDIO_BACKEND_INIT_FAILED; + } + +#ifdef VBOX_WITH_AUDIO_MMNOTIFICATION_CLIENT + /* + * Set up WASAPI device change notifications (Vista+). + */ + if (RTSystemGetNtVersion() >= RTSYSTEM_MAKE_NT_VERSION(6, 0, 0)) + { + /* Get the notification interface (from DrvAudio). */ +# ifdef VBOX_WITH_AUDIO_CALLBACKS + PPDMIHOSTAUDIOPORT pIHostAudioPort = PDMIBASE_QUERY_INTERFACE(pDrvIns->pUpBase, PDMIHOSTAUDIOPORT); + Assert(pIHostAudioPort); +# else + PPDMIHOSTAUDIOPORT pIHostAudioPort = NULL; +# endif +#ifdef RT_EXCEPTIONS_ENABLED + try +#endif + { + pThis->m_pNotificationClient = new DrvHostAudioDSoundMMNotifClient(pIHostAudioPort, + pThis->Cfg.pGuidCapture == NULL, + pThis->Cfg.pGuidPlay == NULL); + } +#ifdef RT_EXCEPTIONS_ENABLED + catch (std::bad_alloc &) + { + return VERR_NO_MEMORY; + } +#else + AssertReturn(pThis->m_pNotificationClient, VERR_NO_MEMORY); +#endif + + hrc = pThis->m_pNotificationClient->Initialize(); + if (SUCCEEDED(hrc)) + { + hrc = pThis->m_pNotificationClient->Register(); + if (SUCCEEDED(hrc)) + LogRel2(("DSound: Notification client is enabled (ver %#RX64)\n", RTSystemGetNtVersion())); + else + { + LogRel(("DSound: Notification client registration failed: %Rhrc\n", hrc)); + return VERR_AUDIO_BACKEND_INIT_FAILED; + } + } + else + { + LogRel(("DSound: Notification client initialization failed: %Rhrc\n", hrc)); + return VERR_AUDIO_BACKEND_INIT_FAILED; + } + } + else + LogRel2(("DSound: Notification client is disabled (ver %#RX64)\n", RTSystemGetNtVersion())); +#endif + + /* + * Initialize configuration values and critical section. + */ + dsoundConfigInit(pThis, pCfg); + return RTCritSectInit(&pThis->CritSect); +} + + +/** + * PDM driver registration. + */ +const PDMDRVREG g_DrvHostDSound = +{ + /* u32Version */ + PDM_DRVREG_VERSION, + /* szName */ + "DSoundAudio", + /* szRCMod */ + "", + /* szR0Mod */ + "", + /* pszDescription */ + "DirectSound Audio host driver", + /* fFlags */ + PDM_DRVREG_FLAGS_HOST_BITS_DEFAULT, + /* fClass. */ + PDM_DRVREG_CLASS_AUDIO, + /* cMaxInstances */ + ~0U, + /* cbInstance */ + sizeof(DRVHOSTDSOUND), + /* pfnConstruct */ + drvHostDSoundConstruct, + /* pfnDestruct */ + drvHostDSoundDestruct, + /* pfnRelocate */ + NULL, + /* pfnIOCtl */ + NULL, + /* pfnPowerOn */ + NULL, + /* pfnReset */ + NULL, + /* pfnSuspend */ + NULL, + /* pfnResume */ + NULL, + /* pfnAttach */ + NULL, + /* pfnDetach */ + NULL, + /* pfnPowerOff */ + NULL, + /* pfnSoftReset */ + NULL, + /* u32EndVersion */ + PDM_DRVREG_VERSION +}; diff --git a/src/VBox/Devices/Audio/DrvHostAudioDSoundMMNotifClient.cpp b/src/VBox/Devices/Audio/DrvHostAudioDSoundMMNotifClient.cpp new file mode 100644 index 00000000..71d438b5 --- /dev/null +++ b/src/VBox/Devices/Audio/DrvHostAudioDSoundMMNotifClient.cpp @@ -0,0 +1,237 @@ +/* $Id: DrvHostAudioDSoundMMNotifClient.cpp $ */ +/** @file + * Host audio driver - DSound - Implementation of the IMMNotificationClient interface to detect audio endpoint changes. + */ + +/* + * Copyright (C) 2017-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 + */ + +#include "DrvHostAudioDSoundMMNotifClient.h" + +#include <iprt/win/windows.h> +#include <mmdeviceapi.h> +#include <iprt/win/endpointvolume.h> +#include <iprt/errcore.h> + +#ifdef LOG_GROUP /** @todo r=bird: wtf? Put it before all other includes like you're supposed to. */ +# undef LOG_GROUP +#endif +#define LOG_GROUP LOG_GROUP_DRV_HOST_AUDIO +#include <VBox/log.h> + + +DrvHostAudioDSoundMMNotifClient::DrvHostAudioDSoundMMNotifClient(PPDMIHOSTAUDIOPORT pInterface, bool fDefaultIn, bool fDefaultOut) + : m_fDefaultIn(fDefaultIn) + , m_fDefaultOut(fDefaultOut) + , m_fRegisteredClient(false) + , m_cRef(1) + , m_pIAudioNotifyFromHost(pInterface) +{ +} + +DrvHostAudioDSoundMMNotifClient::~DrvHostAudioDSoundMMNotifClient(void) +{ +} + +/** + * Registers the mulitmedia notification client implementation. + */ +HRESULT DrvHostAudioDSoundMMNotifClient::Register(void) +{ + HRESULT hr = m_pEnum->RegisterEndpointNotificationCallback(this); + if (SUCCEEDED(hr)) + m_fRegisteredClient = true; + + return hr; +} + +/** + * Unregisters the mulitmedia notification client implementation. + */ +void DrvHostAudioDSoundMMNotifClient::Unregister(void) +{ + if (m_fRegisteredClient) + { + m_pEnum->UnregisterEndpointNotificationCallback(this); + + m_fRegisteredClient = false; + } +} + +/** + * Initializes the mulitmedia notification client implementation. + * + * @return HRESULT + */ +HRESULT DrvHostAudioDSoundMMNotifClient::Initialize(void) +{ + HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), 0, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), + (void **)&m_pEnum); + + LogFunc(("Returning %Rhrc\n", hr)); + return hr; +} + +/** + * Handler implementation which is called when an audio device state + * has been changed. + * + * @return HRESULT + * @param pwstrDeviceId Device ID the state is announced for. + * @param dwNewState New state the device is now in. + */ +STDMETHODIMP DrvHostAudioDSoundMMNotifClient::OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState) +{ + char *pszState = "unknown"; + + switch (dwNewState) + { + case DEVICE_STATE_ACTIVE: + pszState = "active"; + break; + case DEVICE_STATE_DISABLED: + pszState = "disabled"; + break; + case DEVICE_STATE_NOTPRESENT: + pszState = "not present"; + break; + case DEVICE_STATE_UNPLUGGED: + pszState = "unplugged"; + break; + default: + break; + } + + LogRel(("Audio: Device '%ls' has changed state to '%s'\n", pwstrDeviceId, pszState)); + + if (m_pIAudioNotifyFromHost) + m_pIAudioNotifyFromHost->pfnNotifyDevicesChanged(m_pIAudioNotifyFromHost); + + return S_OK; +} + +/** + * Handler implementation which is called when a new audio device has been added. + * + * @return HRESULT + * @param pwstrDeviceId Device ID which has been added. + */ +STDMETHODIMP DrvHostAudioDSoundMMNotifClient::OnDeviceAdded(LPCWSTR pwstrDeviceId) +{ + LogRel(("Audio: Device '%ls' has been added\n", pwstrDeviceId)); + /* Note! It is hard to properly support non-default devices when the backend is DSound, + as DSound talks GUID where-as the pwszDeviceId string we get here is something + completely different. So, ignorining that edge case here. The WasApi backend + supports this, though. */ + if (m_pIAudioNotifyFromHost) + m_pIAudioNotifyFromHost->pfnNotifyDevicesChanged(m_pIAudioNotifyFromHost); + return S_OK; +} + +/** + * Handler implementation which is called when an audio device has been removed. + * + * @return HRESULT + * @param pwstrDeviceId Device ID which has been removed. + */ +STDMETHODIMP DrvHostAudioDSoundMMNotifClient::OnDeviceRemoved(LPCWSTR pwstrDeviceId) +{ + LogRel(("Audio: Device '%ls' has been removed\n", pwstrDeviceId)); + if (m_pIAudioNotifyFromHost) + m_pIAudioNotifyFromHost->pfnNotifyDevicesChanged(m_pIAudioNotifyFromHost); + return S_OK; +} + +/** + * Handler implementation which is called when the device audio device has been + * changed. + * + * @return HRESULT + * @param eFlow Flow direction of the new default device. + * @param eRole Role of the new default device. + * @param pwstrDefaultDeviceId ID of the new default device. + */ +STDMETHODIMP DrvHostAudioDSoundMMNotifClient::OnDefaultDeviceChanged(EDataFlow eFlow, ERole eRole, LPCWSTR pwstrDefaultDeviceId) +{ + /* When the user triggers a default device change, we'll typically get two or + three notifications. Just pick up the one for the multimedia role for now + (dunno if DSound default equals eMultimedia or eConsole, and whether it make + any actual difference). */ + if (eRole == eMultimedia) + { + PDMAUDIODIR enmDir = PDMAUDIODIR_INVALID; + char *pszRole = "unknown"; + if (eFlow == eRender) + { + pszRole = "output"; + if (m_fDefaultOut) + enmDir = PDMAUDIODIR_OUT; + } + else if (eFlow == eCapture) + { + pszRole = "input"; + if (m_fDefaultIn) + enmDir = PDMAUDIODIR_IN; + } + + LogRel(("Audio: Default %s device has been changed to '%ls'\n", pszRole, pwstrDefaultDeviceId)); + + if (m_pIAudioNotifyFromHost) + { + if (enmDir != PDMAUDIODIR_INVALID) + m_pIAudioNotifyFromHost->pfnNotifyDeviceChanged(m_pIAudioNotifyFromHost, enmDir, NULL); + m_pIAudioNotifyFromHost->pfnNotifyDevicesChanged(m_pIAudioNotifyFromHost); + } + } + return S_OK; +} + +STDMETHODIMP DrvHostAudioDSoundMMNotifClient::QueryInterface(REFIID interfaceID, void **ppvInterface) +{ + const IID MY_IID_IMMNotificationClient = __uuidof(IMMNotificationClient); + + if ( IsEqualIID(interfaceID, IID_IUnknown) + || IsEqualIID(interfaceID, MY_IID_IMMNotificationClient)) + { + *ppvInterface = static_cast<IMMNotificationClient*>(this); + AddRef(); + return S_OK; + } + + *ppvInterface = NULL; + return E_NOINTERFACE; +} + +STDMETHODIMP_(ULONG) DrvHostAudioDSoundMMNotifClient::AddRef(void) +{ + return InterlockedIncrement(&m_cRef); +} + +STDMETHODIMP_(ULONG) DrvHostAudioDSoundMMNotifClient::Release(void) +{ + long lRef = InterlockedDecrement(&m_cRef); + if (lRef == 0) + delete this; + + return lRef; +} + diff --git a/src/VBox/Devices/Audio/DrvHostAudioDSoundMMNotifClient.h b/src/VBox/Devices/Audio/DrvHostAudioDSoundMMNotifClient.h new file mode 100644 index 00000000..6ad11bf0 --- /dev/null +++ b/src/VBox/Devices/Audio/DrvHostAudioDSoundMMNotifClient.h @@ -0,0 +1,87 @@ +/* $Id: DrvHostAudioDSoundMMNotifClient.h $ */ +/** @file + * Host audio driver - DSound - Implementation of the IMMNotificationClient interface to detect audio endpoint changes. + */ + +/* + * Copyright (C) 2017-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 + */ + +#ifndef VBOX_INCLUDED_SRC_Audio_DrvHostAudioDSoundMMNotifClient_h +#define VBOX_INCLUDED_SRC_Audio_DrvHostAudioDSoundMMNotifClient_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include <iprt/critsect.h> +#include <iprt/win/windows.h> +#include <mmdeviceapi.h> + +#include <VBox/vmm/pdmaudioifs.h> + + +class DrvHostAudioDSoundMMNotifClient : public IMMNotificationClient +{ +public: + + DrvHostAudioDSoundMMNotifClient(PPDMIHOSTAUDIOPORT pInterface, bool fDefaultIn, bool fDefaultOut); + virtual ~DrvHostAudioDSoundMMNotifClient(); + + HRESULT Initialize(); + + HRESULT Register(void); + void Unregister(void); + + /** @name IUnknown interface + * @{ */ + IFACEMETHODIMP_(ULONG) Release(); + /** @} */ + +private: + + bool m_fDefaultIn; + bool m_fDefaultOut; + bool m_fRegisteredClient; + IMMDeviceEnumerator *m_pEnum; + IMMDevice *m_pEndpoint; + + long m_cRef; + + PPDMIHOSTAUDIOPORT m_pIAudioNotifyFromHost; + + /** @name IMMNotificationClient interface + * @{ */ + IFACEMETHODIMP OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState); + IFACEMETHODIMP OnDeviceAdded(LPCWSTR pwstrDeviceId); + IFACEMETHODIMP OnDeviceRemoved(LPCWSTR pwstrDeviceId); + IFACEMETHODIMP OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDefaultDeviceId); + IFACEMETHODIMP OnPropertyValueChanged(LPCWSTR /*pwstrDeviceId*/, const PROPERTYKEY /*key*/) { return S_OK; } + /** @} */ + + /** @name IUnknown interface + * @{ */ + IFACEMETHODIMP QueryInterface(const IID& iid, void** ppUnk); + IFACEMETHODIMP_(ULONG) AddRef(); + /** @} */ +}; + +#endif /* !VBOX_INCLUDED_SRC_Audio_DrvHostAudioDSoundMMNotifClient_h */ + diff --git a/src/VBox/Devices/Audio/DrvHostAudioDebug.cpp b/src/VBox/Devices/Audio/DrvHostAudioDebug.cpp new file mode 100644 index 00000000..8f496280 --- /dev/null +++ b/src/VBox/Devices/Audio/DrvHostAudioDebug.cpp @@ -0,0 +1,409 @@ +/* $Id: DrvHostAudioDebug.cpp $ */ +/** @file + * Host audio driver - Debug - For dumping and injecting audio data from/to the device emulation. + */ + +/* + * Copyright (C) 2016-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 + */ + + +/********************************************************************************************************************************* +* 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 <iprt/uuid.h> /* For PDMIBASE_2_PDMDRV. */ + +#include "AudioHlp.h" +#include "AudioTest.h" +#include "VBoxDD.h" + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +/** + * Debug host audio stream. + */ +typedef struct DRVHSTAUDDEBUGSTREAM +{ + /** Common part. */ + PDMAUDIOBACKENDSTREAM Core; + /** The stream's acquired configuration. */ + PDMAUDIOSTREAMCFG Cfg; + /** Audio file to dump output to or read input from. */ + PAUDIOHLPFILE pFile; + union + { + AUDIOTESTTONE In; + }; +} DRVHSTAUDDEBUGSTREAM; +/** Pointer to a debug host audio stream. */ +typedef DRVHSTAUDDEBUGSTREAM *PDRVHSTAUDDEBUGSTREAM; + +/** + * Debug audio driver instance data. + * @implements PDMIAUDIOCONNECTOR + */ +typedef struct DRVHSTAUDDEBUG +{ + /** Pointer to the driver instance structure. */ + PPDMDRVINS pDrvIns; + /** Pointer to host audio interface. */ + PDMIHOSTAUDIO IHostAudio; +} DRVHSTAUDDEBUG; +/** Pointer to a debug host audio driver. */ +typedef DRVHSTAUDDEBUG *PDRVHSTAUDDEBUG; + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnGetConfig} + */ +static DECLCALLBACK(int) drvHstAudDebugHA_GetConfig(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDCFG pBackendCfg) +{ + RT_NOREF(pInterface); + AssertPtrReturn(pBackendCfg, VERR_INVALID_POINTER); + + /* + * Fill in the config structure. + */ + RTStrCopy(pBackendCfg->szName, sizeof(pBackendCfg->szName), "DebugAudio"); + pBackendCfg->cbStream = sizeof(DRVHSTAUDDEBUGSTREAM); + pBackendCfg->fFlags = 0; + pBackendCfg->cMaxStreamsOut = 1; /* Output; writing to a file. */ + pBackendCfg->cMaxStreamsIn = 1; /* Input; generates a sine wave. */ + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnGetStatus} + */ +static DECLCALLBACK(PDMAUDIOBACKENDSTS) drvHstAudDebugHA_GetStatus(PPDMIHOSTAUDIO pInterface, PDMAUDIODIR enmDir) +{ + RT_NOREF(pInterface, enmDir); + return PDMAUDIOBACKENDSTS_RUNNING; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamCreate} + */ +static DECLCALLBACK(int) drvHstAudDebugHA_StreamCreate(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + PCPDMAUDIOSTREAMCFG pCfgReq, PPDMAUDIOSTREAMCFG pCfgAcq) +{ + PDRVHSTAUDDEBUG pThis = RT_FROM_MEMBER(pInterface, DRVHSTAUDDEBUG, IHostAudio); + PDRVHSTAUDDEBUGSTREAM pStreamDbg = (PDRVHSTAUDDEBUGSTREAM)pStream; + AssertPtrReturn(pStreamDbg, VERR_INVALID_POINTER); + AssertPtrReturn(pCfgReq, VERR_INVALID_POINTER); + AssertPtrReturn(pCfgAcq, VERR_INVALID_POINTER); + + PDMAudioStrmCfgCopy(&pStreamDbg->Cfg, pCfgAcq); + + if (pCfgReq->enmDir == PDMAUDIODIR_IN) + AudioTestToneInitRandom(&pStreamDbg->In, &pStreamDbg->Cfg.Props); + + int rc = AudioHlpFileCreateAndOpenEx(&pStreamDbg->pFile, AUDIOHLPFILETYPE_WAV, NULL /*use temp dir*/, + pThis->pDrvIns->iInstance, AUDIOHLPFILENAME_FLAGS_NONE, AUDIOHLPFILE_FLAGS_NONE, + &pCfgReq->Props, RTFILE_O_WRITE | RTFILE_O_DENY_WRITE | RTFILE_O_CREATE_REPLACE, + pCfgReq->enmDir == PDMAUDIODIR_IN ? "DebugAudioIn" : "DebugAudioOut"); + if (RT_FAILURE(rc)) + LogRel(("DebugAudio: Failed to creating debug file for %s stream '%s' in the temp directory: %Rrc\n", + pCfgReq->enmDir == PDMAUDIODIR_IN ? "input" : "output", pCfgReq->szName, rc)); + + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDestroy} + */ +static DECLCALLBACK(int) drvHstAudDebugHA_StreamDestroy(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + bool fImmediate) +{ + RT_NOREF(pInterface, fImmediate); + PDRVHSTAUDDEBUGSTREAM pStreamDbg = (PDRVHSTAUDDEBUGSTREAM)pStream; + AssertPtrReturn(pStreamDbg, VERR_INVALID_POINTER); + + if (pStreamDbg->pFile) + { + AudioHlpFileDestroy(pStreamDbg->pFile); + pStreamDbg->pFile = NULL; + } + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamEnable} + */ +static DECLCALLBACK(int) drvHstAudDebugHA_StreamEnable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface, pStream); + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDisable} + */ +static DECLCALLBACK(int) drvHstAudDebugHA_StreamDisable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface, pStream); + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamPause} + */ +static DECLCALLBACK(int) drvHstAudDebugHA_StreamPause(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface, pStream); + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamResume} + */ +static DECLCALLBACK(int) drvHstAudDebugHA_StreamResume(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface, pStream); + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDrain} + */ +static DECLCALLBACK(int) drvHstAudDebugHA_StreamDrain(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface, pStream); + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetState} + */ +static DECLCALLBACK(PDMHOSTAUDIOSTREAMSTATE) drvHstAudDebugHA_StreamGetState(PPDMIHOSTAUDIO pInterface, + PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + AssertPtrReturn(pStream, PDMHOSTAUDIOSTREAMSTATE_INVALID); + return PDMHOSTAUDIOSTREAMSTATE_OKAY; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetPending} + */ +static DECLCALLBACK(uint32_t) drvHstAudDebugHA_StreamGetPending(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface, pStream); + return 0; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetWritable} + */ +static DECLCALLBACK(uint32_t) drvHstAudDebugHA_StreamGetWritable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface, pStream); + return UINT32_MAX; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamPlay} + */ +static DECLCALLBACK(int) drvHstAudDebugHA_StreamPlay(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + const void *pvBuf, uint32_t cbBuf, uint32_t *pcbWritten) +{ + RT_NOREF(pInterface); + PDRVHSTAUDDEBUGSTREAM pStreamDbg = (PDRVHSTAUDDEBUGSTREAM)pStream; + + int rc = AudioHlpFileWrite(pStreamDbg->pFile, pvBuf, cbBuf); + if (RT_SUCCESS(rc)) + *pcbWritten = cbBuf; + else + LogRelMax(32, ("DebugAudio: Writing output failed with %Rrc\n", rc)); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetReadable} + */ +static DECLCALLBACK(uint32_t) drvHstAudDebugHA_StreamGetReadable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PDRVHSTAUDDEBUGSTREAM pStreamDbg = (PDRVHSTAUDDEBUGSTREAM)pStream; + + return PDMAudioPropsMilliToBytes(&pStreamDbg->Cfg.Props, 10 /*ms*/); +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamCapture} + */ +static DECLCALLBACK(int) drvHstAudDebugHA_StreamCapture(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + void *pvBuf, uint32_t cbBuf, uint32_t *pcbRead) +{ + RT_NOREF(pInterface); + PDRVHSTAUDDEBUGSTREAM pStreamDbg = (PDRVHSTAUDDEBUGSTREAM)pStream; +/** @todo rate limit this? */ + + uint32_t cbWritten; + int rc = AudioTestToneGenerate(&pStreamDbg->In, pvBuf, cbBuf, &cbWritten); + if (RT_SUCCESS(rc)) + { + /* + * Write it. + */ + rc = AudioHlpFileWrite(pStreamDbg->pFile, pvBuf, cbWritten); + if (RT_SUCCESS(rc)) + *pcbRead = cbWritten; + } + + if (RT_FAILURE(rc)) + LogRelMax(32, ("DebugAudio: Writing input failed with %Rrc\n", rc)); + + return rc; +} + + +/** + * @interface_method_impl{PDMIBASE,pfnQueryInterface} + */ +static DECLCALLBACK(void *) drvHstAudDebugQueryInterface(PPDMIBASE pInterface, const char *pszIID) +{ + PPDMDRVINS pDrvIns = PDMIBASE_2_PDMDRV(pInterface); + PDRVHSTAUDDEBUG pThis = PDMINS_2_DATA(pDrvIns, PDRVHSTAUDDEBUG); + + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIBASE, &pDrvIns->IBase); + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIHOSTAUDIO, &pThis->IHostAudio); + return NULL; +} + + +/** + * Constructs a Null audio driver instance. + * + * @copydoc FNPDMDRVCONSTRUCT + */ +static DECLCALLBACK(int) drvHstAudDebugConstruct(PPDMDRVINS pDrvIns, PCFGMNODE pCfg, uint32_t fFlags) +{ + RT_NOREF(pCfg, fFlags); + PDMDRV_CHECK_VERSIONS_RETURN(pDrvIns); + PDRVHSTAUDDEBUG pThis = PDMINS_2_DATA(pDrvIns, PDRVHSTAUDDEBUG); + LogRel(("Audio: Initializing DEBUG driver\n")); + + /* + * Init the static parts. + */ + pThis->pDrvIns = pDrvIns; + /* IBase */ + pDrvIns->IBase.pfnQueryInterface = drvHstAudDebugQueryInterface; + /* IHostAudio */ + pThis->IHostAudio.pfnGetConfig = drvHstAudDebugHA_GetConfig; + pThis->IHostAudio.pfnGetDevices = NULL; + pThis->IHostAudio.pfnSetDevice = NULL; + pThis->IHostAudio.pfnGetStatus = drvHstAudDebugHA_GetStatus; + pThis->IHostAudio.pfnDoOnWorkerThread = NULL; + pThis->IHostAudio.pfnStreamConfigHint = NULL; + pThis->IHostAudio.pfnStreamCreate = drvHstAudDebugHA_StreamCreate; + pThis->IHostAudio.pfnStreamInitAsync = NULL; + pThis->IHostAudio.pfnStreamDestroy = drvHstAudDebugHA_StreamDestroy; + pThis->IHostAudio.pfnStreamNotifyDeviceChanged = NULL; + pThis->IHostAudio.pfnStreamEnable = drvHstAudDebugHA_StreamEnable; + pThis->IHostAudio.pfnStreamDisable = drvHstAudDebugHA_StreamDisable; + pThis->IHostAudio.pfnStreamPause = drvHstAudDebugHA_StreamPause; + pThis->IHostAudio.pfnStreamResume = drvHstAudDebugHA_StreamResume; + pThis->IHostAudio.pfnStreamDrain = drvHstAudDebugHA_StreamDrain; + pThis->IHostAudio.pfnStreamGetState = drvHstAudDebugHA_StreamGetState; + pThis->IHostAudio.pfnStreamGetPending = drvHstAudDebugHA_StreamGetPending; + pThis->IHostAudio.pfnStreamGetWritable = drvHstAudDebugHA_StreamGetWritable; + pThis->IHostAudio.pfnStreamPlay = drvHstAudDebugHA_StreamPlay; + pThis->IHostAudio.pfnStreamGetReadable = drvHstAudDebugHA_StreamGetReadable; + pThis->IHostAudio.pfnStreamCapture = drvHstAudDebugHA_StreamCapture; + + return VINF_SUCCESS; +} + +/** + * Char driver registration record. + */ +const PDMDRVREG g_DrvHostDebugAudio = +{ + /* u32Version */ + PDM_DRVREG_VERSION, + /* szName */ + "DebugAudio", + /* szRCMod */ + "", + /* szR0Mod */ + "", + /* pszDescription */ + "Debug audio host driver", + /* fFlags */ + PDM_DRVREG_FLAGS_HOST_BITS_DEFAULT, + /* fClass. */ + PDM_DRVREG_CLASS_AUDIO, + /* cMaxInstances */ + ~0U, + /* cbInstance */ + sizeof(DRVHSTAUDDEBUG), + /* pfnConstruct */ + drvHstAudDebugConstruct, + /* pfnDestruct */ + NULL, + /* pfnRelocate */ + NULL, + /* pfnIOCtl */ + NULL, + /* pfnPowerOn */ + NULL, + /* pfnReset */ + NULL, + /* pfnSuspend */ + NULL, + /* pfnResume */ + NULL, + /* pfnAttach */ + NULL, + /* pfnDetach */ + NULL, + /* pfnPowerOff */ + NULL, + /* pfnSoftReset */ + NULL, + /* u32EndVersion */ + PDM_DRVREG_VERSION +}; diff --git a/src/VBox/Devices/Audio/DrvHostAudioNull.cpp b/src/VBox/Devices/Audio/DrvHostAudioNull.cpp new file mode 100644 index 00000000..34c79f60 --- /dev/null +++ b/src/VBox/Devices/Audio/DrvHostAudioNull.cpp @@ -0,0 +1,328 @@ +/* $Id: DrvHostAudioNull.cpp $ */ +/** @file + * Host audio driver - NULL (bitbucket). + * + * This also acts as a fallback if no other backend is available. + */ + +/* + * Copyright (C) 2006-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 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/uuid.h> /* For PDMIBASE_2_PDMDRV. */ + +#define LOG_GROUP LOG_GROUP_DRV_HOST_AUDIO +#include <VBox/log.h> +#include <VBox/vmm/pdmaudioifs.h> +#include <VBox/vmm/pdmaudioinline.h> + +#include "VBoxDD.h" + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +/** Null audio stream. */ +typedef struct DRVHSTAUDNULLSTREAM +{ + /** Common part. */ + PDMAUDIOBACKENDSTREAM Core; + /** The stream's acquired configuration. */ + PDMAUDIOSTREAMCFG Cfg; +} DRVHSTAUDNULLSTREAM; +/** Pointer to a null audio stream. */ +typedef DRVHSTAUDNULLSTREAM *PDRVHSTAUDNULLSTREAM; + + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnGetConfig} + */ +static DECLCALLBACK(int) drvHstAudNullHA_GetConfig(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDCFG pBackendCfg) +{ + NOREF(pInterface); + AssertPtrReturn(pBackendCfg, VERR_INVALID_POINTER); + + /* + * Fill in the config structure. + */ + RTStrCopy(pBackendCfg->szName, sizeof(pBackendCfg->szName), "NULL audio"); + pBackendCfg->cbStream = sizeof(DRVHSTAUDNULLSTREAM); + pBackendCfg->fFlags = 0; + pBackendCfg->cMaxStreamsOut = 1; /* Output */ + pBackendCfg->cMaxStreamsIn = 2; /* Line input + microphone input. */ + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnGetStatus} + */ +static DECLCALLBACK(PDMAUDIOBACKENDSTS) drvHstAudNullHA_GetStatus(PPDMIHOSTAUDIO pInterface, PDMAUDIODIR enmDir) +{ + RT_NOREF(pInterface, enmDir); + return PDMAUDIOBACKENDSTS_RUNNING; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamCreate} + */ +static DECLCALLBACK(int) drvHstAudNullHA_StreamCreate(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + PCPDMAUDIOSTREAMCFG pCfgReq, PPDMAUDIOSTREAMCFG pCfgAcq) +{ + RT_NOREF(pInterface); + PDRVHSTAUDNULLSTREAM pStreamNull = (PDRVHSTAUDNULLSTREAM)pStream; + AssertPtrReturn(pStreamNull, VERR_INVALID_POINTER); + AssertPtrReturn(pCfgReq, VERR_INVALID_POINTER); + AssertPtrReturn(pCfgAcq, VERR_INVALID_POINTER); + + PDMAudioStrmCfgCopy(&pStreamNull->Cfg, pCfgAcq); + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDestroy} + */ +static DECLCALLBACK(int) drvHstAudNullHA_StreamDestroy(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, bool fImmediate) +{ + RT_NOREF(pInterface, pStream, fImmediate); + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamEnable} + */ +static DECLCALLBACK(int) drvHstAudNullHA_StreamControlStub(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface, pStream); + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetState} + */ +static DECLCALLBACK(PDMHOSTAUDIOSTREAMSTATE) drvHstAudNullHA_StreamGetState(PPDMIHOSTAUDIO pInterface, + PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + AssertPtrReturn(pStream, PDMHOSTAUDIOSTREAMSTATE_INVALID); +#if 0 + /* Bit bucket appraoch where we ignore the output and silence the input buffers. */ + return PDMHOSTAUDIOSTREAMSTATE_OKAY; +#else + /* Approach where the mixer in the devices skips us and saves a few CPU cycles. */ + return PDMHOSTAUDIOSTREAMSTATE_INACTIVE; +#endif +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetPending} + */ +static DECLCALLBACK(uint32_t) drvHstAudNullHA_StreamGetPending(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface, pStream); + return 0; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetWritable} + */ +static DECLCALLBACK(uint32_t) drvHstAudNullHA_StreamGetWritable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface, pStream); + return UINT32_MAX; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamPlay} + */ +static DECLCALLBACK(int) drvHstAudNullHA_StreamPlay(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + const void *pvBuf, uint32_t cbBuf, uint32_t *pcbWritten) +{ + RT_NOREF(pInterface, pStream, pvBuf); + + /* The bitbucket never overflows. */ + *pcbWritten = cbBuf; + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetReadable} + */ +static DECLCALLBACK(uint32_t) drvHstAudNullHA_StreamGetReadable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface, pStream); + /** @todo rate limit this? */ + return UINT32_MAX; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamCapture} + */ +static DECLCALLBACK(int) drvHstAudNullHA_StreamCapture(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + void *pvBuf, uint32_t cbBuf, uint32_t *pcbRead) +{ + RT_NOREF(pInterface); + PDRVHSTAUDNULLSTREAM pStreamNull = (PDRVHSTAUDNULLSTREAM)pStream; + + /** @todo rate limit this? */ + + /* Return silence. */ + PDMAudioPropsClearBuffer(&pStreamNull->Cfg.Props, pvBuf, cbBuf, + PDMAudioPropsBytesToFrames(&pStreamNull->Cfg.Props, cbBuf)); + *pcbRead = cbBuf; + return VINF_SUCCESS; +} + + +/** + * This is used directly by DrvAudio when a backend fails to initialize in a + * non-fatal manner. + */ +DECL_HIDDEN_CONST(PDMIHOSTAUDIO) const g_DrvHostAudioNull = +{ + /* .pfnGetConfig =*/ drvHstAudNullHA_GetConfig, + /* .pfnGetDevices =*/ NULL, + /* .pfnSetDevice =*/ NULL, + /* .pfnGetStatus =*/ drvHstAudNullHA_GetStatus, + /* .pfnDoOnWorkerThread =*/ NULL, + /* .pfnStreamConfigHint =*/ NULL, + /* .pfnStreamCreate =*/ drvHstAudNullHA_StreamCreate, + /* .pfnStreamInitAsync =*/ NULL, + /* .pfnStreamDestroy =*/ drvHstAudNullHA_StreamDestroy, + /* .pfnStreamNotifyDeviceChanged =*/ NULL, + /* .pfnStreamEnable =*/ drvHstAudNullHA_StreamControlStub, + /* .pfnStreamDisable =*/ drvHstAudNullHA_StreamControlStub, + /* .pfnStreamPause =*/ drvHstAudNullHA_StreamControlStub, + /* .pfnStreamResume =*/ drvHstAudNullHA_StreamControlStub, + /* .pfnStreamDrain =*/ drvHstAudNullHA_StreamControlStub, + /* .pfnStreamGetState =*/ drvHstAudNullHA_StreamGetState, + /* .pfnStreamGetPending =*/ drvHstAudNullHA_StreamGetPending, + /* .pfnStreamGetWritable =*/ drvHstAudNullHA_StreamGetWritable, + /* .pfnStreamPlay =*/ drvHstAudNullHA_StreamPlay, + /* .pfnStreamGetReadable =*/ drvHstAudNullHA_StreamGetReadable, + /* .pfnStreamCapture =*/ drvHstAudNullHA_StreamCapture, +}; + + +/** + * @interface_method_impl{PDMIBASE,pfnQueryInterface} + */ +static DECLCALLBACK(void *) drvHstAudNullQueryInterface(PPDMIBASE pInterface, const char *pszIID) +{ + PPDMDRVINS pDrvIns = PDMIBASE_2_PDMDRV(pInterface); + PPDMIHOSTAUDIO pThis = PDMINS_2_DATA(pDrvIns, PPDMIHOSTAUDIO); + + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIBASE, &pDrvIns->IBase); + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIHOSTAUDIO, pThis); + return NULL; +} + + +/** + * Constructs a Null audio driver instance. + * + * @copydoc FNPDMDRVCONSTRUCT + */ +static DECLCALLBACK(int) drvHstAudNullConstruct(PPDMDRVINS pDrvIns, PCFGMNODE pCfg, uint32_t fFlags) +{ + PDMDRV_CHECK_VERSIONS_RETURN(pDrvIns); + PPDMIHOSTAUDIO pThis = PDMINS_2_DATA(pDrvIns, PPDMIHOSTAUDIO); + RT_NOREF(pCfg, fFlags); + LogRel(("Audio: Initializing NULL driver\n")); + + /* + * Init the static parts. + */ + /* IBase */ + pDrvIns->IBase.pfnQueryInterface = drvHstAudNullQueryInterface; + /* IHostAudio */ + *pThis = g_DrvHostAudioNull; + + return VINF_SUCCESS; +} + + +/** + * Char driver registration record. + */ +const PDMDRVREG g_DrvHostNullAudio = +{ + /* u32Version */ + PDM_DRVREG_VERSION, + /* szName */ + "NullAudio", + /* szRCMod */ + "", + /* szR0Mod */ + "", + /* pszDescription */ + "NULL audio host driver", + /* fFlags */ + PDM_DRVREG_FLAGS_HOST_BITS_DEFAULT, + /* fClass. */ + PDM_DRVREG_CLASS_AUDIO, + /* cMaxInstances */ + ~0U, + /* cbInstance */ + sizeof(PDMIHOSTAUDIO), + /* pfnConstruct */ + drvHstAudNullConstruct, + /* pfnDestruct */ + NULL, + /* pfnRelocate */ + NULL, + /* pfnIOCtl */ + NULL, + /* pfnPowerOn */ + NULL, + /* pfnReset */ + NULL, + /* pfnSuspend */ + NULL, + /* pfnResume */ + NULL, + /* pfnAttach */ + NULL, + /* pfnDetach */ + NULL, + /* pfnPowerOff */ + NULL, + /* pfnSoftReset */ + NULL, + /* u32EndVersion */ + PDM_DRVREG_VERSION +}; + diff --git a/src/VBox/Devices/Audio/DrvHostAudioOss.cpp b/src/VBox/Devices/Audio/DrvHostAudioOss.cpp new file mode 100644 index 00000000..d4228fc7 --- /dev/null +++ b/src/VBox/Devices/Audio/DrvHostAudioOss.cpp @@ -0,0 +1,1027 @@ +/* $Id: DrvHostAudioOss.cpp $ */ +/** @file + * Host audio driver - OSS (Open Sound System). + */ + +/* + * 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 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <errno.h> +#include <fcntl.h> +#include <sys/ioctl.h> +#include <sys/mman.h> +#include <sys/soundcard.h> +#include <unistd.h> + +#include <iprt/alloc.h> +#include <iprt/thread.h> +#include <iprt/uuid.h> /* For PDMIBASE_2_PDMDRV. */ + +#define LOG_GROUP LOG_GROUP_DRV_HOST_AUDIO +#include <VBox/log.h> +#include <VBox/vmm/pdmaudioifs.h> +#include <VBox/vmm/pdmaudioinline.h> + +#include "VBoxDD.h" + + +/********************************************************************************************************************************* +* Defines * +*********************************************************************************************************************************/ +#if ((SOUND_VERSION > 360) && (defined(OSS_SYSINFO))) +/* OSS > 3.6 has a new syscall available for querying a bit more detailed information + * about OSS' audio capabilities. This is handy for e.g. Solaris. */ +# define VBOX_WITH_AUDIO_OSS_SYSINFO 1 +#endif + + +/********************************************************************************************************************************* +* Structures * +*********************************************************************************************************************************/ +/** + * OSS audio stream configuration. + */ +typedef struct DRVHSTAUDOSSSTREAMCFG +{ + PDMAUDIOPCMPROPS Props; + uint16_t cFragments; + /** The log2 of cbFragment. */ + uint16_t cbFragmentLog2; + uint32_t cbFragment; +} DRVHSTAUDOSSSTREAMCFG; +/** Pointer to an OSS audio stream configuration. */ +typedef DRVHSTAUDOSSSTREAMCFG *PDRVHSTAUDOSSSTREAMCFG; + +/** + * OSS audio stream. + */ +typedef struct DRVHSTAUDOSSSTREAM +{ + /** Common part. */ + PDMAUDIOBACKENDSTREAM Core; + /** The file descriptor. */ + int hFile; + /** Buffer alignment. */ + uint8_t uAlign; + /** Set if we're draining the stream (output only). */ + bool fDraining; + /** Internal stream byte offset. */ + uint64_t offInternal; + /** The stream's acquired configuration. */ + PDMAUDIOSTREAMCFG Cfg; + /** The acquired OSS configuration. */ + DRVHSTAUDOSSSTREAMCFG OssCfg; + /** Handle to the thread draining output streams. */ + RTTHREAD hThreadDrain; +} DRVHSTAUDOSSSTREAM; +/** Pointer to an OSS audio stream. */ +typedef DRVHSTAUDOSSSTREAM *PDRVHSTAUDOSSSTREAM; + +/** + * OSS host audio driver instance data. + * @implements PDMIAUDIOCONNECTOR + */ +typedef struct DRVHSTAUDOSS +{ + /** Pointer to the driver instance structure. */ + PPDMDRVINS pDrvIns; + /** Pointer to host audio interface. */ + PDMIHOSTAUDIO IHostAudio; + /** Error count for not flooding the release log. + * UINT32_MAX for unlimited logging. */ + uint32_t cLogErrors; +} DRVHSTAUDOSS; +/** Pointer to the instance data for an OSS host audio driver. */ +typedef DRVHSTAUDOSS *PDRVHSTAUDOSS; + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** The path to the output OSS device. */ +static char g_szPathOutputDev[] = "/dev/dsp"; +/** The path to the input OSS device. */ +static char g_szPathInputDev[] = "/dev/dsp"; + + + +static int drvHstAudOssToPdmAudioProps(PPDMAUDIOPCMPROPS pProps, int fmt, int cChannels, int uHz) +{ + switch (fmt) + { + case AFMT_S8: + PDMAudioPropsInit(pProps, 1 /*8-bit*/, true /*signed*/, cChannels, uHz); + break; + + case AFMT_U8: + PDMAudioPropsInit(pProps, 1 /*8-bit*/, false /*signed*/, cChannels, uHz); + break; + + case AFMT_S16_LE: + PDMAudioPropsInitEx(pProps, 2 /*16-bit*/, true /*signed*/, cChannels, uHz, true /*fLittleEndian*/, false /*fRaw*/); + break; + + case AFMT_U16_LE: + PDMAudioPropsInitEx(pProps, 2 /*16-bit*/, false /*signed*/, cChannels, uHz, true /*fLittleEndian*/, false /*fRaw*/); + break; + + case AFMT_S16_BE: + PDMAudioPropsInitEx(pProps, 2 /*16-bit*/, true /*signed*/, cChannels, uHz, false /*fLittleEndian*/, false /*fRaw*/); + break; + + case AFMT_U16_BE: + PDMAudioPropsInitEx(pProps, 2 /*16-bit*/, false /*signed*/, cChannels, uHz, false /*fLittleEndian*/, false /*fRaw*/); + break; + + default: + AssertMsgFailedReturn(("Format %d not supported\n", fmt), VERR_NOT_SUPPORTED); + } + + return VINF_SUCCESS; +} + + +static int drvHstAudOssStreamClose(int *phFile) +{ + if (!phFile || !*phFile || *phFile == -1) + return VINF_SUCCESS; + + int rc; + if (close(*phFile)) + { + rc = RTErrConvertFromErrno(errno); + LogRel(("OSS: Closing stream failed: %s / %Rrc\n", strerror(errno), rc)); + } + else + { + *phFile = -1; + rc = VINF_SUCCESS; + } + + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnGetConfig} + */ +static DECLCALLBACK(int) drvHstAudOssHA_GetConfig(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDCFG pBackendCfg) +{ + RT_NOREF(pInterface); + + /* + * Fill in the config structure. + */ + RTStrCopy(pBackendCfg->szName, sizeof(pBackendCfg->szName), "OSS"); + pBackendCfg->cbStream = sizeof(DRVHSTAUDOSSSTREAM); + pBackendCfg->fFlags = 0; + pBackendCfg->cMaxStreamsIn = 0; + pBackendCfg->cMaxStreamsOut = 0; + + int hFile = open("/dev/dsp", O_WRONLY | O_NONBLOCK, 0); + if (hFile == -1) + { + /* Try opening the mixing device instead. */ + hFile = open("/dev/mixer", O_RDONLY | O_NONBLOCK, 0); + } + if (hFile != -1) + { + int ossVer = -1; + int err = ioctl(hFile, OSS_GETVERSION, &ossVer); + if (err == 0) + { + LogRel2(("OSS: Using version: %d\n", ossVer)); +#ifdef VBOX_WITH_AUDIO_OSS_SYSINFO + oss_sysinfo ossInfo; + RT_ZERO(ossInfo); + err = ioctl(hFile, OSS_SYSINFO, &ossInfo); + if (err == 0) + { + LogRel2(("OSS: Number of DSPs: %d\n", ossInfo.numaudios)); + LogRel2(("OSS: Number of mixers: %d\n", ossInfo.nummixers)); + + int cDev = ossInfo.nummixers; + if (!cDev) + cDev = ossInfo.numaudios; + + pBackendCfg->cMaxStreamsIn = UINT32_MAX; + pBackendCfg->cMaxStreamsOut = UINT32_MAX; + } + else +#endif + { + /* Since we cannot query anything, assume that we have at least + * one input and one output if we found "/dev/dsp" or "/dev/mixer". */ + + pBackendCfg->cMaxStreamsIn = UINT32_MAX; + pBackendCfg->cMaxStreamsOut = UINT32_MAX; + } + } + else + LogRel(("OSS: Unable to determine installed version: %s (%d)\n", strerror(err), err)); + close(hFile); + } + else + LogRel(("OSS: No devices found, audio is not available\n")); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnGetStatus} + */ +static DECLCALLBACK(PDMAUDIOBACKENDSTS) drvHstAudOssHA_GetStatus(PPDMIHOSTAUDIO pInterface, PDMAUDIODIR enmDir) +{ + AssertPtrReturn(pInterface, PDMAUDIOBACKENDSTS_UNKNOWN); + RT_NOREF(enmDir); + + return PDMAUDIOBACKENDSTS_RUNNING; +} + + +static int drvHstAudOssStreamConfigure(int hFile, bool fInput, PDRVHSTAUDOSSSTREAMCFG pOSSReq, PDRVHSTAUDOSSSTREAMCFG pOSSAcq) +{ + /* + * Format. + */ + int iFormat; + switch (PDMAudioPropsSampleSize(&pOSSReq->Props)) + { + case 1: + iFormat = pOSSReq->Props.fSigned ? AFMT_S8 : AFMT_U8; + break; + + case 2: + if (PDMAudioPropsIsLittleEndian(&pOSSReq->Props)) + iFormat = pOSSReq->Props.fSigned ? AFMT_S16_LE : AFMT_U16_LE; + else + iFormat = pOSSReq->Props.fSigned ? AFMT_S16_BE : AFMT_U16_BE; + break; + + default: + LogRel2(("OSS: Unsupported sample size: %u\n", PDMAudioPropsSampleSize(&pOSSReq->Props))); + return VERR_AUDIO_STREAM_COULD_NOT_CREATE; + } + AssertLogRelMsgReturn(ioctl(hFile, SNDCTL_DSP_SAMPLESIZE, &iFormat) >= 0, + ("OSS: Failed to set audio format to %d: %s (%d)\n", iFormat, strerror(errno), errno), + RTErrConvertFromErrno(errno)); + + /* + * Channel count. + */ + int cChannels = PDMAudioPropsChannels(&pOSSReq->Props); + AssertLogRelMsgReturn(ioctl(hFile, SNDCTL_DSP_CHANNELS, &cChannels) >= 0, + ("OSS: Failed to set number of audio channels (%RU8): %s (%d)\n", + PDMAudioPropsChannels(&pOSSReq->Props), strerror(errno), errno), + RTErrConvertFromErrno(errno)); + + /* + * Frequency. + */ + int iFrequenc = pOSSReq->Props.uHz; + AssertLogRelMsgReturn(ioctl(hFile, SNDCTL_DSP_SPEED, &iFrequenc) >= 0, + ("OSS: Failed to set audio frequency to %d Hz: %s (%d)\n", pOSSReq->Props.uHz, strerror(errno), errno), + RTErrConvertFromErrno(errno)); + + /* + * Set fragment size and count. + */ + LogRel2(("OSS: Requested %RU16 %s fragments, %RU32 bytes each\n", + pOSSReq->cFragments, fInput ? "input" : "output", pOSSReq->cbFragment)); + + int mmmmssss = (pOSSReq->cFragments << 16) | pOSSReq->cbFragmentLog2; + AssertLogRelMsgReturn(ioctl(hFile, SNDCTL_DSP_SETFRAGMENT, &mmmmssss) >= 0, + ("OSS: Failed to set %RU16 fragments to %RU32 bytes each: %s (%d)\n", + pOSSReq->cFragments, pOSSReq->cbFragment, strerror(errno), errno), + RTErrConvertFromErrno(errno)); + + /* + * Get parameters and popuplate pOSSAcq. + */ + audio_buf_info BufInfo = { 0, 0, 0, 0 }; + AssertLogRelMsgReturn(ioctl(hFile, fInput ? SNDCTL_DSP_GETISPACE : SNDCTL_DSP_GETOSPACE, &BufInfo) >= 0, + ("OSS: Failed to retrieve %s buffer length: %s (%d)\n", + fInput ? "input" : "output", strerror(errno), errno), + RTErrConvertFromErrno(errno)); + + int rc = drvHstAudOssToPdmAudioProps(&pOSSAcq->Props, iFormat, cChannels, iFrequenc); + if (RT_SUCCESS(rc)) + { + pOSSAcq->cFragments = BufInfo.fragstotal; + pOSSAcq->cbFragment = BufInfo.fragsize; + pOSSAcq->cbFragmentLog2 = ASMBitFirstSetU32(BufInfo.fragsize) - 1; + Assert(RT_BIT_32(pOSSAcq->cbFragmentLog2) == pOSSAcq->cbFragment); + + LogRel2(("OSS: Got %RU16 %s fragments, %RU32 bytes each\n", + pOSSAcq->cFragments, fInput ? "input" : "output", pOSSAcq->cbFragment)); + } + + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamCreate} + */ +static DECLCALLBACK(int) drvHstAudOssHA_StreamCreate(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + PCPDMAUDIOSTREAMCFG pCfgReq, PPDMAUDIOSTREAMCFG pCfgAcq) +{ + AssertPtr(pInterface); RT_NOREF(pInterface); + PDRVHSTAUDOSSSTREAM pStreamOSS = (PDRVHSTAUDOSSSTREAM)pStream; + AssertPtrReturn(pStreamOSS, VERR_INVALID_POINTER); + AssertPtrReturn(pCfgReq, VERR_INVALID_POINTER); + AssertPtrReturn(pCfgAcq, VERR_INVALID_POINTER); + + pStreamOSS->hThreadDrain = NIL_RTTHREAD; + + /* + * Open the device + */ + int rc; + if (pCfgReq->enmDir == PDMAUDIODIR_IN) + pStreamOSS->hFile = open(g_szPathInputDev, O_RDONLY); + else + pStreamOSS->hFile = open(g_szPathOutputDev, O_WRONLY); + if (pStreamOSS->hFile >= 0) + { + /* + * Configure it. + * + * Note! We limit the output channels to mono or stereo for now just + * to keep things simple and avoid wasting time here. If the + * channel count isn't a power of two, our code below trips up + * on the fragment size. We'd also need to try report/get + * channel mappings and whatnot. + */ + DRVHSTAUDOSSSTREAMCFG ReqOssCfg; + RT_ZERO(ReqOssCfg); + + memcpy(&ReqOssCfg.Props, &pCfgReq->Props, sizeof(PDMAUDIOPCMPROPS)); + if (PDMAudioPropsChannels(&ReqOssCfg.Props) > 2) + { + LogRel2(("OSS: Limiting output to two channels, requested %u.\n", PDMAudioPropsChannels(&ReqOssCfg.Props) )); + PDMAudioPropsSetChannels(&ReqOssCfg.Props, 2); + } + + ReqOssCfg.cbFragmentLog2 = 12; + ReqOssCfg.cbFragment = RT_BIT_32(ReqOssCfg.cbFragmentLog2); + uint32_t const cbBuffer = PDMAudioPropsFramesToBytes(&ReqOssCfg.Props, pCfgReq->Backend.cFramesBufferSize); + ReqOssCfg.cFragments = cbBuffer >> ReqOssCfg.cbFragmentLog2; + AssertLogRelStmt(cbBuffer < ((uint32_t)0x7ffe << ReqOssCfg.cbFragmentLog2), ReqOssCfg.cFragments = 0x7ffe); + + rc = drvHstAudOssStreamConfigure(pStreamOSS->hFile, pCfgReq->enmDir == PDMAUDIODIR_IN, &ReqOssCfg, &pStreamOSS->OssCfg); + if (RT_SUCCESS(rc)) + { + pStreamOSS->uAlign = 0; /** @todo r=bird: Where did the correct assignment of this go? */ + + /* + * Complete the stream structure and fill in the pCfgAcq bits. + */ + if ((pStreamOSS->OssCfg.cFragments * pStreamOSS->OssCfg.cbFragment) & pStreamOSS->uAlign) + LogRel(("OSS: Warning: Misaligned playback buffer: Size = %zu, Alignment = %u\n", + pStreamOSS->OssCfg.cFragments * pStreamOSS->OssCfg.cbFragment, pStreamOSS->uAlign + 1)); + + memcpy(&pCfgAcq->Props, &pStreamOSS->OssCfg.Props, sizeof(PDMAUDIOPCMPROPS)); + pCfgAcq->Backend.cFramesPeriod = PDMAudioPropsBytesToFrames(&pCfgAcq->Props, pStreamOSS->OssCfg.cbFragment); + pCfgAcq->Backend.cFramesBufferSize = pCfgAcq->Backend.cFramesPeriod * pStreamOSS->OssCfg.cFragments; + pCfgAcq->Backend.cFramesPreBuffering = (uint64_t)pCfgReq->Backend.cFramesPreBuffering + * pCfgAcq->Backend.cFramesBufferSize + / RT_MAX(pCfgReq->Backend.cFramesBufferSize, 1); + + /* + * Copy the stream config and we're done! + */ + PDMAudioStrmCfgCopy(&pStreamOSS->Cfg, pCfgAcq); + return VINF_SUCCESS; + } + drvHstAudOssStreamClose(&pStreamOSS->hFile); + } + else + { + rc = RTErrConvertFromErrno(errno); + LogRel(("OSS: Failed to open '%s': %s (%d) / %Rrc\n", + pCfgReq->enmDir == PDMAUDIODIR_IN ? g_szPathInputDev : g_szPathOutputDev, strerror(errno), errno, rc)); + } + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDestroy} + */ +static DECLCALLBACK(int) drvHstAudOssHA_StreamDestroy(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, bool fImmediate) +{ + RT_NOREF(pInterface, fImmediate); + PDRVHSTAUDOSSSTREAM pStreamOSS = (PDRVHSTAUDOSSSTREAM)pStream; + AssertPtrReturn(pStreamOSS, VERR_INVALID_POINTER); + + drvHstAudOssStreamClose(&pStreamOSS->hFile); + + if (pStreamOSS->hThreadDrain != NIL_RTTHREAD) + { + int rc = RTThreadWait(pStreamOSS->hThreadDrain, 1, NULL); + AssertRC(rc); + pStreamOSS->hThreadDrain = NIL_RTTHREAD; + } + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamEnable} + */ +static DECLCALLBACK(int) drvHstAudOssHA_StreamEnable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PDRVHSTAUDOSSSTREAM pStreamOSS = (PDRVHSTAUDOSSSTREAM)pStream; + int rc; + + /* + * This is most probably untested... + */ + if (pStreamOSS->fDraining) + { + LogFlowFunc(("Still draining...\n")); + rc = RTThreadWait(pStreamOSS->hThreadDrain, 0 /*ms*/, NULL); + if (RT_FAILURE(rc)) + { + LogFlowFunc(("Resetting...\n")); + ioctl(pStreamOSS->hFile, SNDCTL_DSP_RESET, NULL); + rc = RTThreadWait(pStreamOSS->hThreadDrain, 0 /*ms*/, NULL); + if (RT_FAILURE(rc)) + { + LogFlowFunc(("Poking...\n")); + RTThreadPoke(pStreamOSS->hThreadDrain); + rc = RTThreadWait(pStreamOSS->hThreadDrain, 1 /*ms*/, NULL); + } + } + if (RT_SUCCESS(rc)) + { + LogFlowFunc(("Done draining.\n")); + pStreamOSS->hThreadDrain = NIL_RTTHREAD; + } + else + LogFlowFunc(("No, still draining...\n")); + pStreamOSS->fDraining = false; + } + + /* + * Enable the stream. + */ + int fMask = pStreamOSS->Cfg.enmDir == PDMAUDIODIR_IN ? PCM_ENABLE_INPUT : PCM_ENABLE_OUTPUT; + if (ioctl(pStreamOSS->hFile, SNDCTL_DSP_SETTRIGGER, &fMask) >= 0) + rc = VINF_SUCCESS; + else + { + LogRel(("OSS: Failed to enable output stream: %s (%d)\n", strerror(errno), errno)); + rc = RTErrConvertFromErrno(errno); + } + + LogFlowFunc(("returns %Rrc for '%s'\n", rc, pStreamOSS->Cfg.szName)); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDisable} + */ +static DECLCALLBACK(int) drvHstAudOssHA_StreamDisable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PDRVHSTAUDOSSSTREAM pStreamOSS = (PDRVHSTAUDOSSSTREAM)pStream; + LogFlowFunc(("Stream '%s'\n", pStreamOSS->Cfg.szName)); + int rc; + + /* + * If we're still draining, try kick the thread before we try disable the stream. + */ + if (pStreamOSS->fDraining) + { + LogFlowFunc(("Trying to cancel draining...\n")); + if (pStreamOSS->hThreadDrain != NIL_RTTHREAD) + { + RTThreadPoke(pStreamOSS->hThreadDrain); + rc = RTThreadWait(pStreamOSS->hThreadDrain, 1 /*ms*/, NULL); + if (RT_SUCCESS(rc) || rc == VERR_INVALID_HANDLE) + pStreamOSS->fDraining = false; + else + LogFunc(("Failed to cancel draining (%Rrc)\n", rc)); + } + else + { + LogFlowFunc(("Thread handle is NIL, so we can't be draining\n")); + pStreamOSS->fDraining = false; + } + } + + /* + * The Official documentation says this isn't the right way to stop + * playback. It may work in some implementations but fail in all others... + * Suggest SNDCTL_DSP_RESET / SNDCTL_DSP_HALT. + * + * So, let's do both and see how that works out... + */ + rc = VINF_SUCCESS; + int fMask = 0; + if (ioctl(pStreamOSS->hFile, SNDCTL_DSP_SETTRIGGER, &fMask) >= 0) + LogFlowFunc(("SNDCTL_DSP_SETTRIGGER succeeded\n")); + else + { + LogRel(("OSS: Failed to clear triggers for stream '%s': %s (%d)\n", pStreamOSS->Cfg.szName, strerror(errno), errno)); + rc = RTErrConvertFromErrno(errno); + } + + if (ioctl(pStreamOSS->hFile, SNDCTL_DSP_RESET, NULL) >= 0) + LogFlowFunc(("SNDCTL_DSP_RESET succeeded\n")); + else + { + LogRel(("OSS: Failed to reset stream '%s': %s (%d)\n", pStreamOSS->Cfg.szName, strerror(errno), errno)); + rc = RTErrConvertFromErrno(errno); + } + + LogFlowFunc(("returns %Rrc for '%s'\n", rc, pStreamOSS->Cfg.szName)); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamPause} + */ +static DECLCALLBACK(int) drvHstAudOssHA_StreamPause(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + return drvHstAudOssHA_StreamDisable(pInterface, pStream); +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamResume} + */ +static DECLCALLBACK(int) drvHstAudOssHA_StreamResume(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + return drvHstAudOssHA_StreamEnable(pInterface, pStream); +} + + +/** + * @callback_method_impl{FNRTTHREAD, + * Thread for calling SNDCTL_DSP_SYNC (blocking) on an output stream.} + */ +static DECLCALLBACK(int) drvHstAudOssDrainThread(RTTHREAD ThreadSelf, void *pvUser) +{ + RT_NOREF(ThreadSelf); + PDRVHSTAUDOSSSTREAM pStreamOSS = (PDRVHSTAUDOSSSTREAM)pvUser; + int rc; + + /* Make it blocking (for Linux). */ + int fOrgFlags = fcntl(pStreamOSS->hFile, F_GETFL, 0); + LogFunc(("F_GETFL -> %#x\n", fOrgFlags)); + Assert(fOrgFlags != -1); + if (fOrgFlags != -1 && (fOrgFlags & O_NONBLOCK)) + { + rc = fcntl(pStreamOSS->hFile, F_SETFL, fOrgFlags & ~O_NONBLOCK); + AssertStmt(rc != -1, fOrgFlags = -1); + } + + /* Drain it. */ + LogFunc(("Calling SNDCTL_DSP_SYNC now...\n")); + rc = ioctl(pStreamOSS->hFile, SNDCTL_DSP_SYNC, NULL); + LogFunc(("SNDCTL_DSP_SYNC returned %d / errno=%d\n", rc, errno)); RT_NOREF(rc); + + /* Re-enable non-blocking mode and disable it. */ + if (fOrgFlags != -1) + { + rc = fcntl(pStreamOSS->hFile, F_SETFL, fOrgFlags); + Assert(rc != -1); + + int fMask = 0; + rc = ioctl(pStreamOSS->hFile, SNDCTL_DSP_SETTRIGGER, &fMask); + Assert(rc >= 0); + + pStreamOSS->fDraining = false; + LogFunc(("Restored non-block mode and cleared the trigger mask\n")); + } + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDrain} + */ +static DECLCALLBACK(int) drvHstAudOssHA_StreamDrain(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + PDRVHSTAUDOSS pThis = RT_FROM_MEMBER(pInterface, DRVHSTAUDOSS, IHostAudio); + PDRVHSTAUDOSSSTREAM pStreamOSS = (PDRVHSTAUDOSSSTREAM)pStream; + AssertReturn(pStreamOSS->Cfg.enmDir == PDMAUDIODIR_OUT, VERR_WRONG_ORDER); + + pStreamOSS->fDraining = true; + + /* + * Because the SNDCTL_DSP_SYNC call is blocking on real OSS, + * we kick off a thread to deal with it as we're probably on EMT + * and cannot block for extended periods. + */ + if (pStreamOSS->hThreadDrain != NIL_RTTHREAD) + { + int rc = RTThreadWait(pStreamOSS->hThreadDrain, 0, NULL); + if (RT_SUCCESS(rc)) + { + pStreamOSS->hThreadDrain = NIL_RTTHREAD; + LogFunc(("Cleaned up stale thread handle.\n")); + } + else + { + LogFunc(("Drain thread already running (%Rrc).\n", rc)); + AssertMsg(rc == VERR_TIMEOUT, ("%Rrc\n", rc)); + return rc == VERR_TIMEOUT ? VINF_SUCCESS : rc; + } + } + + int rc = RTThreadCreateF(&pStreamOSS->hThreadDrain, drvHstAudOssDrainThread, pStreamOSS, 0, + RTTHREADTYPE_IO, RTTHREADFLAGS_WAITABLE, "ossdrai%u", pThis->pDrvIns->iInstance); + LogFunc(("Started drain thread: %Rrc\n", rc)); + AssertRCReturn(rc, rc); + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetState} + */ +static DECLCALLBACK(PDMHOSTAUDIOSTREAMSTATE) drvHstAudOssHA_StreamGetState(PPDMIHOSTAUDIO pInterface, + PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PDRVHSTAUDOSSSTREAM pStreamOSS = (PDRVHSTAUDOSSSTREAM)pStream; + AssertPtrReturn(pStreamOSS, PDMHOSTAUDIOSTREAMSTATE_INVALID); + if (!pStreamOSS->fDraining) + return PDMHOSTAUDIOSTREAMSTATE_OKAY; + return PDMHOSTAUDIOSTREAMSTATE_DRAINING; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetWritable} + */ +static DECLCALLBACK(uint32_t) drvHstAudOssHA_StreamGetWritable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PDRVHSTAUDOSSSTREAM pStreamOSS = (PDRVHSTAUDOSSSTREAM)pStream; + AssertPtr(pStreamOSS); + + /* + * The logic here must match what StreamPlay does. + * + * Note! We now use 'bytes' rather than the fragments * fragsize as we used + * to do (up to 2021), as these are documented as obsolete. + */ + audio_buf_info BufInfo = { 0, 0, 0, 0 }; + int rc2 = ioctl(pStreamOSS->hFile, SNDCTL_DSP_GETOSPACE, &BufInfo); + AssertMsgReturn(rc2 >= 0, ("SNDCTL_DSP_GETOSPACE failed: %s (%d)\n", strerror(errno), errno), 0); + + /* Try use the size. */ + uint32_t cbRet; + uint32_t const cbBuf = pStreamOSS->OssCfg.cbFragment * pStreamOSS->OssCfg.cFragments; + if (BufInfo.bytes >= 0 && (unsigned)BufInfo.bytes <= cbBuf) + cbRet = BufInfo.bytes; + else + { + AssertMsgFailed(("Invalid available size: %d\n", BufInfo.bytes)); + AssertMsgReturn(BufInfo.fragments >= 0, ("fragments: %d\n", BufInfo.fragments), 0); + AssertMsgReturn(BufInfo.fragsize >= 0, ("fragsize: %d\n", BufInfo.fragsize), 0); + cbRet = (uint32_t)(BufInfo.fragments * BufInfo.fragsize); + AssertMsgStmt(cbRet <= cbBuf, ("fragsize*fragments: %d, cbBuf=%#x\n", cbRet, cbBuf), 0); + } + + Log4Func(("returns %#x (%u) [cbBuf=%#x]\n", cbRet, cbRet, cbBuf)); + return cbRet; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamPlay} + */ +static DECLCALLBACK(int) drvHstAudOssHA_StreamPlay(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + const void *pvBuf, uint32_t cbBuf, uint32_t *pcbWritten) +{ + RT_NOREF(pInterface); + PDRVHSTAUDOSSSTREAM pStreamOSS = (PDRVHSTAUDOSSSTREAM)pStream; + AssertPtrReturn(pStreamOSS, VERR_INVALID_POINTER); + + /* + * Return immediately if this is a draining service call. + * + * Otherwise the ioctl below will race the drain thread and sometimes fail, + * triggering annoying assertion and release logging. + */ + if (cbBuf || !pStreamOSS->fDraining) + { /* likely */ } + else + { + *pcbWritten = 0; + return VINF_SUCCESS; + } + + /* + * Figure out now much to write (same as drvHstAudOssHA_StreamGetWritable, + * must match exactly). + */ + audio_buf_info BufInfo; + int rc2 = ioctl(pStreamOSS->hFile, SNDCTL_DSP_GETOSPACE, &BufInfo); + AssertLogRelMsgReturn(rc2 >= 0, ("OSS: Failed to retrieve current playback buffer: %s (%d, hFile=%d, rc2=%d)\n", + strerror(errno), errno, pStreamOSS->hFile, rc2), + RTErrConvertFromErrno(errno)); + + uint32_t cbToWrite; + uint32_t const cbStreamBuf = pStreamOSS->OssCfg.cbFragment * pStreamOSS->OssCfg.cFragments; + if (BufInfo.bytes >= 0 && (unsigned)BufInfo.bytes <= cbStreamBuf) + cbToWrite = BufInfo.bytes; + else + { + AssertMsgFailed(("Invalid available size: %d\n", BufInfo.bytes)); + AssertMsgReturn(BufInfo.fragments >= 0, ("fragments: %d\n", BufInfo.fragments), 0); + AssertMsgReturn(BufInfo.fragsize >= 0, ("fragsize: %d\n", BufInfo.fragsize), 0); + cbToWrite = (uint32_t)(BufInfo.fragments * BufInfo.fragsize); + AssertMsgStmt(cbToWrite <= cbStreamBuf, ("fragsize*fragments: %d, cbStreamBuf=%#x\n", cbToWrite, cbStreamBuf), 0); + } + + cbToWrite = RT_MIN(cbToWrite, cbBuf); + Log3Func(("@%#RX64 cbBuf=%#x BufInfo: fragments=%#x fragstotal=%#x fragsize=%#x bytes=%#x %s cbToWrite=%#x\n", + pStreamOSS->offInternal, cbBuf, BufInfo.fragments, BufInfo.fragstotal, BufInfo.fragsize, BufInfo.bytes, + pStreamOSS->Cfg.szName, cbToWrite)); + + /* + * Write. + */ + uint8_t const *pbBuf = (uint8_t const *)pvBuf; + uint32_t cbChunk = cbToWrite; + uint32_t offChunk = 0; + while (cbChunk > 0) + { + ssize_t cbWritten = write(pStreamOSS->hFile, &pbBuf[offChunk], RT_MIN(cbChunk, pStreamOSS->OssCfg.cbFragment)); + if (cbWritten > 0) + { + AssertLogRelMsg(!(cbWritten & pStreamOSS->uAlign), + ("OSS: Misaligned write (written %#zx, alignment %#x)\n", cbWritten, pStreamOSS->uAlign)); + + Assert((uint32_t)cbWritten <= cbChunk); + offChunk += (uint32_t)cbWritten; + cbChunk -= (uint32_t)cbWritten; + pStreamOSS->offInternal += cbWritten; + } + else if (cbWritten == 0) + { + LogFunc(("@%#RX64 write(%#x) returned zeroed (previously wrote %#x bytes)!\n", + pStreamOSS->offInternal, RT_MIN(cbChunk, pStreamOSS->OssCfg.cbFragment), cbWritten)); + break; + } + else + { + LogRel(("OSS: Failed writing output data: %s (%d)\n", strerror(errno), errno)); + return RTErrConvertFromErrno(errno); + } + } + + *pcbWritten = offChunk; + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetReadable} + */ +static DECLCALLBACK(uint32_t) drvHstAudOssHA_StreamGetReadable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PDRVHSTAUDOSSSTREAM pStreamOSS = (PDRVHSTAUDOSSSTREAM)pStream; + AssertPtr(pStreamOSS); + + /* + * Use SNDCTL_DSP_GETISPACE to see how much we can read. + * + * Note! We now use 'bytes' rather than the fragments * fragsize as we used + * to do (up to 2021), as these are documented as obsolete. + */ + audio_buf_info BufInfo = { 0, 0, 0, 0 }; + int rc2 = ioctl(pStreamOSS->hFile, SNDCTL_DSP_GETISPACE, &BufInfo); + AssertMsgReturn(rc2 >= 0, ("SNDCTL_DSP_GETISPACE failed: %s (%d)\n", strerror(errno), errno), 0); + + uint32_t cbRet; + uint32_t const cbBuf = pStreamOSS->OssCfg.cbFragment * pStreamOSS->OssCfg.cFragments; + if (BufInfo.bytes >= 0 && (unsigned)BufInfo.bytes <= cbBuf) + cbRet = BufInfo.bytes; + else + { + AssertMsgFailed(("Invalid available size: %d\n", BufInfo.bytes)); + AssertMsgReturn(BufInfo.fragments >= 0, ("fragments: %d\n", BufInfo.fragments), 0); + AssertMsgReturn(BufInfo.fragsize >= 0, ("fragsize: %d\n", BufInfo.fragsize), 0); + cbRet = (uint32_t)(BufInfo.fragments * BufInfo.fragsize); + AssertMsgStmt(cbRet <= cbBuf, ("fragsize*fragments: %d, cbBuf=%#x\n", cbRet, cbBuf), 0); + } + + /* + * HACK ALERT! To force the stream to start recording, we read a frame + * here if we get back that there are zero bytes available + * and we're at the start of the stream. (We cannot just + * return a frame size, we have to read it, as pre-buffering + * would prevent it from being read.) + */ + if (BufInfo.bytes > 0 || pStreamOSS->offInternal != 0) + { /* likely */ } + else + { + uint32_t cbToRead = PDMAudioPropsFramesToBytes(&pStreamOSS->Cfg.Props, 1); + uint8_t abFrame[256]; + Assert(cbToRead < sizeof(abFrame)); + ssize_t cbRead = read(pStreamOSS->hFile, abFrame, cbToRead); + RT_NOREF(cbRead); + LogFunc(("Dummy read for '%s' returns %zd (errno=%d)\n", pStreamOSS->Cfg.szName, cbRead, errno)); + } + + Log4Func(("returns %#x (%u) [cbBuf=%#x]\n", cbRet, cbRet, cbBuf)); + return cbRet; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamCapture} + */ +static DECLCALLBACK(int) drvHstAudOssHA_StreamCapture(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + void *pvBuf, uint32_t cbBuf, uint32_t *pcbRead) +{ + RT_NOREF(pInterface); + PDRVHSTAUDOSSSTREAM pStreamOSS = (PDRVHSTAUDOSSSTREAM)pStream; + AssertPtrReturn(pStreamOSS, VERR_INVALID_POINTER); + Log3Func(("@%#RX64 cbBuf=%#x %s\n", pStreamOSS->offInternal, cbBuf, pStreamOSS->Cfg.szName)); + + size_t cbToRead = cbBuf; + uint8_t * const pbDst = (uint8_t *)pvBuf; + size_t offWrite = 0; + while (cbToRead > 0) + { + ssize_t cbRead = read(pStreamOSS->hFile, &pbDst[offWrite], cbToRead); + if (cbRead > 0) + { + LogFlowFunc(("cbRead=%zi, offWrite=%zu cbToRead=%zu\n", cbRead, offWrite, cbToRead)); + Assert((ssize_t)cbToRead >= cbRead); + cbToRead -= cbRead; + offWrite += cbRead; + pStreamOSS->offInternal += cbRead; + } + else + { + LogFunc(("cbRead=%zi, offWrite=%zu cbToRead=%zu errno=%d\n", cbRead, offWrite, cbToRead, errno)); + + /* Don't complain about errors if we've retrieved some audio data already. */ + if (cbRead < 0 && offWrite == 0 && errno != EINTR && errno != EAGAIN) + { + AssertStmt(errno != 0, errno = EACCES); + int rc = RTErrConvertFromErrno(errno); + LogFunc(("Failed to read %zu input frames, errno=%d rc=%Rrc\n", cbToRead, errno, rc)); + return rc; + } + break; + } + } + + *pcbRead = offWrite; + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIBASE,pfnQueryInterface} + */ +static DECLCALLBACK(void *) drvHstAudOssQueryInterface(PPDMIBASE pInterface, const char *pszIID) +{ + PPDMDRVINS pDrvIns = PDMIBASE_2_PDMDRV(pInterface); + PDRVHSTAUDOSS pThis = PDMINS_2_DATA(pDrvIns, PDRVHSTAUDOSS); + + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIBASE, &pDrvIns->IBase); + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIHOSTAUDIO, &pThis->IHostAudio); + + return NULL; +} + + +/** + * @interface_method_impl{PDMDRVREG,pfnConstruct, + * Constructs an OSS audio driver instance.} + */ +static DECLCALLBACK(int) drvHstAudOssConstruct(PPDMDRVINS pDrvIns, PCFGMNODE pCfg, uint32_t fFlags) +{ + RT_NOREF(pCfg, fFlags); + PDMDRV_CHECK_VERSIONS_RETURN(pDrvIns); + PDRVHSTAUDOSS pThis = PDMINS_2_DATA(pDrvIns, PDRVHSTAUDOSS); + LogRel(("Audio: Initializing OSS driver\n")); + + /* + * Init the static parts. + */ + pThis->pDrvIns = pDrvIns; + /* IBase */ + pDrvIns->IBase.pfnQueryInterface = drvHstAudOssQueryInterface; + /* IHostAudio */ + pThis->IHostAudio.pfnGetConfig = drvHstAudOssHA_GetConfig; + pThis->IHostAudio.pfnGetDevices = NULL; + pThis->IHostAudio.pfnSetDevice = NULL; + pThis->IHostAudio.pfnGetStatus = drvHstAudOssHA_GetStatus; + pThis->IHostAudio.pfnDoOnWorkerThread = NULL; + pThis->IHostAudio.pfnStreamConfigHint = NULL; + pThis->IHostAudio.pfnStreamCreate = drvHstAudOssHA_StreamCreate; + pThis->IHostAudio.pfnStreamInitAsync = NULL; + pThis->IHostAudio.pfnStreamDestroy = drvHstAudOssHA_StreamDestroy; + pThis->IHostAudio.pfnStreamNotifyDeviceChanged = NULL; + pThis->IHostAudio.pfnStreamEnable = drvHstAudOssHA_StreamEnable; + pThis->IHostAudio.pfnStreamDisable = drvHstAudOssHA_StreamDisable; + pThis->IHostAudio.pfnStreamPause = drvHstAudOssHA_StreamPause; + pThis->IHostAudio.pfnStreamResume = drvHstAudOssHA_StreamResume; + pThis->IHostAudio.pfnStreamDrain = drvHstAudOssHA_StreamDrain; + pThis->IHostAudio.pfnStreamGetState = drvHstAudOssHA_StreamGetState; + pThis->IHostAudio.pfnStreamGetPending = NULL; + pThis->IHostAudio.pfnStreamGetWritable = drvHstAudOssHA_StreamGetWritable; + pThis->IHostAudio.pfnStreamPlay = drvHstAudOssHA_StreamPlay; + pThis->IHostAudio.pfnStreamGetReadable = drvHstAudOssHA_StreamGetReadable; + pThis->IHostAudio.pfnStreamCapture = drvHstAudOssHA_StreamCapture; + + return VINF_SUCCESS; +} + + +/** + * OSS driver registration record. + */ +const PDMDRVREG g_DrvHostOSSAudio = +{ + /* u32Version */ + PDM_DRVREG_VERSION, + /* szName */ + "OSSAudio", + /* szRCMod */ + "", + /* szR0Mod */ + "", + /* pszDescription */ + "OSS audio host driver", + /* fFlags */ + PDM_DRVREG_FLAGS_HOST_BITS_DEFAULT, + /* fClass. */ + PDM_DRVREG_CLASS_AUDIO, + /* cMaxInstances */ + ~0U, + /* cbInstance */ + sizeof(DRVHSTAUDOSS), + /* pfnConstruct */ + drvHstAudOssConstruct, + /* pfnDestruct */ + NULL, + /* pfnRelocate */ + NULL, + /* pfnIOCtl */ + NULL, + /* pfnPowerOn */ + NULL, + /* pfnReset */ + NULL, + /* pfnSuspend */ + NULL, + /* pfnResume */ + NULL, + /* pfnAttach */ + NULL, + /* pfnDetach */ + NULL, + /* pfnPowerOff */ + NULL, + /* pfnSoftReset */ + NULL, + /* u32EndVersion */ + PDM_DRVREG_VERSION +}; + diff --git a/src/VBox/Devices/Audio/DrvHostAudioPulseAudio.cpp b/src/VBox/Devices/Audio/DrvHostAudioPulseAudio.cpp new file mode 100644 index 00000000..647e555a --- /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-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 + */ + + +/********************************************************************************************************************************* +* 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 +}; + diff --git a/src/VBox/Devices/Audio/DrvHostAudioPulseAudioStubs.cpp b/src/VBox/Devices/Audio/DrvHostAudioPulseAudioStubs.cpp new file mode 100644 index 00000000..15f2e34e --- /dev/null +++ b/src/VBox/Devices/Audio/DrvHostAudioPulseAudioStubs.cpp @@ -0,0 +1,375 @@ +/* $Id: DrvHostAudioPulseAudioStubs.cpp $ */ +/** @file + * Stubs for libpulse. + */ + +/* + * Copyright (C) 2006-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 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_DRV_HOST_AUDIO +#include <iprt/assert.h> +#include <iprt/errcore.h> +#include <iprt/ldr.h> +#include <VBox/log.h> +#include <iprt/once.h> + +#include <pulse/pulseaudio.h> + +#include "DrvHostAudioPulseAudioStubs.h" + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +#define VBOX_PULSE_LIB "libpulse.so.0" + +#define PROXY_STUB(function, rettype, signature, shortsig) \ + static rettype (*g_pfn_ ## function) signature; \ + \ + extern "C" rettype VBox_##function signature; \ + rettype VBox_##function signature \ + { \ + return g_pfn_ ## function shortsig; \ + } + +#define PROXY_STUB_VOID(function, signature, shortsig) \ + static void (*g_pfn_ ## function) signature; \ + \ + extern "C" void VBox_##function signature; \ + void VBox_##function signature \ + { \ + g_pfn_ ## function shortsig; \ + } + +PROXY_STUB (pa_bytes_per_second, size_t, + (const pa_sample_spec *spec), + (spec)) +PROXY_STUB (pa_bytes_to_usec, pa_usec_t, + (uint64_t l, const pa_sample_spec *spec), + (l, spec)) +PROXY_STUB (pa_channel_map_init_auto, pa_channel_map*, + (pa_channel_map *m, unsigned channels, pa_channel_map_def_t def), + (m, channels, def)) + +PROXY_STUB (pa_context_connect, int, + (pa_context *c, const char *server, pa_context_flags_t flags, + const pa_spawn_api *api), + (c, server, flags, api)) +PROXY_STUB_VOID(pa_context_disconnect, + (pa_context *c), + (c)) +PROXY_STUB (pa_context_get_server_info, pa_operation*, + (pa_context *c, pa_server_info_cb_t cb, void *userdata), + (c, cb, userdata)) +PROXY_STUB (pa_context_get_sink_info_by_name, pa_operation*, + (pa_context *c, const char *name, pa_sink_info_cb_t cb, void *userdata), + (c, name, cb, userdata)) +PROXY_STUB (pa_context_get_sink_info_list, pa_operation *, + (pa_context *c, pa_sink_info_cb_t cb, void *userdata), + (c, cb, userdata)) +PROXY_STUB (pa_context_get_source_info_by_name, pa_operation*, + (pa_context *c, const char *name, pa_source_info_cb_t cb, void *userdata), + (c, name, cb, userdata)) +PROXY_STUB (pa_context_get_source_info_list, pa_operation *, + (pa_context *c, pa_source_info_cb_t cb, void *userdata), + (c, cb, userdata)) +PROXY_STUB (pa_context_get_state, pa_context_state_t, + (pa_context *c), + (c)) +PROXY_STUB_VOID(pa_context_unref, + (pa_context *c), + (c)) +PROXY_STUB (pa_context_errno, int, + (pa_context *c), + (c)) +PROXY_STUB (pa_context_new, pa_context*, + (pa_mainloop_api *mainloop, const char *name), + (mainloop, name)) +PROXY_STUB_VOID(pa_context_set_state_callback, + (pa_context *c, pa_context_notify_cb_t cb, void *userdata), + (c, cb, userdata)) + +PROXY_STUB (pa_frame_size, size_t, + (const pa_sample_spec *spec), + (spec)) +PROXY_STUB (pa_get_library_version, const char *, (void), ()) +PROXY_STUB_VOID(pa_operation_unref, + (pa_operation *o), + (o)) +PROXY_STUB (pa_operation_get_state, pa_operation_state_t, + (pa_operation *o), + (o)) +PROXY_STUB_VOID(pa_operation_cancel, + (pa_operation *o), + (o)) + +PROXY_STUB (pa_rtclock_now, pa_usec_t, + (void), + ()) +PROXY_STUB (pa_sample_format_to_string, const char*, + (pa_sample_format_t f), + (f)) +PROXY_STUB (pa_sample_spec_valid, int, + (const pa_sample_spec *spec), + (spec)) +PROXY_STUB (pa_strerror, const char*, + (int error), + (error)) + +#if PA_PROTOCOL_VERSION >= 16 +PROXY_STUB (pa_stream_connect_playback, int, + (pa_stream *s, const char *dev, const pa_buffer_attr *attr, + pa_stream_flags_t flags, const pa_cvolume *volume, pa_stream *sync_stream), + (s, dev, attr, flags, volume, sync_stream)) +#else +PROXY_STUB (pa_stream_connect_playback, int, + (pa_stream *s, const char *dev, const pa_buffer_attr *attr, + pa_stream_flags_t flags, pa_cvolume *volume, pa_stream *sync_stream), + (s, dev, attr, flags, volume, sync_stream)) +#endif +PROXY_STUB (pa_stream_connect_record, int, + (pa_stream *s, const char *dev, const pa_buffer_attr *attr, + pa_stream_flags_t flags), + (s, dev, attr, flags)) +PROXY_STUB (pa_stream_disconnect, int, + (pa_stream *s), + (s)) +PROXY_STUB (pa_stream_get_sample_spec, const pa_sample_spec*, + (pa_stream *s), + (s)) +PROXY_STUB_VOID(pa_stream_set_latency_update_callback, + (pa_stream *p, pa_stream_notify_cb_t cb, void *userdata), + (p, cb, userdata)) +PROXY_STUB (pa_stream_write, int, + (pa_stream *p, const void *data, size_t bytes, pa_free_cb_t free_cb, + int64_t offset, pa_seek_mode_t seek), + (p, data, bytes, free_cb, offset, seek)) +PROXY_STUB_VOID(pa_stream_unref, + (pa_stream *s), + (s)) +PROXY_STUB (pa_stream_get_state, pa_stream_state_t, + (pa_stream *p), + (p)) +PROXY_STUB (pa_stream_get_latency, int, + (pa_stream *s, pa_usec_t *r_usec, int *negative), + (s, r_usec, negative)) +PROXY_STUB (pa_stream_get_timing_info, pa_timing_info*, + (pa_stream *s), + (s)) +PROXY_STUB (pa_stream_readable_size, size_t, + (pa_stream *p), + (p)) +PROXY_STUB (pa_stream_set_buffer_attr, pa_operation *, + (pa_stream *s, const pa_buffer_attr *attr, pa_stream_success_cb_t cb, void *userdata), + (s, attr, cb, userdata)) +PROXY_STUB_VOID(pa_stream_set_state_callback, + (pa_stream *s, pa_stream_notify_cb_t cb, void *userdata), + (s, cb, userdata)) +PROXY_STUB_VOID(pa_stream_set_underflow_callback, + (pa_stream *s, pa_stream_notify_cb_t cb, void *userdata), + (s, cb, userdata)) +PROXY_STUB_VOID(pa_stream_set_overflow_callback, + (pa_stream *s, pa_stream_notify_cb_t cb, void *userdata), + (s, cb, userdata)) +PROXY_STUB_VOID(pa_stream_set_write_callback, + (pa_stream *s, pa_stream_request_cb_t cb, void *userdata), + (s, cb, userdata)) +PROXY_STUB (pa_stream_flush, pa_operation*, + (pa_stream *s, pa_stream_success_cb_t cb, void *userdata), + (s, cb, userdata)) +PROXY_STUB (pa_stream_drain, pa_operation*, + (pa_stream *s, pa_stream_success_cb_t cb, void *userdata), + (s, cb, userdata)) +PROXY_STUB (pa_stream_trigger, pa_operation*, + (pa_stream *s, pa_stream_success_cb_t cb, void *userdata), + (s, cb, userdata)) +PROXY_STUB (pa_stream_new, pa_stream*, + (pa_context *c, const char *name, const pa_sample_spec *ss, + const pa_channel_map *map), + (c, name, ss, map)) +PROXY_STUB (pa_stream_get_buffer_attr, const pa_buffer_attr*, + (pa_stream *s), + (s)) +PROXY_STUB (pa_stream_peek, int, + (pa_stream *p, const void **data, size_t *bytes), + (p, data, bytes)) +PROXY_STUB (pa_stream_cork, pa_operation*, + (pa_stream *s, int b, pa_stream_success_cb_t cb, void *userdata), + (s, b, cb, userdata)) +PROXY_STUB (pa_stream_drop, int, + (pa_stream *p), + (p)) +PROXY_STUB (pa_stream_writable_size, size_t, + (pa_stream *p), + (p)) + +PROXY_STUB_VOID(pa_threaded_mainloop_stop, + (pa_threaded_mainloop *m), + (m)) +PROXY_STUB (pa_threaded_mainloop_get_api, pa_mainloop_api*, + (pa_threaded_mainloop *m), + (m)) +PROXY_STUB_VOID(pa_threaded_mainloop_free, + (pa_threaded_mainloop* m), + (m)) +PROXY_STUB_VOID(pa_threaded_mainloop_signal, + (pa_threaded_mainloop *m, int wait_for_accept), + (m, wait_for_accept)) +PROXY_STUB_VOID(pa_threaded_mainloop_unlock, + (pa_threaded_mainloop *m), + (m)) +PROXY_STUB (pa_threaded_mainloop_new, pa_threaded_mainloop *, + (void), + ()) +PROXY_STUB_VOID(pa_threaded_mainloop_wait, + (pa_threaded_mainloop *m), + (m)) +PROXY_STUB (pa_threaded_mainloop_start, int, + (pa_threaded_mainloop *m), + (m)) +PROXY_STUB_VOID(pa_threaded_mainloop_lock, + (pa_threaded_mainloop *m), + (m)) + +PROXY_STUB (pa_usec_to_bytes, size_t, + (pa_usec_t t, const pa_sample_spec *spec), + (t, spec)) + +#define FUNC_ENTRY(function) { #function , (void (**)(void)) & g_pfn_ ## function } +static struct +{ + const char *pszName; + void (**pfn)(void); +} const g_aImportedFunctions[] = +{ + FUNC_ENTRY(pa_bytes_per_second), + FUNC_ENTRY(pa_bytes_to_usec), + FUNC_ENTRY(pa_channel_map_init_auto), + + FUNC_ENTRY(pa_context_connect), + FUNC_ENTRY(pa_context_disconnect), + FUNC_ENTRY(pa_context_get_server_info), + FUNC_ENTRY(pa_context_get_sink_info_by_name), + FUNC_ENTRY(pa_context_get_sink_info_list), + FUNC_ENTRY(pa_context_get_source_info_by_name), + FUNC_ENTRY(pa_context_get_source_info_list), + FUNC_ENTRY(pa_context_get_state), + FUNC_ENTRY(pa_context_unref), + FUNC_ENTRY(pa_context_errno), + FUNC_ENTRY(pa_context_new), + FUNC_ENTRY(pa_context_set_state_callback), + + FUNC_ENTRY(pa_frame_size), + FUNC_ENTRY(pa_get_library_version), + FUNC_ENTRY(pa_operation_unref), + FUNC_ENTRY(pa_operation_get_state), + FUNC_ENTRY(pa_operation_cancel), + FUNC_ENTRY(pa_rtclock_now), + FUNC_ENTRY(pa_sample_format_to_string), + FUNC_ENTRY(pa_sample_spec_valid), + FUNC_ENTRY(pa_strerror), + + FUNC_ENTRY(pa_stream_connect_playback), + FUNC_ENTRY(pa_stream_connect_record), + FUNC_ENTRY(pa_stream_disconnect), + FUNC_ENTRY(pa_stream_get_sample_spec), + FUNC_ENTRY(pa_stream_set_latency_update_callback), + FUNC_ENTRY(pa_stream_write), + FUNC_ENTRY(pa_stream_unref), + FUNC_ENTRY(pa_stream_get_state), + FUNC_ENTRY(pa_stream_get_latency), + FUNC_ENTRY(pa_stream_get_timing_info), + FUNC_ENTRY(pa_stream_readable_size), + FUNC_ENTRY(pa_stream_set_buffer_attr), + FUNC_ENTRY(pa_stream_set_state_callback), + FUNC_ENTRY(pa_stream_set_underflow_callback), + FUNC_ENTRY(pa_stream_set_overflow_callback), + FUNC_ENTRY(pa_stream_set_write_callback), + FUNC_ENTRY(pa_stream_flush), + FUNC_ENTRY(pa_stream_drain), + FUNC_ENTRY(pa_stream_trigger), + FUNC_ENTRY(pa_stream_new), + FUNC_ENTRY(pa_stream_get_buffer_attr), + FUNC_ENTRY(pa_stream_peek), + FUNC_ENTRY(pa_stream_cork), + FUNC_ENTRY(pa_stream_drop), + FUNC_ENTRY(pa_stream_writable_size), + + FUNC_ENTRY(pa_threaded_mainloop_stop), + FUNC_ENTRY(pa_threaded_mainloop_get_api), + FUNC_ENTRY(pa_threaded_mainloop_free), + FUNC_ENTRY(pa_threaded_mainloop_signal), + FUNC_ENTRY(pa_threaded_mainloop_unlock), + FUNC_ENTRY(pa_threaded_mainloop_new), + FUNC_ENTRY(pa_threaded_mainloop_wait), + FUNC_ENTRY(pa_threaded_mainloop_start), + FUNC_ENTRY(pa_threaded_mainloop_lock), + + FUNC_ENTRY(pa_usec_to_bytes) +}; +#undef FUNC_ENTRY + +/** Init once. */ +static RTONCE g_PulseAudioLibInitOnce = RTONCE_INITIALIZER; + +/** @callback_method_impl{FNRTONCE} */ +static DECLCALLBACK(int32_t) drvHostAudioPulseLibInitOnce(void *pvUser) +{ + RT_NOREF(pvUser); + LogFlowFunc(("\n")); + + RTLDRMOD hMod = NIL_RTLDRMOD; + int rc = RTLdrLoadSystemEx(VBOX_PULSE_LIB, RTLDRLOAD_FLAGS_NO_UNLOAD, &hMod); + if (RT_SUCCESS(rc)) + { + for (unsigned i = 0; i < RT_ELEMENTS(g_aImportedFunctions); i++) + { + rc = RTLdrGetSymbol(hMod, g_aImportedFunctions[i].pszName, (void **)g_aImportedFunctions[i].pfn); + if (RT_FAILURE(rc)) + { + LogRelFunc(("Failed to resolve function #%u: '%s' (%Rrc)\n", i, g_aImportedFunctions[i].pszName, rc)); + break; + } + } + + RTLdrClose(hMod); + } + else + LogRelFunc(("Failed to load library %s: %Rrc\n", VBOX_PULSE_LIB, rc)); + return rc; +} + +/** + * Try to dynamically load the PulseAudio libraries. + * + * @returns VBox status code. + */ +int audioLoadPulseLib(void) +{ + LogFlowFunc(("\n")); + return RTOnce(&g_PulseAudioLibInitOnce, drvHostAudioPulseLibInitOnce, NULL); +} + diff --git a/src/VBox/Devices/Audio/DrvHostAudioPulseAudioStubs.h b/src/VBox/Devices/Audio/DrvHostAudioPulseAudioStubs.h new file mode 100644 index 00000000..a80050d9 --- /dev/null +++ b/src/VBox/Devices/Audio/DrvHostAudioPulseAudioStubs.h @@ -0,0 +1,41 @@ +/* $Id: DrvHostAudioPulseAudioStubs.h $ */ +/** @file + * Stubs for libpulse. + */ + +/* + * Copyright (C) 2006-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 + */ + +#ifndef VBOX_INCLUDED_SRC_Audio_DrvHostAudioPulseAudioStubs_h +#define VBOX_INCLUDED_SRC_Audio_DrvHostAudioPulseAudioStubs_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include <iprt/cdefs.h> + +RT_C_DECLS_BEGIN +extern int audioLoadPulseLib(void); +RT_C_DECLS_END + +#endif /* !VBOX_INCLUDED_SRC_Audio_DrvHostAudioPulseAudioStubs_h */ + diff --git a/src/VBox/Devices/Audio/DrvHostAudioPulseAudioStubsMangling.h b/src/VBox/Devices/Audio/DrvHostAudioPulseAudioStubsMangling.h new file mode 100644 index 00000000..ef7323bb --- /dev/null +++ b/src/VBox/Devices/Audio/DrvHostAudioPulseAudioStubsMangling.h @@ -0,0 +1,105 @@ +/* $Id: DrvHostAudioPulseAudioStubsMangling.h $ */ +/** @file + * Mangle libpulse symbols. + * + * This is necessary on hosts which don't support the -fvisibility gcc switch. + */ + +/* + * Copyright (C) 2013-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 + */ + +#ifndef VBOX_INCLUDED_SRC_Audio_DrvHostAudioPulseAudioStubsMangling_h +#define VBOX_INCLUDED_SRC_Audio_DrvHostAudioPulseAudioStubsMangling_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#define PULSE_MANGLER(symbol) VBox_##symbol + +#define pa_bytes_per_second PULSE_MANGLER(pa_bytes_per_second) +#define pa_bytes_to_usec PULSE_MANGLER(pa_bytes_to_usec) +#define pa_channel_map_init_auto PULSE_MANGLER(pa_channel_map_init_auto) + +#define pa_context_connect PULSE_MANGLER(pa_context_connect) +#define pa_context_disconnect PULSE_MANGLER(pa_context_disconnect) +#define pa_context_get_server_info PULSE_MANGLER(pa_context_get_server_info) +#define pa_context_get_sink_info_by_name PULSE_MANGLER(pa_context_get_sink_info_by_name) +#define pa_context_get_sink_info_list PULSE_MANGLER(pa_context_get_sink_info_list) +#define pa_context_get_source_info_by_name PULSE_MANGLER(pa_context_get_source_info_by_name) +#define pa_context_get_source_info_list PULSE_MANGLER(pa_context_get_source_info_list) +#define pa_context_get_state PULSE_MANGLER(pa_context_get_state) +#define pa_context_unref PULSE_MANGLER(pa_context_unref) +#define pa_context_errno PULSE_MANGLER(pa_context_errno) +#define pa_context_new PULSE_MANGLER(pa_context_new) +#define pa_context_set_state_callback PULSE_MANGLER(pa_context_set_state_callback) + +#define pa_frame_size PULSE_MANGLER(pa_frame_size) +#define pa_get_library_version PULSE_MANGLER(pa_get_library_version) +#define pa_operation_unref PULSE_MANGLER(pa_operation_unref) +#define pa_operation_get_state PULSE_MANGLER(pa_operation_get_state) +#define pa_operation_cancel PULSE_MANGLER(pa_operation_cancel) +#define pa_rtclock_now PULSE_MANGLER(pa_rtclock_now) +#define pa_sample_format_to_string PULSE_MANGLER(pa_sample_format_to_string) +#define pa_sample_spec_valid PULSE_MANGLER(pa_sample_spec_valid) + +#define pa_stream_connect_playback PULSE_MANGLER(pa_stream_connect_playback) +#define pa_stream_connect_record PULSE_MANGLER(pa_stream_connect_record) +#define pa_stream_cork PULSE_MANGLER(pa_stream_cork) +#define pa_stream_disconnect PULSE_MANGLER(pa_stream_disconnect) +#define pa_stream_drop PULSE_MANGLER(pa_stream_drop) +#define pa_stream_get_sample_spec PULSE_MANGLER(pa_stream_get_sample_spec) +#define pa_stream_set_latency_update_callback PULSE_MANGLER(pa_stream_set_latency_update_callback) +#define pa_stream_write PULSE_MANGLER(pa_stream_write) +#define pa_stream_unref PULSE_MANGLER(pa_stream_unref) +#define pa_stream_get_state PULSE_MANGLER(pa_stream_get_state) +#define pa_stream_get_latency PULSE_MANGLER(pa_stream_get_latency) +#define pa_stream_get_timing_info PULSE_MANGLER(pa_stream_get_timing_info) +#define pa_stream_set_buffer_attr PULSE_MANGLER(pa_stream_set_buffer_attr) +#define pa_stream_set_state_callback PULSE_MANGLER(pa_stream_set_state_callback) +#define pa_stream_set_underflow_callback PULSE_MANGLER(pa_stream_set_underflow_callback) +#define pa_stream_set_overflow_callback PULSE_MANGLER(pa_stream_set_overflow_callback) +#define pa_stream_set_write_callback PULSE_MANGLER(pa_stream_set_write_callback) +#define pa_stream_flush PULSE_MANGLER(pa_stream_flush) +#define pa_stream_drain PULSE_MANGLER(pa_stream_drain) +#define pa_stream_trigger PULSE_MANGLER(pa_stream_trigger) +#define pa_stream_new PULSE_MANGLER(pa_stream_new) +#define pa_stream_get_buffer_attr PULSE_MANGLER(pa_stream_get_buffer_attr) +#define pa_stream_peek PULSE_MANGLER(pa_stream_peek) +#define pa_stream_readable_size PULSE_MANGLER(pa_stream_readable_size) +#define pa_stream_writable_size PULSE_MANGLER(pa_stream_writable_size) + +#define pa_strerror PULSE_MANGLER(pa_strerror) + +#define pa_threaded_mainloop_stop PULSE_MANGLER(pa_threaded_mainloop_stop) +#define pa_threaded_mainloop_get_api PULSE_MANGLER(pa_threaded_mainloop_get_api) +#define pa_threaded_mainloop_free PULSE_MANGLER(pa_threaded_mainloop_free) +#define pa_threaded_mainloop_signal PULSE_MANGLER(pa_threaded_mainloop_signal) +#define pa_threaded_mainloop_unlock PULSE_MANGLER(pa_threaded_mainloop_unlock) +#define pa_threaded_mainloop_new PULSE_MANGLER(pa_threaded_mainloop_new) +#define pa_threaded_mainloop_wait PULSE_MANGLER(pa_threaded_mainloop_wait) +#define pa_threaded_mainloop_start PULSE_MANGLER(pa_threaded_mainloop_start) +#define pa_threaded_mainloop_lock PULSE_MANGLER(pa_threaded_mainloop_lock) + +#define pa_usec_to_bytes PULSE_MANGLER(pa_usec_to_bytes) + +#endif /* !VBOX_INCLUDED_SRC_Audio_DrvHostAudioPulseAudioStubsMangling_h */ + diff --git a/src/VBox/Devices/Audio/DrvHostAudioValidationKit.cpp b/src/VBox/Devices/Audio/DrvHostAudioValidationKit.cpp new file mode 100644 index 00000000..2b90f7fd --- /dev/null +++ b/src/VBox/Devices/Audio/DrvHostAudioValidationKit.cpp @@ -0,0 +1,1747 @@ +/* $Id: DrvHostAudioValidationKit.cpp $ */ +/** @file + * Host audio driver - ValidationKit - For dumping and injecting audio data from/to the device emulation. + */ + +/* + * Copyright (C) 2016-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 + */ + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_DRV_HOST_AUDIO +#include <iprt/dir.h> +#include <iprt/env.h> +#include <iprt/mem.h> +#include <iprt/path.h> +#include <iprt/semaphore.h> +#include <iprt/stream.h> +#include <iprt/uuid.h> /* For PDMIBASE_2_PDMDRV. */ + +#include <VBox/log.h> +#include <VBox/vmm/pdmaudioifs.h> +#include <VBox/vmm/pdmaudioinline.h> + +#include "VBoxDD.h" +#include "AudioHlp.h" +#include "AudioTest.h" +#include "AudioTestService.h" + + +#ifdef DEBUG_andy +/** Enables dumping audio streams to the temporary directory for debugging. */ +# define VBOX_WITH_AUDIO_VALKIT_DUMP_STREAMS +#endif + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +/** + * Structure for keeping a Validation Kit input/output stream. + */ +typedef struct VALKITAUDIOSTREAM +{ + /** Common part. */ + PDMAUDIOBACKENDSTREAM Core; + /** The stream's acquired configuration. */ + PDMAUDIOSTREAMCFG Cfg; +#ifdef VBOX_WITH_AUDIO_VALKIT_DUMP_STREAMS + /** Audio file to dump output to. */ + PAUDIOHLPFILE pFile; +#endif +} VALKITAUDIOSTREAM; +/** Pointer to a Validation Kit stream. */ +typedef VALKITAUDIOSTREAM *PVALKITAUDIOSTREAM; + +/** + * Test tone-specific instance data. + */ +typedef struct VALKITTESTTONEDATA +{ + /* Test tone beacon to use. + * Will be re-used for pre/post beacons. */ + AUDIOTESTTONEBEACON Beacon; + union + { + struct + { + /** How many bytes to write. */ + uint64_t cbToWrite; + /** How many bytes already written. */ + uint64_t cbWritten; + } Rec; + struct + { + /** How many bytes to read. */ + uint64_t cbToRead; + /** How many bytes already read. */ + uint64_t cbRead; + } Play; + } u; + /** The test tone instance to use. */ + AUDIOTESTTONE Tone; + /** The test tone parameters to use. */ + AUDIOTESTTONEPARMS Parms; +} VALKITTESTTONEDATA; + +/** + * Structure keeping a single Validation Kit test. + */ +typedef struct VALKITTESTDATA +{ + /** The list node. */ + RTLISTNODE Node; + /** Index in test sequence (0-based). */ + uint32_t idxTest; + /** Current test set entry to process. */ + PAUDIOTESTENTRY pEntry; + /** Current test state. */ + AUDIOTESTSTATE enmState; + /** Current test object to process. */ + AUDIOTESTOBJ Obj; + /** Stream configuration to use for this test. */ + PDMAUDIOSTREAMCFG StreamCfg; + union + { + /** Test tone-specific data. */ + VALKITTESTTONEDATA TestTone; + } t; + /** Time stamp (real, in ms) when test got registered. */ + uint64_t msRegisteredTS; + /** Time stamp (real, in ms) when test started. */ + uint64_t msStartedTS; +} VALKITTESTDATA; +/** Pointer to Validation Kit test data. */ +typedef VALKITTESTDATA *PVALKITTESTDATA; + +/** + * Validation Kit audio driver instance data. + * @implements PDMIAUDIOCONNECTOR + */ +typedef struct DRVHOSTVALKITAUDIO +{ + /** Pointer to the driver instance structure. */ + PPDMDRVINS pDrvIns; + /** Pointer to host audio interface. */ + PDMIHOSTAUDIO IHostAudio; + /** Total number of bytes played since driver construction. */ + uint64_t cbPlayedTotal; + /** Total number of bytes recorded since driver construction. */ + uint64_t cbRecordedTotal; + /** Total number of bytes silence was played in a consequtive block so far. + * Will be reset once audible data is being played (again). */ + uint64_t cbPlayedSilence; + /** Total number of bytes audio (audible or not) was played while no active + * audio test was registered / available. */ + uint64_t cbPlayedNoTest; + /** Temporary path to use. */ + char szPathTemp[RTPATH_MAX]; + /** Output path to use. */ + char szPathOut[RTPATH_MAX]; + /** Current test set being handled. + * At the moment only one test set can be around at a time. */ + AUDIOTESTSET Set; + /** Number of total tests in \a lstTestsRec and \a lstTestsPlay. */ + uint32_t cTestsTotal; + /** Number of tests in \a lstTestsRec. */ + uint32_t cTestsRec; + /** List keeping the recording tests (FIFO). */ + RTLISTANCHOR lstTestsRec; + /** Pointer to current recording test being processed. + * NULL if no current test active. */ + PVALKITTESTDATA pTestCurRec; + /** Number of tests in \a lstTestsPlay. */ + uint32_t cTestsPlay; + /** List keeping the recording tests (FIFO). */ + RTLISTANCHOR lstTestsPlay; + /** Pointer to current playback test being processed. + * NULL if no current test active. */ + PVALKITTESTDATA pTestCurPlay; + /** Critical section for serializing access across threads. */ + RTCRITSECT CritSect; + /** Whether the test set needs to end. + * Needed for packing up (to archive) and termination, as capturing and playback + * can run in asynchronous threads. */ + bool fTestSetEnd; + /** Event semaphore for waiting on the current test set to end. */ + RTSEMEVENT EventSemEnded; + /** The Audio Test Service (ATS) instance. */ + ATSSERVER Srv; + /** Absolute path to the packed up test set archive. + * Keep it simple for now and only support one (open) archive at a time. */ + char szTestSetArchive[RTPATH_MAX]; + /** File handle to the (opened) test set archive for reading. */ + RTFILE hTestSetArchive; + +} DRVHOSTVALKITAUDIO; +/** Pointer to a Validation Kit host audio driver instance. */ +typedef DRVHOSTVALKITAUDIO *PDRVHOSTVALKITAUDIO; + + +/********************************************************************************************************************************* +* Internal test handling code * +*********************************************************************************************************************************/ + +/** + * Unregisters a ValKit test, common code. + * + * @param pThis ValKit audio driver instance. + * @param pTst Test to unregister. + * The pointer will be invalid afterwards. + */ +static void drvHostValKiUnregisterTest(PDRVHOSTVALKITAUDIO pThis, PVALKITTESTDATA pTst) +{ + AssertPtrReturnVoid(pTst); + + RTListNodeRemove(&pTst->Node); + + AudioTestObjClose(pTst->Obj); + pTst->Obj = NULL; + + if (pTst->pEntry) /* Set set entry assign? Mark as done. */ + { + AssertPtrReturnVoid(pTst->pEntry); + pTst->pEntry = NULL; + } + + RTMemFree(pTst); + pTst = NULL; + + Assert(pThis->cTestsTotal); + pThis->cTestsTotal--; + if (pThis->cTestsTotal == 0) + { + if (ASMAtomicReadBool(&pThis->fTestSetEnd)) + { + int rc2 = RTSemEventSignal(pThis->EventSemEnded); + AssertRC(rc2); + } + } +} + +/** + * Unregisters a ValKit recording test. + * + * @param pThis ValKit audio driver instance. + * @param pTst Test to unregister. + * The pointer will be invalid afterwards. + */ +static void drvHostValKiUnregisterRecTest(PDRVHOSTVALKITAUDIO pThis, PVALKITTESTDATA pTst) +{ + Assert(pThis->cTestsRec); + pThis->cTestsRec--; + + drvHostValKiUnregisterTest(pThis, pTst); +} + +/** + * Unregisters a ValKit playback test. + * + * @param pThis ValKit audio driver instance. + * @param pTst Test to unregister. + * The pointer will be invalid afterwards. + */ +static void drvHostValKiUnregisterPlayTest(PDRVHOSTVALKITAUDIO pThis, PVALKITTESTDATA pTst) +{ + Assert(pThis->cTestsPlay); + pThis->cTestsPlay--; + + drvHostValKiUnregisterTest(pThis, pTst); +} + +/** + * Performs some internal cleanup / housekeeping of all registered tests. + * + * @param pThis ValKit audio driver instance. + */ +static void drvHostValKitCleanup(PDRVHOSTVALKITAUDIO pThis) +{ + LogRel(("ValKit: Cleaning up ...\n")); + + if ( pThis->cTestsTotal + && ( !pThis->cbPlayedTotal + && !pThis->cbRecordedTotal) + ) + { + LogRel(("ValKit: Warning: Did not get any audio data to play or record altough tests were configured\n\n")); + LogRel(("ValKit: Hints:\n" + "ValKit: - Audio device emulation configured and enabled for the VM?\n" + "ValKit: - Audio input and/or output enabled for the VM?\n" + "ValKit: - Is the guest able to play / record sound at all?\n" + "ValKit: - Is the guest's audio mixer or input / output sinks muted?\n" + "ValKit: - Audio stack misconfiguration / bug?\n\n")); + } + + if (pThis->cTestsRec) + LogRel(("ValKit: Warning: %RU32 guest recording tests still outstanding:\n", pThis->cTestsRec)); + + PVALKITTESTDATA pTst, pTstNext; + RTListForEachSafe(&pThis->lstTestsRec, pTst, pTstNext, VALKITTESTDATA, Node) + { + if (pTst->enmState != AUDIOTESTSTATE_DONE) + LogRel(("ValKit: \tWarning: Test #%RU32 (recording) not done yet (state is '%s')\n", + pTst->idxTest, AudioTestStateToStr(pTst->enmState))); + + if (pTst->t.TestTone.u.Rec.cbToWrite > pTst->t.TestTone.u.Rec.cbWritten) + { + size_t const cbOutstanding = pTst->t.TestTone.u.Rec.cbToWrite - pTst->t.TestTone.u.Rec.cbWritten; + if (cbOutstanding) + LogRel(("ValKit: \tWarning: Recording test #%RU32 has %RU64 bytes (%RU64ms) outstanding (%RU8%% left)\n", + pTst->idxTest, cbOutstanding, PDMAudioPropsBytesToMilli(&pTst->t.TestTone.Parms.Props, (uint32_t)cbOutstanding), + 100 - (pTst->t.TestTone.u.Rec.cbWritten * 100) / RT_MAX(pTst->t.TestTone.u.Rec.cbToWrite, 1))); + } + drvHostValKiUnregisterRecTest(pThis, pTst); + } + + if (pThis->cTestsPlay) + LogRel(("ValKit: Warning: %RU32 guest playback tests still outstanding:\n", pThis->cTestsPlay)); + + RTListForEachSafe(&pThis->lstTestsPlay, pTst, pTstNext, VALKITTESTDATA, Node) + { + if (pTst->enmState != AUDIOTESTSTATE_DONE) + LogRel(("ValKit: \tWarning: Test #%RU32 (playback) not done yet (state is '%s')\n", + pTst->idxTest, AudioTestStateToStr(pTst->enmState))); + + if (pTst->t.TestTone.u.Play.cbToRead > pTst->t.TestTone.u.Play.cbRead) + { + size_t const cbOutstanding = pTst->t.TestTone.u.Play.cbToRead - pTst->t.TestTone.u.Play.cbRead; + if (cbOutstanding) + LogRel(("ValKit: \tWarning: Playback test #%RU32 has %RU64 bytes (%RU64ms) outstanding (%RU8%% left)\n", + pTst->idxTest, cbOutstanding, PDMAudioPropsBytesToMilli(&pTst->t.TestTone.Parms.Props, (uint32_t)cbOutstanding), + 100 - (pTst->t.TestTone.u.Play.cbRead * 100) / RT_MAX(pTst->t.TestTone.u.Play.cbToRead, 1))); + } + drvHostValKiUnregisterPlayTest(pThis, pTst); + } + + Assert(pThis->cTestsRec == 0); + Assert(pThis->cTestsPlay == 0); + + if (pThis->cbPlayedNoTest) + { + LogRel2(("ValKit: Warning: Guest was playing back audio when no playback test is active (%RU64 bytes total)\n", + pThis->cbPlayedNoTest)); + pThis->cbPlayedNoTest = 0; + } +} + + +/********************************************************************************************************************************* +* ATS callback implementations * +*********************************************************************************************************************************/ + +/** @copydoc ATSCALLBACKS::pfnHowdy */ +static DECLCALLBACK(int) drvHostValKitHowdy(void const *pvUser) +{ + PDRVHOSTVALKITAUDIO pThis = (PDRVHOSTVALKITAUDIO)pvUser; + RT_NOREF(pThis); + + LogRel(("ValKit: Client connected\n")); + + return VINF_SUCCESS; +} + +/** @copydoc ATSCALLBACKS::pfnBye */ +static DECLCALLBACK(int) drvHostValKitBye(void const *pvUser) +{ + PDRVHOSTVALKITAUDIO pThis = (PDRVHOSTVALKITAUDIO)pvUser; + RT_NOREF(pThis); + + LogRel(("ValKit: Client disconnected\n")); + + return VINF_SUCCESS; +} + +/** @copydoc ATSCALLBACKS::pfnTestSetBegin */ +static DECLCALLBACK(int) drvHostValKitTestSetBegin(void const *pvUser, const char *pszTag) +{ + PDRVHOSTVALKITAUDIO pThis = (PDRVHOSTVALKITAUDIO)pvUser; + + LogRel(("ValKit: Beginning test set '%s'\n", pszTag)); + + int rc = RTCritSectEnter(&pThis->CritSect); + if (RT_SUCCESS(rc)) + { + rc = AudioTestSetCreate(&pThis->Set, pThis->szPathTemp, pszTag); + + int rc2 = RTCritSectLeave(&pThis->CritSect); + if (RT_SUCCESS(rc)) + rc = rc2; + } + + if (RT_FAILURE(rc)) + LogRel(("ValKit: Beginning test set failed with %Rrc\n", rc)); + + return rc; +} + +/** @copydoc ATSCALLBACKS::pfnTestSetEnd */ +static DECLCALLBACK(int) drvHostValKitTestSetEnd(void const *pvUser, const char *pszTag) +{ + PDRVHOSTVALKITAUDIO pThis = (PDRVHOSTVALKITAUDIO)pvUser; + + LogRel(("ValKit: Ending test set '%s'\n", pszTag)); + + int rc = RTCritSectEnter(&pThis->CritSect); + if (RT_SUCCESS(rc)) + { + const PAUDIOTESTSET pSet = &pThis->Set; + + const char *pszTagSet = AudioTestSetGetTag(pSet); + if (RTStrCmp(pszTagSet, pszTag) != 0) + { + LogRel(("ValKit: Error: Current test does not match test set to end ('%s' vs '%s')\n", pszTagSet, pszTag)); + + int rc2 = RTCritSectLeave(&pThis->CritSect); + AssertRC(rc2); + + return VERR_NOT_FOUND; /* Return to the caller. */ + } + + LogRel(("ValKit: Test set has %RU32 tests total, %RU32 (still) running, %RU32 failures total so far\n", + AudioTestSetGetTestsTotal(pSet), AudioTestSetGetTestsRunning(pSet), AudioTestSetGetTotalFailures(pSet))); + LogRel(("ValKit: %RU32 tests still registered total (%RU32 play, %RU32 record)\n", + pThis->cTestsTotal, pThis->cTestsPlay, pThis->cTestsRec)); + + if ( AudioTestSetIsRunning(pSet) + || pThis->cTestsTotal) + { + ASMAtomicWriteBool(&pThis->fTestSetEnd, true); + + rc = RTCritSectLeave(&pThis->CritSect); + if (RT_SUCCESS(rc)) + { + LogRel(("ValKit: Waiting for all tests of set '%s' to end ...\n", pszTag)); + rc = RTSemEventWait(pThis->EventSemEnded, RT_MS_5SEC); + if (RT_FAILURE(rc)) + { + LogRel(("ValKit: Waiting for tests of set '%s' to end failed with %Rrc\n", pszTag, rc)); + + /* The verification on the host will tell us later which tests did run and which didn't (anymore). + * So continue and pack (plus transfer) the test set to the host. */ + if (rc == VERR_TIMEOUT) + rc = VINF_SUCCESS; + } + + int rc2 = RTCritSectEnter(&pThis->CritSect); + if (RT_SUCCESS(rc)) + rc = rc2; + } + } + + if (RT_SUCCESS(rc)) + { + LogRel(("ValKit: Closing test set '%s' ...\n", pszTag)); + + /* Close the test set first. */ + rc = AudioTestSetClose(pSet); + if (RT_SUCCESS(rc)) + { + /* Before destroying the test environment, pack up the test set so + * that it's ready for transmission. */ + rc = AudioTestSetPack(pSet, pThis->szPathOut, pThis->szTestSetArchive, sizeof(pThis->szTestSetArchive)); + if (RT_SUCCESS(rc)) + { + LogRel(("ValKit: Packed up to '%s'\n", pThis->szTestSetArchive)); + } + else + LogRel(("ValKit: Packing up test set failed with %Rrc\n", rc)); + + /* Do some internal housekeeping. */ + drvHostValKitCleanup(pThis); + +#ifndef DEBUG_andy + int rc2 = AudioTestSetWipe(pSet); + if (RT_SUCCESS(rc)) + rc = rc2; +#endif + } + else + LogRel(("ValKit: Closing test set failed with %Rrc\n", rc)); + + int rc2 = AudioTestSetDestroy(pSet); + if (RT_FAILURE(rc2)) + { + LogRel(("ValKit: Destroying test set failed with %Rrc\n", rc)); + if (RT_SUCCESS(rc)) + rc = rc2; + } + } + + int rc2 = RTCritSectLeave(&pThis->CritSect); + if (RT_SUCCESS(rc)) + rc = rc2; + } + + if (RT_FAILURE(rc)) + LogRel(("ValKit: Ending test set failed with %Rrc\n", rc)); + + return rc; +} + +/** @copydoc ATSCALLBACKS::pfnTonePlay + * + * Creates and registers a new test tone guest recording test. + * This backend will play (inject) input data to the guest. + */ +static DECLCALLBACK(int) drvHostValKitRegisterGuestRecTest(void const *pvUser, PAUDIOTESTTONEPARMS pToneParms) +{ + PDRVHOSTVALKITAUDIO pThis = (PDRVHOSTVALKITAUDIO)pvUser; + + PVALKITTESTDATA pTst = (PVALKITTESTDATA)RTMemAllocZ(sizeof(VALKITTESTDATA)); + AssertPtrReturn(pTst, VERR_NO_MEMORY); + + pTst->enmState = AUDIOTESTSTATE_INIT; + + memcpy(&pTst->t.TestTone.Parms, pToneParms, sizeof(AUDIOTESTTONEPARMS)); + + PPDMAUDIOPCMPROPS const pProps = &pTst->t.TestTone.Parms.Props; + + AssertReturn(pTst->t.TestTone.Parms.msDuration, VERR_INVALID_PARAMETER); + AssertReturn(PDMAudioPropsAreValid(pProps), VERR_INVALID_PARAMETER); + + AudioTestToneInit(&pTst->t.TestTone.Tone, pProps, pTst->t.TestTone.Parms.dbFreqHz); + + pTst->t.TestTone.u.Rec.cbToWrite = PDMAudioPropsMilliToBytes(pProps, + pTst->t.TestTone.Parms.msDuration); + + /* We inject a pre + post beacon before + after the actual test tone. + * We always start with the pre beacon. */ + AudioTestBeaconInit(&pTst->t.TestTone.Beacon, pToneParms->Hdr.idxTest, AUDIOTESTTONEBEACONTYPE_PLAY_PRE, pProps); + + int rc = RTCritSectEnter(&pThis->CritSect); + if (RT_SUCCESS(rc)) + { + LogRel(("ValKit: Registering guest recording test #%RU32 (%RU32ms, %RU64 bytes) as test #%RU32\n", + pThis->cTestsRec, pTst->t.TestTone.Parms.msDuration, pTst->t.TestTone.u.Rec.cbToWrite, + pToneParms->Hdr.idxTest)); + + const uint32_t cbBeacon = AudioTestBeaconGetSize(&pTst->t.TestTone.Beacon); + if (cbBeacon) + LogRel2(("ValKit: Test #%RU32: Uses 2 x %RU32 bytes of pre/post beacons\n", + pToneParms->Hdr.idxTest, cbBeacon)); + + RTListAppend(&pThis->lstTestsRec, &pTst->Node); + + pTst->msRegisteredTS = RTTimeMilliTS(); + pTst->idxTest = pToneParms->Hdr.idxTest; /* Use the test ID from the host (so that the beacon IDs match). */ + + pThis->cTestsRec++; + pThis->cTestsTotal++; + + int rc2 = RTCritSectLeave(&pThis->CritSect); + AssertRC(rc2); + } + + return VINF_SUCCESS; +} + +/** @copydoc ATSCALLBACKS::pfnToneRecord + * + * Creates and registers a new test tone guest playback test. + * This backend will record the guest output data. + */ +static DECLCALLBACK(int) drvHostValKitRegisterGuestPlayTest(void const *pvUser, PAUDIOTESTTONEPARMS pToneParms) +{ + PDRVHOSTVALKITAUDIO pThis = (PDRVHOSTVALKITAUDIO)pvUser; + + PVALKITTESTDATA pTst = (PVALKITTESTDATA)RTMemAllocZ(sizeof(VALKITTESTDATA)); + AssertPtrReturn(pTst, VERR_NO_MEMORY); + + pTst->enmState = AUDIOTESTSTATE_INIT; + + memcpy(&pTst->t.TestTone.Parms, pToneParms, sizeof(AUDIOTESTTONEPARMS)); + + PPDMAUDIOPCMPROPS const pProps = &pTst->t.TestTone.Parms.Props; + + AssertReturn(pTst->t.TestTone.Parms.msDuration, VERR_INVALID_PARAMETER); + AssertReturn(PDMAudioPropsAreValid(pProps), VERR_INVALID_PARAMETER); + + pTst->t.TestTone.u.Play.cbToRead = PDMAudioPropsMilliToBytes(pProps, + pTst->t.TestTone.Parms.msDuration); + + /* We play a pre + post beacon before + after the actual test tone. + * We always start with the pre beacon. */ + AudioTestBeaconInit(&pTst->t.TestTone.Beacon, pToneParms->Hdr.idxTest, AUDIOTESTTONEBEACONTYPE_PLAY_PRE, pProps); + + int rc = RTCritSectEnter(&pThis->CritSect); + if (RT_SUCCESS(rc)) + { + LogRel(("ValKit: Registering guest playback test #%RU32 (%RU32ms, %RU64 bytes) as test #%RU32\n", + pThis->cTestsPlay, pTst->t.TestTone.Parms.msDuration, pTst->t.TestTone.u.Play.cbToRead, + pToneParms->Hdr.idxTest)); + + const uint32_t cbBeacon = AudioTestBeaconGetSize(&pTst->t.TestTone.Beacon); + if (cbBeacon) + LogRel2(("ValKit: Test #%RU32: Uses x %RU32 bytes of pre/post beacons\n", + pToneParms->Hdr.idxTest, cbBeacon)); + + RTListAppend(&pThis->lstTestsPlay, &pTst->Node); + + pTst->msRegisteredTS = RTTimeMilliTS(); + pTst->idxTest = pToneParms->Hdr.idxTest; /* Use the test ID from the host (so that the beacon IDs match). */ + + pThis->cTestsTotal++; + pThis->cTestsPlay++; + + int rc2 = RTCritSectLeave(&pThis->CritSect); + AssertRC(rc2); + } + + return VINF_SUCCESS; +} + +/** @copydoc ATSCALLBACKS::pfnTestSetSendBegin */ +static DECLCALLBACK(int) drvHostValKitTestSetSendBeginCallback(void const *pvUser, const char *pszTag) +{ + PDRVHOSTVALKITAUDIO pThis = (PDRVHOSTVALKITAUDIO)pvUser; + + int rc = RTCritSectEnter(&pThis->CritSect); + if (RT_SUCCESS(rc)) + { + if (RTFileExists(pThis->szTestSetArchive)) /* Has the archive successfully been created yet? */ + { + rc = RTFileOpen(&pThis->hTestSetArchive, pThis->szTestSetArchive, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_WRITE); + if (RT_SUCCESS(rc)) + { + uint64_t uSize; + rc = RTFileQuerySize(pThis->hTestSetArchive, &uSize); + if (RT_SUCCESS(rc)) + LogRel(("ValKit: Sending test set '%s' (%zu bytes)\n", pThis->szTestSetArchive, uSize)); + } + } + else + rc = VERR_FILE_NOT_FOUND; + + int rc2 = RTCritSectLeave(&pThis->CritSect); + if (RT_SUCCESS(rc)) + rc = rc2; + } + + if (RT_FAILURE(rc)) + LogRel(("ValKit: Beginning to send test set '%s' failed with %Rrc\n", pszTag, rc)); + + return rc; +} + +/** @copydoc ATSCALLBACKS::pfnTestSetSendRead */ +static DECLCALLBACK(int) drvHostValKitTestSetSendReadCallback(void const *pvUser, + const char *pszTag, void *pvBuf, size_t cbBuf, size_t *pcbRead) +{ + PDRVHOSTVALKITAUDIO pThis = (PDRVHOSTVALKITAUDIO)pvUser; + + int rc = RTCritSectEnter(&pThis->CritSect); + if (RT_SUCCESS(rc)) + { + if (RTFileIsValid(pThis->hTestSetArchive)) + { + rc = RTFileRead(pThis->hTestSetArchive, pvBuf, cbBuf, pcbRead); + } + else + rc = VERR_WRONG_ORDER; + + int rc2 = RTCritSectLeave(&pThis->CritSect); + if (RT_SUCCESS(rc)) + rc = rc2; + } + + if (RT_FAILURE(rc)) + LogRel(("ValKit: Reading from test set '%s' failed with %Rrc\n", pszTag, rc)); + + return rc; +} + +/** @copydoc ATSCALLBACKS::pfnTestSetSendEnd */ +static DECLCALLBACK(int) drvHostValKitTestSetSendEndCallback(void const *pvUser, const char *pszTag) +{ + PDRVHOSTVALKITAUDIO pThis = (PDRVHOSTVALKITAUDIO)pvUser; + + int rc = RTCritSectEnter(&pThis->CritSect); + if (RT_SUCCESS(rc)) + { + if (RTFileIsValid(pThis->hTestSetArchive)) + { + rc = RTFileClose(pThis->hTestSetArchive); + if (RT_SUCCESS(rc)) + pThis->hTestSetArchive = NIL_RTFILE; + } + + int rc2 = RTCritSectLeave(&pThis->CritSect); + if (RT_SUCCESS(rc)) + rc = rc2; + } + + if (RT_FAILURE(rc)) + LogRel(("ValKit: Ending to send test set '%s' failed with %Rrc\n", pszTag, rc)); + + return rc; +} + + +/********************************************************************************************************************************* +* PDMIHOSTAUDIO interface implementation * +*********************************************************************************************************************************/ + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnGetConfig} + */ +static DECLCALLBACK(int) drvHostValKitAudioHA_GetConfig(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDCFG pBackendCfg) +{ + RT_NOREF(pInterface); + AssertPtrReturn(pBackendCfg, VERR_INVALID_POINTER); + + /* + * Fill in the config structure. + */ + RTStrCopy(pBackendCfg->szName, sizeof(pBackendCfg->szName), "Validation Kit"); + pBackendCfg->cbStream = sizeof(VALKITAUDIOSTREAM); + pBackendCfg->fFlags = 0; + pBackendCfg->cMaxStreamsOut = 1; /* Output (Playback). */ + pBackendCfg->cMaxStreamsIn = 1; /* Input (Recording). */ + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnGetStatus} + */ +static DECLCALLBACK(PDMAUDIOBACKENDSTS) drvHostValKitAudioHA_GetStatus(PPDMIHOSTAUDIO pInterface, PDMAUDIODIR enmDir) +{ + RT_NOREF(enmDir); + AssertPtrReturn(pInterface, PDMAUDIOBACKENDSTS_UNKNOWN); + + return PDMAUDIOBACKENDSTS_RUNNING; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamCreate} + */ +static DECLCALLBACK(int) drvHostValKitAudioHA_StreamCreate(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + PCPDMAUDIOSTREAMCFG pCfgReq, PPDMAUDIOSTREAMCFG pCfgAcq) +{ + PDRVHOSTVALKITAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVHOSTVALKITAUDIO, IHostAudio); + PVALKITAUDIOSTREAM pStreamValKit = (PVALKITAUDIOSTREAM)pStream; + AssertPtrReturn(pStreamValKit, VERR_INVALID_POINTER); + AssertPtrReturn(pCfgReq, VERR_INVALID_POINTER); + AssertPtrReturn(pCfgAcq, VERR_INVALID_POINTER); + RT_NOREF(pThis); + + PDMAudioStrmCfgCopy(&pStreamValKit->Cfg, pCfgAcq); + + int rc2; +#ifdef VBOX_WITH_AUDIO_VALKIT_DUMP_STREAMS + rc2 = AudioHlpFileCreateAndOpenEx(&pStreamValKit->pFile, AUDIOHLPFILETYPE_WAV, NULL /*use temp dir*/, + pThis->pDrvIns->iInstance, AUDIOHLPFILENAME_FLAGS_NONE, AUDIOHLPFILE_FLAGS_NONE, + &pCfgReq->Props, RTFILE_O_WRITE | RTFILE_O_DENY_WRITE | RTFILE_O_CREATE_REPLACE, + pCfgReq->enmDir == PDMAUDIODIR_IN ? "ValKitAudioIn" : "ValKitAudioOut"); + if (RT_FAILURE(rc2)) + LogRel(("ValKit: Failed to creating debug file for %s stream '%s' in the temp directory: %Rrc\n", + pCfgReq->enmDir == PDMAUDIODIR_IN ? "input" : "output", pCfgReq->szName, rc2)); +#endif + + int rc = RTCritSectEnter(&pThis->CritSect); + if (RT_SUCCESS(rc)) + { + if (pThis->pTestCurRec == NULL) + { + pThis->pTestCurRec = RTListGetFirst(&pThis->lstTestsRec, VALKITTESTDATA, Node); + if (pThis->pTestCurRec) + LogRel(("ValKit: Next guest recording test in queue is test #%RU32\n", pThis->pTestCurRec->idxTest)); + } + + PVALKITTESTDATA pTst = pThis->pTestCurRec; + + /* If we have a test registered and in the queue coming up next, use + * the beacon size (if any, could be 0) as pre-buffering requirement. */ + if (pTst) + { + const uint32_t cFramesBeacon = PDMAudioPropsBytesToFrames(&pCfgAcq->Props, + AudioTestBeaconGetSize(&pTst->t.TestTone.Beacon)); + if (cFramesBeacon) /* Only assign if not 0, otherwise stay with the default. */ + pCfgAcq->Backend.cFramesPreBuffering = cFramesBeacon; + } + + rc2 = RTCritSectLeave(&pThis->CritSect); + AssertRC(rc2); + } + + return rc; +} + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDestroy} + */ +static DECLCALLBACK(int) drvHostValKitAudioHA_StreamDestroy(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + bool fImmediate) +{ + RT_NOREF(pInterface, fImmediate); + PVALKITAUDIOSTREAM pStreamValKit = (PVALKITAUDIOSTREAM)pStream; + AssertPtrReturn(pStreamValKit, VERR_INVALID_POINTER); + +#ifdef VBOX_WITH_AUDIO_VALKIT_DUMP_STREAMS + if (pStreamValKit->pFile) + { + AudioHlpFileDestroy(pStreamValKit->pFile); + pStreamValKit->pFile = NULL; + } +#endif + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamEnable} + */ +static DECLCALLBACK(int) drvHostValKitAudioHA_StreamEnable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface, pStream); + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDisable} + */ +static DECLCALLBACK(int) drvHostValKitAudioHA_StreamDisable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface, pStream); + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamPause} + */ +static DECLCALLBACK(int) drvHostValKitAudioHA_StreamPause(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface, pStream); + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamResume} + */ +static DECLCALLBACK(int) drvHostValKitAudioHA_StreamResume(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface, pStream); + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDrain} + */ +static DECLCALLBACK(int) drvHostValKitAudioHA_StreamDrain(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface, pStream); + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetReadable} + */ +static DECLCALLBACK(uint32_t) drvHostValKitAudioHA_StreamGetReadable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + PDRVHOSTVALKITAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVHOSTVALKITAUDIO, IHostAudio); + PVALKITAUDIOSTREAM pStreamValKit = (PVALKITAUDIOSTREAM)pStream; + + if (pStreamValKit->Cfg.enmDir == PDMAUDIODIR_OUT) + { + LogRel(("ValKit: Warning: Trying to read from non-input stream '%s' -- report this bug!\n", + pStreamValKit->Cfg.szName)); + return 0; + } + + /* We return UINT32_MAX by default (when no tests are running [anymore] for not being marked + * as "unreliable stream" in the audio mixer. See audioMixerSinkUpdateInput(). */ + uint32_t cbReadable = UINT32_MAX; + + int rc = RTCritSectEnter(&pThis->CritSect); + if (RT_SUCCESS(rc)) + { + if (pThis->pTestCurRec == NULL) + { + pThis->pTestCurRec = RTListGetFirst(&pThis->lstTestsRec, VALKITTESTDATA, Node); + if (pThis->pTestCurRec) + LogRel(("ValKit: Next guest recording test in queue is test #%RU32\n", pThis->pTestCurRec->idxTest)); + } + + PVALKITTESTDATA pTst = pThis->pTestCurRec; + if (pTst) + { + switch (pTst->enmState) + { + case AUDIOTESTSTATE_INIT: + RT_FALL_THROUGH(); + case AUDIOTESTSTATE_PRE: + RT_FALL_THROUGH(); + case AUDIOTESTSTATE_POST: + { + cbReadable = AudioTestBeaconGetRemaining(&pTst->t.TestTone.Beacon); + break; + } + + case AUDIOTESTSTATE_RUN: + { + AssertBreakStmt(pTst->t.TestTone.u.Rec.cbToWrite >= pTst->t.TestTone.u.Rec.cbWritten, + rc = VERR_INVALID_STATE); + cbReadable = pTst->t.TestTone.u.Rec.cbToWrite - pTst->t.TestTone.u.Rec.cbWritten; + break; + } + + case AUDIOTESTSTATE_DONE: + RT_FALL_THROUGH(); + default: + break; + } + + LogRel2(("ValKit: Test #%RU32: Reporting %RU32 bytes readable (state is '%s')\n", + pTst->idxTest, cbReadable, AudioTestStateToStr(pTst->enmState))); + + if (cbReadable == 0) + LogRel2(("ValKit: Test #%RU32: Warning: Not readable anymore (state is '%s'), returning 0\n", + pTst->idxTest, AudioTestStateToStr(pTst->enmState))); + } + + int rc2 = RTCritSectLeave(&pThis->CritSect); + AssertRC(rc2); + } + + if (RT_FAILURE(rc)) + LogRel(("ValKit: Reporting readable bytes failed with %Rrc\n", rc)); + + Log3Func(("returns %#x (%RU32)\n", cbReadable, cbReadable)); + return cbReadable; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetWritable} + */ +static DECLCALLBACK(uint32_t) drvHostValKitAudioHA_StreamGetWritable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + PDRVHOSTVALKITAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVHOSTVALKITAUDIO, IHostAudio); + RT_NOREF(pStream); + + uint32_t cbWritable = UINT32_MAX; + PVALKITTESTDATA pTst = NULL; + + int rc = RTCritSectEnter(&pThis->CritSect); + if (RT_SUCCESS(rc)) + { + pTst = pThis->pTestCurPlay; + + if (pTst) + { + switch (pTst->enmState) + { + case AUDIOTESTSTATE_PRE: + RT_FALL_THROUGH(); + case AUDIOTESTSTATE_POST: + { + cbWritable = AudioTestBeaconGetRemaining(&pTst->t.TestTone.Beacon); + break; + } + + case AUDIOTESTSTATE_RUN: + { + AssertReturn(pTst->t.TestTone.u.Play.cbToRead >= pTst->t.TestTone.u.Play.cbRead, 0); + cbWritable = pTst->t.TestTone.u.Play.cbToRead - pTst->t.TestTone.u.Play.cbRead; + break; + } + + default: + break; + } + + LogRel2(("ValKit: Test #%RU32: Reporting %RU32 bytes writable (state is '%s')\n", + pTst->idxTest, cbWritable, AudioTestStateToStr(pTst->enmState))); + + if (cbWritable == 0) + { + LogRel2(("ValKit: Test #%RU32: Warning: Not writable anymore (state is '%s'), returning UINT32_MAX\n", + pTst->idxTest, AudioTestStateToStr(pTst->enmState))); + cbWritable = UINT32_MAX; + } + } + else + LogRel2(("ValKit: Reporting UINT32_MAX bytes writable (no playback test running)\n")); + + int rc2 = RTCritSectLeave(&pThis->CritSect); + AssertRC(rc2); + } + + return cbWritable; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetState} + */ +static DECLCALLBACK(PDMHOSTAUDIOSTREAMSTATE) drvHostValKitAudioHA_StreamGetState(PPDMIHOSTAUDIO pInterface, + PPDMAUDIOBACKENDSTREAM pStream) +{ + AssertPtrReturn(pStream, PDMHOSTAUDIOSTREAMSTATE_INVALID); + +#if 0 + PDRVHOSTVALKITAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVHOSTVALKITAUDIO, IHostAudio); + PDMHOSTAUDIOSTREAMSTATE enmState = PDMHOSTAUDIOSTREAMSTATE_NOT_WORKING; + + if (pStream->pStream->Cfg.enmDir == PDMAUDIODIR_IN) + { + int rc2 = RTCritSectEnter(&pThis->CritSect); + if (RT_SUCCESS(rc2)) + { + enmState = pThis->cTestsRec == 0 + ? PDMHOSTAUDIOSTREAMSTATE_INACTIVE : PDMHOSTAUDIOSTREAMSTATE_OKAY; + + rc2 = RTCritSectLeave(&pThis->CritSect); + AssertRC(rc2); + } + } + else + enmState = PDMHOSTAUDIOSTREAMSTATE_OKAY; + + return enmState; +#else + RT_NOREF(pInterface, pStream); + return PDMHOSTAUDIOSTREAMSTATE_OKAY; +#endif +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamPlay} + */ +static DECLCALLBACK(int) drvHostValKitAudioHA_StreamPlay(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + const void *pvBuf, uint32_t cbBuf, uint32_t *pcbWritten) +{ + if (cbBuf == 0) + { + /* Fend off draining calls. */ + *pcbWritten = 0; + return VINF_SUCCESS; + } + + PDRVHOSTVALKITAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVHOSTVALKITAUDIO, IHostAudio); + PVALKITTESTDATA pTst = NULL; + + int rc2; +#ifdef VBOX_WITH_AUDIO_VALKIT_DUMP_STREAMS + PVALKITAUDIOSTREAM pStrmValKit = (PVALKITAUDIOSTREAM)pStream; + rc2 = AudioHlpFileWrite(pStrmValKit->pFile, pvBuf, cbBuf); + AssertRC(rc2); +#endif + + /* Flag indicating whether the whole block we're going to play is silence or not. */ + bool const fIsAllSilence = PDMAudioPropsIsBufferSilence(&pStream->pStream->Cfg.Props, pvBuf, cbBuf); + + int rc = RTCritSectEnter(&pThis->CritSect); + if (RT_SUCCESS(rc)) + { + pThis->cbPlayedTotal += cbBuf; /* Do a bit of accounting. */ + + if (pThis->pTestCurPlay == NULL) + { + pThis->pTestCurPlay = RTListGetFirst(&pThis->lstTestsPlay, VALKITTESTDATA, Node); + if (pThis->pTestCurPlay) + LogRel(("ValKit: Next guest playback test in queue is test #%RU32\n", pThis->pTestCurPlay->idxTest)); + } + + pTst = pThis->pTestCurPlay; + + rc2 = RTCritSectLeave(&pThis->CritSect); + AssertRC(rc2); + } + + if (pTst == NULL) /* Empty list? */ + { + pThis->cbPlayedNoTest += cbBuf; + + *pcbWritten = cbBuf; + return VINF_SUCCESS; + } + + if (pThis->cbPlayedNoTest) + { + LogRel(("ValKit: Warning: Guest was playing back audio (%RU64 bytes, %RU64ms) when no playback test is active\n", + pThis->cbPlayedNoTest, PDMAudioPropsBytesToMilli(&pStream->pStream->Cfg.Props, pThis->cbPlayedNoTest))); + pThis->cbPlayedNoTest = 0; + } + + if (fIsAllSilence) + { + pThis->cbPlayedSilence += cbBuf; + } + else /* Audible data */ + { + if (pThis->cbPlayedSilence) + LogRel(("ValKit: Guest was playing back %RU64 bytes (%RU64ms) of silence\n", + pThis->cbPlayedSilence, PDMAudioPropsBytesToMilli(&pStream->pStream->Cfg.Props, pThis->cbPlayedSilence))); + pThis->cbPlayedSilence = 0; + } + + LogRel3(("ValKit: Test #%RU32: Playing stream '%s' (%RU32 bytes / %RU64ms) -- state is '%s' ...\n", + pTst->idxTest, pStream->pStream->Cfg.szName, + cbBuf, PDMAudioPropsBytesToMilli(&pStream->pStream->Cfg.Props, cbBuf), + AudioTestStateToStr(pTst->enmState))); + + LogRel4(("ValKit: Playback audio data (%RU32 bytes):\n" + "%.*Rhxd\n", cbBuf, cbBuf, pvBuf)); + + if (pTst->enmState == AUDIOTESTSTATE_INIT) /* Test not started yet? */ + { + AUDIOTESTPARMS Parms; + RT_ZERO(Parms); + Parms.enmDir = PDMAUDIODIR_IN; + Parms.enmType = AUDIOTESTTYPE_TESTTONE_RECORD; + Parms.TestTone = pTst->t.TestTone.Parms; + + rc = AudioTestSetTestBegin(&pThis->Set, "Recording audio data from guest", + &Parms, &pTst->pEntry); + if (RT_SUCCESS(rc)) + rc = AudioTestSetObjCreateAndRegister(&pThis->Set, "host-tone-rec.pcm", &pTst->Obj); + + if (RT_SUCCESS(rc)) + { + pTst->msStartedTS = RTTimeMilliTS(); + LogRel(("ValKit: Test #%RU32: Recording audio data (%RU16Hz, %RU32ms) for host test #%RU32 started (delay is %RU32ms)\n", + pTst->idxTest, (uint16_t)Parms.TestTone.dbFreqHz, Parms.TestTone.msDuration, + Parms.TestTone.Hdr.idxTest, RTTimeMilliTS() - pTst->msRegisteredTS)); + + char szTimeCreated[RTTIME_STR_LEN]; + RTTimeToString(&Parms.TestTone.Hdr.tsCreated, szTimeCreated, sizeof(szTimeCreated)); + LogRel(("ValKit: Test created (caller UTC): %s\n", szTimeCreated)); + + pTst->enmState = AUDIOTESTSTATE_PRE; + } + } + + uint32_t cbWritten = 0; + uint8_t *auBuf = (uint8_t *)pvBuf; + + uint64_t const msStartedTS = RTTimeMilliTS(); + + while (cbWritten < cbBuf) + { + switch (pTst->enmState) + { + case AUDIOTESTSTATE_PRE: + RT_FALL_THROUGH(); + case AUDIOTESTSTATE_POST: + { + PAUDIOTESTTONEBEACON pBeacon = &pTst->t.TestTone.Beacon; + + LogRel3(("ValKit: Test #%RU32: %RU32 bytes (%RU64ms) beacon data remaining\n", + pTst->idxTest, + AudioTestBeaconGetRemaining(pBeacon), + PDMAudioPropsBytesToMilli(&pStream->pStream->Cfg.Props, AudioTestBeaconGetRemaining(pBeacon)))); + + bool fGoToNextStage = false; + + if ( AudioTestBeaconGetSize(pBeacon) + && !AudioTestBeaconIsComplete(pBeacon)) + { + bool const fStarted = AudioTestBeaconGetRemaining(pBeacon) == AudioTestBeaconGetSize(pBeacon); + + size_t off = 0; /* Points at the data right *after* the found beacon data on return. */ + rc2 = AudioTestBeaconAddConsecutive(pBeacon, auBuf, cbBuf - cbWritten, &off); + if (RT_SUCCESS(rc2)) + { + cbWritten += (uint32_t)off; + auBuf += off; + } + else /* No beacon data found. */ + { + LogRel2(("ValKit: Test #%RU32: Warning: Beacon data for '%s' not found (%Rrc) - Skipping ...\n", + pTst->idxTest, AudioTestBeaconTypeGetName(pBeacon->enmType), rc2)); + cbWritten = cbBuf; /* Skip all. */ + break; + } + + if (fStarted) + LogRel2(("ValKit: Test #%RU32: Detection of %s beacon started (%RU64ms played so far)\n", + pTst->idxTest, AudioTestBeaconTypeGetName(pBeacon->enmType), + PDMAudioPropsBytesToMilli(&pStream->pStream->Cfg.Props, pThis->cbPlayedTotal))); + if (AudioTestBeaconIsComplete(pBeacon)) + { + LogRel2(("ValKit: Test #%RU32: Detection of %s beacon ended\n", + pTst->idxTest, AudioTestBeaconTypeGetName(pBeacon->enmType))); + + fGoToNextStage = true; + } + } + else + fGoToNextStage = true; + + if (fGoToNextStage) + { + if (pTst->enmState == AUDIOTESTSTATE_PRE) + pTst->enmState = AUDIOTESTSTATE_RUN; + else if (pTst->enmState == AUDIOTESTSTATE_POST) + pTst->enmState = AUDIOTESTSTATE_DONE; + } + break; + } + + case AUDIOTESTSTATE_RUN: + { + uint32_t const cbRemaining = pTst->t.TestTone.u.Play.cbToRead - pTst->t.TestTone.u.Play.cbRead; + + LogRel3(("ValKit: Test #%RU32: %RU32 bytes (%RU64ms) audio data remaining\n", + pTst->idxTest, cbRemaining, PDMAudioPropsBytesToMilli(&pStream->pStream->Cfg.Props, cbRemaining))); + + /* Don't read more than we're told to. + * After the actual test tone data there might come a post beacon which also + * needs to be handled in the AUDIOTESTSTATE_POST state then. */ + const uint32_t cbData = RT_MIN(cbBuf - cbWritten, cbRemaining); + + pTst->t.TestTone.u.Play.cbRead += cbData; + + cbWritten += cbData; + auBuf += cbData; + + const bool fComplete = pTst->t.TestTone.u.Play.cbRead >= pTst->t.TestTone.u.Play.cbToRead; + if (fComplete) + { + LogRel(("ValKit: Test #%RU32: Recording audio data ended (took %RU32ms)\n", + pTst->idxTest, RTTimeMilliTS() - pTst->msStartedTS)); + + pTst->enmState = AUDIOTESTSTATE_POST; + + /* Re-use the beacon object, but this time it's the post beacon. */ + AudioTestBeaconInit(&pTst->t.TestTone.Beacon, pTst->idxTest, AUDIOTESTTONEBEACONTYPE_PLAY_POST, + &pTst->t.TestTone.Parms.Props); + } + break; + } + + case AUDIOTESTSTATE_DONE: + { + /* Handled below. */ + break; + } + + default: + AssertFailed(); + break; + } + + if (pTst->enmState == AUDIOTESTSTATE_DONE) + break; + + if (RTTimeMilliTS() - msStartedTS > RT_MS_30SEC) + { + LogRel(("ValKit: Test #%RU32: Error: Playback processing timed out -- please report this bug!\n", pTst->idxTest)); + break; + } + } + + LogRel3(("ValKit: Test #%RU32: Played %RU32/%RU32 bytes\n", pTst->idxTest, cbWritten, cbBuf)); + + rc = AudioTestObjWrite(pTst->Obj, pvBuf, cbWritten); + AssertRC(rc); + + if (pTst->enmState == AUDIOTESTSTATE_DONE) + { + AudioTestSetTestDone(pTst->pEntry); + + rc = RTCritSectEnter(&pThis->CritSect); + if (RT_SUCCESS(rc)) + { + drvHostValKiUnregisterPlayTest(pThis, pTst); + + pThis->pTestCurPlay = NULL; + pTst = NULL; + + rc2 = RTCritSectLeave(&pThis->CritSect); + if (RT_SUCCESS(rc)) + rc = rc2; + } + } + + if (RT_FAILURE(rc)) + { + if ( pTst + && pTst->pEntry) + AudioTestSetTestFailed(pTst->pEntry, rc, "Recording audio data failed"); + LogRel(("ValKit: Recording audio data failed with %Rrc\n", rc)); + } + + *pcbWritten = cbWritten; + + return VINF_SUCCESS; /** @todo Return rc here? */ +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamCapture} + */ +static DECLCALLBACK(int) drvHostValKitAudioHA_StreamCapture(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + void *pvBuf, uint32_t cbBuf, uint32_t *pcbRead) +{ + RT_NOREF(pStream); + + if (cbBuf == 0) + { + /* Fend off draining calls. */ + *pcbRead = 0; + return VINF_SUCCESS; + } + + PDRVHOSTVALKITAUDIO pThis = RT_FROM_MEMBER(pInterface, DRVHOSTVALKITAUDIO, IHostAudio); + PVALKITTESTDATA pTst = NULL; + + LogRel3(("ValKit: Capturing stream '%s' (%RU32 bytes / %RU64ms -- %RU64 bytes / %RU64ms total so far) ...\n", + pStream->pStream->Cfg.szName, + cbBuf, PDMAudioPropsBytesToMilli(&pStream->pStream->Cfg.Props, cbBuf), + pThis->cbRecordedTotal, PDMAudioPropsBytesToMilli(&pStream->pStream->Cfg.Props, pThis->cbRecordedTotal))); + + int rc = RTCritSectEnter(&pThis->CritSect); + if (RT_SUCCESS(rc)) + { + if (pThis->pTestCurRec == NULL) + { + pThis->pTestCurRec = RTListGetFirst(&pThis->lstTestsRec, VALKITTESTDATA, Node); + if (pThis->pTestCurRec) + LogRel(("ValKit: Next guest recording test in queue is test #%RU32\n", pThis->pTestCurRec->idxTest)); + } + + pTst = pThis->pTestCurRec; + + int rc2 = RTCritSectLeave(&pThis->CritSect); + AssertRC(rc2); + } + + LogRel4(("ValKit: Capture audio data (%RU32 bytes):\n" + "%.*Rhxd\n", cbBuf, cbBuf, pvBuf)); + + if (pTst == NULL) /* Empty list? */ + { + LogRel(("ValKit: Warning: Guest is trying to record audio data when no recording test is active\n")); + + /** @todo Not sure yet why this happens after all data has been captured sometimes, + * but the guest side just will record silence and the audio test verification + * will have to deal with (and/or report) it then. */ + PDMAudioPropsClearBuffer(&pStream->pStream->Cfg.Props, pvBuf, cbBuf, + PDMAudioPropsBytesToFrames(&pStream->pStream->Cfg.Props, cbBuf)); + + *pcbRead = cbBuf; /* Just report back stuff as being "recorded" (silence). */ + return VINF_SUCCESS; + } + + uint32_t cbWritten = 0; + + switch (pTst->enmState) + { + case AUDIOTESTSTATE_INIT: /* Test not started yet? */ + { + AUDIOTESTPARMS Parms; + RT_ZERO(Parms); + Parms.enmDir = PDMAUDIODIR_OUT; + Parms.enmType = AUDIOTESTTYPE_TESTTONE_PLAY; + Parms.TestTone = pTst->t.TestTone.Parms; + + rc = AudioTestSetTestBegin(&pThis->Set, "Injecting audio input data to guest", + &Parms, &pTst->pEntry); + if (RT_SUCCESS(rc)) + rc = AudioTestSetObjCreateAndRegister(&pThis->Set, "host-tone-play.pcm", &pTst->Obj); + + if (RT_SUCCESS(rc)) + { + pTst->msStartedTS = RTTimeMilliTS(); + LogRel(("ValKit: Test #%RU32: Injecting audio input data (%RU16Hz, %RU32ms, %RU32 bytes) for host test #%RU32 started (delay is %RU32ms)\n", + pTst->idxTest, (uint16_t)pTst->t.TestTone.Tone.rdFreqHz, + pTst->t.TestTone.Parms.msDuration, pTst->t.TestTone.u.Rec.cbToWrite, + Parms.TestTone.Hdr.idxTest, RTTimeMilliTS() - pTst->msRegisteredTS)); + + char szTimeCreated[RTTIME_STR_LEN]; + RTTimeToString(&Parms.TestTone.Hdr.tsCreated, szTimeCreated, sizeof(szTimeCreated)); + LogRel2(("ValKit: Test created (caller UTC): %s\n", szTimeCreated)); + + pTst->enmState = AUDIOTESTSTATE_PRE; + } + else + break; + + RT_FALL_THROUGH(); + } + + case AUDIOTESTSTATE_PRE: + RT_FALL_THROUGH(); + case AUDIOTESTSTATE_POST: + { + bool fGoToNextStage = false; + + PAUDIOTESTTONEBEACON pBeacon = &pTst->t.TestTone.Beacon; + if ( AudioTestBeaconGetSize(pBeacon) + && !AudioTestBeaconIsComplete(pBeacon)) + { + bool const fStarted = AudioTestBeaconGetRemaining(pBeacon) == AudioTestBeaconGetSize(pBeacon); + + uint32_t const cbBeaconRemaining = AudioTestBeaconGetRemaining(pBeacon); + AssertBreakStmt(cbBeaconRemaining, VERR_WRONG_ORDER); + + /* Limit to exactly one beacon (pre or post). */ + uint32_t const cbToWrite = RT_MIN(cbBuf, cbBeaconRemaining); + + rc = AudioTestBeaconWrite(pBeacon, pvBuf, cbToWrite); + if (RT_SUCCESS(rc)) + cbWritten = cbToWrite; + + if (fStarted) + LogRel2(("ValKit: Test #%RU32: Writing %s beacon begin\n", + pTst->idxTest, AudioTestBeaconTypeGetName(pBeacon->enmType))); + if (AudioTestBeaconIsComplete(pBeacon)) + { + LogRel2(("ValKit: Test #%RU32: Writing %s beacon end\n", + pTst->idxTest, AudioTestBeaconTypeGetName(pBeacon->enmType))); + + fGoToNextStage = true; + } + } + else + fGoToNextStage = true; + + if (fGoToNextStage) + { + if (pTst->enmState == AUDIOTESTSTATE_PRE) + pTst->enmState = AUDIOTESTSTATE_RUN; + else if (pTst->enmState == AUDIOTESTSTATE_POST) + pTst->enmState = AUDIOTESTSTATE_DONE; + } + break; + } + + case AUDIOTESTSTATE_RUN: + { + uint32_t const cbToWrite = RT_MIN(cbBuf, pTst->t.TestTone.u.Rec.cbToWrite - pTst->t.TestTone.u.Rec.cbWritten); + if (cbToWrite) + rc = AudioTestToneGenerate(&pTst->t.TestTone.Tone, pvBuf, cbToWrite, &cbWritten); + if ( RT_SUCCESS(rc) + && cbWritten) + { + Assert(cbWritten == cbToWrite); + pTst->t.TestTone.u.Rec.cbWritten += cbWritten; + } + + LogRel3(("ValKit: Test #%RU32: Supplied %RU32 bytes of (capturing) audio data (%RU32 bytes left)\n", + pTst->idxTest, cbWritten, pTst->t.TestTone.u.Rec.cbToWrite - pTst->t.TestTone.u.Rec.cbWritten)); + + const bool fComplete = pTst->t.TestTone.u.Rec.cbWritten >= pTst->t.TestTone.u.Rec.cbToWrite; + if (fComplete) + { + LogRel(("ValKit: Test #%RU32: Recording done (took %RU32ms)\n", + pTst->idxTest, RTTimeMilliTS() - pTst->msStartedTS)); + + pTst->enmState = AUDIOTESTSTATE_POST; + + /* Re-use the beacon object, but this time it's the post beacon. */ + AudioTestBeaconInit(&pTst->t.TestTone.Beacon, pTst->idxTest, AUDIOTESTTONEBEACONTYPE_PLAY_POST, + &pTst->t.TestTone.Parms.Props); + } + break; + } + + case AUDIOTESTSTATE_DONE: + { + /* Handled below. */ + break; + } + + default: + AssertFailed(); + break; + } + + if (RT_SUCCESS(rc)) + rc = AudioTestObjWrite(pTst->Obj, pvBuf, cbWritten); + + if (pTst->enmState == AUDIOTESTSTATE_DONE) + { + AudioTestSetTestDone(pTst->pEntry); + + rc = RTCritSectEnter(&pThis->CritSect); + if (RT_SUCCESS(rc)) + { + drvHostValKiUnregisterRecTest(pThis, pTst); + + pThis->pTestCurRec = NULL; + pTst = NULL; + + int rc2 = RTCritSectLeave(&pThis->CritSect); + AssertRC(rc2); + } + } + + if (RT_FAILURE(rc)) + { + if (pTst->pEntry) + AudioTestSetTestFailed(pTst->pEntry, rc, "Injecting audio input data failed"); + LogRel(("ValKit: Test #%RU32: Failed with %Rrc\n", pTst->idxTest, rc)); + } + + pThis->cbRecordedTotal += cbWritten; /* Do a bit of accounting. */ + + *pcbRead = cbWritten; + + Log3Func(("returns %Rrc *pcbRead=%#x (%#x/%#x), %#x total\n", + rc, cbWritten, pTst ? pTst->t.TestTone.u.Rec.cbWritten : 0, pTst ? pTst->t.TestTone.u.Rec.cbToWrite : 0, + pThis->cbRecordedTotal)); + return VINF_SUCCESS; /** @todo Return rc here? */ +} + + +/** + * @interface_method_impl{PDMIBASE,pfnQueryInterface} + */ +static DECLCALLBACK(void *) drvHostValKitAudioQueryInterface(PPDMIBASE pInterface, const char *pszIID) +{ + PPDMDRVINS pDrvIns = PDMIBASE_2_PDMDRV(pInterface); + PDRVHOSTVALKITAUDIO pThis = PDMINS_2_DATA(pDrvIns, PDRVHOSTVALKITAUDIO); + + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIBASE, &pDrvIns->IBase); + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIHOSTAUDIO, &pThis->IHostAudio); + return NULL; +} + + +/** + * Constructs a VaKit audio driver instance. + * + * @copydoc FNPDMDRVCONSTRUCT + */ +static DECLCALLBACK(int) drvHostValKitAudioConstruct(PPDMDRVINS pDrvIns, PCFGMNODE pCfg, uint32_t fFlags) +{ + RT_NOREF(pCfg, fFlags); + PDMDRV_CHECK_VERSIONS_RETURN(pDrvIns); + PDRVHOSTVALKITAUDIO pThis = PDMINS_2_DATA(pDrvIns, PDRVHOSTVALKITAUDIO); + LogRel(("Audio: Initializing VALKIT driver\n")); + + /* + * Init the static parts. + */ + pThis->pDrvIns = pDrvIns; + /* IBase */ + pDrvIns->IBase.pfnQueryInterface = drvHostValKitAudioQueryInterface; + /* IHostAudio */ + pThis->IHostAudio.pfnGetConfig = drvHostValKitAudioHA_GetConfig; + pThis->IHostAudio.pfnGetDevices = NULL; + pThis->IHostAudio.pfnGetStatus = drvHostValKitAudioHA_GetStatus; + pThis->IHostAudio.pfnDoOnWorkerThread = NULL; + pThis->IHostAudio.pfnStreamConfigHint = NULL; + pThis->IHostAudio.pfnStreamCreate = drvHostValKitAudioHA_StreamCreate; + pThis->IHostAudio.pfnStreamInitAsync = NULL; + pThis->IHostAudio.pfnStreamDestroy = drvHostValKitAudioHA_StreamDestroy; + pThis->IHostAudio.pfnStreamNotifyDeviceChanged = NULL; + pThis->IHostAudio.pfnStreamEnable = drvHostValKitAudioHA_StreamEnable; + pThis->IHostAudio.pfnStreamDisable = drvHostValKitAudioHA_StreamDisable; + pThis->IHostAudio.pfnStreamPause = drvHostValKitAudioHA_StreamPause; + pThis->IHostAudio.pfnStreamResume = drvHostValKitAudioHA_StreamResume; + pThis->IHostAudio.pfnStreamDrain = drvHostValKitAudioHA_StreamDrain; + pThis->IHostAudio.pfnStreamGetReadable = drvHostValKitAudioHA_StreamGetReadable; + pThis->IHostAudio.pfnStreamGetWritable = drvHostValKitAudioHA_StreamGetWritable; + pThis->IHostAudio.pfnStreamGetPending = NULL; + pThis->IHostAudio.pfnStreamGetState = drvHostValKitAudioHA_StreamGetState; + pThis->IHostAudio.pfnStreamPlay = drvHostValKitAudioHA_StreamPlay; + pThis->IHostAudio.pfnStreamCapture = drvHostValKitAudioHA_StreamCapture; + + int rc = RTCritSectInit(&pThis->CritSect); + AssertRCReturn(rc, rc); + rc = RTSemEventCreate(&pThis->EventSemEnded); + AssertRCReturn(rc, rc); + + pThis->cbPlayedTotal = 0; + pThis->cbRecordedTotal = 0; + pThis->cbPlayedSilence = 0; + pThis->cbPlayedNoTest = 0; + + pThis->cTestsTotal = 0; + pThis->fTestSetEnd = false; + + RTListInit(&pThis->lstTestsRec); + pThis->cTestsRec = 0; + RTListInit(&pThis->lstTestsPlay); + pThis->cTestsPlay = 0; + + ATSCALLBACKS Callbacks; + RT_ZERO(Callbacks); + Callbacks.pfnHowdy = drvHostValKitHowdy; + Callbacks.pfnBye = drvHostValKitBye; + Callbacks.pfnTestSetBegin = drvHostValKitTestSetBegin; + Callbacks.pfnTestSetEnd = drvHostValKitTestSetEnd; + Callbacks.pfnTonePlay = drvHostValKitRegisterGuestRecTest; + Callbacks.pfnToneRecord = drvHostValKitRegisterGuestPlayTest; + Callbacks.pfnTestSetSendBegin = drvHostValKitTestSetSendBeginCallback; + Callbacks.pfnTestSetSendRead = drvHostValKitTestSetSendReadCallback; + Callbacks.pfnTestSetSendEnd = drvHostValKitTestSetSendEndCallback; + Callbacks.pvUser = pThis; + + /** @todo Make this configurable via CFGM. */ + const char *pszBindAddr = "127.0.0.1"; /* Only reachable for localhost for now. */ + uint32_t uBindPort = ATS_TCP_DEF_BIND_PORT_VALKIT; + + LogRel2(("ValKit: Debug logging enabled\n")); + + LogRel(("ValKit: Starting Audio Test Service (ATS) at %s:%RU32...\n", + pszBindAddr, uBindPort)); + + /* Dont' use rc here, as this will be reported back to PDM and will prevent VBox + * from starting -- not critical but warn the user though. */ + int rc2 = AudioTestSvcInit(&pThis->Srv, &Callbacks); + if (RT_SUCCESS(rc2)) + { + RTGETOPTUNION Val; + RT_ZERO(Val); + + Val.u32 = ATSCONNMODE_SERVER; /** @todo No client connection mode needed here (yet). Make this configurable via CFGM. */ + rc2 = AudioTestSvcHandleOption(&pThis->Srv, ATSTCPOPT_CONN_MODE, &Val); + AssertRC(rc2); + + Val.psz = pszBindAddr; + rc2 = AudioTestSvcHandleOption(&pThis->Srv, ATSTCPOPT_BIND_ADDRESS, &Val); + AssertRC(rc2); + + Val.u16 = uBindPort; + rc2 = AudioTestSvcHandleOption(&pThis->Srv, ATSTCPOPT_BIND_PORT, &Val); + AssertRC(rc2); + + rc2 = AudioTestSvcStart(&pThis->Srv); + } + + if (RT_SUCCESS(rc2)) + { + LogRel(("ValKit: Audio Test Service (ATS) running\n")); + + /** @todo Let the following be customizable by CFGM later. */ + rc2 = AudioTestPathCreateTemp(pThis->szPathTemp, sizeof(pThis->szPathTemp), "ValKitAudio"); + if (RT_SUCCESS(rc2)) + { + LogRel(("ValKit: Using temp dir '%s'\n", pThis->szPathTemp)); + rc2 = AudioTestPathGetTemp(pThis->szPathOut, sizeof(pThis->szPathOut)); + if (RT_SUCCESS(rc2)) + LogRel(("ValKit: Using output dir '%s'\n", pThis->szPathOut)); + } + } + + if (RT_FAILURE(rc2)) + LogRel(("ValKit: Error starting Audio Test Service (ATS), rc=%Rrc -- tests *will* fail!\n", rc2)); + + if (RT_FAILURE(rc)) /* This one *is* critical though. */ + LogRel(("ValKit: Initialization failed, rc=%Rrc\n", rc)); + + return rc; +} + +static DECLCALLBACK(void) drvHostValKitAudioDestruct(PPDMDRVINS pDrvIns) +{ + PDMDRV_CHECK_VERSIONS_RETURN_VOID(pDrvIns); + PDRVHOSTVALKITAUDIO pThis = PDMINS_2_DATA(pDrvIns, PDRVHOSTVALKITAUDIO); + + LogRel(("ValKit: Shutting down Audio Test Service (ATS) ...\n")); + + int rc = AudioTestSvcStop(&pThis->Srv); + if (RT_SUCCESS(rc)) + rc = AudioTestSvcDestroy(&pThis->Srv); + + if (RT_SUCCESS(rc)) + { + LogRel(("ValKit: Shutdown of Audio Test Service (ATS) complete\n")); + drvHostValKitCleanup(pThis); + } + else + LogRel(("ValKit: Shutdown of Audio Test Service (ATS) failed, rc=%Rrc\n", rc)); + + /* Try cleaning up a bit. */ + RTDirRemove(pThis->szPathTemp); + RTDirRemove(pThis->szPathOut); + + RTSemEventDestroy(pThis->EventSemEnded); + + if (RTCritSectIsInitialized(&pThis->CritSect)) + { + int rc2 = RTCritSectDelete(&pThis->CritSect); + if (RT_SUCCESS(rc)) + rc = rc2; + } + + if (RT_FAILURE(rc)) + LogRel(("ValKit: Destruction failed, rc=%Rrc\n", rc)); +} + +/** + * Char driver registration record. + */ +const PDMDRVREG g_DrvHostValidationKitAudio = +{ + /* u32Version */ + PDM_DRVREG_VERSION, + /* szName */ + "ValidationKitAudio", + /* szRCMod */ + "", + /* szR0Mod */ + "", + /* pszDescription */ + "ValidationKitAudio audio host driver", + /* fFlags */ + PDM_DRVREG_FLAGS_HOST_BITS_DEFAULT, + /* fClass. */ + PDM_DRVREG_CLASS_AUDIO, + /* cMaxInstances */ + ~0U, + /* cbInstance */ + sizeof(DRVHOSTVALKITAUDIO), + /* pfnConstruct */ + drvHostValKitAudioConstruct, + /* pfnDestruct */ + drvHostValKitAudioDestruct, + /* pfnRelocate */ + NULL, + /* pfnIOCtl */ + NULL, + /* pfnPowerOn */ + NULL, + /* pfnReset */ + NULL, + /* pfnSuspend */ + NULL, + /* pfnResume */ + NULL, + /* pfnAttach */ + NULL, + /* pfnDetach */ + NULL, + /* pfnPowerOff */ + NULL, + /* pfnSoftReset */ + NULL, + /* u32EndVersion */ + PDM_DRVREG_VERSION +}; + diff --git a/src/VBox/Devices/Audio/DrvHostAudioWasApi.cpp b/src/VBox/Devices/Audio/DrvHostAudioWasApi.cpp new file mode 100644 index 00000000..800f066e --- /dev/null +++ b/src/VBox/Devices/Audio/DrvHostAudioWasApi.cpp @@ -0,0 +1,3350 @@ +/* $Id: DrvHostAudioWasApi.cpp $ */ +/** @file + * Host audio driver - Windows Audio Session API. + */ + +/* + * Copyright (C) 2021-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 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_DRV_HOST_AUDIO +/*#define INITGUID - defined in VBoxhostAudioDSound.cpp already */ +#include <VBox/log.h> +#include <iprt/win/windows.h> +#include <Mmdeviceapi.h> +#include <iprt/win/audioclient.h> +#include <functiondiscoverykeys_devpkey.h> +#include <AudioSessionTypes.h> +#ifndef AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY +# define AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY UINT32_C(0x08000000) +#endif +#ifndef AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM +# define AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM UINT32_C(0x80000000) +#endif + +#include <VBox/vmm/pdmaudioinline.h> +#include <VBox/vmm/pdmaudiohostenuminline.h> + +#include <iprt/rand.h> +#include <iprt/semaphore.h> +#include <iprt/utf16.h> +#include <iprt/uuid.h> + +#include <new> /* std::bad_alloc */ + +#include "VBoxDD.h" + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +/** Max GetCurrentPadding value we accept (to make sure it's safe to convert to bytes). */ +#define VBOX_WASAPI_MAX_PADDING UINT32_C(0x007fffff) + +/** Maximum number of cached device configs in each direction. + * The number 4 was picked at random. */ +#define VBOX_WASAPI_MAX_TOTAL_CONFIG_ENTRIES 4 + +#if 0 +/** @name WM_DRVHOSTAUDIOWAS_XXX - Worker thread messages. + * @{ */ +#define WM_DRVHOSTAUDIOWAS_PURGE_CACHE (WM_APP + 3) +/** @} */ +#endif + + +/** @name DRVHOSTAUDIOWAS_DO_XXX - Worker thread operations. + * @{ */ +#define DRVHOSTAUDIOWAS_DO_PURGE_CACHE ((uintptr_t)0x49f37300 + 1) +#define DRVHOSTAUDIOWAS_DO_PRUNE_CACHE ((uintptr_t)0x49f37300 + 2) +#define DRVHOSTAUDIOWAS_DO_STREAM_DEV_SWITCH ((uintptr_t)0x49f37300 + 3) +/** @} */ + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +class DrvHostAudioWasMmNotifyClient; + +/** Pointer to the cache entry for a host audio device (+dir). */ +typedef struct DRVHOSTAUDIOWASCACHEDEV *PDRVHOSTAUDIOWASCACHEDEV; + +/** + * Cached pre-initialized audio client for a device. + * + * The activation and initialization of an IAudioClient has been observed to be + * very very slow (> 100 ms) and not suitable to be done on an EMT. So, we'll + * pre-initialize the device clients at construction time and when the default + * device changes to try avoid this problem. + * + * A client is returned to the cache after we're done with it, provided it still + * works fine. + */ +typedef struct DRVHOSTAUDIOWASCACHEDEVCFG +{ + /** Entry in DRVHOSTAUDIOWASCACHEDEV::ConfigList. */ + RTLISTNODE ListEntry; + /** The device. */ + PDRVHOSTAUDIOWASCACHEDEV pDevEntry; + /** The cached audio client. */ + IAudioClient *pIAudioClient; + /** Output streams: The render client interface. */ + IAudioRenderClient *pIAudioRenderClient; + /** Input streams: The capture client interface. */ + IAudioCaptureClient *pIAudioCaptureClient; + /** The configuration. */ + PDMAUDIOPCMPROPS Props; + /** The buffer size in frames. */ + uint32_t cFramesBufferSize; + /** The device/whatever period in frames. */ + uint32_t cFramesPeriod; + /** The setup status code. + * This is set to VERR_AUDIO_STREAM_INIT_IN_PROGRESS while the asynchronous + * initialization is still running. */ + int volatile rcSetup; + /** Creation timestamp (just for reference). */ + uint64_t nsCreated; + /** Init complete timestamp (just for reference). */ + uint64_t nsInited; + /** When it was last used. */ + uint64_t nsLastUsed; + /** The stringified properties. */ + char szProps[32]; +} DRVHOSTAUDIOWASCACHEDEVCFG; +/** Pointer to a pre-initialized audio client. */ +typedef DRVHOSTAUDIOWASCACHEDEVCFG *PDRVHOSTAUDIOWASCACHEDEVCFG; + +/** + * Per audio device (+ direction) cache entry. + */ +typedef struct DRVHOSTAUDIOWASCACHEDEV +{ + /** Entry in DRVHOSTAUDIOWAS::CacheHead. */ + RTLISTNODE ListEntry; + /** The MM device associated with the stream. */ + IMMDevice *pIDevice; + /** The direction of the device. */ + PDMAUDIODIR enmDir; +#if 0 /* According to https://social.msdn.microsoft.com/Forums/en-US/1d974d90-6636-4121-bba3-a8861d9ab92a, + these were always support just missing from the SDK. */ + /** Support for AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM: -1=unknown, 0=no, 1=yes. */ + int8_t fSupportsAutoConvertPcm; + /** Support for AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY: -1=unknown, 0=no, 1=yes. */ + int8_t fSupportsSrcDefaultQuality; +#endif + /** List of cached configurations (DRVHOSTAUDIOWASCACHEDEVCFG). */ + RTLISTANCHOR ConfigList; + /** The device ID length in RTUTF16 units. */ + size_t cwcDevId; + /** The device ID. */ + RTUTF16 wszDevId[RT_FLEXIBLE_ARRAY]; +} DRVHOSTAUDIOWASCACHEDEV; + + +/** + * Data for a WASABI stream. + */ +typedef struct DRVHOSTAUDIOWASSTREAM +{ + /** Common part. */ + PDMAUDIOBACKENDSTREAM Core; + + /** Entry in DRVHOSTAUDIOWAS::StreamHead. */ + RTLISTNODE ListEntry; + /** The stream's acquired configuration. */ + PDMAUDIOSTREAMCFG Cfg; + /** Cache entry to be relased when destroying the stream. */ + PDRVHOSTAUDIOWASCACHEDEVCFG pDevCfg; + + /** Set if the stream is enabled. */ + bool fEnabled; + /** Set if the stream is started (playing/capturing). */ + bool fStarted; + /** Set if the stream is draining (output only). */ + bool fDraining; + /** Set if we should restart the stream on resume (saved pause state). */ + bool fRestartOnResume; + /** Set if we're switching to a new output/input device. */ + bool fSwitchingDevice; + + /** The RTTimeMilliTS() deadline for the draining of this stream (output). */ + uint64_t msDrainDeadline; + /** Internal stream offset (bytes). */ + uint64_t offInternal; + /** The RTTimeMilliTS() at the end of the last transfer. */ + uint64_t msLastTransfer; + + /** Input: Current capture buffer (advanced as we read). */ + uint8_t *pbCapture; + /** Input: The number of bytes left in the current capture buffer. */ + uint32_t cbCapture; + /** Input: The full size of what pbCapture is part of (for ReleaseBuffer). */ + uint32_t cFramesCaptureToRelease; + + /** Critical section protecting: . */ + RTCRITSECT CritSect; + /** Buffer that drvHostWasStreamStatusString uses. */ + char szStatus[128]; +} DRVHOSTAUDIOWASSTREAM; +/** Pointer to a WASABI stream. */ +typedef DRVHOSTAUDIOWASSTREAM *PDRVHOSTAUDIOWASSTREAM; + + +/** + * WASAPI-specific device entry. + */ +typedef struct DRVHOSTAUDIOWASDEV +{ + /** The core structure. */ + PDMAUDIOHOSTDEV Core; + /** The device ID (flexible length). */ + RTUTF16 wszDevId[RT_FLEXIBLE_ARRAY]; +} DRVHOSTAUDIOWASDEV; +/** Pointer to a DirectSound device entry. */ +typedef DRVHOSTAUDIOWASDEV *PDRVHOSTAUDIOWASDEV; + + +/** + * Data for a WASAPI host audio instance. + */ +typedef struct DRVHOSTAUDIOWAS +{ + /** The audio host audio interface we export. */ + PDMIHOSTAUDIO IHostAudio; + /** Pointer to the PDM driver instance. */ + PPDMDRVINS pDrvIns; + /** Audio device enumerator instance that we use for getting the default + * devices (or specific ones if overriden by config). Also used for + * implementing enumeration. */ + IMMDeviceEnumerator *pIEnumerator; + /** The upwards interface. */ + PPDMIHOSTAUDIOPORT pIHostAudioPort; + /** The output device ID, NULL for default. + * Protected by DrvHostAudioWasMmNotifyClient::m_CritSect. */ + PRTUTF16 pwszOutputDevId; + /** The input device ID, NULL for default. + * Protected by DrvHostAudioWasMmNotifyClient::m_CritSect. */ + PRTUTF16 pwszInputDevId; + + /** Pointer to the MM notification client instance. */ + DrvHostAudioWasMmNotifyClient *pNotifyClient; + /** The input device to use. This can be NULL if there wasn't a suitable one + * around when we last looked or if it got removed/disabled/whatever. + * All access must be done inside the pNotifyClient critsect. */ + IMMDevice *pIDeviceInput; + /** The output device to use. This can be NULL if there wasn't a suitable one + * around when we last looked or if it got removed/disabled/whatever. + * All access must be done inside the pNotifyClient critsect. */ + IMMDevice *pIDeviceOutput; + + /** List of streams (DRVHOSTAUDIOWASSTREAM). + * Requires CritSect ownership. */ + RTLISTANCHOR StreamHead; + /** Serializing access to StreamHead. */ + RTCRITSECTRW CritSectStreamList; + + /** List of cached devices (DRVHOSTAUDIOWASCACHEDEV). + * Protected by CritSectCache */ + RTLISTANCHOR CacheHead; + /** Serializing access to CacheHead. */ + RTCRITSECT CritSectCache; + /** Semaphore for signalling that cache purge is done and that the destructor + * can do cleanups. */ + RTSEMEVENTMULTI hEvtCachePurge; + /** Total number of device config entire for capturing. + * This includes in-use ones. */ + uint32_t volatile cCacheEntriesIn; + /** Total number of device config entire for playback. + * This includes in-use ones. */ + uint32_t volatile cCacheEntriesOut; + +#if 0 + /** The worker thread. */ + RTTHREAD hWorkerThread; + /** The TID of the worker thread (for posting messages to it). */ + DWORD idWorkerThread; + /** The fixed wParam value for the worker thread. */ + WPARAM uWorkerThreadFixedParam; +#endif +} DRVHOSTAUDIOWAS; +/** Pointer to the data for a WASAPI host audio driver instance. */ +typedef DRVHOSTAUDIOWAS *PDRVHOSTAUDIOWAS; + + + + +/** + * Gets the stream status. + * + * @returns Pointer to stream status string. + * @param pStreamWas The stream to get the status for. + */ +static const char *drvHostWasStreamStatusString(PDRVHOSTAUDIOWASSTREAM pStreamWas) +{ + static RTSTRTUPLE const s_aEnable[2] = + { + { RT_STR_TUPLE("DISABLED") }, + { RT_STR_TUPLE("ENABLED ") }, + }; + PCRTSTRTUPLE pTuple = &s_aEnable[pStreamWas->fEnabled]; + memcpy(pStreamWas->szStatus, pTuple->psz, pTuple->cch); + size_t off = pTuple->cch; + + static RTSTRTUPLE const s_aStarted[2] = + { + { RT_STR_TUPLE(" STOPPED") }, + { RT_STR_TUPLE(" STARTED") }, + }; + pTuple = &s_aStarted[pStreamWas->fStarted]; + memcpy(&pStreamWas->szStatus[off], pTuple->psz, pTuple->cch); + off += pTuple->cch; + + static RTSTRTUPLE const s_aDraining[2] = + { + { RT_STR_TUPLE("") }, + { RT_STR_TUPLE(" DRAINING") }, + }; + pTuple = &s_aDraining[pStreamWas->fDraining]; + memcpy(&pStreamWas->szStatus[off], pTuple->psz, pTuple->cch); + off += pTuple->cch; + + Assert(off < sizeof(pStreamWas->szStatus)); + pStreamWas->szStatus[off] = '\0'; + return pStreamWas->szStatus; +} + + +/********************************************************************************************************************************* +* IMMNotificationClient implementation +*********************************************************************************************************************************/ +/** + * Multimedia notification client. + * + * We want to know when the default device changes so we can switch running + * streams to use the new one and so we can pre-activate it in preparation + * for new streams. + */ +class DrvHostAudioWasMmNotifyClient : public IMMNotificationClient +{ +private: + /** Reference counter. */ + uint32_t volatile m_cRefs; + /** The WASAPI host audio driver instance data. + * @note This can be NULL. Only access after entering critical section. */ + PDRVHOSTAUDIOWAS m_pDrvWas; + /** Critical section serializing access to m_pDrvWas. */ + RTCRITSECT m_CritSect; + +public: + /** + * @throws int on critical section init failure. + */ + DrvHostAudioWasMmNotifyClient(PDRVHOSTAUDIOWAS a_pDrvWas) + : m_cRefs(1) + , m_pDrvWas(a_pDrvWas) + { + RT_ZERO(m_CritSect); + } + + virtual ~DrvHostAudioWasMmNotifyClient() RT_NOEXCEPT + { + if (RTCritSectIsInitialized(&m_CritSect)) + RTCritSectDelete(&m_CritSect); + } + + /** + * Initializes the critical section. + * @note Must be buildable w/o exceptions enabled, so cannot do this from the + * constructor. */ + int init(void) RT_NOEXCEPT + { + return RTCritSectInit(&m_CritSect); + } + + /** + * Called by drvHostAudioWasDestruct to set m_pDrvWas to NULL. + */ + void notifyDriverDestroyed() RT_NOEXCEPT + { + RTCritSectEnter(&m_CritSect); + m_pDrvWas = NULL; + RTCritSectLeave(&m_CritSect); + } + + /** + * Enters the notification critsect for getting at the IMMDevice members in + * PDMHOSTAUDIOWAS. + */ + void lockEnter() RT_NOEXCEPT + { + RTCritSectEnter(&m_CritSect); + } + + /** + * Leaves the notification critsect. + */ + void lockLeave() RT_NOEXCEPT + { + RTCritSectLeave(&m_CritSect); + } + + /** @name IUnknown interface + * @{ */ + IFACEMETHODIMP_(ULONG) AddRef() + { + uint32_t cRefs = ASMAtomicIncU32(&m_cRefs); + AssertMsg(cRefs < 64, ("%#x\n", cRefs)); + Log6Func(("returns %u\n", cRefs)); + return cRefs; + } + + IFACEMETHODIMP_(ULONG) Release() + { + uint32_t cRefs = ASMAtomicDecU32(&m_cRefs); + AssertMsg(cRefs < 64, ("%#x\n", cRefs)); + if (cRefs == 0) + delete this; + Log6Func(("returns %u\n", cRefs)); + return cRefs; + } + + IFACEMETHODIMP QueryInterface(const IID &rIID, void **ppvInterface) + { + if (IsEqualIID(rIID, IID_IUnknown)) + *ppvInterface = static_cast<IUnknown *>(this); + else if (IsEqualIID(rIID, __uuidof(IMMNotificationClient))) + *ppvInterface = static_cast<IMMNotificationClient *>(this); + else + { + LogFunc(("Unknown rIID={%RTuuid}\n", &rIID)); + *ppvInterface = NULL; + return E_NOINTERFACE; + } + Log6Func(("returns S_OK + %p\n", *ppvInterface)); + return S_OK; + } + /** @} */ + + /** @name IMMNotificationClient interface + * @{ */ + IFACEMETHODIMP OnDeviceStateChanged(LPCWSTR pwszDeviceId, DWORD dwNewState) + { + RT_NOREF(pwszDeviceId, dwNewState); + Log7Func(("pwszDeviceId=%ls dwNewState=%u (%#x)\n", pwszDeviceId, dwNewState, dwNewState)); + + /* + * Just trigger device re-enumeration. + */ + notifyDeviceChanges(); + + /** @todo do we need to check for our devices here too? Not when using a + * default device. But when using a specific device, we could perhaps + * re-init the stream when dwNewState indicates precense. We might + * also take action when a devices ceases to be operating, but again + * only for non-default devices, probably... */ + + return S_OK; + } + + IFACEMETHODIMP OnDeviceAdded(LPCWSTR pwszDeviceId) + { + RT_NOREF(pwszDeviceId); + Log7Func(("pwszDeviceId=%ls\n", pwszDeviceId)); + + /* + * Is this a device we're interested in? Grab the enumerator if it is. + */ + bool fOutput = false; + IMMDeviceEnumerator *pIEnumerator = NULL; + RTCritSectEnter(&m_CritSect); + if ( m_pDrvWas != NULL + && ( (fOutput = RTUtf16ICmp(m_pDrvWas->pwszOutputDevId, pwszDeviceId) == 0) + || RTUtf16ICmp(m_pDrvWas->pwszInputDevId, pwszDeviceId) == 0)) + { + pIEnumerator = m_pDrvWas->pIEnumerator; + if (pIEnumerator /* paranoia */) + pIEnumerator->AddRef(); + } + RTCritSectLeave(&m_CritSect); + if (pIEnumerator) + { + /* + * Get the device and update it. + */ + IMMDevice *pIDevice = NULL; + HRESULT hrc = pIEnumerator->GetDevice(pwszDeviceId, &pIDevice); + if (SUCCEEDED(hrc)) + setDevice(fOutput, pIDevice, pwszDeviceId, __PRETTY_FUNCTION__); + else + LogRelMax(64, ("WasAPI: Failed to get %s device '%ls' (OnDeviceAdded): %Rhrc\n", + fOutput ? "output" : "input", pwszDeviceId, hrc)); + pIEnumerator->Release(); + + /* + * Trigger device re-enumeration. + */ + notifyDeviceChanges(); + } + return S_OK; + } + + IFACEMETHODIMP OnDeviceRemoved(LPCWSTR pwszDeviceId) + { + RT_NOREF(pwszDeviceId); + Log7Func(("pwszDeviceId=%ls\n", pwszDeviceId)); + + /* + * Is this a device we're interested in? Then set it to NULL. + */ + bool fOutput = false; + RTCritSectEnter(&m_CritSect); + if ( m_pDrvWas != NULL + && ( (fOutput = RTUtf16ICmp(m_pDrvWas->pwszOutputDevId, pwszDeviceId) == 0) + || RTUtf16ICmp(m_pDrvWas->pwszInputDevId, pwszDeviceId) == 0)) + { + RTCritSectLeave(&m_CritSect); + setDevice(fOutput, NULL, pwszDeviceId, __PRETTY_FUNCTION__); + } + else + RTCritSectLeave(&m_CritSect); + + /* + * Trigger device re-enumeration. + */ + notifyDeviceChanges(); + return S_OK; + } + + IFACEMETHODIMP OnDefaultDeviceChanged(EDataFlow enmFlow, ERole enmRole, LPCWSTR pwszDefaultDeviceId) + { + /* + * Are we interested in this device? If so grab the enumerator. + */ + IMMDeviceEnumerator *pIEnumerator = NULL; + RTCritSectEnter(&m_CritSect); + if ( m_pDrvWas != NULL + && ( (enmFlow == eRender && enmRole == eMultimedia && !m_pDrvWas->pwszOutputDevId) + || (enmFlow == eCapture && enmRole == eMultimedia && !m_pDrvWas->pwszInputDevId))) + { + pIEnumerator = m_pDrvWas->pIEnumerator; + if (pIEnumerator /* paranoia */) + pIEnumerator->AddRef(); + } + RTCritSectLeave(&m_CritSect); + if (pIEnumerator) + { + /* + * Get the device and update it. + */ + IMMDevice *pIDevice = NULL; + HRESULT hrc = pIEnumerator->GetDefaultAudioEndpoint(enmFlow, enmRole, &pIDevice); + if (SUCCEEDED(hrc)) + setDevice(enmFlow == eRender, pIDevice, pwszDefaultDeviceId, __PRETTY_FUNCTION__); + else + LogRelMax(64, ("WasAPI: Failed to get default %s device (OnDefaultDeviceChange): %Rhrc\n", + enmFlow == eRender ? "output" : "input", hrc)); + pIEnumerator->Release(); + + /* + * Trigger device re-enumeration. + */ + notifyDeviceChanges(); + } + + Log7Func(("enmFlow=%d enmRole=%d pwszDefaultDeviceId=%ls\n", enmFlow, enmRole, pwszDefaultDeviceId)); + return S_OK; + } + + IFACEMETHODIMP OnPropertyValueChanged(LPCWSTR pwszDeviceId, const PROPERTYKEY Key) + { + RT_NOREF(pwszDeviceId, Key); + Log7Func(("pwszDeviceId=%ls Key={%RTuuid, %u (%#x)}\n", pwszDeviceId, &Key.fmtid, Key.pid, Key.pid)); + return S_OK; + } + /** @} */ + +private: + /** + * Sets DRVHOSTAUDIOWAS::pIDeviceOutput or DRVHOSTAUDIOWAS::pIDeviceInput to @a pIDevice. + */ + void setDevice(bool fOutput, IMMDevice *pIDevice, LPCWSTR pwszDeviceId, const char *pszCaller) + { + RT_NOREF(pszCaller, pwszDeviceId); + + RTCritSectEnter(&m_CritSect); + + /* + * Update our internal device reference. + */ + if (m_pDrvWas) + { + if (fOutput) + { + Log7((LOG_FN_FMT ": Changing output device from %p to %p (%ls)\n", + pszCaller, m_pDrvWas->pIDeviceOutput, pIDevice, pwszDeviceId)); + if (m_pDrvWas->pIDeviceOutput) + m_pDrvWas->pIDeviceOutput->Release(); + m_pDrvWas->pIDeviceOutput = pIDevice; + } + else + { + Log7((LOG_FN_FMT ": Changing input device from %p to %p (%ls)\n", + pszCaller, m_pDrvWas->pIDeviceInput, pIDevice, pwszDeviceId)); + if (m_pDrvWas->pIDeviceInput) + m_pDrvWas->pIDeviceInput->Release(); + m_pDrvWas->pIDeviceInput = pIDevice; + } + } + else if (pIDevice) + pIDevice->Release(); + + /* + * Tell DrvAudio that the device has changed for one of the directions. + * + * We have to exit the critsect when doing so, or we'll create a locking + * order violation. So, try make sure the VM won't be destroyed while + * till DrvAudio have entered its critical section... + */ + if (m_pDrvWas) + { + PPDMIHOSTAUDIOPORT const pIHostAudioPort = m_pDrvWas->pIHostAudioPort; + if (pIHostAudioPort) + { + VMSTATE const enmVmState = PDMDrvHlpVMState(m_pDrvWas->pDrvIns); + if (enmVmState < VMSTATE_POWERING_OFF) + { + RTCritSectLeave(&m_CritSect); + pIHostAudioPort->pfnNotifyDeviceChanged(pIHostAudioPort, fOutput ? PDMAUDIODIR_OUT : PDMAUDIODIR_IN, NULL); + return; + } + LogFlowFunc(("Ignoring change: enmVmState=%d\n", enmVmState)); + } + } + + RTCritSectLeave(&m_CritSect); + } + + /** + * Tell DrvAudio to re-enumerate devices when it get a chance. + * + * We exit the critsect here too before calling DrvAudio just to be on the safe + * side (see setDevice()), even though the current DrvAudio code doesn't take + * any critsects. + */ + void notifyDeviceChanges(void) + { + RTCritSectEnter(&m_CritSect); + if (m_pDrvWas) + { + PPDMIHOSTAUDIOPORT const pIHostAudioPort = m_pDrvWas->pIHostAudioPort; + if (pIHostAudioPort) + { + VMSTATE const enmVmState = PDMDrvHlpVMState(m_pDrvWas->pDrvIns); + if (enmVmState < VMSTATE_POWERING_OFF) + { + RTCritSectLeave(&m_CritSect); + pIHostAudioPort->pfnNotifyDevicesChanged(pIHostAudioPort); + return; + } + LogFlowFunc(("Ignoring change: enmVmState=%d\n", enmVmState)); + } + } + RTCritSectLeave(&m_CritSect); + } +}; + + +/********************************************************************************************************************************* +* Pre-configured audio client cache. * +*********************************************************************************************************************************/ +#define WAS_CACHE_MAX_ENTRIES_SAME_DEVICE 2 + +/** + * Converts from PDM stream config to windows WAVEFORMATEXTENSIBLE struct. + * + * @param pProps The PDM audio PCM properties to convert from. + * @param pFmt The windows structure to initialize. + */ +static void drvHostAudioWasWaveFmtExtFromProps(PCPDMAUDIOPCMPROPS pProps, PWAVEFORMATEXTENSIBLE pFmt) +{ + RT_ZERO(*pFmt); + pFmt->Format.wFormatTag = WAVE_FORMAT_PCM; + pFmt->Format.nChannels = PDMAudioPropsChannels(pProps); + pFmt->Format.wBitsPerSample = PDMAudioPropsSampleBits(pProps); + pFmt->Format.nSamplesPerSec = PDMAudioPropsHz(pProps); + pFmt->Format.nBlockAlign = PDMAudioPropsFrameSize(pProps); + pFmt->Format.nAvgBytesPerSec = PDMAudioPropsFramesToBytes(pProps, PDMAudioPropsHz(pProps)); + pFmt->Format.cbSize = 0; /* No extra data specified. */ + + /* + * We need to use the extensible structure if there are more than two channels + * or if the channels have non-standard assignments. + */ + if ( pFmt->Format.nChannels > 2 + || ( pFmt->Format.nChannels == 1 + ? pProps->aidChannels[0] != PDMAUDIOCHANNELID_MONO + : pProps->aidChannels[0] != PDMAUDIOCHANNELID_FRONT_LEFT + || pProps->aidChannels[1] != PDMAUDIOCHANNELID_FRONT_RIGHT)) + { + pFmt->Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; + pFmt->Format.cbSize = sizeof(*pFmt) - sizeof(pFmt->Format); + pFmt->Samples.wValidBitsPerSample = PDMAudioPropsSampleBits(pProps); + pFmt->SubFormat = KSDATAFORMAT_SUBTYPE_PCM; + pFmt->dwChannelMask = 0; + unsigned const cSrcChannels = pFmt->Format.nChannels; + for (unsigned i = 0; i < cSrcChannels; i++) + if ( pProps->aidChannels[i] >= PDMAUDIOCHANNELID_FIRST_STANDARD + && pProps->aidChannels[i] < PDMAUDIOCHANNELID_END_STANDARD) + pFmt->dwChannelMask |= RT_BIT_32(pProps->aidChannels[i] - PDMAUDIOCHANNELID_FIRST_STANDARD); + else + pFmt->Format.nChannels -= 1; + } +} + + +#if 0 /* unused */ +/** + * Converts from windows WAVEFORMATEX and stream props to PDM audio properties. + * + * @returns VINF_SUCCESS on success, VERR_AUDIO_STREAM_COULD_NOT_CREATE if not + * supported. + * @param pProps The output properties structure. + * @param pFmt The windows wave format structure. + * @param pszStream The stream name for error logging. + * @param pwszDevId The device ID for error logging. + */ +static int drvHostAudioWasCacheWaveFmtExToProps(PPDMAUDIOPCMPROPS pProps, WAVEFORMATEX const *pFmt, + const char *pszStream, PCRTUTF16 pwszDevId) +{ + if (pFmt->wFormatTag == WAVE_FORMAT_PCM) + { + if ( pFmt->wBitsPerSample == 8 + || pFmt->wBitsPerSample == 16 + || pFmt->wBitsPerSample == 32) + { + if (pFmt->nChannels > 0 && pFmt->nChannels < 16) + { + if (pFmt->nSamplesPerSec >= 4096 && pFmt->nSamplesPerSec <= 768000) + { + PDMAudioPropsInit(pProps, pFmt->wBitsPerSample / 8, true /*fSigned*/, pFmt->nChannels, pFmt->nSamplesPerSec); + if (PDMAudioPropsFrameSize(pProps) == pFmt->nBlockAlign) + return VINF_SUCCESS; + } + } + } + } + LogRelMax(64, ("WasAPI: Error! Unsupported stream format for '%s' suggested by '%ls':\n" + "WasAPI: wFormatTag = %RU16 (expected %d)\n" + "WasAPI: nChannels = %RU16 (expected 1..15)\n" + "WasAPI: nSamplesPerSec = %RU32 (expected 4096..768000)\n" + "WasAPI: nAvgBytesPerSec = %RU32\n" + "WasAPI: nBlockAlign = %RU16\n" + "WasAPI: wBitsPerSample = %RU16 (expected 8, 16, or 32)\n" + "WasAPI: cbSize = %RU16\n", + pszStream, pwszDevId, pFmt->wFormatTag, WAVE_FORMAT_PCM, pFmt->nChannels, pFmt->nSamplesPerSec, pFmt->nAvgBytesPerSec, + pFmt->nBlockAlign, pFmt->wBitsPerSample, pFmt->cbSize)); + return VERR_AUDIO_STREAM_COULD_NOT_CREATE; +} +#endif + + +/** + * Destroys a devie config cache entry. + * + * @param pThis The WASAPI host audio driver instance data. + * @param pDevCfg Device config entry. Must not be in the list. + */ +static void drvHostAudioWasCacheDestroyDevConfig(PDRVHOSTAUDIOWAS pThis, PDRVHOSTAUDIOWASCACHEDEVCFG pDevCfg) +{ + if (pDevCfg->pDevEntry->enmDir == PDMAUDIODIR_IN) + ASMAtomicDecU32(&pThis->cCacheEntriesIn); + else + ASMAtomicDecU32(&pThis->cCacheEntriesOut); + + uint32_t cTypeClientRefs = 0; + if (pDevCfg->pIAudioCaptureClient) + { + cTypeClientRefs = pDevCfg->pIAudioCaptureClient->Release(); + pDevCfg->pIAudioCaptureClient = NULL; + } + + if (pDevCfg->pIAudioRenderClient) + { + cTypeClientRefs = pDevCfg->pIAudioRenderClient->Release(); + pDevCfg->pIAudioRenderClient = NULL; + } + + uint32_t cClientRefs = 0; + if (pDevCfg->pIAudioClient /* paranoia */) + { + cClientRefs = pDevCfg->pIAudioClient->Release(); + pDevCfg->pIAudioClient = NULL; + } + + Log8Func(("Destroying cache config entry: '%ls: %s' - cClientRefs=%u cTypeClientRefs=%u\n", + pDevCfg->pDevEntry->wszDevId, pDevCfg->szProps, cClientRefs, cTypeClientRefs)); + RT_NOREF(cClientRefs, cTypeClientRefs); + + pDevCfg->pDevEntry = NULL; + RTMemFree(pDevCfg); +} + + +/** + * Destroys a device cache entry. + * + * @param pThis The WASAPI host audio driver instance data. + * @param pDevEntry The device entry. Must not be in the cache! + */ +static void drvHostAudioWasCacheDestroyDevEntry(PDRVHOSTAUDIOWAS pThis, PDRVHOSTAUDIOWASCACHEDEV pDevEntry) +{ + Log8Func(("Destroying cache entry: %p - '%ls'\n", pDevEntry, pDevEntry->wszDevId)); + + PDRVHOSTAUDIOWASCACHEDEVCFG pDevCfg, pDevCfgNext; + RTListForEachSafe(&pDevEntry->ConfigList, pDevCfg, pDevCfgNext, DRVHOSTAUDIOWASCACHEDEVCFG, ListEntry) + { + drvHostAudioWasCacheDestroyDevConfig(pThis, pDevCfg); + } + + uint32_t cDevRefs = 0; + if (pDevEntry->pIDevice /* paranoia */) + { + cDevRefs = pDevEntry->pIDevice->Release(); + pDevEntry->pIDevice = NULL; + } + + pDevEntry->cwcDevId = 0; + pDevEntry->wszDevId[0] = '\0'; + RTMemFree(pDevEntry); + Log8Func(("Destroyed cache entry: %p cDevRefs=%u\n", pDevEntry, cDevRefs)); +} + + +/** + * Prunes the cache. + */ +static void drvHostAudioWasCachePrune(PDRVHOSTAUDIOWAS pThis) +{ + /* + * Prune each direction separately. + */ + struct + { + PDMAUDIODIR enmDir; + uint32_t volatile *pcEntries; + } aWork[] = { { PDMAUDIODIR_IN, &pThis->cCacheEntriesIn }, { PDMAUDIODIR_OUT, &pThis->cCacheEntriesOut }, }; + for (uint32_t iWork = 0; iWork < RT_ELEMENTS(aWork); iWork++) + { + /* + * Remove the least recently used entry till we're below the threshold + * or there are no more inactive entries. + */ + LogFlowFunc(("iWork=%u cEntries=%u\n", iWork, *aWork[iWork].pcEntries)); + while (*aWork[iWork].pcEntries > VBOX_WASAPI_MAX_TOTAL_CONFIG_ENTRIES) + { + RTCritSectEnter(&pThis->CritSectCache); + PDRVHOSTAUDIOWASCACHEDEVCFG pLeastRecentlyUsed = NULL; + PDRVHOSTAUDIOWASCACHEDEV pDevEntry; + RTListForEach(&pThis->CacheHead, pDevEntry, DRVHOSTAUDIOWASCACHEDEV, ListEntry) + { + if (pDevEntry->enmDir == aWork[iWork].enmDir) + { + PDRVHOSTAUDIOWASCACHEDEVCFG pHeadCfg = RTListGetFirst(&pDevEntry->ConfigList, + DRVHOSTAUDIOWASCACHEDEVCFG, ListEntry); + if ( pHeadCfg + && (!pLeastRecentlyUsed || pHeadCfg->nsLastUsed < pLeastRecentlyUsed->nsLastUsed)) + pLeastRecentlyUsed = pHeadCfg; + } + } + if (pLeastRecentlyUsed) + RTListNodeRemove(&pLeastRecentlyUsed->ListEntry); + RTCritSectLeave(&pThis->CritSectCache); + + if (!pLeastRecentlyUsed) + break; + drvHostAudioWasCacheDestroyDevConfig(pThis, pLeastRecentlyUsed); + } + } +} + + +/** + * Purges all the entries in the cache. + */ +static void drvHostAudioWasCachePurge(PDRVHOSTAUDIOWAS pThis, bool fOnWorker) +{ + for (;;) + { + RTCritSectEnter(&pThis->CritSectCache); + PDRVHOSTAUDIOWASCACHEDEV pDevEntry = RTListRemoveFirst(&pThis->CacheHead, DRVHOSTAUDIOWASCACHEDEV, ListEntry); + RTCritSectLeave(&pThis->CritSectCache); + if (!pDevEntry) + break; + drvHostAudioWasCacheDestroyDevEntry(pThis, pDevEntry); + } + + if (fOnWorker) + { + int rc = RTSemEventMultiSignal(pThis->hEvtCachePurge); + AssertRC(rc); + } +} + + +/** + * Looks up a specific configuration. + * + * @returns Pointer to the device config (removed from cache) on success. NULL + * if no matching config found. + * @param pDevEntry Where to perform the lookup. + * @param pProps The config properties to match. + */ +static PDRVHOSTAUDIOWASCACHEDEVCFG +drvHostAudioWasCacheLookupLocked(PDRVHOSTAUDIOWASCACHEDEV pDevEntry, PCPDMAUDIOPCMPROPS pProps) +{ + PDRVHOSTAUDIOWASCACHEDEVCFG pDevCfg; + RTListForEach(&pDevEntry->ConfigList, pDevCfg, DRVHOSTAUDIOWASCACHEDEVCFG, ListEntry) + { + if (PDMAudioPropsAreEqual(&pDevCfg->Props, pProps)) + { + RTListNodeRemove(&pDevCfg->ListEntry); + pDevCfg->nsLastUsed = RTTimeNanoTS(); + return pDevCfg; + } + } + return NULL; +} + + +/** + * Initializes a device config entry. + * + * This is usually done on the worker thread. + * + * @returns VBox status code. + * @param pDevCfg The device configuration entry to initialize. + */ +static int drvHostAudioWasCacheInitConfig(PDRVHOSTAUDIOWASCACHEDEVCFG pDevCfg) +{ + /* + * Assert some sanity given that we migth be called on the worker thread + * and pDevCfg being a message parameter. + */ + AssertPtrReturn(pDevCfg, VERR_INTERNAL_ERROR_2); + AssertReturn(pDevCfg->rcSetup == VERR_AUDIO_STREAM_INIT_IN_PROGRESS, VERR_INTERNAL_ERROR_2); + AssertReturn(pDevCfg->pIAudioClient == NULL, VERR_INTERNAL_ERROR_2); + AssertReturn(pDevCfg->pIAudioCaptureClient == NULL, VERR_INTERNAL_ERROR_2); + AssertReturn(pDevCfg->pIAudioRenderClient == NULL, VERR_INTERNAL_ERROR_2); + AssertReturn(PDMAudioPropsAreValid(&pDevCfg->Props), VERR_INTERNAL_ERROR_2); + + PDRVHOSTAUDIOWASCACHEDEV pDevEntry = pDevCfg->pDevEntry; + AssertPtrReturn(pDevEntry, VERR_INTERNAL_ERROR_2); + AssertPtrReturn(pDevEntry->pIDevice, VERR_INTERNAL_ERROR_2); + AssertReturn(pDevEntry->enmDir == PDMAUDIODIR_IN || pDevEntry->enmDir == PDMAUDIODIR_OUT, VERR_INTERNAL_ERROR_2); + + /* + * First we need an IAudioClient interface for calling IsFormatSupported + * on so we can get guidance as to what to do next. + * + * Initially, I thought the AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM was not + * supported all the way back to Vista and that we'd had to try different + * things here to get the most optimal format. However, according to + * https://social.msdn.microsoft.com/Forums/en-US/1d974d90-6636-4121-bba3-a8861d9ab92a + * it is supported, just maybe missing from the SDK or something... + * + * I'll leave the IsFormatSupported call here as it gives us a clue as to + * what exactly the WAS needs to convert our audio stream into/from. + */ + Log8Func(("Activating an IAudioClient for '%ls' ...\n", pDevEntry->wszDevId)); + IAudioClient *pIAudioClient = NULL; + HRESULT hrc = pDevEntry->pIDevice->Activate(__uuidof(IAudioClient), CLSCTX_ALL, + NULL /*pActivationParams*/, (void **)&pIAudioClient); + Log8Func(("Activate('%ls', IAudioClient) -> %Rhrc\n", pDevEntry->wszDevId, hrc)); + if (FAILED(hrc)) + { + LogRelMax(64, ("WasAPI: Activate(%ls, IAudioClient) failed: %Rhrc\n", pDevEntry->wszDevId, hrc)); + pDevCfg->nsInited = RTTimeNanoTS(); + pDevCfg->nsLastUsed = pDevCfg->nsInited; + return pDevCfg->rcSetup = VERR_AUDIO_STREAM_COULD_NOT_CREATE; + } + + WAVEFORMATEXTENSIBLE WaveFmtExt; + drvHostAudioWasWaveFmtExtFromProps(&pDevCfg->Props, &WaveFmtExt); + + PWAVEFORMATEX pClosestMatch = NULL; + hrc = pIAudioClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &WaveFmtExt.Format, &pClosestMatch); + + /* + * If the format is supported, go ahead and initialize the client instance. + * + * The docs talks about AUDCLNT_E_UNSUPPORTED_FORMAT being success too, but + * that doesn't seem to be the case (at least not for mixing up the + * WAVEFORMATEX::wFormatTag values). Seems that is the standard return code + * if there is anything it doesn't grok. + */ + if (SUCCEEDED(hrc)) + { + if (hrc == S_OK) + Log8Func(("IsFormatSupported(,%s,) -> S_OK + %p: requested format is supported\n", pDevCfg->szProps, pClosestMatch)); + else + Log8Func(("IsFormatSupported(,%s,) -> %Rhrc + %p: %uch S%u %uHz\n", pDevCfg->szProps, hrc, pClosestMatch, + pClosestMatch ? pClosestMatch->nChannels : 0, pClosestMatch ? pClosestMatch->wBitsPerSample : 0, + pClosestMatch ? pClosestMatch->nSamplesPerSec : 0)); + + REFERENCE_TIME const cBufferSizeInNtTicks = PDMAudioPropsFramesToNtTicks(&pDevCfg->Props, pDevCfg->cFramesBufferSize); + uint32_t fInitFlags = AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM + | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY; + hrc = pIAudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, fInitFlags, cBufferSizeInNtTicks, + 0 /*cPeriodicityInNtTicks*/, &WaveFmtExt.Format, NULL /*pAudioSessionGuid*/); + Log8Func(("Initialize(,%x, %RI64, %s,) -> %Rhrc\n", fInitFlags, cBufferSizeInNtTicks, pDevCfg->szProps, hrc)); + if (SUCCEEDED(hrc)) + { + /* + * The direction specific client interface. + */ + if (pDevEntry->enmDir == PDMAUDIODIR_IN) + hrc = pIAudioClient->GetService(__uuidof(IAudioCaptureClient), (void **)&pDevCfg->pIAudioCaptureClient); + else + hrc = pIAudioClient->GetService(__uuidof(IAudioRenderClient), (void **)&pDevCfg->pIAudioRenderClient); + Log8Func(("GetService -> %Rhrc + %p\n", hrc, pDevEntry->enmDir == PDMAUDIODIR_IN + ? (void *)pDevCfg->pIAudioCaptureClient : (void *)pDevCfg->pIAudioRenderClient)); + if (SUCCEEDED(hrc)) + { + /* + * Obtain the actual stream format and buffer config. + */ + UINT32 cFramesBufferSize = 0; + REFERENCE_TIME cDefaultPeriodInNtTicks = 0; + REFERENCE_TIME cMinimumPeriodInNtTicks = 0; + REFERENCE_TIME cLatencyinNtTicks = 0; + hrc = pIAudioClient->GetBufferSize(&cFramesBufferSize); + if (SUCCEEDED(hrc)) + { + hrc = pIAudioClient->GetDevicePeriod(&cDefaultPeriodInNtTicks, &cMinimumPeriodInNtTicks); + if (SUCCEEDED(hrc)) + { + hrc = pIAudioClient->GetStreamLatency(&cLatencyinNtTicks); + if (SUCCEEDED(hrc)) + { + LogRel2(("WasAPI: Aquired buffer parameters for %s:\n" + "WasAPI: cFramesBufferSize = %RU32\n" + "WasAPI: cDefaultPeriodInNtTicks = %RI64\n" + "WasAPI: cMinimumPeriodInNtTicks = %RI64\n" + "WasAPI: cLatencyinNtTicks = %RI64\n", + pDevCfg->szProps, cFramesBufferSize, cDefaultPeriodInNtTicks, + cMinimumPeriodInNtTicks, cLatencyinNtTicks)); + + pDevCfg->pIAudioClient = pIAudioClient; + pDevCfg->cFramesBufferSize = cFramesBufferSize; + pDevCfg->cFramesPeriod = PDMAudioPropsNanoToFrames(&pDevCfg->Props, + cDefaultPeriodInNtTicks * 100); + pDevCfg->nsInited = RTTimeNanoTS(); + pDevCfg->nsLastUsed = pDevCfg->nsInited; + pDevCfg->rcSetup = VINF_SUCCESS; + + if (pClosestMatch) + CoTaskMemFree(pClosestMatch); + Log8Func(("returns VINF_SUCCESS (%p (%s) inited in %'RU64 ns)\n", + pDevCfg, pDevCfg->szProps, pDevCfg->nsInited - pDevCfg->nsCreated)); + return VINF_SUCCESS; + } + LogRelMax(64, ("WasAPI: GetStreamLatency failed: %Rhrc\n", hrc)); + } + else + LogRelMax(64, ("WasAPI: GetDevicePeriod failed: %Rhrc\n", hrc)); + } + else + LogRelMax(64, ("WasAPI: GetBufferSize failed: %Rhrc\n", hrc)); + + if (pDevCfg->pIAudioCaptureClient) + { + pDevCfg->pIAudioCaptureClient->Release(); + pDevCfg->pIAudioCaptureClient = NULL; + } + + if (pDevCfg->pIAudioRenderClient) + { + pDevCfg->pIAudioRenderClient->Release(); + pDevCfg->pIAudioRenderClient = NULL; + } + } + else + LogRelMax(64, ("WasAPI: IAudioClient::GetService(%s) failed: %Rhrc\n", pDevCfg->szProps, hrc)); + } + else + LogRelMax(64, ("WasAPI: IAudioClient::Initialize(%s) failed: %Rhrc\n", pDevCfg->szProps, hrc)); + } + else + LogRelMax(64,("WasAPI: IAudioClient::IsFormatSupported(,%s,) failed: %Rhrc\n", pDevCfg->szProps, hrc)); + + pIAudioClient->Release(); + if (pClosestMatch) + CoTaskMemFree(pClosestMatch); + pDevCfg->nsInited = RTTimeNanoTS(); + pDevCfg->nsLastUsed = 0; + Log8Func(("returns VERR_AUDIO_STREAM_COULD_NOT_CREATE (inited in %'RU64 ns)\n", pDevCfg->nsInited - pDevCfg->nsCreated)); + return pDevCfg->rcSetup = VERR_AUDIO_STREAM_COULD_NOT_CREATE; +} + + +/** + * Worker for drvHostAudioWasCacheLookupOrCreate. + * + * If lookup fails, a new entry will be created. + * + * @note Called holding the lock, returning without holding it! + */ +static int drvHostAudioWasCacheLookupOrCreateConfig(PDRVHOSTAUDIOWAS pThis, PDRVHOSTAUDIOWASCACHEDEV pDevEntry, + PCPDMAUDIOSTREAMCFG pCfgReq, bool fOnWorker, + PDRVHOSTAUDIOWASCACHEDEVCFG *ppDevCfg) +{ + /* + * Check if we've got a matching config. + */ + PDRVHOSTAUDIOWASCACHEDEVCFG pDevCfg = drvHostAudioWasCacheLookupLocked(pDevEntry, &pCfgReq->Props); + if (pDevCfg) + { + *ppDevCfg = pDevCfg; + RTCritSectLeave(&pThis->CritSectCache); + Log8Func(("Config cache hit '%s' on '%ls': %p\n", pDevCfg->szProps, pDevEntry->wszDevId, pDevCfg)); + return VINF_SUCCESS; + } + + RTCritSectLeave(&pThis->CritSectCache); + + /* + * Allocate an device config entry and hand the creation task over to the + * worker thread, unless we're already on it. + */ + pDevCfg = (PDRVHOSTAUDIOWASCACHEDEVCFG)RTMemAllocZ(sizeof(*pDevCfg)); + AssertReturn(pDevCfg, VERR_NO_MEMORY); + RTListInit(&pDevCfg->ListEntry); + pDevCfg->pDevEntry = pDevEntry; + pDevCfg->rcSetup = VERR_AUDIO_STREAM_INIT_IN_PROGRESS; + pDevCfg->Props = pCfgReq->Props; + pDevCfg->cFramesBufferSize = pCfgReq->Backend.cFramesBufferSize; + PDMAudioPropsToString(&pDevCfg->Props, pDevCfg->szProps, sizeof(pDevCfg->szProps)); + pDevCfg->nsCreated = RTTimeNanoTS(); + pDevCfg->nsLastUsed = pDevCfg->nsCreated; + + uint32_t cCacheEntries; + if (pDevCfg->pDevEntry->enmDir == PDMAUDIODIR_IN) + cCacheEntries = ASMAtomicIncU32(&pThis->cCacheEntriesIn); + else + cCacheEntries = ASMAtomicIncU32(&pThis->cCacheEntriesOut); + if (cCacheEntries > VBOX_WASAPI_MAX_TOTAL_CONFIG_ENTRIES) + { + LogFlowFunc(("Trigger cache pruning.\n")); + int rc2 = pThis->pIHostAudioPort->pfnDoOnWorkerThread(pThis->pIHostAudioPort, NULL /*pStream*/, + DRVHOSTAUDIOWAS_DO_PRUNE_CACHE, NULL /*pvUser*/); + AssertRCStmt(rc2, drvHostAudioWasCachePrune(pThis)); + } + + if (!fOnWorker) + { + *ppDevCfg = pDevCfg; + LogFlowFunc(("Doing the rest of the work on %p via pfnStreamInitAsync...\n", pDevCfg)); + return VINF_AUDIO_STREAM_ASYNC_INIT_NEEDED; + } + + /* + * Initialize the entry on the calling thread. + */ + int rc = drvHostAudioWasCacheInitConfig(pDevCfg); + AssertRC(pDevCfg->rcSetup == rc); + if (RT_SUCCESS(rc)) + rc = pDevCfg->rcSetup; /* paranoia */ + if (RT_SUCCESS(rc)) + { + *ppDevCfg = pDevCfg; + LogFlowFunc(("Returning %p\n", pDevCfg)); + return VINF_SUCCESS; + } + RTMemFree(pDevCfg); + *ppDevCfg = NULL; + return rc; +} + + +/** + * Looks up the given device + config combo in the cache, creating a new entry + * if missing. + * + * @returns VBox status code. + * @retval VINF_AUDIO_STREAM_ASYNC_INIT_NEEDED if @a fOnWorker is @c false and + * we created a new entry that needs initalization by calling + * drvHostAudioWasCacheInitConfig() on it. + * @param pThis The WASAPI host audio driver instance data. + * @param pIDevice The device to look up. + * @param pCfgReq The configuration to look up. + * @param fOnWorker Set if we're on a worker thread, otherwise false. When + * set to @c true, VINF_AUDIO_STREAM_ASYNC_INIT_NEEDED will + * not be returned and a new entry will be fully + * initialized before returning. + * @param ppDevCfg Where to return the requested device config. + */ +static int drvHostAudioWasCacheLookupOrCreate(PDRVHOSTAUDIOWAS pThis, IMMDevice *pIDevice, PCPDMAUDIOSTREAMCFG pCfgReq, + bool fOnWorker, PDRVHOSTAUDIOWASCACHEDEVCFG *ppDevCfg) +{ + *ppDevCfg = NULL; + + /* + * Get the device ID so we can perform the lookup. + */ + int rc = VERR_AUDIO_STREAM_COULD_NOT_CREATE; + LPWSTR pwszDevId = NULL; + HRESULT hrc = pIDevice->GetId(&pwszDevId); + if (SUCCEEDED(hrc)) + { + size_t cwcDevId = RTUtf16Len(pwszDevId); + + /* + * The cache has two levels, so first the device entry. + */ + PDRVHOSTAUDIOWASCACHEDEV pDevEntry; + RTCritSectEnter(&pThis->CritSectCache); + RTListForEach(&pThis->CacheHead, pDevEntry, DRVHOSTAUDIOWASCACHEDEV, ListEntry) + { + if ( pDevEntry->cwcDevId == cwcDevId + && pDevEntry->enmDir == pCfgReq->enmDir + && RTUtf16Cmp(pDevEntry->wszDevId, pwszDevId) == 0) + { + CoTaskMemFree(pwszDevId); + Log8Func(("Cache hit for device '%ls': %p\n", pDevEntry->wszDevId, pDevEntry)); + return drvHostAudioWasCacheLookupOrCreateConfig(pThis, pDevEntry, pCfgReq, fOnWorker, ppDevCfg); + } + } + RTCritSectLeave(&pThis->CritSectCache); + + /* + * Device not in the cache, add it. + */ + pDevEntry = (PDRVHOSTAUDIOWASCACHEDEV)RTMemAllocZVar(RT_UOFFSETOF_DYN(DRVHOSTAUDIOWASCACHEDEV, wszDevId[cwcDevId + 1])); + if (pDevEntry) + { + pIDevice->AddRef(); + pDevEntry->pIDevice = pIDevice; + pDevEntry->enmDir = pCfgReq->enmDir; + pDevEntry->cwcDevId = cwcDevId; +#if 0 + pDevEntry->fSupportsAutoConvertPcm = -1; + pDevEntry->fSupportsSrcDefaultQuality = -1; +#endif + RTListInit(&pDevEntry->ConfigList); + memcpy(pDevEntry->wszDevId, pwszDevId, cwcDevId * sizeof(RTUTF16)); + pDevEntry->wszDevId[cwcDevId] = '\0'; + + CoTaskMemFree(pwszDevId); + pwszDevId = NULL; + + /* + * Before adding the device, check that someone didn't race us adding it. + */ + RTCritSectEnter(&pThis->CritSectCache); + PDRVHOSTAUDIOWASCACHEDEV pDevEntry2; + RTListForEach(&pThis->CacheHead, pDevEntry2, DRVHOSTAUDIOWASCACHEDEV, ListEntry) + { + if ( pDevEntry2->cwcDevId == cwcDevId + && pDevEntry2->enmDir == pCfgReq->enmDir + && RTUtf16Cmp(pDevEntry2->wszDevId, pDevEntry->wszDevId) == 0) + { + pIDevice->Release(); + RTMemFree(pDevEntry); + pDevEntry = NULL; + + Log8Func(("Lost race adding device '%ls': %p\n", pDevEntry2->wszDevId, pDevEntry2)); + return drvHostAudioWasCacheLookupOrCreateConfig(pThis, pDevEntry2, pCfgReq, fOnWorker, ppDevCfg); + } + } + RTListPrepend(&pThis->CacheHead, &pDevEntry->ListEntry); + + Log8Func(("Added device '%ls' to cache: %p\n", pDevEntry->wszDevId, pDevEntry)); + return drvHostAudioWasCacheLookupOrCreateConfig(pThis, pDevEntry, pCfgReq, fOnWorker, ppDevCfg); + } + CoTaskMemFree(pwszDevId); + } + else + LogRelMax(64, ("WasAPI: GetId failed (lookup): %Rhrc\n", hrc)); + return rc; +} + + +/** + * Return the given config to the cache. + * + * @param pThis The WASAPI host audio driver instance data. + * @param pDevCfg The device config to put back. + */ +static void drvHostAudioWasCachePutBack(PDRVHOSTAUDIOWAS pThis, PDRVHOSTAUDIOWASCACHEDEVCFG pDevCfg) +{ + /* + * Reset the audio client to see that it works and to make sure it's in a sensible state. + */ + HRESULT hrc = pDevCfg->pIAudioClient ? pDevCfg->pIAudioClient->Reset() + : pDevCfg->rcSetup == VERR_AUDIO_STREAM_INIT_IN_PROGRESS ? S_OK : E_FAIL; + if (SUCCEEDED(hrc)) + { + Log8Func(("Putting %p/'%s' back\n", pDevCfg, pDevCfg->szProps)); + RTCritSectEnter(&pThis->CritSectCache); + RTListAppend(&pDevCfg->pDevEntry->ConfigList, &pDevCfg->ListEntry); + uint32_t const cEntries = pDevCfg->pDevEntry->enmDir == PDMAUDIODIR_IN ? pThis->cCacheEntriesIn : pThis->cCacheEntriesOut; + RTCritSectLeave(&pThis->CritSectCache); + + /* Trigger pruning if we're over the threshold. */ + if (cEntries > VBOX_WASAPI_MAX_TOTAL_CONFIG_ENTRIES) + { + LogFlowFunc(("Trigger cache pruning.\n")); + int rc2 = pThis->pIHostAudioPort->pfnDoOnWorkerThread(pThis->pIHostAudioPort, NULL /*pStream*/, + DRVHOSTAUDIOWAS_DO_PRUNE_CACHE, NULL /*pvUser*/); + AssertRCStmt(rc2, drvHostAudioWasCachePrune(pThis)); + } + } + else + { + Log8Func(("IAudioClient::Reset failed (%Rhrc) on %p/'%s', destroying it.\n", hrc, pDevCfg, pDevCfg->szProps)); + drvHostAudioWasCacheDestroyDevConfig(pThis, pDevCfg); + } +} + + +static void drvHostWasCacheConfigHinting(PDRVHOSTAUDIOWAS pThis, PPDMAUDIOSTREAMCFG pCfgReq, bool fOnWorker) +{ + /* + * Get the device. + */ + pThis->pNotifyClient->lockEnter(); + IMMDevice *pIDevice = pCfgReq->enmDir == PDMAUDIODIR_IN ? pThis->pIDeviceInput : pThis->pIDeviceOutput; + if (pIDevice) + pIDevice->AddRef(); + pThis->pNotifyClient->lockLeave(); + if (pIDevice) + { + /* + * Look up the config and put it back. + */ + PDRVHOSTAUDIOWASCACHEDEVCFG pDevCfg = NULL; + int rc = drvHostAudioWasCacheLookupOrCreate(pThis, pIDevice, pCfgReq, fOnWorker, &pDevCfg); + LogFlowFunc(("pDevCfg=%p rc=%Rrc\n", pDevCfg, rc)); + if (pDevCfg && RT_SUCCESS(rc)) + drvHostAudioWasCachePutBack(pThis, pDevCfg); + pIDevice->Release(); + } +} + + +/** + * Prefills the cache. + * + * @param pThis The WASAPI host audio driver instance data. + */ +static void drvHostAudioWasCacheFill(PDRVHOSTAUDIOWAS pThis) +{ +#if 0 /* we don't have the buffer config nor do we really know which frequences to expect */ + Log8Func(("enter\n")); + struct + { + PCRTUTF16 pwszDevId; + PDMAUDIODIR enmDir; + } aToCache[] = + { + { pThis->pwszInputDevId, PDMAUDIODIR_IN }, + { pThis->pwszOutputDevId, PDMAUDIODIR_OUT } + }; + for (unsigned i = 0; i < RT_ELEMENTS(aToCache); i++) + { + PCRTUTF16 pwszDevId = aToCache[i].pwszDevId; + IMMDevice *pIDevice = NULL; + HRESULT hrc; + if (pwszDevId) + hrc = pThis->pIEnumerator->GetDevice(pwszDevId, &pIDevice); + else + { + hrc = pThis->pIEnumerator->GetDefaultAudioEndpoint(aToCache[i].enmDir == PDMAUDIODIR_IN ? eCapture : eRender, + eMultimedia, &pIDevice); + pwszDevId = aToCache[i].enmDir == PDMAUDIODIR_IN ? L"{Default-In}" : L"{Default-Out}"; + } + if (SUCCEEDED(hrc)) + { + PDMAUDIOSTREAMCFG Cfg = { aToCache[i].enmDir, { PDMAUDIOPLAYBACKDST_INVALID }, + PDMAUDIOPCMPROPS_INITIALIZER(2, true, 2, 44100, false) }; + Cfg.Backend.cFramesBufferSize = PDMAudioPropsMilliToFrames(&Cfg.Props, 300); + PDRVHOSTAUDIOWASCACHEDEVCFG pDevCfg = drvHostAudioWasCacheLookupOrCreate(pThis, pIDevice, &Cfg); + if (pDevCfg) + drvHostAudioWasCachePutBack(pThis, pDevCfg); + + pIDevice->Release(); + } + else + LogRelMax(64, ("WasAPI: Failed to open audio device '%ls' (pre-caching): %Rhrc\n", pwszDevId, hrc)); + } + Log8Func(("leave\n")); +#else + RT_NOREF(pThis); +#endif +} + + +/********************************************************************************************************************************* +* Worker thread * +*********************************************************************************************************************************/ +#if 0 + +/** + * @callback_method_impl{FNRTTHREAD, + * Asynchronous thread for setting up audio client configs.} + */ +static DECLCALLBACK(int) drvHostWasWorkerThread(RTTHREAD hThreadSelf, void *pvUser) +{ + PDRVHOSTAUDIOWAS pThis = (PDRVHOSTAUDIOWAS)pvUser; + + /* + * We need to set the thread ID so others can post us thread messages. + * And before we signal that we're ready, make sure we've got a message queue. + */ + pThis->idWorkerThread = GetCurrentThreadId(); + LogFunc(("idWorkerThread=%#x (%u)\n", pThis->idWorkerThread, pThis->idWorkerThread)); + + MSG Msg; + PeekMessageW(&Msg, NULL, WM_USER, WM_USER, PM_NOREMOVE); + + int rc = RTThreadUserSignal(hThreadSelf); + AssertRC(rc); + + /* + * Message loop. + */ + BOOL fRet; + while ((fRet = GetMessageW(&Msg, NULL, 0, 0)) != FALSE) + { + if (fRet != -1) + { + TranslateMessage(&Msg); + Log9Func(("Msg: time=%u: msg=%#x l=%p w=%p for hwnd=%p\n", Msg.time, Msg.message, Msg.lParam, Msg.wParam, Msg.hwnd)); + switch (Msg.message) + { + case WM_DRVHOSTAUDIOWAS_PURGE_CACHE: + { + AssertMsgBreak(Msg.wParam == pThis->uWorkerThreadFixedParam, ("%p\n", Msg.wParam)); + AssertBreak(Msg.hwnd == NULL); + AssertBreak(Msg.lParam == 0); + + drvHostAudioWasCachePurge(pThis, false /*fOnWorker*/); + break; + } + + default: + break; + } + DispatchMessageW(&Msg); + } + else + AssertMsgFailed(("GetLastError()=%u\n", GetLastError())); + } + + LogFlowFunc(("Pre-quit cache purge...\n")); + drvHostAudioWasCachePurge(pThis, false /*fOnWorker*/); + + LogFunc(("Quits\n")); + return VINF_SUCCESS; +} +#endif + + +/********************************************************************************************************************************* +* PDMIHOSTAUDIO * +*********************************************************************************************************************************/ + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnGetConfig} + */ +static DECLCALLBACK(int) drvHostAudioWasHA_GetConfig(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDCFG pBackendCfg) +{ + RT_NOREF(pInterface); + AssertPtrReturn(pInterface, VERR_INVALID_POINTER); + AssertPtrReturn(pBackendCfg, VERR_INVALID_POINTER); + + + /* + * Fill in the config structure. + */ + RTStrCopy(pBackendCfg->szName, sizeof(pBackendCfg->szName), "WasAPI"); + pBackendCfg->cbStream = sizeof(DRVHOSTAUDIOWASSTREAM); + pBackendCfg->fFlags = PDMAUDIOBACKEND_F_ASYNC_HINT; + pBackendCfg->cMaxStreamsIn = UINT32_MAX; + pBackendCfg->cMaxStreamsOut = UINT32_MAX; + + return VINF_SUCCESS; +} + + +/** + * Queries information for @a pDevice and adds an entry to the enumeration. + * + * @returns VBox status code. + * @param pDevEnm The enumeration to add the device to. + * @param pIDevice The device. + * @param enmType The type of device. + * @param fDefault Whether it's the default device. + */ +static int drvHostWasEnumAddDev(PPDMAUDIOHOSTENUM pDevEnm, IMMDevice *pIDevice, EDataFlow enmType, bool fDefault) +{ + int rc = VINF_SUCCESS; /* ignore most errors */ + RT_NOREF(fDefault); /** @todo default device marking/skipping. */ + + /* + * Gather the necessary properties. + */ + IPropertyStore *pProperties = NULL; + HRESULT hrc = pIDevice->OpenPropertyStore(STGM_READ, &pProperties); + if (SUCCEEDED(hrc)) + { + /* Get the friendly name (string). */ + PROPVARIANT VarName; + PropVariantInit(&VarName); + hrc = pProperties->GetValue(PKEY_Device_FriendlyName, &VarName); + if (SUCCEEDED(hrc)) + { + /* Get the device ID (string). */ + LPWSTR pwszDevId = NULL; + hrc = pIDevice->GetId(&pwszDevId); + if (SUCCEEDED(hrc)) + { + size_t const cwcDevId = RTUtf16Len(pwszDevId); + + /* Get the device format (blob). */ + PROPVARIANT VarFormat; + PropVariantInit(&VarFormat); + hrc = pProperties->GetValue(PKEY_AudioEngine_DeviceFormat, &VarFormat); + if (SUCCEEDED(hrc)) + { + WAVEFORMATEX const * const pFormat = (WAVEFORMATEX const *)VarFormat.blob.pBlobData; + AssertPtr(pFormat); /* Observed sometimes being NULL on windows 7 sp1. */ + + /* + * Create a enumeration entry for it. + */ + size_t const cbId = RTUtf16CalcUtf8Len(pwszDevId) + 1; + size_t const cbName = RTUtf16CalcUtf8Len(VarName.pwszVal) + 1; + size_t const cbDev = RT_ALIGN_Z( RT_OFFSETOF(DRVHOSTAUDIOWASDEV, wszDevId) + + (cwcDevId + 1) * sizeof(RTUTF16), + 64); + PDRVHOSTAUDIOWASDEV pDev = (PDRVHOSTAUDIOWASDEV)PDMAudioHostDevAlloc(cbDev, cbName, cbId); + if (pDev) + { + pDev->Core.enmType = PDMAUDIODEVICETYPE_BUILTIN; + pDev->Core.enmUsage = enmType == eRender ? PDMAUDIODIR_OUT : PDMAUDIODIR_IN; + if (fDefault) + pDev->Core.fFlags = enmType == eRender ? PDMAUDIOHOSTDEV_F_DEFAULT_OUT : PDMAUDIOHOSTDEV_F_DEFAULT_IN; + if (enmType == eRender) + pDev->Core.cMaxOutputChannels = RT_VALID_PTR(pFormat) ? pFormat->nChannels : 2; + else + pDev->Core.cMaxInputChannels = RT_VALID_PTR(pFormat) ? pFormat->nChannels : 1; + + memcpy(pDev->wszDevId, pwszDevId, cwcDevId * sizeof(RTUTF16)); + pDev->wszDevId[cwcDevId] = '\0'; + + Assert(pDev->Core.pszName); + rc = RTUtf16ToUtf8Ex(VarName.pwszVal, RTSTR_MAX, &pDev->Core.pszName, cbName, NULL); + if (RT_SUCCESS(rc)) + { + Assert(pDev->Core.pszId); + rc = RTUtf16ToUtf8Ex(pDev->wszDevId, RTSTR_MAX, &pDev->Core.pszId, cbId, NULL); + if (RT_SUCCESS(rc)) + PDMAudioHostEnumAppend(pDevEnm, &pDev->Core); + else + PDMAudioHostDevFree(&pDev->Core); + } + else + PDMAudioHostDevFree(&pDev->Core); + } + else + rc = VERR_NO_MEMORY; + PropVariantClear(&VarFormat); + } + else + LogFunc(("Failed to get PKEY_AudioEngine_DeviceFormat: %Rhrc\n", hrc)); + CoTaskMemFree(pwszDevId); + } + else + LogFunc(("Failed to get the device ID: %Rhrc\n", hrc)); + PropVariantClear(&VarName); + } + else + LogFunc(("Failed to get PKEY_Device_FriendlyName: %Rhrc\n", hrc)); + pProperties->Release(); + } + else + LogFunc(("OpenPropertyStore failed: %Rhrc\n", hrc)); + + if (hrc == E_OUTOFMEMORY && RT_SUCCESS_NP(rc)) + rc = VERR_NO_MEMORY; + return rc; +} + + +/** + * Does a (Re-)enumeration of the host's playback + capturing devices. + * + * @return VBox status code. + * @param pThis The WASAPI host audio driver instance data. + * @param pDevEnm Where to store the enumerated devices. + */ +static int drvHostWasEnumerateDevices(PDRVHOSTAUDIOWAS pThis, PPDMAUDIOHOSTENUM pDevEnm) +{ + LogRel2(("WasAPI: Enumerating devices ...\n")); + + int rc = VINF_SUCCESS; + for (unsigned idxPass = 0; idxPass < 2 && RT_SUCCESS(rc); idxPass++) + { + EDataFlow const enmType = idxPass == 0 ? EDataFlow::eRender : EDataFlow::eCapture; + + /* Get the default device first. */ + IMMDevice *pIDefaultDevice = NULL; + HRESULT hrc = pThis->pIEnumerator->GetDefaultAudioEndpoint(enmType, eMultimedia, &pIDefaultDevice); + if (SUCCEEDED(hrc)) + rc = drvHostWasEnumAddDev(pDevEnm, pIDefaultDevice, enmType, true); + else + pIDefaultDevice = NULL; + + /* Enumerate the devices. */ + IMMDeviceCollection *pCollection = NULL; + hrc = pThis->pIEnumerator->EnumAudioEndpoints(enmType, DEVICE_STATE_ACTIVE /*| DEVICE_STATE_UNPLUGGED?*/, &pCollection); + if (SUCCEEDED(hrc) && pCollection != NULL) + { + UINT cDevices = 0; + hrc = pCollection->GetCount(&cDevices); + if (SUCCEEDED(hrc)) + { + for (UINT idxDevice = 0; idxDevice < cDevices && RT_SUCCESS(rc); idxDevice++) + { + IMMDevice *pIDevice = NULL; + hrc = pCollection->Item(idxDevice, &pIDevice); + if (SUCCEEDED(hrc) && pIDevice) + { + if (pIDevice != pIDefaultDevice) + rc = drvHostWasEnumAddDev(pDevEnm, pIDevice, enmType, false); + pIDevice->Release(); + } + } + } + pCollection->Release(); + } + else + LogRelMax(10, ("EnumAudioEndpoints(%s) failed: %Rhrc\n", idxPass == 0 ? "output" : "input", hrc)); + + if (pIDefaultDevice) + pIDefaultDevice->Release(); + } + + LogRel2(("WasAPI: Enumerating devices done - %u device (%Rrc)\n", pDevEnm->cDevices, rc)); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnGetDevices} + */ +static DECLCALLBACK(int) drvHostAudioWasHA_GetDevices(PPDMIHOSTAUDIO pInterface, PPDMAUDIOHOSTENUM pDeviceEnum) +{ + PDRVHOSTAUDIOWAS pThis = RT_FROM_MEMBER(pInterface, DRVHOSTAUDIOWAS, IHostAudio); + AssertPtrReturn(pDeviceEnum, VERR_INVALID_POINTER); + + PDMAudioHostEnumInit(pDeviceEnum); + int rc = drvHostWasEnumerateDevices(pThis, pDeviceEnum); + if (RT_FAILURE(rc)) + PDMAudioHostEnumDelete(pDeviceEnum); + + LogFlowFunc(("Returning %Rrc\n", rc)); + return rc; +} + + +/** + * Worker for drvHostAudioWasHA_SetDevice. + */ +static int drvHostAudioWasSetDeviceWorker(PDRVHOSTAUDIOWAS pThis, const char *pszId, PRTUTF16 *ppwszDevId, IMMDevice **ppIDevice, + EDataFlow enmFlow, PDMAUDIODIR enmDir, const char *pszWhat) +{ + pThis->pNotifyClient->lockEnter(); + + /* + * Did anything actually change? + */ + if ( (pszId == NULL) != (*ppwszDevId == NULL) + || ( pszId + && RTUtf16ICmpUtf8(*ppwszDevId, pszId) != 0)) + { + /* + * Duplicate the ID. + */ + PRTUTF16 pwszDevId = NULL; + if (pszId) + { + int rc = RTStrToUtf16(pszId, &pwszDevId); + AssertRCReturnStmt(rc, pThis->pNotifyClient->lockLeave(), rc); + } + + /* + * Try get the device. + */ + IMMDevice *pIDevice = NULL; + HRESULT hrc; + if (pwszDevId) + hrc = pThis->pIEnumerator->GetDevice(pwszDevId, &pIDevice); + else + hrc = pThis->pIEnumerator->GetDefaultAudioEndpoint(enmFlow, eMultimedia, &pIDevice); + LogFlowFunc(("Got device %p (%Rhrc)\n", pIDevice, hrc)); + if (FAILED(hrc)) + { + LogRel(("WasAPI: Failed to get IMMDevice for %s audio device '%s' (SetDevice): %Rhrc\n", + pszWhat, pszId ? pszId : "{default}", hrc)); + pIDevice = NULL; + } + + /* + * Make the switch. + */ + LogRel(("PulseAudio: Changing %s device: '%ls' -> '%s'\n", + pszWhat, *ppwszDevId ? *ppwszDevId : L"{Default}", pszId ? pszId : "{Default}")); + + if (*ppIDevice) + (*ppIDevice)->Release(); + *ppIDevice = pIDevice; + + RTUtf16Free(*ppwszDevId); + *ppwszDevId = pwszDevId; + + /* + * Only notify the driver above us. + */ + PPDMIHOSTAUDIOPORT const pIHostAudioPort = pThis->pIHostAudioPort; + pThis->pNotifyClient->lockLeave(); + + if (pIHostAudioPort) + { + LogFlowFunc(("Notifying parent driver about %s device change...\n", pszWhat)); + pIHostAudioPort->pfnNotifyDeviceChanged(pIHostAudioPort, enmDir, NULL); + } + } + else + { + pThis->pNotifyClient->lockLeave(); + LogFunc(("No %s device change\n", pszWhat)); + } + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnSetDevice} + */ +static DECLCALLBACK(int) drvHostAudioWasHA_SetDevice(PPDMIHOSTAUDIO pInterface, PDMAUDIODIR enmDir, const char *pszId) +{ + PDRVHOSTAUDIOWAS pThis = RT_FROM_MEMBER(pInterface, DRVHOSTAUDIOWAS, 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 = NULL; + else + AssertReturn(strlen(pszId) < 1024, VERR_INVALID_NAME); + LogFunc(("enmDir=%d pszId=%s\n", enmDir, pszId)); + + /* + * Do the updating. + */ + if (enmDir == PDMAUDIODIR_IN || enmDir == PDMAUDIODIR_DUPLEX) + { + int rc = drvHostAudioWasSetDeviceWorker(pThis, pszId, &pThis->pwszInputDevId, &pThis->pIDeviceInput, + eCapture, PDMAUDIODIR_IN, "input"); + AssertRCReturn(rc, rc); + } + + if (enmDir == PDMAUDIODIR_OUT || enmDir == PDMAUDIODIR_DUPLEX) + { + int rc = drvHostAudioWasSetDeviceWorker(pThis, pszId, &pThis->pwszOutputDevId, &pThis->pIDeviceOutput, + eRender, PDMAUDIODIR_OUT, "output"); + AssertRCReturn(rc, rc); + } + + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnGetStatus} + */ +static DECLCALLBACK(PDMAUDIOBACKENDSTS) drvHostAudioWasHA_GetStatus(PPDMIHOSTAUDIO pInterface, PDMAUDIODIR enmDir) +{ + RT_NOREF(pInterface, enmDir); + return PDMAUDIOBACKENDSTS_RUNNING; +} + + +/** + * Performs the actual switching of device config. + * + * Worker for drvHostAudioWasDoStreamDevSwitch() and + * drvHostAudioWasHA_StreamNotifyDeviceChanged(). + */ +static void drvHostAudioWasCompleteStreamDevSwitch(PDRVHOSTAUDIOWAS pThis, PDRVHOSTAUDIOWASSTREAM pStreamWas, + PDRVHOSTAUDIOWASCACHEDEVCFG pDevCfg) +{ + RTCritSectEnter(&pStreamWas->CritSect); + + /* Do the switch. */ + PDRVHOSTAUDIOWASCACHEDEVCFG pDevCfgOld = pStreamWas->pDevCfg; + pStreamWas->pDevCfg = pDevCfg; + + /* The new stream is neither started nor draining. */ + pStreamWas->fStarted = false; + pStreamWas->fDraining = false; + + /* Device switching is done now. */ + pStreamWas->fSwitchingDevice = false; + + /* Stop the old stream or Reset() will fail when putting it back into the cache. */ + if (pStreamWas->fEnabled && pDevCfgOld->pIAudioClient) + pDevCfgOld->pIAudioClient->Stop(); + + RTCritSectLeave(&pStreamWas->CritSect); + + /* Notify DrvAudio. */ + pThis->pIHostAudioPort->pfnStreamNotifyDeviceChanged(pThis->pIHostAudioPort, &pStreamWas->Core, false /*fReInit*/); + + /* Put the old config back into the cache. */ + drvHostAudioWasCachePutBack(pThis, pDevCfgOld); + + LogFlowFunc(("returns with '%s' state: %s\n", pStreamWas->Cfg.szName, drvHostWasStreamStatusString(pStreamWas) )); +} + + +/** + * Called on a worker thread to initialize a new device config and switch the + * given stream to using it. + * + * @sa drvHostAudioWasHA_StreamNotifyDeviceChanged + */ +static void drvHostAudioWasDoStreamDevSwitch(PDRVHOSTAUDIOWAS pThis, PDRVHOSTAUDIOWASSTREAM pStreamWas, + PDRVHOSTAUDIOWASCACHEDEVCFG pDevCfg) +{ + /* + * Do the initializing. + */ + int rc = drvHostAudioWasCacheInitConfig(pDevCfg); + if (RT_SUCCESS(rc)) + drvHostAudioWasCompleteStreamDevSwitch(pThis, pStreamWas, pDevCfg); + else + { + LogRelMax(64, ("WasAPI: Failed to set up new device config '%ls:%s' for stream '%s': %Rrc\n", + pDevCfg->pDevEntry->wszDevId, pDevCfg->szProps, pStreamWas->Cfg.szName, rc)); + drvHostAudioWasCacheDestroyDevConfig(pThis, pDevCfg); + pThis->pIHostAudioPort->pfnStreamNotifyDeviceChanged(pThis->pIHostAudioPort, &pStreamWas->Core, true /*fReInit*/); + } +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnDoOnWorkerThread} + */ +static DECLCALLBACK(void) drvHostAudioWasHA_DoOnWorkerThread(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + uintptr_t uUser, void *pvUser) +{ + PDRVHOSTAUDIOWAS pThis = RT_FROM_MEMBER(pInterface, DRVHOSTAUDIOWAS, IHostAudio); + LogFlowFunc(("uUser=%#zx pStream=%p pvUser=%p\n", uUser, pStream, pvUser)); + + switch (uUser) + { + case DRVHOSTAUDIOWAS_DO_PURGE_CACHE: + Assert(pStream == NULL); + Assert(pvUser == NULL); + drvHostAudioWasCachePurge(pThis, true /*fOnWorker*/); + break; + + case DRVHOSTAUDIOWAS_DO_PRUNE_CACHE: + Assert(pStream == NULL); + Assert(pvUser == NULL); + drvHostAudioWasCachePrune(pThis); + break; + + case DRVHOSTAUDIOWAS_DO_STREAM_DEV_SWITCH: + AssertPtr(pStream); + AssertPtr(pvUser); + drvHostAudioWasDoStreamDevSwitch(pThis, (PDRVHOSTAUDIOWASSTREAM)pStream, (PDRVHOSTAUDIOWASCACHEDEVCFG)pvUser); + break; + + default: + AssertMsgFailedBreak(("%#zx\n", uUser)); + } +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamConfigHint} + * + * @note This is called on a DrvAudio worker thread. + */ +static DECLCALLBACK(void) drvHostAudioWasHA_StreamConfigHint(PPDMIHOSTAUDIO pInterface, PPDMAUDIOSTREAMCFG pCfg) +{ +#if 0 /* disable to test async stream creation. */ + PDRVHOSTAUDIOWAS pThis = RT_FROM_MEMBER(pInterface, DRVHOSTAUDIOWAS, IHostAudio); + LogFlowFunc(("pCfg=%p\n", pCfg)); + + drvHostWasCacheConfigHinting(pThis, pCfg); +#else + RT_NOREF(pInterface, pCfg); +#endif +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamCreate} + */ +static DECLCALLBACK(int) drvHostAudioWasHA_StreamCreate(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + PCPDMAUDIOSTREAMCFG pCfgReq, PPDMAUDIOSTREAMCFG pCfgAcq) +{ + PDRVHOSTAUDIOWAS pThis = RT_FROM_MEMBER(pInterface, DRVHOSTAUDIOWAS, IHostAudio); + PDRVHOSTAUDIOWASSTREAM pStreamWas = (PDRVHOSTAUDIOWASSTREAM)pStream; + AssertPtrReturn(pStreamWas, 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)); + + const char * const pszStreamType = pCfgReq->enmDir == PDMAUDIODIR_IN ? "capture" : "playback"; RT_NOREF(pszStreamType); + LogFlowFunc(("enmPath=%s '%s'\n", PDMAudioPathGetName(pCfgReq->enmPath), pCfgReq->szName)); +#if defined(RTLOG_REL_ENABLED) || defined(LOG_ENABLED) + char szTmp[64]; +#endif + LogRel2(("WasAPI: Opening %s stream '%s' (%s)\n", pCfgReq->szName, pszStreamType, + PDMAudioPropsToString(&pCfgReq->Props, szTmp, sizeof(szTmp)))); + + RTListInit(&pStreamWas->ListEntry); + + /* + * Do configuration conversion. + */ + WAVEFORMATEXTENSIBLE WaveFmtExt; + drvHostAudioWasWaveFmtExtFromProps(&pCfgReq->Props, &WaveFmtExt); + LogRel2(("WasAPI: Requested %s format for '%s':\n" + "WasAPI: wFormatTag = %#RX16\n" + "WasAPI: nChannels = %RU16\n" + "WasAPI: nSamplesPerSec = %RU32\n" + "WasAPI: nAvgBytesPerSec = %RU32\n" + "WasAPI: nBlockAlign = %RU16\n" + "WasAPI: wBitsPerSample = %RU16\n" + "WasAPI: cbSize = %RU16\n" + "WasAPI: cBufferSizeInNtTicks = %RU64\n", + pszStreamType, pCfgReq->szName, WaveFmtExt.Format.wFormatTag, WaveFmtExt.Format.nChannels, + WaveFmtExt.Format.nSamplesPerSec, WaveFmtExt.Format.nAvgBytesPerSec, WaveFmtExt.Format.nBlockAlign, + WaveFmtExt.Format.wBitsPerSample, WaveFmtExt.Format.cbSize, + PDMAudioPropsFramesToNtTicks(&pCfgReq->Props, pCfgReq->Backend.cFramesBufferSize) )); + if (WaveFmtExt.Format.cbSize != 0) + LogRel2(("WasAPI: dwChannelMask = %#RX32\n" + "WasAPI: wValidBitsPerSample = %RU16\n", + WaveFmtExt.dwChannelMask, WaveFmtExt.Samples.wValidBitsPerSample)); + + /* Set up the acquired format here as channel count + layout may have + changed and need to be communicated to caller and used in cache lookup. */ + *pCfgAcq = *pCfgReq; + if (WaveFmtExt.Format.cbSize != 0) + { + PDMAudioPropsSetChannels(&pCfgAcq->Props, WaveFmtExt.Format.nChannels); + uint8_t idCh = 0; + for (unsigned iBit = 0; iBit < 32 && idCh < WaveFmtExt.Format.nChannels; iBit++) + if (WaveFmtExt.dwChannelMask & RT_BIT_32(iBit)) + { + pCfgAcq->Props.aidChannels[idCh] = (unsigned)PDMAUDIOCHANNELID_FIRST_STANDARD + iBit; + idCh++; + } + Assert(idCh == WaveFmtExt.Format.nChannels); + } + + /* + * Get the device we're supposed to use. + * (We cache this as it takes ~2ms to get the default device on a random W10 19042 system.) + */ + pThis->pNotifyClient->lockEnter(); + IMMDevice *pIDevice = pCfgReq->enmDir == PDMAUDIODIR_IN ? pThis->pIDeviceInput : pThis->pIDeviceOutput; + if (pIDevice) + pIDevice->AddRef(); + pThis->pNotifyClient->lockLeave(); + + PRTUTF16 pwszDevId = pCfgReq->enmDir == PDMAUDIODIR_IN ? pThis->pwszInputDevId : pThis->pwszOutputDevId; + PRTUTF16 const pwszDevIdDesc = pwszDevId ? pwszDevId : pCfgReq->enmDir == PDMAUDIODIR_IN ? L"{Default-In}" : L"{Default-Out}"; + if (!pIDevice) + { + /* This might not strictly be necessary anymore, however it shouldn't + hurt and may be useful when using specific devices. */ + HRESULT hrc; + if (pwszDevId) + hrc = pThis->pIEnumerator->GetDevice(pwszDevId, &pIDevice); + else + hrc = pThis->pIEnumerator->GetDefaultAudioEndpoint(pCfgReq->enmDir == PDMAUDIODIR_IN ? eCapture : eRender, + eMultimedia, &pIDevice); + LogFlowFunc(("Got device %p (%Rhrc)\n", pIDevice, hrc)); + if (FAILED(hrc)) + { + LogRelMax(64, ("WasAPI: Failed to open audio %s device '%ls': %Rhrc\n", pszStreamType, pwszDevIdDesc, hrc)); + return VERR_AUDIO_STREAM_COULD_NOT_CREATE; + } + } + + /* + * Ask the cache to retrieve or instantiate the requested configuration. + */ + /** @todo make it return a status code too and retry if the default device + * was invalidated/changed while we where working on it here. */ + PDRVHOSTAUDIOWASCACHEDEVCFG pDevCfg = NULL; + int rc = drvHostAudioWasCacheLookupOrCreate(pThis, pIDevice, pCfgAcq, false /*fOnWorker*/, &pDevCfg); + + pIDevice->Release(); + pIDevice = NULL; + + if (pDevCfg && RT_SUCCESS(rc)) + { + pStreamWas->pDevCfg = pDevCfg; + + pCfgAcq->Props = pDevCfg->Props; + pCfgAcq->Backend.cFramesBufferSize = pDevCfg->cFramesBufferSize; + pCfgAcq->Backend.cFramesPeriod = pDevCfg->cFramesPeriod; + pCfgAcq->Backend.cFramesPreBuffering = pCfgReq->Backend.cFramesPreBuffering * pDevCfg->cFramesBufferSize + / RT_MAX(pCfgReq->Backend.cFramesBufferSize, 1); + + PDMAudioStrmCfgCopy(&pStreamWas->Cfg, pCfgAcq); + + /* Finally, the critical section. */ + int rc2 = RTCritSectInit(&pStreamWas->CritSect); + if (RT_SUCCESS(rc2)) + { + RTCritSectRwEnterExcl(&pThis->CritSectStreamList); + RTListAppend(&pThis->StreamHead, &pStreamWas->ListEntry); + RTCritSectRwLeaveExcl(&pThis->CritSectStreamList); + + if (pStreamWas->pDevCfg->pIAudioClient != NULL) + { + LogFlowFunc(("returns VINF_SUCCESS\n", rc)); + return VINF_SUCCESS; + } + LogFlowFunc(("returns VINF_AUDIO_STREAM_ASYNC_INIT_NEEDED\n", rc)); + return VINF_AUDIO_STREAM_ASYNC_INIT_NEEDED; + } + + LogRelMax(64, ("WasAPI: Failed to create critical section for stream.\n")); + drvHostAudioWasCachePutBack(pThis, pDevCfg); + pStreamWas->pDevCfg = NULL; + } + else + LogRelMax(64, ("WasAPI: Failed to setup %s on audio device '%ls' (%Rrc).\n", pszStreamType, pwszDevIdDesc, rc)); + + LogFlowFunc(("returns %Rrc\n", rc)); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamInitAsync} + */ +static DECLCALLBACK(int) drvHostAudioWasHA_StreamInitAsync(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + bool fDestroyed) +{ + RT_NOREF(pInterface); + PDRVHOSTAUDIOWASSTREAM pStreamWas = (PDRVHOSTAUDIOWASSTREAM)pStream; + AssertPtrReturn(pStreamWas, VERR_INVALID_POINTER); + LogFlowFunc(("Stream '%s'%s\n", pStreamWas->Cfg.szName, fDestroyed ? " - destroyed!" : "")); + + /* + * Assert sane preconditions for this call. + */ + AssertPtrReturn(pStreamWas->Core.pStream, VERR_INTERNAL_ERROR); + AssertPtrReturn(pStreamWas->pDevCfg, VERR_INTERNAL_ERROR_2); + AssertPtrReturn(pStreamWas->pDevCfg->pDevEntry, VERR_INTERNAL_ERROR_3); + AssertPtrReturn(pStreamWas->pDevCfg->pDevEntry->pIDevice, VERR_INTERNAL_ERROR_4); + AssertReturn(pStreamWas->pDevCfg->pDevEntry->enmDir == pStreamWas->Core.pStream->Cfg.enmDir, VERR_INTERNAL_ERROR_4); + AssertReturn(pStreamWas->pDevCfg->pIAudioClient == NULL, VERR_INTERNAL_ERROR_5); + AssertReturn(pStreamWas->pDevCfg->pIAudioRenderClient == NULL, VERR_INTERNAL_ERROR_5); + AssertReturn(pStreamWas->pDevCfg->pIAudioCaptureClient == NULL, VERR_INTERNAL_ERROR_5); + + /* + * Do the job. + */ + int rc; + if (!fDestroyed) + rc = drvHostAudioWasCacheInitConfig(pStreamWas->pDevCfg); + else + { + AssertReturn(pStreamWas->pDevCfg->rcSetup == VERR_AUDIO_STREAM_INIT_IN_PROGRESS, VERR_INTERNAL_ERROR_2); + pStreamWas->pDevCfg->rcSetup = VERR_WRONG_ORDER; + rc = VINF_SUCCESS; + } + + LogFlowFunc(("returns %Rrc (%s)\n", rc, pStreamWas->Cfg.szName)); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDestroy} + */ +static DECLCALLBACK(int) drvHostAudioWasHA_StreamDestroy(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + bool fImmediate) +{ + PDRVHOSTAUDIOWAS pThis = RT_FROM_MEMBER(pInterface, DRVHOSTAUDIOWAS, IHostAudio); + PDRVHOSTAUDIOWASSTREAM pStreamWas = (PDRVHOSTAUDIOWASSTREAM)pStream; + AssertPtrReturn(pStreamWas, VERR_INVALID_POINTER); + LogFlowFunc(("Stream '%s'\n", pStreamWas->Cfg.szName)); + RT_NOREF(fImmediate); + HRESULT hrc; + + if (RTCritSectIsInitialized(&pStreamWas->CritSect)) + { + RTCritSectRwEnterExcl(&pThis->CritSectStreamList); + RTListNodeRemove(&pStreamWas->ListEntry); + RTCritSectRwLeaveExcl(&pThis->CritSectStreamList); + + RTCritSectDelete(&pStreamWas->CritSect); + } + + if (pStreamWas->fStarted && pStreamWas->pDevCfg && pStreamWas->pDevCfg->pIAudioClient) + { + hrc = pStreamWas->pDevCfg->pIAudioClient->Stop(); + LogFunc(("Stop('%s') -> %Rhrc\n", pStreamWas->Cfg.szName, hrc)); + pStreamWas->fStarted = false; + } + + if (pStreamWas->cFramesCaptureToRelease) + { + hrc = pStreamWas->pDevCfg->pIAudioCaptureClient->ReleaseBuffer(0); + Log4Func(("Releasing capture buffer (%#x frames): %Rhrc\n", pStreamWas->cFramesCaptureToRelease, hrc)); + pStreamWas->cFramesCaptureToRelease = 0; + pStreamWas->pbCapture = NULL; + pStreamWas->cbCapture = 0; + } + + if (pStreamWas->pDevCfg) + { + drvHostAudioWasCachePutBack(pThis, pStreamWas->pDevCfg); + pStreamWas->pDevCfg = NULL; + } + + LogFlowFunc(("returns\n")); + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamNotifyDeviceChanged} + */ +static DECLCALLBACK(void) drvHostAudioWasHA_StreamNotifyDeviceChanged(PPDMIHOSTAUDIO pInterface, + PPDMAUDIOBACKENDSTREAM pStream, void *pvUser) +{ + PDRVHOSTAUDIOWAS pThis = RT_FROM_MEMBER(pInterface, DRVHOSTAUDIOWAS, IHostAudio); + PDRVHOSTAUDIOWASSTREAM pStreamWas = (PDRVHOSTAUDIOWASSTREAM)pStream; + LogFlowFunc(("pStreamWas=%p (%s)\n", pStreamWas, pStreamWas->Cfg.szName)); + RT_NOREF(pvUser); + + /* + * See if we've got a cached config for the new device around. + * We ignore this entirely, for now at least, if the device was + * disconnected and there is no replacement. + */ + pThis->pNotifyClient->lockEnter(); + IMMDevice *pIDevice = pStreamWas->Cfg.enmDir == PDMAUDIODIR_IN ? pThis->pIDeviceInput : pThis->pIDeviceOutput; + if (pIDevice) + pIDevice->AddRef(); + pThis->pNotifyClient->lockLeave(); + if (pIDevice) + { + PDRVHOSTAUDIOWASCACHEDEVCFG pDevCfg = NULL; + int rc = drvHostAudioWasCacheLookupOrCreate(pThis, pIDevice, &pStreamWas->Cfg, false /*fOnWorker*/, &pDevCfg); + + pIDevice->Release(); + pIDevice = NULL; + + /* + * If we have a working audio client, just do the switch. + */ + if (RT_SUCCESS(rc) && pDevCfg->pIAudioClient) + { + LogFlowFunc(("New device config is ready already!\n")); + Assert(rc == VINF_SUCCESS); + drvHostAudioWasCompleteStreamDevSwitch(pThis, pStreamWas, pDevCfg); + } + /* + * Otherwise create one asynchronously on a worker thread. + */ + else if (RT_SUCCESS(rc)) + { + LogFlowFunc(("New device config needs async init ...\n")); + Assert(rc == VINF_AUDIO_STREAM_ASYNC_INIT_NEEDED); + + RTCritSectEnter(&pStreamWas->CritSect); + pStreamWas->fSwitchingDevice = true; + RTCritSectLeave(&pStreamWas->CritSect); + + pThis->pIHostAudioPort->pfnStreamNotifyPreparingDeviceSwitch(pThis->pIHostAudioPort, &pStreamWas->Core); + + rc = pThis->pIHostAudioPort->pfnDoOnWorkerThread(pThis->pIHostAudioPort, &pStreamWas->Core, + DRVHOSTAUDIOWAS_DO_STREAM_DEV_SWITCH, pDevCfg); + AssertRCStmt(rc, drvHostAudioWasDoStreamDevSwitch(pThis, pStreamWas, pDevCfg)); + } + else + { + LogRelMax(64, ("WasAPI: Failed to create new device config '%ls:%s' for stream '%s': %Rrc\n", + pDevCfg->pDevEntry->wszDevId, pDevCfg->szProps, pStreamWas->Cfg.szName, rc)); + + pThis->pIHostAudioPort->pfnStreamNotifyDeviceChanged(pThis->pIHostAudioPort, &pStreamWas->Core, true /*fReInit*/); + } + } + else + LogFlowFunc(("no new device, leaving it as-is\n")); +} + + +/** + * Wrapper for starting a stream. + * + * @returns VBox status code. + * @param pThis The WASAPI host audio driver instance data. + * @param pStreamWas The stream. + * @param pszOperation The operation we're doing. + */ +static int drvHostAudioWasStreamStartWorker(PDRVHOSTAUDIOWAS pThis, PDRVHOSTAUDIOWASSTREAM pStreamWas, const char *pszOperation) +{ + HRESULT hrc = pStreamWas->pDevCfg->pIAudioClient->Start(); + LogFlow(("%s: Start(%s) returns %Rhrc\n", pszOperation, pStreamWas->Cfg.szName, hrc)); + AssertStmt(hrc != AUDCLNT_E_NOT_STOPPED, hrc = S_OK); + if (SUCCEEDED(hrc)) + { + pStreamWas->fStarted = true; + return VINF_SUCCESS; + } + + /** @todo try re-setup the stuff on AUDCLNT_E_DEVICEINVALIDATED. + * Need some way of telling the caller (e.g. playback, capture) so they can + * retry what they're doing */ + RT_NOREF(pThis); + + pStreamWas->fStarted = false; + LogRelMax(64, ("WasAPI: Starting '%s' failed (%s): %Rhrc\n", pStreamWas->Cfg.szName, pszOperation, hrc)); + return VERR_AUDIO_STREAM_NOT_READY; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamEnable} + */ +static DECLCALLBACK(int) drvHostAudioWasHA_StreamEnable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + PDRVHOSTAUDIOWAS pThis = RT_FROM_MEMBER(pInterface, DRVHOSTAUDIOWAS, IHostAudio); + PDRVHOSTAUDIOWASSTREAM pStreamWas = (PDRVHOSTAUDIOWASSTREAM)pStream; + LogFlowFunc(("Stream '%s' {%s}\n", pStreamWas->Cfg.szName, drvHostWasStreamStatusString(pStreamWas))); + HRESULT hrc; + RTCritSectEnter(&pStreamWas->CritSect); + + Assert(!pStreamWas->fEnabled); + Assert(!pStreamWas->fStarted); + + /* + * We always reset the buffer before enabling the stream (normally never necessary). + */ + if (pStreamWas->cFramesCaptureToRelease) + { + hrc = pStreamWas->pDevCfg->pIAudioCaptureClient->ReleaseBuffer(pStreamWas->cFramesCaptureToRelease); + Log4Func(("Releasing capture buffer (%#x frames): %Rhrc\n", pStreamWas->cFramesCaptureToRelease, hrc)); + pStreamWas->cFramesCaptureToRelease = 0; + pStreamWas->pbCapture = NULL; + pStreamWas->cbCapture = 0; + } + + hrc = pStreamWas->pDevCfg->pIAudioClient->Reset(); + if (FAILED(hrc)) + LogRelMax(64, ("WasAPI: Stream reset failed when enabling '%s': %Rhrc\n", pStreamWas->Cfg.szName, hrc)); + pStreamWas->offInternal = 0; + pStreamWas->fDraining = false; + pStreamWas->fEnabled = true; + pStreamWas->fRestartOnResume = false; + + /* + * Input streams will start capturing, while output streams will only start + * playing once we get some audio data to play. + */ + int rc = VINF_SUCCESS; + if (pStreamWas->Cfg.enmDir == PDMAUDIODIR_IN) + rc = drvHostAudioWasStreamStartWorker(pThis, pStreamWas, "enable"); + else + Assert(pStreamWas->Cfg.enmDir == PDMAUDIODIR_OUT); + + RTCritSectLeave(&pStreamWas->CritSect); + LogFlowFunc(("returns %Rrc\n", rc)); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDisable} + */ +static DECLCALLBACK(int) drvHostAudioWasHA_StreamDisable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PDRVHOSTAUDIOWASSTREAM pStreamWas = (PDRVHOSTAUDIOWASSTREAM)pStream; + LogFlowFunc(("cMsLastTransfer=%RI64 ms, stream '%s' {%s} \n", + pStreamWas->msLastTransfer ? RTTimeMilliTS() - pStreamWas->msLastTransfer : -1, + pStreamWas->Cfg.szName, drvHostWasStreamStatusString(pStreamWas) )); + RTCritSectEnter(&pStreamWas->CritSect); + + /* + * Always try stop it (draining or no). + */ + pStreamWas->fEnabled = false; + pStreamWas->fRestartOnResume = false; + Assert(!pStreamWas->fDraining || pStreamWas->Cfg.enmDir == PDMAUDIODIR_OUT); + + int rc = VINF_SUCCESS; + if (pStreamWas->fStarted) + { + HRESULT hrc = pStreamWas->pDevCfg->pIAudioClient->Stop(); + LogFlowFunc(("Stop(%s) returns %Rhrc\n", pStreamWas->Cfg.szName, hrc)); + if (FAILED(hrc)) + { + LogRelMax(64, ("WasAPI: Stopping '%s' failed (disable): %Rhrc\n", pStreamWas->Cfg.szName, hrc)); + rc = VERR_GENERAL_FAILURE; + } + pStreamWas->fStarted = false; + pStreamWas->fDraining = false; + } + + RTCritSectLeave(&pStreamWas->CritSect); + LogFlowFunc(("returns %Rrc {%s}\n", rc, drvHostWasStreamStatusString(pStreamWas))); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamPause} + * + * @note Basically the same as drvHostAudioWasHA_StreamDisable, just w/o the + * buffer resetting and fEnabled change. + */ +static DECLCALLBACK(int) drvHostAudioWasHA_StreamPause(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PDRVHOSTAUDIOWASSTREAM pStreamWas = (PDRVHOSTAUDIOWASSTREAM)pStream; + LogFlowFunc(("cMsLastTransfer=%RI64 ms, stream '%s' {%s} \n", + pStreamWas->msLastTransfer ? RTTimeMilliTS() - pStreamWas->msLastTransfer : -1, + pStreamWas->Cfg.szName, drvHostWasStreamStatusString(pStreamWas) )); + RTCritSectEnter(&pStreamWas->CritSect); + + /* + * Unless we're draining the stream, stop it if it's started. + */ + int rc = VINF_SUCCESS; + if (pStreamWas->fStarted && !pStreamWas->fDraining) + { + pStreamWas->fRestartOnResume = true; + + HRESULT hrc = pStreamWas->pDevCfg->pIAudioClient->Stop(); + LogFlowFunc(("Stop(%s) returns %Rhrc\n", pStreamWas->Cfg.szName, hrc)); + if (FAILED(hrc)) + { + LogRelMax(64, ("WasAPI: Stopping '%s' failed (pause): %Rhrc\n", pStreamWas->Cfg.szName, hrc)); + rc = VERR_GENERAL_FAILURE; + } + pStreamWas->fStarted = false; + } + else + { + pStreamWas->fRestartOnResume = false; + if (pStreamWas->fDraining) + { + LogFunc(("Stream '%s' is draining\n", pStreamWas->Cfg.szName)); + Assert(pStreamWas->fStarted); + } + } + + RTCritSectLeave(&pStreamWas->CritSect); + LogFlowFunc(("returns %Rrc {%s}\n", rc, drvHostWasStreamStatusString(pStreamWas))); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamResume} + */ +static DECLCALLBACK(int) drvHostAudioWasHA_StreamResume(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + PDRVHOSTAUDIOWAS pThis = RT_FROM_MEMBER(pInterface, DRVHOSTAUDIOWAS, IHostAudio); + PDRVHOSTAUDIOWASSTREAM pStreamWas = (PDRVHOSTAUDIOWASSTREAM)pStream; + LogFlowFunc(("Stream '%s' {%s}\n", pStreamWas->Cfg.szName, drvHostWasStreamStatusString(pStreamWas))); + RTCritSectEnter(&pStreamWas->CritSect); + + /* + * Resume according to state saved by drvHostAudioWasHA_StreamPause. + */ + int rc; + if (pStreamWas->fRestartOnResume) + rc = drvHostAudioWasStreamStartWorker(pThis, pStreamWas, "resume"); + else + rc = VINF_SUCCESS; + pStreamWas->fRestartOnResume = false; + + RTCritSectLeave(&pStreamWas->CritSect); + LogFlowFunc(("returns %Rrc {%s}\n", rc, drvHostWasStreamStatusString(pStreamWas))); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamDrain} + */ +static DECLCALLBACK(int) drvHostAudioWasHA_StreamDrain(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PDRVHOSTAUDIOWASSTREAM pStreamWas = (PDRVHOSTAUDIOWASSTREAM)pStream; + AssertReturn(pStreamWas->Cfg.enmDir == PDMAUDIODIR_OUT, VERR_INVALID_PARAMETER); + LogFlowFunc(("cMsLastTransfer=%RI64 ms, stream '%s' {%s} \n", + pStreamWas->msLastTransfer ? RTTimeMilliTS() - pStreamWas->msLastTransfer : -1, + pStreamWas->Cfg.szName, drvHostWasStreamStatusString(pStreamWas) )); + + /* + * If the stram was started, calculate when the buffered data has finished + * playing and switch to drain mode. DrvAudio will keep on calling + * pfnStreamPlay with an empty buffer while we're draining, so we'll use + * that for checking the deadline and finally stopping the stream. + */ + RTCritSectEnter(&pStreamWas->CritSect); + int rc = VINF_SUCCESS; + if (pStreamWas->fStarted) + { + if (!pStreamWas->fDraining) + { + uint64_t const msNow = RTTimeMilliTS(); + uint64_t msDrainDeadline = 0; + UINT32 cFramesPending = 0; + HRESULT hrc = pStreamWas->pDevCfg->pIAudioClient->GetCurrentPadding(&cFramesPending); + if (SUCCEEDED(hrc)) + msDrainDeadline = msNow + + PDMAudioPropsFramesToMilli(&pStreamWas->Cfg.Props, + RT_MIN(cFramesPending, + pStreamWas->Cfg.Backend.cFramesBufferSize * 2)) + + 1 /*fudge*/; + else + { + msDrainDeadline = msNow; + LogRelMax(64, ("WasAPI: GetCurrentPadding fail on '%s' when starting draining: %Rhrc\n", + pStreamWas->Cfg.szName, hrc)); + } + pStreamWas->msDrainDeadline = msDrainDeadline; + pStreamWas->fDraining = true; + } + else + LogFlowFunc(("Already draining '%s' ...\n", pStreamWas->Cfg.szName)); + } + else + { + LogFlowFunc(("Drain requested for '%s', but not started playback...\n", pStreamWas->Cfg.szName)); + AssertStmt(!pStreamWas->fDraining, pStreamWas->fDraining = false); + } + RTCritSectLeave(&pStreamWas->CritSect); + + LogFlowFunc(("returns %Rrc {%s}\n", rc, drvHostWasStreamStatusString(pStreamWas))); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetState} + */ +static DECLCALLBACK(PDMHOSTAUDIOSTREAMSTATE) drvHostAudioWasHA_StreamGetState(PPDMIHOSTAUDIO pInterface, + PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PDRVHOSTAUDIOWASSTREAM pStreamWas = (PDRVHOSTAUDIOWASSTREAM)pStream; + AssertPtrReturn(pStreamWas, PDMHOSTAUDIOSTREAMSTATE_INVALID); + + PDMHOSTAUDIOSTREAMSTATE enmState; + AssertPtr(pStreamWas->pDevCfg); + if (pStreamWas->pDevCfg /*paranoia*/) + { + if (RT_SUCCESS(pStreamWas->pDevCfg->rcSetup)) + { + if (!pStreamWas->fDraining) + enmState = PDMHOSTAUDIOSTREAMSTATE_OKAY; + else + { + Assert(pStreamWas->Cfg.enmDir == PDMAUDIODIR_OUT); + enmState = PDMHOSTAUDIOSTREAMSTATE_DRAINING; + } + } + else if ( pStreamWas->pDevCfg->rcSetup == VERR_AUDIO_STREAM_INIT_IN_PROGRESS + || pStreamWas->fSwitchingDevice ) + enmState = PDMHOSTAUDIOSTREAMSTATE_INITIALIZING; + else + enmState = PDMHOSTAUDIOSTREAMSTATE_NOT_WORKING; + } + else if (pStreamWas->fSwitchingDevice) + enmState = PDMHOSTAUDIOSTREAMSTATE_INITIALIZING; + else + enmState = PDMHOSTAUDIOSTREAMSTATE_NOT_WORKING; + + LogFlowFunc(("returns %d for '%s' {%s}\n", enmState, pStreamWas->Cfg.szName, drvHostWasStreamStatusString(pStreamWas))); + return enmState; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetPending} + */ +static DECLCALLBACK(uint32_t) drvHostAudioWasHA_StreamGetPending(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PDRVHOSTAUDIOWASSTREAM pStreamWas = (PDRVHOSTAUDIOWASSTREAM)pStream; + AssertPtrReturn(pStreamWas, 0); + LogFlowFunc(("Stream '%s' {%s}\n", pStreamWas->Cfg.szName, drvHostWasStreamStatusString(pStreamWas))); + AssertReturn(pStreamWas->Cfg.enmDir == PDMAUDIODIR_OUT, 0); + + uint32_t cbPending = 0; + RTCritSectEnter(&pStreamWas->CritSect); + + if ( pStreamWas->Cfg.enmDir == PDMAUDIODIR_OUT + && pStreamWas->pDevCfg->pIAudioClient /* paranoia */) + { + if (pStreamWas->fStarted) + { + UINT32 cFramesPending = 0; + HRESULT hrc = pStreamWas->pDevCfg->pIAudioClient->GetCurrentPadding(&cFramesPending); + if (SUCCEEDED(hrc)) + { + AssertMsg(cFramesPending <= pStreamWas->Cfg.Backend.cFramesBufferSize, + ("cFramesPending=%#x cFramesBufferSize=%#x\n", + cFramesPending, pStreamWas->Cfg.Backend.cFramesBufferSize)); + cbPending = PDMAudioPropsFramesToBytes(&pStreamWas->Cfg.Props, RT_MIN(cFramesPending, VBOX_WASAPI_MAX_PADDING)); + } + else + LogRelMax(64, ("WasAPI: GetCurrentPadding failed on '%s': %Rhrc\n", pStreamWas->Cfg.szName, hrc)); + } + } + + RTCritSectLeave(&pStreamWas->CritSect); + + LogFlowFunc(("returns %#x (%u) {%s}\n", cbPending, cbPending, drvHostWasStreamStatusString(pStreamWas))); + return cbPending; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetWritable} + */ +static DECLCALLBACK(uint32_t) drvHostAudioWasHA_StreamGetWritable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PDRVHOSTAUDIOWASSTREAM pStreamWas = (PDRVHOSTAUDIOWASSTREAM)pStream; + AssertPtrReturn(pStreamWas, 0); + LogFlowFunc(("Stream '%s' {%s}\n", pStreamWas->Cfg.szName, drvHostWasStreamStatusString(pStreamWas))); + Assert(pStreamWas->Cfg.enmDir == PDMAUDIODIR_OUT); + + uint32_t cbWritable = 0; + RTCritSectEnter(&pStreamWas->CritSect); + + if ( pStreamWas->Cfg.enmDir == PDMAUDIODIR_OUT + && pStreamWas->pDevCfg->pIAudioClient /* paranoia */) + { + UINT32 cFramesPending = 0; + HRESULT hrc = pStreamWas->pDevCfg->pIAudioClient->GetCurrentPadding(&cFramesPending); + if (SUCCEEDED(hrc)) + { + if (cFramesPending < pStreamWas->Cfg.Backend.cFramesBufferSize) + cbWritable = PDMAudioPropsFramesToBytes(&pStreamWas->Cfg.Props, + pStreamWas->Cfg.Backend.cFramesBufferSize - cFramesPending); + else if (cFramesPending > pStreamWas->Cfg.Backend.cFramesBufferSize) + { + LogRelMax(64, ("WasAPI: Warning! GetCurrentPadding('%s') return too high: cFramesPending=%#x > cFramesBufferSize=%#x\n", + pStreamWas->Cfg.szName, cFramesPending, pStreamWas->Cfg.Backend.cFramesBufferSize)); + AssertMsgFailed(("cFramesPending=%#x > cFramesBufferSize=%#x\n", + cFramesPending, pStreamWas->Cfg.Backend.cFramesBufferSize)); + } + } + else + LogRelMax(64, ("WasAPI: GetCurrentPadding failed on '%s': %Rhrc\n", pStreamWas->Cfg.szName, hrc)); + } + + RTCritSectLeave(&pStreamWas->CritSect); + + LogFlowFunc(("returns %#x (%u) {%s}\n", cbWritable, cbWritable, drvHostWasStreamStatusString(pStreamWas))); + return cbWritable; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamPlay} + */ +static DECLCALLBACK(int) drvHostAudioWasHA_StreamPlay(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + const void *pvBuf, uint32_t cbBuf, uint32_t *pcbWritten) +{ + PDRVHOSTAUDIOWAS pThis = RT_FROM_MEMBER(pInterface, DRVHOSTAUDIOWAS, IHostAudio); + PDRVHOSTAUDIOWASSTREAM pStreamWas = (PDRVHOSTAUDIOWASSTREAM)pStream; + AssertPtrReturn(pStreamWas, VERR_INVALID_POINTER); + AssertPtrReturn(pcbWritten, VERR_INVALID_POINTER); + if (cbBuf) + AssertPtrReturn(pvBuf, VERR_INVALID_POINTER); + Assert(PDMAudioPropsIsSizeAligned(&pStreamWas->Cfg.Props, cbBuf)); + + RTCritSectEnter(&pStreamWas->CritSect); + if (pStreamWas->fEnabled) + { /* likely */ } + else + { + RTCritSectLeave(&pStreamWas->CritSect); + *pcbWritten = 0; + LogFunc(("Skipping %#x byte write to disabled stream {%s}\n", cbBuf, drvHostWasStreamStatusString(pStreamWas))); + return VINF_SUCCESS; + } + Log4Func(("cbBuf=%#x stream '%s' {%s}\n", cbBuf, pStreamWas->Cfg.szName, drvHostWasStreamStatusString(pStreamWas) )); + + /* + * Transfer loop. + */ + int rc = VINF_SUCCESS; + uint32_t cReInits = 0; + uint32_t cbWritten = 0; + while (cbBuf > 0) + { + AssertBreakStmt(pStreamWas->pDevCfg && pStreamWas->pDevCfg->pIAudioRenderClient && pStreamWas->pDevCfg->pIAudioClient, + rc = VERR_AUDIO_STREAM_NOT_READY); + + /* + * Figure out how much we can possibly write. + */ + UINT32 cFramesPending = 0; + uint32_t cbWritable = 0; + HRESULT hrc = pStreamWas->pDevCfg->pIAudioClient->GetCurrentPadding(&cFramesPending); + if (SUCCEEDED(hrc)) + cbWritable = PDMAudioPropsFramesToBytes(&pStreamWas->Cfg.Props, + pStreamWas->Cfg.Backend.cFramesBufferSize + - RT_MIN(cFramesPending, pStreamWas->Cfg.Backend.cFramesBufferSize)); + else + { + LogRelMax(64, ("WasAPI: GetCurrentPadding(%s) failed during playback: %Rhrc (@%#RX64)\n", + pStreamWas->Cfg.szName, hrc, pStreamWas->offInternal)); + /** @todo reinit on AUDCLNT_E_DEVICEINVALIDATED? */ + rc = VERR_AUDIO_STREAM_NOT_READY; + break; + } + if (cbWritable <= PDMAudioPropsFrameSize(&pStreamWas->Cfg.Props)) + break; + + uint32_t const cbToWrite = PDMAudioPropsFloorBytesToFrame(&pStreamWas->Cfg.Props, RT_MIN(cbWritable, cbBuf)); + uint32_t const cFramesToWrite = PDMAudioPropsBytesToFrames(&pStreamWas->Cfg.Props, cbToWrite); + Assert(PDMAudioPropsFramesToBytes(&pStreamWas->Cfg.Props, cFramesToWrite) == cbToWrite); + Log3Func(("@%#RX64: cFramesPending=%#x -> cbWritable=%#x cbToWrite=%#x cFramesToWrite=%#x {%s}\n", + pStreamWas->offInternal, cFramesPending, cbWritable, cbToWrite, cFramesToWrite, + drvHostWasStreamStatusString(pStreamWas) )); + + /* + * Get the buffer, copy the data into it, and relase it back to the WAS machinery. + */ + BYTE *pbData = NULL; + hrc = pStreamWas->pDevCfg->pIAudioRenderClient->GetBuffer(cFramesToWrite, &pbData); + if (SUCCEEDED(hrc)) + { + memcpy(pbData, pvBuf, cbToWrite); + hrc = pStreamWas->pDevCfg->pIAudioRenderClient->ReleaseBuffer(cFramesToWrite, 0 /*fFlags*/); + if (SUCCEEDED(hrc)) + { + /* + * Before we advance the buffer position (so we can resubmit it + * after re-init), make sure we've successfully started stream. + */ + if (pStreamWas->fStarted) + { } + else + { + rc = drvHostAudioWasStreamStartWorker(pThis, pStreamWas, "play"); + if (rc == VINF_SUCCESS) + { /* likely */ } + else if (RT_SUCCESS(rc) && ++cReInits < 5) + continue; /* re-submit buffer after re-init */ + else + break; + } + + /* advance. */ + pvBuf = (uint8_t *)pvBuf + cbToWrite; + cbBuf -= cbToWrite; + cbWritten += cbToWrite; + pStreamWas->offInternal += cbToWrite; + } + else + { + LogRelMax(64, ("WasAPI: ReleaseBuffer(%#x) failed on '%s' during playback: %Rhrc (@%#RX64)\n", + cFramesToWrite, pStreamWas->Cfg.szName, hrc, pStreamWas->offInternal)); + /** @todo reinit on AUDCLNT_E_DEVICEINVALIDATED? */ + rc = VERR_AUDIO_STREAM_NOT_READY; + break; + } + } + else + { + LogRelMax(64, ("WasAPI: GetBuffer(%#x) failed on '%s' during playback: %Rhrc (@%#RX64)\n", + cFramesToWrite, pStreamWas->Cfg.szName, hrc, pStreamWas->offInternal)); + /** @todo reinit on AUDCLNT_E_DEVICEINVALIDATED? */ + rc = VERR_AUDIO_STREAM_NOT_READY; + break; + } + } + + /* + * Do draining deadline processing. + */ + uint64_t const msNow = RTTimeMilliTS(); + if ( !pStreamWas->fDraining + || msNow < pStreamWas->msDrainDeadline) + { /* likely */ } + else + { + LogRel2(("WasAPI: Stopping draining of '%s' {%s} ...\n", pStreamWas->Cfg.szName, drvHostWasStreamStatusString(pStreamWas))); + HRESULT hrc = pStreamWas->pDevCfg->pIAudioClient->Stop(); + if (FAILED(hrc)) + LogRelMax(64, ("WasAPI: Failed to stop draining stream '%s': %Rhrc\n", pStreamWas->Cfg.szName, hrc)); + pStreamWas->fDraining = false; + pStreamWas->fStarted = false; + pStreamWas->fEnabled = false; + } + + /* + * Done. + */ + uint64_t const msPrev = pStreamWas->msLastTransfer; RT_NOREF(msPrev); + if (cbWritten) + pStreamWas->msLastTransfer = msNow; + + RTCritSectLeave(&pStreamWas->CritSect); + + *pcbWritten = cbWritten; + if (RT_SUCCESS(rc) || !cbWritten) + { } + else + { + LogFlowFunc(("Suppressing %Rrc to report %#x bytes written\n", rc, cbWritten)); + rc = VINF_SUCCESS; + } + LogFlowFunc(("@%#RX64: rc=%Rrc cbWritten=%RU32 cMsDelta=%RU64 (%RU64 -> %RU64) {%s}\n", pStreamWas->offInternal, rc, cbWritten, + msPrev ? msNow - msPrev : 0, msPrev, pStreamWas->msLastTransfer, drvHostWasStreamStatusString(pStreamWas) )); + return rc; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamGetReadable} + */ +static DECLCALLBACK(uint32_t) drvHostAudioWasHA_StreamGetReadable(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface); + PDRVHOSTAUDIOWASSTREAM pStreamWas = (PDRVHOSTAUDIOWASSTREAM)pStream; + AssertPtrReturn(pStreamWas, 0); + Assert(pStreamWas->Cfg.enmDir == PDMAUDIODIR_IN); + + uint32_t cbReadable = 0; + RTCritSectEnter(&pStreamWas->CritSect); + + if (pStreamWas->pDevCfg->pIAudioCaptureClient /* paranoia */) + { + UINT32 cFramesPending = 0; + HRESULT hrc = pStreamWas->pDevCfg->pIAudioClient->GetCurrentPadding(&cFramesPending); + if (SUCCEEDED(hrc)) + { + /* An unreleased buffer is included in the pending frame count, so subtract + whatever we've got hanging around since the previous pfnStreamCapture call. */ + AssertMsgStmt(cFramesPending >= pStreamWas->cFramesCaptureToRelease, + ("%#x vs %#x\n", cFramesPending, pStreamWas->cFramesCaptureToRelease), + cFramesPending = pStreamWas->cFramesCaptureToRelease); + cFramesPending -= pStreamWas->cFramesCaptureToRelease; + + /* Add what we've got left in said buffer. */ + uint32_t cFramesCurPacket = PDMAudioPropsBytesToFrames(&pStreamWas->Cfg.Props, pStreamWas->cbCapture); + cFramesPending += cFramesCurPacket; + + /* Paranoia: Make sure we don't exceed the buffer size. */ + AssertMsgStmt(cFramesPending <= pStreamWas->Cfg.Backend.cFramesBufferSize, + ("cFramesPending=%#x cFramesCaptureToRelease=%#x cFramesCurPacket=%#x cFramesBufferSize=%#x\n", + cFramesPending, pStreamWas->cFramesCaptureToRelease, cFramesCurPacket, + pStreamWas->Cfg.Backend.cFramesBufferSize), + cFramesPending = pStreamWas->Cfg.Backend.cFramesBufferSize); + + cbReadable = PDMAudioPropsFramesToBytes(&pStreamWas->Cfg.Props, cFramesPending); + } + else + LogRelMax(64, ("WasAPI: GetCurrentPadding failed on '%s': %Rhrc\n", pStreamWas->Cfg.szName, hrc)); + } + + RTCritSectLeave(&pStreamWas->CritSect); + + LogFlowFunc(("returns %#x (%u) {%s}\n", cbReadable, cbReadable, drvHostWasStreamStatusString(pStreamWas))); + return cbReadable; +} + + +/** + * @interface_method_impl{PDMIHOSTAUDIO,pfnStreamCapture} + */ +static DECLCALLBACK(int) drvHostAudioWasHA_StreamCapture(PPDMIHOSTAUDIO pInterface, PPDMAUDIOBACKENDSTREAM pStream, + void *pvBuf, uint32_t cbBuf, uint32_t *pcbRead) +{ + RT_NOREF(pInterface); //PDRVHOSTAUDIOWAS pThis = RT_FROM_MEMBER(pInterface, DRVHOSTAUDIOWAS, IHostAudio); + PDRVHOSTAUDIOWASSTREAM pStreamWas = (PDRVHOSTAUDIOWASSTREAM)pStream; + AssertPtrReturn(pStreamWas, 0); + AssertPtrReturn(pvBuf, VERR_INVALID_POINTER); + AssertReturn(cbBuf, VERR_INVALID_PARAMETER); + AssertPtrReturn(pcbRead, VERR_INVALID_POINTER); + Assert(PDMAudioPropsIsSizeAligned(&pStreamWas->Cfg.Props, cbBuf)); + + RTCritSectEnter(&pStreamWas->CritSect); + if (pStreamWas->fEnabled) + { /* likely */ } + else + { + RTCritSectLeave(&pStreamWas->CritSect); + *pcbRead = 0; + LogFunc(("Skipping %#x byte read from disabled stream {%s}\n", cbBuf, drvHostWasStreamStatusString(pStreamWas))); + return VINF_SUCCESS; + } + Log4Func(("cbBuf=%#x stream '%s' {%s}\n", cbBuf, pStreamWas->Cfg.szName, drvHostWasStreamStatusString(pStreamWas) )); + + + /* + * Transfer loop. + */ + int rc = VINF_SUCCESS; + uint32_t cbRead = 0; + uint32_t const cbFrame = PDMAudioPropsFrameSize(&pStreamWas->Cfg.Props); + while (cbBuf >= cbFrame) + { + AssertBreakStmt(pStreamWas->pDevCfg->pIAudioCaptureClient && pStreamWas->pDevCfg->pIAudioClient, rc = VERR_AUDIO_STREAM_NOT_READY); + + /* + * Anything pending from last call? + * (This is rather similar to the Pulse interface.) + */ + if (pStreamWas->cFramesCaptureToRelease) + { + uint32_t const cbToCopy = RT_MIN(pStreamWas->cbCapture, cbBuf); + memcpy(pvBuf, pStreamWas->pbCapture, cbToCopy); + pvBuf = (uint8_t *)pvBuf + cbToCopy; + cbBuf -= cbToCopy; + cbRead += cbToCopy; + pStreamWas->offInternal += cbToCopy; + pStreamWas->pbCapture += cbToCopy; + pStreamWas->cbCapture -= cbToCopy; + if (!pStreamWas->cbCapture) + { + HRESULT hrc = pStreamWas->pDevCfg->pIAudioCaptureClient->ReleaseBuffer(pStreamWas->cFramesCaptureToRelease); + Log4Func(("@%#RX64: Releasing capture buffer (%#x frames): %Rhrc\n", + pStreamWas->offInternal, pStreamWas->cFramesCaptureToRelease, hrc)); + if (SUCCEEDED(hrc)) + { + pStreamWas->cFramesCaptureToRelease = 0; + pStreamWas->pbCapture = NULL; + } + else + { + LogRelMax(64, ("WasAPI: ReleaseBuffer(%s) failed during capture: %Rhrc (@%#RX64)\n", + pStreamWas->Cfg.szName, hrc, pStreamWas->offInternal)); + /** @todo reinit on AUDCLNT_E_DEVICEINVALIDATED? */ + rc = VERR_AUDIO_STREAM_NOT_READY; + break; + } + } + if (cbBuf < cbFrame) + break; + } + + /* + * Figure out if there is any data available to be read now. (Docs hint that we can not + * skip this and go straight for GetBuffer or we risk getting unwritten buffer space back). + */ + UINT32 cFramesCaptured = 0; + HRESULT hrc = pStreamWas->pDevCfg->pIAudioCaptureClient->GetNextPacketSize(&cFramesCaptured); + if (SUCCEEDED(hrc)) + { + if (!cFramesCaptured) + break; + } + else + { + LogRelMax(64, ("WasAPI: GetNextPacketSize(%s) failed during capture: %Rhrc (@%#RX64)\n", + pStreamWas->Cfg.szName, hrc, pStreamWas->offInternal)); + /** @todo reinit on AUDCLNT_E_DEVICEINVALIDATED? */ + rc = VERR_AUDIO_STREAM_NOT_READY; + break; + } + + /* + * Get the buffer. + */ + cFramesCaptured = 0; + UINT64 uQpsNtTicks = 0; + UINT64 offDevice = 0; + DWORD fBufFlags = 0; + BYTE *pbData = NULL; + hrc = pStreamWas->pDevCfg->pIAudioCaptureClient->GetBuffer(&pbData, &cFramesCaptured, &fBufFlags, &offDevice, &uQpsNtTicks); + Log4Func(("@%#RX64: GetBuffer -> %Rhrc pbData=%p cFramesCaptured=%#x fBufFlags=%#x offDevice=%#RX64 uQpcNtTicks=%#RX64\n", + pStreamWas->offInternal, hrc, pbData, cFramesCaptured, fBufFlags, offDevice, uQpsNtTicks)); + if (SUCCEEDED(hrc)) + { + Assert(cFramesCaptured < VBOX_WASAPI_MAX_PADDING); + pStreamWas->pbCapture = pbData; + pStreamWas->cFramesCaptureToRelease = cFramesCaptured; + pStreamWas->cbCapture = PDMAudioPropsFramesToBytes(&pStreamWas->Cfg.Props, cFramesCaptured); + /* Just loop and re-use the copying code above. Can optimize later. */ + } + else + { + LogRelMax(64, ("WasAPI: GetBuffer() failed on '%s' during capture: %Rhrc (@%#RX64)\n", + pStreamWas->Cfg.szName, hrc, pStreamWas->offInternal)); + /** @todo reinit on AUDCLNT_E_DEVICEINVALIDATED? */ + rc = VERR_AUDIO_STREAM_NOT_READY; + break; + } + } + + /* + * Done. + */ + uint64_t const msPrev = pStreamWas->msLastTransfer; RT_NOREF(msPrev); + uint64_t const msNow = RTTimeMilliTS(); + if (cbRead) + pStreamWas->msLastTransfer = msNow; + + RTCritSectLeave(&pStreamWas->CritSect); + + *pcbRead = cbRead; + if (RT_SUCCESS(rc) || !cbRead) + { } + else + { + LogFlowFunc(("Suppressing %Rrc to report %#x bytes read\n", rc, cbRead)); + rc = VINF_SUCCESS; + } + LogFlowFunc(("@%#RX64: rc=%Rrc cbRead=%#RX32 cMsDelta=%RU64 (%RU64 -> %RU64) {%s}\n", pStreamWas->offInternal, rc, cbRead, + msPrev ? msNow - msPrev : 0, msPrev, pStreamWas->msLastTransfer, drvHostWasStreamStatusString(pStreamWas) )); + return rc; +} + + +/********************************************************************************************************************************* +* PDMDRVINS::IBase Interface * +*********************************************************************************************************************************/ + +/** + * @callback_method_impl{PDMIBASE,pfnQueryInterface} + */ +static DECLCALLBACK(void *) drvHostAudioWasQueryInterface(PPDMIBASE pInterface, const char *pszIID) +{ + PPDMDRVINS pDrvIns = PDMIBASE_2_PDMDRV(pInterface); + PDRVHOSTAUDIOWAS pThis = PDMINS_2_DATA(pDrvIns, PDRVHOSTAUDIOWAS); + + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIBASE, &pDrvIns->IBase); + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIHOSTAUDIO, &pThis->IHostAudio); + return NULL; +} + + +/********************************************************************************************************************************* +* PDMDRVREG Interface * +*********************************************************************************************************************************/ + +/** + * @callback_method_impl{FNPDMDRVDESTRUCT, pfnDestruct} + */ +static DECLCALLBACK(void) drvHostAudioWasPowerOff(PPDMDRVINS pDrvIns) +{ + PDRVHOSTAUDIOWAS pThis = PDMINS_2_DATA(pDrvIns, PDRVHOSTAUDIOWAS); + + /* + * Start purging the cache asynchronously before we get to destruct. + * This might speed up VM shutdown a tiny fraction and also stress + * the shutting down of the thread pool a little. + */ +#if 0 + if (pThis->hWorkerThread != NIL_RTTHREAD) + { + BOOL fRc = PostThreadMessageW(pThis->idWorkerThread, WM_DRVHOSTAUDIOWAS_PURGE_CACHE, pThis->uWorkerThreadFixedParam, 0); + LogFlowFunc(("Posted WM_DRVHOSTAUDIOWAS_PURGE_CACHE: %d\n", fRc)); + Assert(fRc); RT_NOREF(fRc); + } +#else + if (!RTListIsEmpty(&pThis->CacheHead) && pThis->pIHostAudioPort) + { + int rc = RTSemEventMultiCreate(&pThis->hEvtCachePurge); + if (RT_SUCCESS(rc)) + { + rc = pThis->pIHostAudioPort->pfnDoOnWorkerThread(pThis->pIHostAudioPort, NULL/*pStream*/, + DRVHOSTAUDIOWAS_DO_PURGE_CACHE, NULL /*pvUser*/); + if (RT_FAILURE(rc)) + { + LogFunc(("pfnDoOnWorkerThread/DRVHOSTAUDIOWAS_DO_PURGE_CACHE failed: %Rrc\n", rc)); + RTSemEventMultiDestroy(pThis->hEvtCachePurge); + pThis->hEvtCachePurge = NIL_RTSEMEVENTMULTI; + } + } + } +#endif + + /* + * Deregister the notification client to reduce the risk of notifications + * comming in while we're being detatched or the VM is being destroyed. + */ + if (pThis->pNotifyClient) + { + pThis->pNotifyClient->notifyDriverDestroyed(); + pThis->pIEnumerator->UnregisterEndpointNotificationCallback(pThis->pNotifyClient); + pThis->pNotifyClient->Release(); + pThis->pNotifyClient = NULL; + } +} + + +/** + * @callback_method_impl{FNPDMDRVDESTRUCT, pfnDestruct} + */ +static DECLCALLBACK(void) drvHostAudioWasDestruct(PPDMDRVINS pDrvIns) +{ + PDRVHOSTAUDIOWAS pThis = PDMINS_2_DATA(pDrvIns, PDRVHOSTAUDIOWAS); + PDMDRV_CHECK_VERSIONS_RETURN_VOID(pDrvIns); + LogFlowFuncEnter(); + + /* + * Release the notification client first. + */ + if (pThis->pNotifyClient) + { + pThis->pNotifyClient->notifyDriverDestroyed(); + pThis->pIEnumerator->UnregisterEndpointNotificationCallback(pThis->pNotifyClient); + pThis->pNotifyClient->Release(); + pThis->pNotifyClient = NULL; + } + +#if 0 + if (pThis->hWorkerThread != NIL_RTTHREAD) + { + BOOL fRc = PostThreadMessageW(pThis->idWorkerThread, WM_QUIT, 0, 0); + Assert(fRc); RT_NOREF(fRc); + + int rc = RTThreadWait(pThis->hWorkerThread, RT_MS_15SEC, NULL); + AssertRC(rc); + } +#endif + + if (RTCritSectIsInitialized(&pThis->CritSectCache)) + { + drvHostAudioWasCachePurge(pThis, false /*fOnWorker*/); + if (pThis->hEvtCachePurge != NIL_RTSEMEVENTMULTI) + RTSemEventMultiWait(pThis->hEvtCachePurge, RT_MS_30SEC); + RTCritSectDelete(&pThis->CritSectCache); + } + + if (pThis->hEvtCachePurge != NIL_RTSEMEVENTMULTI) + { + RTSemEventMultiDestroy(pThis->hEvtCachePurge); + pThis->hEvtCachePurge = NIL_RTSEMEVENTMULTI; + } + + if (pThis->pIEnumerator) + { + uint32_t cRefs = pThis->pIEnumerator->Release(); RT_NOREF(cRefs); + LogFlowFunc(("cRefs=%d\n", cRefs)); + } + + if (pThis->pIDeviceOutput) + { + pThis->pIDeviceOutput->Release(); + pThis->pIDeviceOutput = NULL; + } + + if (pThis->pIDeviceInput) + { + pThis->pIDeviceInput->Release(); + pThis->pIDeviceInput = NULL; + } + + + if (RTCritSectRwIsInitialized(&pThis->CritSectStreamList)) + RTCritSectRwDelete(&pThis->CritSectStreamList); + + LogFlowFuncLeave(); +} + + +/** + * @callback_method_impl{FNPDMDRVCONSTRUCT, pfnConstruct} + */ +static DECLCALLBACK(int) drvHostAudioWasConstruct(PPDMDRVINS pDrvIns, PCFGMNODE pCfg, uint32_t fFlags) +{ + PDMDRV_CHECK_VERSIONS_RETURN(pDrvIns); + PDRVHOSTAUDIOWAS pThis = PDMINS_2_DATA(pDrvIns, PDRVHOSTAUDIOWAS); + PCPDMDRVHLPR3 pHlp = pDrvIns->pHlpR3; + RT_NOREF(fFlags, pCfg); + + /* + * Init basic data members and interfaces. + */ + pThis->pDrvIns = pDrvIns; + pThis->hEvtCachePurge = NIL_RTSEMEVENTMULTI; +#if 0 + pThis->hWorkerThread = NIL_RTTHREAD; + pThis->idWorkerThread = 0; +#endif + RTListInit(&pThis->StreamHead); + RTListInit(&pThis->CacheHead); + /* IBase */ + pDrvIns->IBase.pfnQueryInterface = drvHostAudioWasQueryInterface; + /* IHostAudio */ + pThis->IHostAudio.pfnGetConfig = drvHostAudioWasHA_GetConfig; + pThis->IHostAudio.pfnGetDevices = drvHostAudioWasHA_GetDevices; + pThis->IHostAudio.pfnSetDevice = drvHostAudioWasHA_SetDevice; + pThis->IHostAudio.pfnGetStatus = drvHostAudioWasHA_GetStatus; + pThis->IHostAudio.pfnDoOnWorkerThread = drvHostAudioWasHA_DoOnWorkerThread; + pThis->IHostAudio.pfnStreamConfigHint = drvHostAudioWasHA_StreamConfigHint; + pThis->IHostAudio.pfnStreamCreate = drvHostAudioWasHA_StreamCreate; + pThis->IHostAudio.pfnStreamInitAsync = drvHostAudioWasHA_StreamInitAsync; + pThis->IHostAudio.pfnStreamDestroy = drvHostAudioWasHA_StreamDestroy; + pThis->IHostAudio.pfnStreamNotifyDeviceChanged = drvHostAudioWasHA_StreamNotifyDeviceChanged; + pThis->IHostAudio.pfnStreamEnable = drvHostAudioWasHA_StreamEnable; + pThis->IHostAudio.pfnStreamDisable = drvHostAudioWasHA_StreamDisable; + pThis->IHostAudio.pfnStreamPause = drvHostAudioWasHA_StreamPause; + pThis->IHostAudio.pfnStreamResume = drvHostAudioWasHA_StreamResume; + pThis->IHostAudio.pfnStreamDrain = drvHostAudioWasHA_StreamDrain; + pThis->IHostAudio.pfnStreamGetState = drvHostAudioWasHA_StreamGetState; + pThis->IHostAudio.pfnStreamGetPending = drvHostAudioWasHA_StreamGetPending; + pThis->IHostAudio.pfnStreamGetWritable = drvHostAudioWasHA_StreamGetWritable; + pThis->IHostAudio.pfnStreamPlay = drvHostAudioWasHA_StreamPlay; + pThis->IHostAudio.pfnStreamGetReadable = drvHostAudioWasHA_StreamGetReadable; + pThis->IHostAudio.pfnStreamCapture = drvHostAudioWasHA_StreamCapture; + + /* + * Validate and read the configuration. + */ + PDMDRV_VALIDATE_CONFIG_RETURN(pDrvIns, "VmName|VmUuid|InputDeviceID|OutputDeviceID", ""); + + char szTmp[1024]; + int rc = pHlp->pfnCFGMQueryStringDef(pCfg, "InputDeviceID", szTmp, sizeof(szTmp), ""); + AssertMsgRCReturn(rc, ("Confguration error: Failed to read \"InputDeviceID\" as string: rc=%Rrc\n", rc), rc); + if (szTmp[0]) + { + rc = RTStrToUtf16(szTmp, &pThis->pwszInputDevId); + AssertRCReturn(rc, rc); + } + + rc = pHlp->pfnCFGMQueryStringDef(pCfg, "OutputDeviceID", szTmp, sizeof(szTmp), ""); + AssertMsgRCReturn(rc, ("Confguration error: Failed to read \"OutputDeviceID\" as string: rc=%Rrc\n", rc), rc); + if (szTmp[0]) + { + rc = RTStrToUtf16(szTmp, &pThis->pwszOutputDevId); + AssertRCReturn(rc, rc); + } + + AssertMsgReturn(PDMDrvHlpNoAttach(pDrvIns) == VERR_PDM_NO_ATTACHED_DRIVER, + ("Configuration error: Not possible to attach anything to this driver!\n"), + VERR_PDM_DRVINS_NO_ATTACH); + + /* + * Initialize the critical sections early. + */ + rc = RTCritSectRwInit(&pThis->CritSectStreamList); + AssertRCReturn(rc, rc); + + rc = RTCritSectInit(&pThis->CritSectCache); + AssertRCReturn(rc, rc); + + /* + * Create an enumerator instance that we can get the default devices from + * as well as do enumeration thru. + */ + HRESULT hrc = CoCreateInstance(__uuidof(MMDeviceEnumerator), 0, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), + (void **)&pThis->pIEnumerator); + if (FAILED(hrc)) + { + pThis->pIEnumerator = NULL; + LogRel(("WasAPI: Failed to create an MMDeviceEnumerator object: %Rhrc\n", hrc)); + return VERR_AUDIO_BACKEND_INIT_FAILED; + } + AssertPtr(pThis->pIEnumerator); + + /* + * Resolve the interface to the driver above us. + */ + pThis->pIHostAudioPort = PDMIBASE_QUERY_INTERFACE(pDrvIns->pUpBase, PDMIHOSTAUDIOPORT); + AssertPtrReturn(pThis->pIHostAudioPort, VERR_PDM_MISSING_INTERFACE_ABOVE); + + /* + * Instantiate and register the notification client with the enumerator. + * + * Failure here isn't considered fatal at this time as we'll just miss + * default device changes. + */ +#ifdef RT_EXCEPTIONS_ENABLED + try { pThis->pNotifyClient = new DrvHostAudioWasMmNotifyClient(pThis); } + catch (std::bad_alloc &) { return VERR_NO_MEMORY; } +#else + pThis->pNotifyClient = new DrvHostAudioWasMmNotifyClient(pThis); + AssertReturn(pThis->pNotifyClient, VERR_NO_MEMORY); +#endif + rc = pThis->pNotifyClient->init(); + AssertRCReturn(rc, rc); + + hrc = pThis->pIEnumerator->RegisterEndpointNotificationCallback(pThis->pNotifyClient); + AssertMsg(SUCCEEDED(hrc), ("%Rhrc\n", hrc)); + if (FAILED(hrc)) + { + LogRel(("WasAPI: RegisterEndpointNotificationCallback failed: %Rhrc (ignored)\n" + "WasAPI: Warning! Will not be able to detect default device changes!\n")); + pThis->pNotifyClient->notifyDriverDestroyed(); + pThis->pNotifyClient->Release(); + pThis->pNotifyClient = NULL; + } + + /* + * Retrieve the input and output device. + */ + IMMDevice *pIDeviceInput = NULL; + if (pThis->pwszInputDevId) + hrc = pThis->pIEnumerator->GetDevice(pThis->pwszInputDevId, &pIDeviceInput); + else + hrc = pThis->pIEnumerator->GetDefaultAudioEndpoint(eCapture, eMultimedia, &pIDeviceInput); + if (SUCCEEDED(hrc)) + LogFlowFunc(("pIDeviceInput=%p\n", pIDeviceInput)); + else + { + LogRel(("WasAPI: Failed to get audio input device '%ls': %Rhrc\n", + pThis->pwszInputDevId ? pThis->pwszInputDevId : L"{Default}", hrc)); + pIDeviceInput = NULL; + } + + IMMDevice *pIDeviceOutput = NULL; + if (pThis->pwszOutputDevId) + hrc = pThis->pIEnumerator->GetDevice(pThis->pwszOutputDevId, &pIDeviceOutput); + else + hrc = pThis->pIEnumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, &pIDeviceOutput); + if (SUCCEEDED(hrc)) + LogFlowFunc(("pIDeviceOutput=%p\n", pIDeviceOutput)); + else + { + LogRel(("WasAPI: Failed to get audio output device '%ls': %Rhrc\n", + pThis->pwszOutputDevId ? pThis->pwszOutputDevId : L"{Default}", hrc)); + pIDeviceOutput = NULL; + } + + /* Carefully place them in the instance data: */ + pThis->pNotifyClient->lockEnter(); + + if (pThis->pIDeviceInput) + pThis->pIDeviceInput->Release(); + pThis->pIDeviceInput = pIDeviceInput; + + if (pThis->pIDeviceOutput) + pThis->pIDeviceOutput->Release(); + pThis->pIDeviceOutput = pIDeviceOutput; + + pThis->pNotifyClient->lockLeave(); + +#if 0 + /* + * Create the worker thread. This thread has a message loop and will be + * signalled by DrvHostAudioWasMmNotifyClient while the VM is paused/whatever, + * so better make it a regular thread rather than PDM thread. + */ + pThis->uWorkerThreadFixedParam = (WPARAM)RTRandU64(); + rc = RTThreadCreateF(&pThis->hWorkerThread, drvHostWasWorkerThread, pThis, 0 /*cbStack*/, RTTHREADTYPE_DEFAULT, + RTTHREADFLAGS_WAITABLE | RTTHREADFLAGS_COM_MTA, "WasWork%u", pDrvIns->iInstance); + AssertRCReturn(rc, rc); + + rc = RTThreadUserWait(pThis->hWorkerThread, RT_MS_10SEC); + AssertRC(rc); +#endif + + /* + * Prime the cache. + */ + drvHostAudioWasCacheFill(pThis); + + return VINF_SUCCESS; +} + + +/** + * PDM driver registration for WasAPI. + */ +const PDMDRVREG g_DrvHostAudioWas = +{ + /* u32Version */ + PDM_DRVREG_VERSION, + /* szName */ + "HostAudioWas", + /* szRCMod */ + "", + /* szR0Mod */ + "", + /* pszDescription */ + "Windows Audio Session API (WASAPI) host audio driver", + /* fFlags */ + PDM_DRVREG_FLAGS_HOST_BITS_DEFAULT, + /* fClass. */ + PDM_DRVREG_CLASS_AUDIO, + /* cMaxInstances */ + ~0U, + /* cbInstance */ + sizeof(DRVHOSTAUDIOWAS), + /* pfnConstruct */ + drvHostAudioWasConstruct, + /* pfnDestruct */ + drvHostAudioWasDestruct, + /* pfnRelocate */ + NULL, + /* pfnIOCtl */ + NULL, + /* pfnPowerOn */ + NULL, + /* pfnReset */ + NULL, + /* pfnSuspend */ + NULL, + /* pfnResume */ + NULL, + /* pfnAttach */ + NULL, + /* pfnDetach */ + NULL, + /* pfnPowerOff */ + drvHostAudioWasPowerOff, + /* pfnSoftReset */ + NULL, + /* u32EndVersion */ + PDM_DRVREG_VERSION +}; + diff --git a/src/VBox/Devices/Audio/Makefile.kup b/src/VBox/Devices/Audio/Makefile.kup new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/VBox/Devices/Audio/Makefile.kup diff --git a/src/VBox/Devices/Audio/testcase/Makefile.kmk b/src/VBox/Devices/Audio/testcase/Makefile.kmk new file mode 100644 index 00000000..e5211a54 --- /dev/null +++ b/src/VBox/Devices/Audio/testcase/Makefile.kmk @@ -0,0 +1,83 @@ +# $Id: Makefile.kmk $ +## @file +# Sub-Makefile for the audio testcases. +# + +# +# 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 +# + +SUB_DEPTH = ../../../../.. +include $(KBUILD_PATH)/subheader.kmk + +if defined(VBOX_WITH_TESTCASES) && !defined(VBOX_ONLY_ADDITIONS) && !defined(VBOX_ONLY_SDK) + + PROGRAMS += tstAudioMixBuffer + TESTING += $(tstAudioMixBuffer_0_OUTDIR)/tstAudioMixBuffer.run + + tstAudioMixBuffer_TEMPLATE = VBOXR3TSTEXE + tstAudioMixBuffer_DEFS = TESTCASE VBOX_AUDIO_MIX_BUFFER_TESTCASE + tstAudioMixBuffer_DEFS.debug = VBOX_WITH_EF_WRAPS + tstAudioMixBuffer_SOURCES = \ + tstAudioMixBuffer.cpp \ + ../AudioMixBuffer.cpp \ + ../AudioHlp.cpp + tstAudioMixBuffer_LIBS = $(LIB_RUNTIME) + + $$(tstAudioMixBuffer_0_OUTDIR)/tstAudioMixBuffer.run: $$(tstAudioMixBuffer_1_STAGE_TARGET) + export VBOX_LOG_DEST=nofile; $(tstAudioMixBuffer_1_STAGE_TARGET) quiet + $(QUIET)$(APPEND) -t "$@" "done" + + ifeq ($(KBUILD_TARGET),win.amd64) # Note: Only runs on Windows 8 or newer. + tstAudioClient3_TEMPLATE = VBOXR3TSTEXE + tstAudioClient3_DEFS = TESTCASE + tstAudioClient3_DEFS.debug = VBOX_WITH_EF_WRAPS + tstAudioClient3_SOURCES = tstAudioClient3.cpp + tstAudioClient3_LIBS = $(LIB_RUNTIME) + tstAudioClient3_INCS = \ + $(KBUILD_DEVTOOLS)/win.x86/sdk/v10.0.17134.0/Include/10.0.17134.0/um \ + $(KBUILD_DEVTOOLS)/win.x86/sdk/v10.0.17134.0/include/10.0.17134.0/shared + + # Requires manual execution / verification. + PROGRAMS += tstAudioClient3 + endif + + PROGRAMS += tstAudioTestService + TESTING += $(tstAudioTestService_0_OUTDIR)/tstAudioTestService.run + + tstAudioTestService_TEMPLATE = VBOXR3TSTEXE + tstAudioTestService_DEFS = TESTCASE + tstAudioTestService_DEFS.debug = VBOX_WITH_EF_WRAPS + tstAudioTestService_SOURCES = \ + tstAudioTestService.cpp \ + ../AudioTestService.cpp \ + ../AudioTestServiceProtocol.cpp \ + ../AudioTestServiceTcp.cpp \ + ../AudioTestServiceClient.cpp + tstAudioTestService_LIBS = $(LIB_RUNTIME) + + $$(tstAudioTestService_0_OUTDIR)/tstAudioTestService.run: $$(tstAudioTestService_1_STAGE_TARGET) + export VBOX_LOG_DEST=nofile; $(tstAudioTestService_1_STAGE_TARGET) quiet + $(QUIET)$(APPEND) -t "$@" "done" + +endif + +include $(FILE_KBUILD_SUB_FOOTER) diff --git a/src/VBox/Devices/Audio/testcase/tstAudioClient3.cpp b/src/VBox/Devices/Audio/testcase/tstAudioClient3.cpp new file mode 100644 index 00000000..13cdc7db --- /dev/null +++ b/src/VBox/Devices/Audio/testcase/tstAudioClient3.cpp @@ -0,0 +1,112 @@ +/* $Id: tstAudioClient3.cpp $ */ +/** @file + * Audio testcase - Tests for the IAudioClient3 interface. + */ + +/* + * Copyright (C) 2021-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 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ + +#include <iprt/errcore.h> +#include <iprt/initterm.h> +#include <iprt/mem.h> +#include <iprt/rand.h> +#include <iprt/stream.h> +#include <iprt/string.h> +#include <iprt/test.h> + +#include <iprt/win/windows.h> + +#include <Audioclient.h> +#include <mmdeviceapi.h> + +int main(int argc, char **argv) +{ + RTR3InitExe(argc, &argv, 0); + + /* + * Initialize IPRT and create the test. + */ + RTTEST hTest; + int rc = RTTestInitAndCreate("tstAudioMixBuffer", &hTest); + if (rc) + return rc; + RTTestBanner(hTest); + + /* Note: IAudioClient3 is supported on Win8 or newer. */ + + /** @todo Very crude for now, lacks error checking and such. Later. */ + + HRESULT hr = CoInitialize(NULL); + + IMMDeviceEnumerator* pEnumerator; + hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, + __uuidof(IMMDeviceEnumerator), + reinterpret_cast<void**>(&pEnumerator)); + + IMMDevice* pDevice; + hr = pEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &pDevice); + + IAudioClient3* pAudioClient; + hr = pDevice->Activate(__uuidof(IAudioClient3), CLSCTX_ALL, NULL, reinterpret_cast<void**>(&pAudioClient)); + + WAVEFORMATEX* pFormat; + hr = pAudioClient->GetMixFormat(&pFormat); + + UINT32 defaultPeriodInFrames; + UINT32 fundamentalPeriodInFrames; + UINT32 minPeriodInFrames; + UINT32 maxPeriodInFrames; + hr = pAudioClient->GetSharedModeEnginePeriod(pFormat, + &defaultPeriodInFrames, + &fundamentalPeriodInFrames, + &minPeriodInFrames, + &maxPeriodInFrames); + + RTTestPrintf(hTest, RTTESTLVL_ALWAYS, "def=%RU32, fundamental=%RU32, min=%RU32, max=%RU32\n", + defaultPeriodInFrames, fundamentalPeriodInFrames, minPeriodInFrames, maxPeriodInFrames); + + uint32_t cfDefault = defaultPeriodInFrames * 2; + + RTTestPrintf(hTest, RTTESTLVL_ALWAYS, "Trying to set %RU32 as default ...\n", cfDefault); + + hr = pAudioClient->InitializeSharedAudioStream(0, cfDefault, pFormat, NULL); + if (hr != S_OK) + RTTestPrintf(hTest, RTTESTLVL_ALWAYS, "Unable to set new period"); + else + { + RTTestPrintf(hTest, RTTESTLVL_ALWAYS, "OK"); + + hr = pAudioClient->Start(); + + /** @todo Do some waiting / testing here. */ + } + + /* + * Summary + */ + return RTTestSummaryAndDestroy(hTest); +} diff --git a/src/VBox/Devices/Audio/testcase/tstAudioMixBuffer.cpp b/src/VBox/Devices/Audio/testcase/tstAudioMixBuffer.cpp new file mode 100644 index 00000000..45d263b1 --- /dev/null +++ b/src/VBox/Devices/Audio/testcase/tstAudioMixBuffer.cpp @@ -0,0 +1,916 @@ +/* $Id: tstAudioMixBuffer.cpp $ */ +/** @file + * Audio testcase - Mixing buffer. + */ + +/* + * 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 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/errcore.h> +#include <iprt/initterm.h> +#include <iprt/mem.h> +#include <iprt/rand.h> +#include <iprt/stream.h> +#include <iprt/string.h> +#include <iprt/test.h> + +#include <VBox/vmm/pdm.h> +#include <VBox/vmm/pdmaudioinline.h> + +#include "../AudioMixBuffer.h" +#include "../AudioHlp.h" + +#define _USE_MATH_DEFINES +#include <math.h> /* sin, M_PI */ + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +#ifdef RT_LITTLE_ENDIAN +bool const g_fLittleEndian = true; +#else +bool const g_fLittleEndian = false; +#endif + + +static void tstBasics(RTTEST hTest) +{ + RTTestSub(hTest, "Basics"); + + const PDMAUDIOPCMPROPS Cfg441StereoS16 = PDMAUDIOPCMPROPS_INITIALIZER( + /* a_cb: */ 2, + /* a_fSigned: */ true, + /* a_cChannels: */ 2, + /* a_uHz: */ 44100, + /* a_fSwapEndian: */ false + ); + const PDMAUDIOPCMPROPS Cfg441StereoU16 = PDMAUDIOPCMPROPS_INITIALIZER( + /* a_cb: */ 2, + /* a_fSigned: */ false, + /* a_cChannels: */ 2, + /* a_uHz: */ 44100, + /* a_fSwapEndian: */ false + ); + const PDMAUDIOPCMPROPS Cfg441StereoU32 = PDMAUDIOPCMPROPS_INITIALIZER( + /* a_cb: */ 4, + /* a_fSigned: */ false, + /* a_cChannels: */ 2, + /* a_uHz: */ 44100, + /* a_fSwapEndian: */ false + ); + + RTTESTI_CHECK(PDMAudioPropsGetBitrate(&Cfg441StereoS16) == 44100*4*8); + RTTESTI_CHECK(PDMAudioPropsGetBitrate(&Cfg441StereoU16) == 44100*4*8); + RTTESTI_CHECK(PDMAudioPropsGetBitrate(&Cfg441StereoU32) == 44100*8*8); + + RTTESTI_CHECK(AudioHlpPcmPropsAreValidAndSupported(&Cfg441StereoS16)); + RTTESTI_CHECK(AudioHlpPcmPropsAreValidAndSupported(&Cfg441StereoU16)); + RTTESTI_CHECK(AudioHlpPcmPropsAreValidAndSupported(&Cfg441StereoU32)); + + + RTTESTI_CHECK_MSG(PDMAUDIOPCMPROPS_F2B(&Cfg441StereoS16, 1) == 4, + ("got %x, expected 4\n", PDMAUDIOPCMPROPS_F2B(&Cfg441StereoS16, 1))); + RTTESTI_CHECK_MSG(PDMAUDIOPCMPROPS_F2B(&Cfg441StereoU16, 1) == 4, + ("got %x, expected 4\n", PDMAUDIOPCMPROPS_F2B(&Cfg441StereoU16, 1))); + RTTESTI_CHECK_MSG(PDMAUDIOPCMPROPS_F2B(&Cfg441StereoU32, 1) == 8, + ("got %x, expected 4\n", PDMAUDIOPCMPROPS_F2B(&Cfg441StereoU32, 1))); + + RTTESTI_CHECK_MSG(PDMAudioPropsBytesPerFrame(&Cfg441StereoS16) == 4, + ("got %x, expected 4\n", PDMAudioPropsBytesPerFrame(&Cfg441StereoS16))); + RTTESTI_CHECK_MSG(PDMAudioPropsBytesPerFrame(&Cfg441StereoU16) == 4, + ("got %x, expected 4\n", PDMAudioPropsBytesPerFrame(&Cfg441StereoU16))); + RTTESTI_CHECK_MSG(PDMAudioPropsBytesPerFrame(&Cfg441StereoU32) == 8, + ("got %x, expected 4\n", PDMAudioPropsBytesPerFrame(&Cfg441StereoU32))); + + uint32_t u32; + for (uint32_t i = 0; i < 256; i += 8) + { + RTTESTI_CHECK(PDMAudioPropsIsSizeAligned(&Cfg441StereoU32, i) == true); + for (uint32_t j = 1; j < 8; j++) + RTTESTI_CHECK(PDMAudioPropsIsSizeAligned(&Cfg441StereoU32, i + j) == false); + for (uint32_t j = 0; j < 8; j++) + RTTESTI_CHECK(PDMAudioPropsFloorBytesToFrame(&Cfg441StereoU32, i + j) == i); + } + for (uint32_t i = 0; i < 4096; i += 4) + { + RTTESTI_CHECK(PDMAudioPropsIsSizeAligned(&Cfg441StereoS16, i) == true); + for (uint32_t j = 1; j < 4; j++) + RTTESTI_CHECK(PDMAudioPropsIsSizeAligned(&Cfg441StereoS16, i + j) == false); + for (uint32_t j = 0; j < 4; j++) + RTTESTI_CHECK(PDMAudioPropsFloorBytesToFrame(&Cfg441StereoS16, i + j) == i); + } + + RTTESTI_CHECK_MSG((u32 = PDMAudioPropsFramesToBytes(&Cfg441StereoS16, 44100)) == 44100 * 2 * 2, + ("cb=%RU32\n", u32)); + RTTESTI_CHECK_MSG((u32 = PDMAudioPropsFramesToBytes(&Cfg441StereoS16, 2)) == 2 * 2 * 2, + ("cb=%RU32\n", u32)); + RTTESTI_CHECK_MSG((u32 = PDMAudioPropsFramesToBytes(&Cfg441StereoS16, 1)) == 4, + ("cb=%RU32\n", u32)); + RTTESTI_CHECK_MSG((u32 = PDMAudioPropsFramesToBytes(&Cfg441StereoU16, 1)) == 4, + ("cb=%RU32\n", u32)); + RTTESTI_CHECK_MSG((u32 = PDMAudioPropsFramesToBytes(&Cfg441StereoU32, 1)) == 8, + ("cb=%RU32\n", u32)); + + RTTESTI_CHECK_MSG((u32 = PDMAudioPropsBytesToFrames(&Cfg441StereoS16, 4)) == 1, ("cb=%RU32\n", u32)); + RTTESTI_CHECK_MSG((u32 = PDMAudioPropsBytesToFrames(&Cfg441StereoU16, 4)) == 1, ("cb=%RU32\n", u32)); + RTTESTI_CHECK_MSG((u32 = PDMAudioPropsBytesToFrames(&Cfg441StereoU32, 8)) == 1, ("cb=%RU32\n", u32)); + + uint64_t u64; + RTTESTI_CHECK_MSG((u64 = PDMAudioPropsBytesToNano(&Cfg441StereoS16, 44100 * 2 * 2)) == RT_NS_1SEC, + ("ns=%RU64\n", u64)); + RTTESTI_CHECK_MSG((u64 = PDMAudioPropsBytesToMicro(&Cfg441StereoS16, 44100 * 2 * 2)) == RT_US_1SEC, + ("us=%RU64\n", u64)); + RTTESTI_CHECK_MSG((u64 = PDMAudioPropsBytesToMilli(&Cfg441StereoS16, 44100 * 2 * 2)) == RT_MS_1SEC, + ("ms=%RU64\n", u64)); + + RTTESTI_CHECK_MSG((u64 = PDMAudioPropsFramesToNano(&Cfg441StereoS16, 44100)) == RT_NS_1SEC, ("ns=%RU64\n", u64)); + RTTESTI_CHECK_MSG((u64 = PDMAudioPropsFramesToNano(&Cfg441StereoS16, 1)) == 22675, ("ns=%RU64\n", u64)); + RTTESTI_CHECK_MSG((u64 = PDMAudioPropsFramesToNano(&Cfg441StereoS16, 31)) == 702947, ("ns=%RU64\n", u64)); + RTTESTI_CHECK_MSG((u64 = PDMAudioPropsFramesToNano(&Cfg441StereoS16, 255)) == 5782312, ("ns=%RU64\n", u64)); + //RTTESTI_CHECK_MSG((u64 = DrvAudioHlpFramesToMicro(&Cfg441StereoS16, 44100)) == RT_US_1SEC, + // ("us=%RU64\n", u64)); + RTTESTI_CHECK_MSG((u64 = PDMAudioPropsFramesToMilli(&Cfg441StereoS16, 44100)) == RT_MS_1SEC, ("ms=%RU64\n", u64)); + RTTESTI_CHECK_MSG((u64 = PDMAudioPropsFramesToMilli(&Cfg441StereoS16, 255)) == 5, ("ms=%RU64\n", u64)); + + RTTESTI_CHECK_MSG((u32 = PDMAudioPropsNanoToFrames(&Cfg441StereoS16, RT_NS_1SEC)) == 44100, ("cb=%RU32\n", u32)); + RTTESTI_CHECK_MSG((u32 = PDMAudioPropsNanoToFrames(&Cfg441StereoS16, 215876)) == 10, ("cb=%RU32\n", u32)); + RTTESTI_CHECK_MSG((u32 = PDMAudioPropsMilliToFrames(&Cfg441StereoS16, RT_MS_1SEC)) == 44100, ("cb=%RU32\n", u32)); + RTTESTI_CHECK_MSG((u32 = PDMAudioPropsMilliToFrames(&Cfg441StereoU32, 6)) == 265, ("cb=%RU32\n", u32)); + + RTTESTI_CHECK_MSG((u32 = PDMAudioPropsNanoToBytes(&Cfg441StereoS16, RT_NS_1SEC)) == 44100*2*2, ("cb=%RU32\n", u32)); + RTTESTI_CHECK_MSG((u32 = PDMAudioPropsNanoToBytes(&Cfg441StereoS16, 702947)) == 31*2*2, ("cb=%RU32\n", u32)); + RTTESTI_CHECK_MSG((u32 = PDMAudioPropsMilliToBytes(&Cfg441StereoS16, RT_MS_1SEC)) == 44100*2*2, ("cb=%RU32\n", u32)); + RTTESTI_CHECK_MSG((u32 = PDMAudioPropsMilliToBytes(&Cfg441StereoS16, 5)) == 884, ("cb=%RU32\n", u32)); + + /* DrvAudioHlpClearBuf: */ + uint8_t *pbPage; + int rc = RTTestGuardedAlloc(hTest, HOST_PAGE_SIZE, 0, false /*fHead*/, (void **)&pbPage); + RTTESTI_CHECK_RC_OK_RETV(rc); + + memset(pbPage, 0x42, HOST_PAGE_SIZE); + PDMAudioPropsClearBuffer(&Cfg441StereoS16, pbPage, HOST_PAGE_SIZE, HOST_PAGE_SIZE / 4); + RTTESTI_CHECK(ASMMemIsZero(pbPage, HOST_PAGE_SIZE)); + + memset(pbPage, 0x42, HOST_PAGE_SIZE); + PDMAudioPropsClearBuffer(&Cfg441StereoU16, pbPage, HOST_PAGE_SIZE, HOST_PAGE_SIZE / 4); + for (uint32_t off = 0; off < HOST_PAGE_SIZE; off += 2) + RTTESTI_CHECK_MSG(pbPage[off] == 0 && pbPage[off + 1] == 0x80, ("off=%#x: %#x %x\n", off, pbPage[off], pbPage[off + 1])); + + memset(pbPage, 0x42, HOST_PAGE_SIZE); + PDMAudioPropsClearBuffer(&Cfg441StereoU32, pbPage, HOST_PAGE_SIZE, HOST_PAGE_SIZE / 8); + for (uint32_t off = 0; off < HOST_PAGE_SIZE; off += 4) + RTTESTI_CHECK(pbPage[off] == 0 && pbPage[off + 1] == 0 && pbPage[off + 2] == 0 && pbPage[off + 3] == 0x80); + + + RTTestDisableAssertions(hTest); + memset(pbPage, 0x42, HOST_PAGE_SIZE); + PDMAudioPropsClearBuffer(&Cfg441StereoS16, pbPage, HOST_PAGE_SIZE, HOST_PAGE_SIZE); /* should adjust down the frame count. */ + RTTESTI_CHECK(ASMMemIsZero(pbPage, HOST_PAGE_SIZE)); + + memset(pbPage, 0x42, HOST_PAGE_SIZE); + PDMAudioPropsClearBuffer(&Cfg441StereoU16, pbPage, HOST_PAGE_SIZE, HOST_PAGE_SIZE); /* should adjust down the frame count. */ + for (uint32_t off = 0; off < HOST_PAGE_SIZE; off += 2) + RTTESTI_CHECK_MSG(pbPage[off] == 0 && pbPage[off + 1] == 0x80, ("off=%#x: %#x %x\n", off, pbPage[off], pbPage[off + 1])); + + memset(pbPage, 0x42, HOST_PAGE_SIZE); + PDMAudioPropsClearBuffer(&Cfg441StereoU32, pbPage, HOST_PAGE_SIZE, HOST_PAGE_SIZE); /* should adjust down the frame count. */ + for (uint32_t off = 0; off < HOST_PAGE_SIZE; off += 4) + RTTESTI_CHECK(pbPage[off] == 0 && pbPage[off + 1] == 0 && pbPage[off + 2] == 0 && pbPage[off + 3] == 0x80); + RTTestRestoreAssertions(hTest); + + RTTestGuardedFree(hTest, pbPage); +} + + +static void tstSimple(RTTEST hTest) +{ + RTTestSub(hTest, "Simple"); + + /* 44100Hz, 2 Channels, S16 */ + PDMAUDIOPCMPROPS config = PDMAUDIOPCMPROPS_INITIALIZER( + 2, /* Bytes */ + true, /* Signed */ + 2, /* Channels */ + 44100, /* Hz */ + false /* Swap Endian */ + ); + + RTTESTI_CHECK(AudioHlpPcmPropsAreValidAndSupported(&config)); + + uint32_t cBufSize = _1K; + + /* + * General stuff. + */ + AUDIOMIXBUF mb; + RTTESTI_CHECK_RC_OK_RETV(AudioMixBufInit(&mb, "Single", &config, cBufSize)); + RTTESTI_CHECK(AudioMixBufSize(&mb) == cBufSize); + RTTESTI_CHECK(AUDIOMIXBUF_B2F(&mb, AudioMixBufSizeBytes(&mb)) == cBufSize); + RTTESTI_CHECK(AUDIOMIXBUF_F2B(&mb, AudioMixBufSize(&mb)) == AudioMixBufSizeBytes(&mb)); + RTTESTI_CHECK(AudioMixBufFree(&mb) == cBufSize); + RTTESTI_CHECK(AUDIOMIXBUF_F2B(&mb, AudioMixBufFree(&mb)) == AudioMixBufFreeBytes(&mb)); + + AUDIOMIXBUFWRITESTATE WriteState; + RTTESTI_CHECK_RC(AudioMixBufInitWriteState(&mb, &WriteState, &config), VINF_SUCCESS); + + AUDIOMIXBUFPEEKSTATE PeekState; + RTTESTI_CHECK_RC(AudioMixBufInitPeekState(&mb, &PeekState, &config), VINF_SUCCESS); + + /* + * A few writes (used to be the weird absolute writes). + */ + uint32_t cFramesRead = 0, cFramesWritten = 0, cFramesWrittenAbs = 0; + int16_t aFrames16[2] = { 0xAA, 0xBB }; + int32_t aFrames32[2] = { 0xCC, 0xDD }; + + RTTESTI_CHECK(AudioMixBufUsed(&mb) == 0); + + AudioMixBufWrite(&mb, &WriteState, &aFrames16, sizeof(aFrames16), 0 /*offDstFrame*/, cBufSize / 4, &cFramesWritten); + RTTESTI_CHECK(cFramesWritten == 1 /* Frames */); + RTTESTI_CHECK(AudioMixBufUsed(&mb) == 0); + AudioMixBufCommit(&mb, cFramesWritten); + RTTESTI_CHECK(AudioMixBufUsed(&mb) == 1); + RTTESTI_CHECK(AudioMixBufReadPos(&mb) == 0); + RTTESTI_CHECK(AudioMixBufWritePos(&mb) == 1); + + AudioMixBufWrite(&mb, &WriteState, &aFrames32, sizeof(aFrames32), 0 /*offDstFrame*/, cBufSize / 4, &cFramesWritten); + RTTESTI_CHECK(cFramesWritten == 2 /* Frames */); + AudioMixBufCommit(&mb, cFramesWritten); + RTTESTI_CHECK(AudioMixBufUsed(&mb) == 3); + RTTESTI_CHECK(AudioMixBufReadPos(&mb) == 0); + RTTESTI_CHECK(AudioMixBufWritePos(&mb) == 3); + + /* Pretend we read the frames.*/ + AudioMixBufAdvance(&mb, 3); + RTTESTI_CHECK(AudioMixBufUsed(&mb) == 0); + RTTESTI_CHECK(AudioMixBufReadPos(&mb) == 3); + RTTESTI_CHECK(AudioMixBufWritePos(&mb) == 3); + + /* Fill up the buffer completely and check wraps. */ + + uint32_t cbSamples = PDMAudioPropsFramesToBytes(&config, cBufSize); + uint16_t *paSamples = (uint16_t *)RTMemAlloc(cbSamples); + RTTESTI_CHECK_RETV(paSamples); + AudioMixBufWrite(&mb, &WriteState, paSamples, cbSamples, 0 /*offDstFrame*/, cBufSize, &cFramesWritten); + RTTESTI_CHECK(cFramesWritten == cBufSize); + AudioMixBufCommit(&mb, cFramesWritten); + RTTESTI_CHECK(AudioMixBufUsed(&mb) == cBufSize); + RTTESTI_CHECK(AudioMixBufReadPos(&mb) == 3); + RTTESTI_CHECK(AudioMixBufWritePos(&mb) == 3); + RTMemFree(paSamples); + cbSamples = 0; + + /* + * Writes and reads (used to be circular). + */ + AudioMixBufDrop(&mb); + + cFramesWrittenAbs = AudioMixBufUsed(&mb); + + uint32_t cToWrite = AudioMixBufSize(&mb) - cFramesWrittenAbs - 1; /* -1 as padding plus -2 frames for above. */ + for (uint32_t i = 0; i < cToWrite; i++) + { + AudioMixBufWrite(&mb, &WriteState, &aFrames16[0], sizeof(aFrames16), 0 /*offDstFrame*/, 1, &cFramesWritten); + RTTESTI_CHECK(cFramesWritten == 1); + AudioMixBufCommit(&mb, cFramesWritten); + } + RTTESTI_CHECK(!AudioMixBufIsEmpty(&mb)); + RTTESTI_CHECK(AudioMixBufFree(&mb) == 1); + RTTESTI_CHECK(AudioMixBufFreeBytes(&mb) == AUDIOMIXBUF_F2B(&mb, 1U)); + RTTESTI_CHECK(AudioMixBufUsed(&mb) == cToWrite + cFramesWrittenAbs /* + last absolute write */); + + AudioMixBufWrite(&mb, &WriteState, &aFrames16[0], sizeof(aFrames16), 0 /*offDstFrame*/, 1, &cFramesWritten); + RTTESTI_CHECK(cFramesWritten == 1); + AudioMixBufCommit(&mb, cFramesWritten); + RTTESTI_CHECK(AudioMixBufFree(&mb) == 0); + RTTESTI_CHECK(AudioMixBufFreeBytes(&mb) == AUDIOMIXBUF_F2B(&mb, 0U)); + RTTESTI_CHECK(AudioMixBufUsed(&mb) == cBufSize); + + /* Reads. */ + RTTESTI_CHECK(AudioMixBufReadPos(&mb) == 0); + uint32_t cbRead; + uint16_t aFrames16Buf[RT_ELEMENTS(aFrames16)]; + uint32_t cToRead = AudioMixBufSize(&mb) - cFramesWrittenAbs - 1; + for (uint32_t i = 0; i < cToRead; i++) + { + AudioMixBufPeek(&mb, 0 /*offSrcFrame*/, 1, &cFramesRead, &PeekState, aFrames16Buf, sizeof(aFrames16Buf), &cbRead); + RTTESTI_CHECK(cFramesRead == 1); + RTTESTI_CHECK(cbRead == sizeof(aFrames16Buf)); + AudioMixBufAdvance(&mb, cFramesRead); + RTTESTI_CHECK(AudioMixBufReadPos(&mb) == i + 1); + } + RTTESTI_CHECK(!AudioMixBufIsEmpty(&mb)); + RTTESTI_CHECK(AudioMixBufFree(&mb) == AudioMixBufSize(&mb) - cFramesWrittenAbs - 1); + RTTESTI_CHECK(AudioMixBufFreeBytes(&mb) == AUDIOMIXBUF_F2B(&mb, cBufSize - cFramesWrittenAbs - 1)); + RTTESTI_CHECK(AudioMixBufUsed(&mb) == cBufSize - cToRead); + + AudioMixBufPeek(&mb, 0 /*offSrcFrame*/, 1, &cFramesRead, &PeekState, aFrames16Buf, sizeof(aFrames16Buf), &cbRead); + RTTESTI_CHECK(cFramesRead == 1); + RTTESTI_CHECK(cbRead == sizeof(aFrames16Buf)); + AudioMixBufAdvance(&mb, cFramesRead); + RTTESTI_CHECK(AudioMixBufFree(&mb) == cBufSize - cFramesWrittenAbs); + RTTESTI_CHECK(AudioMixBufFreeBytes(&mb) == AUDIOMIXBUF_F2B(&mb, cBufSize - cFramesWrittenAbs)); + RTTESTI_CHECK(AudioMixBufUsed(&mb) == cFramesWrittenAbs); + RTTESTI_CHECK(AudioMixBufReadPos(&mb) == 0); + + AudioMixBufTerm(&mb); +} + +/** @name Eight test samples represented in all basic formats. + * @{ */ +static uint8_t const g_au8TestSamples[8] = { 0x1, 0x11, 0x32, 0x7f, 0x80, 0x81, 0xbe, 0xff }; +static int8_t const g_ai8TestSamples[8] = { -127, -111, -78, -1, 0, 1, 62, 127 }; +static uint16_t const g_au16TestSamples[8] = { 0x100, 0x1100, 0x3200, 0x7f00, 0x8000, 0x8100, 0xbe00, 0xff00 }; +static int16_t const g_ai16TestSamples[8] = { -32512, -28416, -19968, -256, 0, 256, 15872, 32512 }; +static uint32_t const g_au32TestSamples[8] = { 0x1000000, 0x11000000, 0x32000000, 0x7f000000, 0x80000000, 0x81000000, 0xbe000000, 0xff000000 }; +static int32_t const g_ai32TestSamples[8] = { -2130706432, -1862270976, -1308622848, -16777216, 0, 16777216, 1040187392, 2130706432 }; +static int64_t const g_ai64TestSamples[8] = { -2130706432, -1862270976, -1308622848, -16777216, 0, 16777216, 1040187392, 2130706432 }; +static struct { void const *apv[2]; uint32_t cb; } g_aTestSamples[] = +{ + /* 0/0: */ { { NULL, NULL }, 0 }, + /* 1/8: */ { { g_au8TestSamples, g_ai8TestSamples }, sizeof( g_au8TestSamples) }, + /* 2/16: */ { { g_au16TestSamples, g_ai16TestSamples }, sizeof(g_au16TestSamples) }, + /* 3/24: */ { { NULL, NULL }, 0 }, + /* 4/32: */ { { g_au32TestSamples, g_ai32TestSamples }, sizeof(g_au32TestSamples) }, + /* 5: */ { { NULL, NULL }, 0 }, + /* 6: */ { { NULL, NULL }, 0 }, + /* 7: */ { { NULL, NULL }, 0 }, + /* 8:64 */ { { NULL, g_ai64TestSamples }, sizeof(g_ai64TestSamples) }, /* raw */ +}; +/** @} */ + +/** Fills a buffer with samples from an g_aTestSamples entry. */ +static uint32_t tstFillBuf(PCPDMAUDIOPCMPROPS pCfg, void const *pvTestSamples, uint32_t iTestSample, + uint8_t *pbBuf, uint32_t cFrames) +{ + uint8_t const cTestSamples = RT_ELEMENTS(g_au8TestSamples); + + cFrames *= PDMAudioPropsChannels(pCfg); + switch (PDMAudioPropsSampleSize(pCfg)) + { + case 1: + { + uint8_t const * const pau8TestSamples = (uint8_t const *)pvTestSamples; + uint8_t *pu8Dst = (uint8_t *)pbBuf; + while (cFrames-- > 0) + { + *pu8Dst++ = pau8TestSamples[iTestSample]; + iTestSample = (iTestSample + 1) % cTestSamples; + } + break; + } + + case 2: + { + uint16_t const * const pau16TestSamples = (uint16_t const *)pvTestSamples; + uint16_t *pu16Dst = (uint16_t *)pbBuf; + while (cFrames-- > 0) + { + *pu16Dst++ = pau16TestSamples[iTestSample]; + iTestSample = (iTestSample + 1) % cTestSamples; + } + break; + } + + case 4: + { + uint32_t const * const pau32TestSamples = (uint32_t const *)pvTestSamples; + uint32_t *pu32Dst = (uint32_t *)pbBuf; + while (cFrames-- > 0) + { + *pu32Dst++ = pau32TestSamples[iTestSample]; + iTestSample = (iTestSample + 1) % cTestSamples; + } + break; + } + + case 8: + { + uint64_t const * const pau64TestSamples = (uint64_t const *)pvTestSamples; + uint64_t *pu64Dst = (uint64_t *)pbBuf; + while (cFrames-- > 0) + { + *pu64Dst++ = pau64TestSamples[iTestSample]; + iTestSample = (iTestSample + 1) % cTestSamples; + } + break; + } + + default: + AssertFailedBreak(); + } + return iTestSample; +} + + +static void tstConversion(RTTEST hTest, uint8_t cSrcBits, bool fSrcSigned, uint8_t cSrcChs, + uint8_t cDstBits, bool fDstSigned, uint8_t cDstChs) +{ + RTTestSubF(hTest, "Conv %uch %c%u to %uch %c%u", cSrcChs, fSrcSigned ? 'S' : 'U', cSrcBits, + cDstChs, fDstSigned ? 'S' : 'U', cDstBits); + + PDMAUDIOPCMPROPS CfgSrc, CfgDst; + PDMAudioPropsInitEx(&CfgSrc, cSrcBits / 8, fSrcSigned, cSrcChs, 44100, g_fLittleEndian, cSrcBits == 64 /*fRaw*/); + PDMAudioPropsInitEx(&CfgDst, cDstBits / 8, fDstSigned, cDstChs, 44100, g_fLittleEndian, cDstBits == 64 /*fRaw*/); + + void const * const pvSrcTestSamples = g_aTestSamples[cSrcBits / 8].apv[fSrcSigned]; + void const * const pvDstTestSamples = g_aTestSamples[cDstBits / 8].apv[fDstSigned]; + uint32_t const cMixBufFrames = RTRandU32Ex(128, 16384); + uint32_t const cIterations = RTRandU32Ex(256, 1536); + uint32_t const cbSrcBuf = PDMAudioPropsFramesToBytes(&CfgSrc, cMixBufFrames + 64); + uint8_t * const pbSrcBuf = (uint8_t *)RTMemAllocZ(cbSrcBuf); + uint32_t const cbDstBuf = PDMAudioPropsFramesToBytes(&CfgDst, cMixBufFrames + 64); + uint8_t * const pbDstBuf = (uint8_t *)RTMemAllocZ(cbDstBuf); + uint8_t * const pbDstExpect = (uint8_t *)RTMemAllocZ(cbDstBuf); + RTTESTI_CHECK_RETV(pbSrcBuf); + RTTESTI_CHECK_RETV(pbDstBuf); + RTTESTI_CHECK_RETV(pbDstExpect); + + AUDIOMIXBUF MixBuf; + RTTESTI_CHECK_RC_RETV(AudioMixBufInit(&MixBuf, "FormatOutputConversion", &CfgSrc, cMixBufFrames), VINF_SUCCESS); + AUDIOMIXBUFWRITESTATE WriteState; + RTTESTI_CHECK_RC_RETV(AudioMixBufInitWriteState(&MixBuf, &WriteState, &CfgSrc), VINF_SUCCESS); + AUDIOMIXBUFWRITESTATE WriteStateIgnZero = WriteState; RT_NOREF(WriteStateIgnZero); + AUDIOMIXBUFPEEKSTATE PeekState; + RTTESTI_CHECK_RC_RETV(AudioMixBufInitPeekState(&MixBuf, &PeekState, &CfgDst), VINF_SUCCESS); + + uint32_t iSrcTestSample = 0; + uint32_t iDstTestSample = 0; + //RTTestPrintf(hTest, RTTESTLVL_ALWAYS, "cIterations=%u\n", cIterations); + for (uint32_t iIteration = 0; iIteration < cIterations; iIteration++) + { + /* Write some frames to the buffer. */ + uint32_t const cSrcFramesToWrite = iIteration < 16 ? iIteration + 1 + : AudioMixBufFree(&MixBuf) ? RTRandU32Ex(1, AudioMixBufFree(&MixBuf)) : 0; + if (cSrcFramesToWrite > 0) + { + uint32_t const cbSrcToWrite = PDMAudioPropsFramesToBytes(&CfgSrc, cSrcFramesToWrite); + uint32_t cFrames = RTRandU32(); + switch (RTRandU32Ex(0, 3)) + { + default: + iSrcTestSample = tstFillBuf(&CfgSrc, pvSrcTestSamples, iSrcTestSample, pbSrcBuf, cSrcFramesToWrite); + AudioMixBufWrite(&MixBuf, &WriteState, pbSrcBuf, cbSrcToWrite, 0 /*offDstFrame*/, cSrcFramesToWrite, &cFrames); + RTTESTI_CHECK(cFrames == cSrcFramesToWrite); + break; + + case 1: /* zero & blend */ + AudioMixBufSilence(&MixBuf, &WriteStateIgnZero, 0 /*offFrame*/, cSrcFramesToWrite); + iSrcTestSample = tstFillBuf(&CfgSrc, pvSrcTestSamples, iSrcTestSample, pbSrcBuf, cSrcFramesToWrite); + AudioMixBufBlend(&MixBuf, &WriteState, pbSrcBuf, cbSrcToWrite, 0 /*offDstFrame*/, cSrcFramesToWrite, &cFrames); + RTTESTI_CHECK(cFrames == cSrcFramesToWrite); + break; + + case 2: /* blend same equal data twice */ + { + AUDIOMIXBUFWRITESTATE WriteStateSame = WriteState; + iSrcTestSample = tstFillBuf(&CfgSrc, pvSrcTestSamples, iSrcTestSample, pbSrcBuf, cSrcFramesToWrite); + AudioMixBufWrite(&MixBuf, &WriteState, pbSrcBuf, cbSrcToWrite, 0 /*offDstFrame*/, cSrcFramesToWrite, &cFrames); + RTTESTI_CHECK(cFrames == cSrcFramesToWrite); + AudioMixBufBlend(&MixBuf, &WriteStateSame, pbSrcBuf, cbSrcToWrite, 0 /*offDstFrame*/, cSrcFramesToWrite, &cFrames); + RTTESTI_CHECK(cFrames == cSrcFramesToWrite); + break; + } + case 3: /* write & blend with zero */ + { + AUDIOMIXBUFWRITESTATE WriteStateSame = WriteState; + iSrcTestSample = tstFillBuf(&CfgSrc, pvSrcTestSamples, iSrcTestSample, pbSrcBuf, cSrcFramesToWrite); + AudioMixBufWrite(&MixBuf, &WriteState, pbSrcBuf, cbSrcToWrite, 0 /*offDstFrame*/, cSrcFramesToWrite, &cFrames); + RTTESTI_CHECK(cFrames == cSrcFramesToWrite); + PDMAudioPropsClearBuffer(&CfgSrc, pbSrcBuf, cbSrcToWrite, cSrcFramesToWrite); + AudioMixBufBlend(&MixBuf, &WriteStateSame, pbSrcBuf, cbSrcToWrite, 0 /*offDstFrame*/, cSrcFramesToWrite, &cFrames); + RTTESTI_CHECK(cFrames == cSrcFramesToWrite); + break; + } + } + AudioMixBufCommit(&MixBuf, cSrcFramesToWrite); + } + + /* Read some frames back. */ + uint32_t const cUsed = AudioMixBufUsed(&MixBuf); + uint32_t const cDstFramesToRead = iIteration < 16 ? iIteration + 1 : iIteration + 5 >= cIterations ? cUsed + : cUsed ? RTRandU32Ex(1, cUsed) : 0; + if (cDstFramesToRead > 0) + { + uint32_t const cbDstToRead = PDMAudioPropsFramesToBytes(&CfgDst, cDstFramesToRead); + uint32_t cbRead = RTRandU32(); + uint32_t cFrames = RTRandU32(); + RTRandBytes(pbDstBuf, cbDstToRead); + AudioMixBufPeek(&MixBuf, 0 /*offSrcFrame*/, (iIteration & 3) != 2 ? cDstFramesToRead : cUsed, &cFrames, + &PeekState, pbDstBuf, (iIteration & 3) != 3 ? cbDstToRead : cbDstBuf, &cbRead); + RTTESTI_CHECK(cFrames == cDstFramesToRead); + RTTESTI_CHECK(cbRead == cbDstToRead); + AudioMixBufAdvance(&MixBuf, cFrames); + + /* Verify if we can. */ + if (PDMAudioPropsChannels(&CfgSrc) == PDMAudioPropsChannels(&CfgDst)) + { + iDstTestSample = tstFillBuf(&CfgDst, pvDstTestSamples, iDstTestSample, pbDstExpect, cFrames); + if (memcmp(pbDstExpect, pbDstBuf, cbRead) == 0) + { /* likely */ } + else + { + RTTestFailed(hTest, + "mismatch: %.*Rhxs\n" + "expected: %.*Rhxs\n" + "iIteration=%u cDstFramesToRead=%u cbRead=%#x\n", + RT_MIN(cbRead, 48), pbDstBuf, + RT_MIN(cbRead, 48), pbDstExpect, + iIteration, cDstFramesToRead, cbRead); + break; + } + } + } + } + + AudioMixBufTerm(&MixBuf); + RTMemFree(pbSrcBuf); + RTMemFree(pbDstBuf); + RTMemFree(pbDstExpect); +} + + +#if 0 /** @todo rewrite to non-parent/child setup */ +static void tstDownsampling(RTTEST hTest, uint32_t uFromHz, uint32_t uToHz) +{ + RTTestSubF(hTest, "Downsampling %u to %u Hz (S16)", uFromHz, uToHz); + + struct { int16_t l, r; } + aSrcFrames[4096], + aDstFrames[4096]; + + /* Parent (destination) buffer is xxxHz 2ch S16 */ + uint32_t const cFramesParent = RTRandU32Ex(16, RT_ELEMENTS(aDstFrames)); + PDMAUDIOPCMPROPS const CfgDst = PDMAUDIOPCMPROPS_INITIALIZER(2 /*cbSample*/, true /*fSigned*/, 2 /*ch*/, uToHz, false /*fSwap*/); + RTTESTI_CHECK(AudioHlpPcmPropsAreValid(&CfgDst)); + AUDIOMIXBUF Parent; + RTTESTI_CHECK_RC_OK_RETV(AudioMixBufInit(&Parent, "ParentDownsampling", &CfgDst, cFramesParent)); + + /* Child (source) buffer is yyykHz 2ch S16 */ + PDMAUDIOPCMPROPS const CfgSrc = PDMAUDIOPCMPROPS_INITIALIZER(2 /*cbSample*/, true /*fSigned*/, 2 /*ch*/, uFromHz, false /*fSwap*/); + RTTESTI_CHECK(AudioHlpPcmPropsAreValid(&CfgSrc)); + uint32_t const cFramesChild = RTRandU32Ex(32, RT_ELEMENTS(aSrcFrames)); + AUDIOMIXBUF Child; + RTTESTI_CHECK_RC_OK_RETV(AudioMixBufInit(&Child, "ChildDownsampling", &CfgSrc, cFramesChild)); + RTTESTI_CHECK_RC_OK_RETV(AudioMixBufLinkTo(&Child, &Parent)); + + /* + * Test parameters. + */ + uint32_t const cMaxSrcFrames = RT_MIN(cFramesParent * uFromHz / uToHz - 1, cFramesChild); + uint32_t const cIterations = RTRandU32Ex(4, 128); + RTTestErrContext(hTest, "cFramesParent=%RU32 cFramesChild=%RU32 cMaxSrcFrames=%RU32 cIterations=%RU32", + cFramesParent, cFramesChild, cMaxSrcFrames, cIterations); + RTTestPrintf(hTest, RTTESTLVL_DEBUG, "cFramesParent=%RU32 cFramesChild=%RU32 cMaxSrcFrames=%RU32 cIterations=%RU32\n", + cFramesParent, cFramesChild, cMaxSrcFrames, cIterations); + + /* + * We generate a simple "A" sine wave as input. + */ + uint32_t iSrcFrame = 0; + uint32_t iDstFrame = 0; + double rdFixed = 2.0 * M_PI * 440.0 /* A */ / PDMAudioPropsHz(&CfgSrc); /* Fixed sin() input. */ + for (uint32_t i = 0; i < cIterations; i++) + { + RTTestPrintf(hTest, RTTESTLVL_DEBUG, "i=%RU32\n", i); + + /* + * Generate source frames and write them. + */ + uint32_t const cSrcFrames = i < cIterations / 2 + ? RTRandU32Ex(2, cMaxSrcFrames) & ~(uint32_t)1 + : RTRandU32Ex(1, cMaxSrcFrames - 1) | 1; + for (uint32_t j = 0; j < cSrcFrames; j++, iSrcFrame++) + aSrcFrames[j].r = aSrcFrames[j].l = 32760 /*Amplitude*/ * sin(rdFixed * iSrcFrame); + + uint32_t cSrcFramesWritten = UINT32_MAX / 2; + RTTESTI_CHECK_RC_OK_BREAK(AudioMixBufWriteAt(&Child, 0, &aSrcFrames, cSrcFrames * sizeof(aSrcFrames[0]), + &cSrcFramesWritten)); + RTTESTI_CHECK_MSG_BREAK(cSrcFrames == cSrcFramesWritten, + ("cSrcFrames=%RU32 vs cSrcFramesWritten=%RU32\n", cSrcFrames, cSrcFramesWritten)); + + /* + * Mix them. + */ + uint32_t cSrcFramesMixed = UINT32_MAX / 2; + RTTESTI_CHECK_RC_OK_BREAK(AudioMixBufMixToParent(&Child, cSrcFramesWritten, &cSrcFramesMixed)); + RTTESTI_CHECK_MSG(AudioMixBufUsed(&Child) == 0, ("%RU32\n", AudioMixBufUsed(&Child))); + RTTESTI_CHECK_MSG_BREAK(cSrcFramesWritten == cSrcFramesMixed, + ("cSrcFramesWritten=%RU32 cSrcFramesMixed=%RU32\n", cSrcFramesWritten, cSrcFramesMixed)); + RTTESTI_CHECK_MSG_BREAK(AudioMixBufUsed(&Child) == 0, ("%RU32\n", AudioMixBufUsed(&Child))); + + /* + * Read out the parent buffer. + */ + uint32_t cDstFrames = AudioMixBufUsed(&Parent); + while (cDstFrames > 0) + { + uint32_t cFramesRead = UINT32_MAX / 2; + RTTESTI_CHECK_RC_OK_BREAK(AudioMixBufAcquireReadBlock(&Parent, aDstFrames, sizeof(aDstFrames), &cFramesRead)); + RTTESTI_CHECK_MSG(cFramesRead > 0 && cFramesRead <= cDstFrames, + ("cFramesRead=%RU32 cDstFrames=%RU32\n", cFramesRead, cDstFrames)); + + AudioMixBufReleaseReadBlock(&Parent, cFramesRead); + AudioMixBufFinish(&Parent, cFramesRead); + + iDstFrame += cFramesRead; + cDstFrames -= cFramesRead; + RTTESTI_CHECK(AudioMixBufUsed(&Parent) == cDstFrames); + } + } + + RTTESTI_CHECK(AudioMixBufUsed(&Parent) == 0); + RTTESTI_CHECK(AudioMixBufLive(&Child) == 0); + uint32_t const cDstMinExpect = (uint64_t)iSrcFrame * uToHz / uFromHz; + uint32_t const cDstMaxExpect = ((uint64_t)iSrcFrame * uToHz + uFromHz - 1) / uFromHz; + RTTESTI_CHECK_MSG(iDstFrame == cDstMinExpect || iDstFrame == cDstMaxExpect, + ("iSrcFrame=%#x -> %#x,%#x; iDstFrame=%#x\n", iSrcFrame, cDstMinExpect, cDstMaxExpect, iDstFrame)); + + AudioMixBufDestroy(&Parent); + AudioMixBufDestroy(&Child); +} +#endif + + +static void tstNewPeek(RTTEST hTest, uint32_t uFromHz, uint32_t uToHz) +{ + RTTestSubF(hTest, "New peek %u to %u Hz (S16)", uFromHz, uToHz); + + struct { int16_t l, r; } + aSrcFrames[4096], + aDstFrames[4096]; + + /* Mix buffer is uFromHz 2ch S16 */ + uint32_t const cFrames = RTRandU32Ex(16, RT_ELEMENTS(aSrcFrames)); + PDMAUDIOPCMPROPS const CfgSrc = PDMAUDIOPCMPROPS_INITIALIZER(2 /*cbSample*/, true /*fSigned*/, 2 /*ch*/, uFromHz, false /*fSwap*/); + RTTESTI_CHECK(AudioHlpPcmPropsAreValidAndSupported(&CfgSrc)); + AUDIOMIXBUF MixBuf; + RTTESTI_CHECK_RC_OK_RETV(AudioMixBufInit(&MixBuf, "NewPeekMixBuf", &CfgSrc, cFrames)); + + /* Write state (source). */ + AUDIOMIXBUFWRITESTATE WriteState; + RTTESTI_CHECK_RC_OK_RETV(AudioMixBufInitWriteState(&MixBuf, &WriteState, &CfgSrc)); + + /* Peek state (destination) is uToHz 2ch S16 */ + PDMAUDIOPCMPROPS const CfgDst = PDMAUDIOPCMPROPS_INITIALIZER(2 /*cbSample*/, true /*fSigned*/, 2 /*ch*/, uToHz, false /*fSwap*/); + RTTESTI_CHECK(AudioHlpPcmPropsAreValidAndSupported(&CfgDst)); + AUDIOMIXBUFPEEKSTATE PeekState; + RTTESTI_CHECK_RC_OK_RETV(AudioMixBufInitPeekState(&MixBuf, &PeekState, &CfgDst)); + + /* + * Test parameters. + */ + uint32_t const cMaxSrcFrames = RT_MIN(cFrames * uFromHz / uToHz - 1, cFrames); + uint32_t const cIterations = RTRandU32Ex(64, 1024); + RTTestErrContext(hTest, "cFrames=%RU32 cMaxSrcFrames=%RU32 cIterations=%RU32", cFrames, cMaxSrcFrames, cIterations); + RTTestPrintf(hTest, RTTESTLVL_DEBUG, "cFrames=%RU32 cMaxSrcFrames=%RU32 cIterations=%RU32\n", + cFrames, cMaxSrcFrames, cIterations); + + /* + * We generate a simple "A" sine wave as input. + */ + uint32_t iSrcFrame = 0; + uint32_t iDstFrame = 0; + double rdFixed = 2.0 * M_PI * 440.0 /* A */ / PDMAudioPropsHz(&CfgSrc); /* Fixed sin() input. */ + for (uint32_t i = 0; i < cIterations; i++) + { + RTTestPrintf(hTest, RTTESTLVL_DEBUG, "i=%RU32\n", i); + + /* + * Generate source frames and write them. + */ + uint32_t const cSrcFrames = i < cIterations / 2 + ? RTRandU32Ex(2, cMaxSrcFrames) & ~(uint32_t)1 + : RTRandU32Ex(1, cMaxSrcFrames - 1) | 1; + for (uint32_t j = 0; j < cSrcFrames; j++, iSrcFrame++) + aSrcFrames[j].r = aSrcFrames[j].l = 32760 /*Amplitude*/ * sin(rdFixed * iSrcFrame); + + uint32_t cSrcFramesWritten = UINT32_MAX / 2; + AudioMixBufWrite(&MixBuf, &WriteState, &aSrcFrames[0], cSrcFrames * sizeof(aSrcFrames[0]), + 0 /*offDstFrame*/, cSrcFrames, &cSrcFramesWritten); + RTTESTI_CHECK_MSG_BREAK(cSrcFrames == cSrcFramesWritten, + ("cSrcFrames=%RU32 vs cSrcFramesWritten=%RU32 cLiveFrames=%RU32\n", + cSrcFrames, cSrcFramesWritten, AudioMixBufUsed(&MixBuf))); + AudioMixBufCommit(&MixBuf, cSrcFrames); + + /* + * Read out all the frames using the peek function. + */ + uint32_t offSrcFrame = 0; + while (offSrcFrame < cSrcFramesWritten) + { + uint32_t cSrcFramesToRead = cSrcFramesWritten - offSrcFrame; + uint32_t cTmp = (uint64_t)cSrcFramesToRead * uToHz / uFromHz; + if (cTmp + 32 >= RT_ELEMENTS(aDstFrames)) + cSrcFramesToRead = ((uint64_t)RT_ELEMENTS(aDstFrames) - 32) * uFromHz / uToHz; /* kludge */ + + uint32_t cSrcFramesPeeked = UINT32_MAX / 4; + uint32_t cbDstPeeked = UINT32_MAX / 2; + RTRandBytes(aDstFrames, sizeof(aDstFrames)); + AudioMixBufPeek(&MixBuf, offSrcFrame, cSrcFramesToRead, &cSrcFramesPeeked, + &PeekState, aDstFrames, sizeof(aDstFrames), &cbDstPeeked); + uint32_t cDstFramesPeeked = PDMAudioPropsBytesToFrames(&CfgDst, cbDstPeeked); + RTTESTI_CHECK(cbDstPeeked > 0 || cSrcFramesPeeked > 0); + + if (uFromHz == uToHz) + { + for (uint32_t iDst = 0; iDst < cDstFramesPeeked; iDst++) + if (memcmp(&aDstFrames[iDst], &aSrcFrames[offSrcFrame + iDst], sizeof(aSrcFrames[0])) != 0) + RTTestFailed(hTest, "Frame #%u differs: %#x / %#x, expected %#x / %#x\n", iDstFrame + iDst, + aDstFrames[iDst].l, aDstFrames[iDst].r, + aSrcFrames[iDst + offSrcFrame].l, aSrcFrames[iDst + offSrcFrame].r); + } + + offSrcFrame += cSrcFramesPeeked; + iDstFrame += cDstFramesPeeked; + } + + /* + * Then advance. + */ + AudioMixBufAdvance(&MixBuf, cSrcFrames); + RTTESTI_CHECK(AudioMixBufUsed(&MixBuf) == 0); + } + + /** @todo this is a bit lax... */ + uint32_t const cDstMinExpect = ((uint64_t)iSrcFrame * uToHz - uFromHz - 1) / uFromHz; + uint32_t const cDstMaxExpect = ((uint64_t)iSrcFrame * uToHz + uFromHz - 1) / uFromHz; + RTTESTI_CHECK_MSG(iDstFrame >= cDstMinExpect && iDstFrame <= cDstMaxExpect, + ("iSrcFrame=%#x -> %#x..%#x; iDstFrame=%#x (delta %d)\n", + iSrcFrame, cDstMinExpect, cDstMaxExpect, iDstFrame, (cDstMinExpect + cDstMaxExpect) / 2 - iDstFrame)); + + AudioMixBufTerm(&MixBuf); +} + +/* Test volume control. */ +static void tstVolume(RTTEST hTest) +{ + RTTestSub(hTest, "Volume control (44.1kHz S16 2ch)"); + uint32_t const cBufSize = 256; + + /* + * Configure a mixbuf where we read and write 44.1kHz S16 2ch. + */ + PDMAUDIOPCMPROPS const Cfg = PDMAUDIOPCMPROPS_INITIALIZER( + 2, /* Bytes */ + true, /* Signed */ + 2, /* Channels */ + 44100, /* Hz */ + false /* Swap Endian */ + ); + AUDIOMIXBUF MixBuf; + RTTESTI_CHECK_RC_RETV(AudioMixBufInit(&MixBuf, "Volume", &Cfg, cBufSize), VINF_SUCCESS); + + AUDIOMIXBUFWRITESTATE WriteState; + RTTESTI_CHECK_RC_RETV(AudioMixBufInitWriteState(&MixBuf, &WriteState, &Cfg), VINF_SUCCESS); + + AUDIOMIXBUFPEEKSTATE PeekState; + RTTESTI_CHECK_RC_RETV(AudioMixBufInitPeekState(&MixBuf, &PeekState, &Cfg), VINF_SUCCESS); + + /* + * A few 16-bit signed test samples. + */ + static int16_t const s_aFrames16S[16] = + { + INT16_MIN, INT16_MIN + 1, -128, -64, -4, -1, 0, 1, + 2, 255, 256, INT16_MAX / 2, INT16_MAX - 2, INT16_MAX - 1, INT16_MAX, 0, + }; + + /* + * 1) Full volume/0dB attenuation (255). + */ + PDMAUDIOVOLUME Vol = PDMAUDIOVOLUME_INITIALIZER_MAX; + AudioMixBufSetVolume(&MixBuf, &Vol); + + /* Write all the test frames to the mixer buffer: */ + uint32_t cFramesWritten; + AudioMixBufWrite(&MixBuf, &WriteState, &s_aFrames16S[0], sizeof(s_aFrames16S), 0 /*offDstFrame*/, cBufSize, &cFramesWritten); + RTTESTI_CHECK(cFramesWritten == RT_ELEMENTS(s_aFrames16S) / 2); + AudioMixBufCommit(&MixBuf, cFramesWritten); + + /* Read them back. We should get them back just like we wrote them. */ + uint16_t au16Buf[cBufSize * 2]; + uint32_t cFramesPeeked; + uint32_t cbPeeked; + AudioMixBufPeek(&MixBuf, 0 /*offSrcFrame*/, cFramesWritten, &cFramesPeeked, &PeekState, au16Buf, sizeof(au16Buf), &cbPeeked); + RTTESTI_CHECK(cFramesPeeked == cFramesWritten); + RTTESTI_CHECK(cbPeeked == PDMAudioPropsFramesToBytes(&Cfg, cFramesPeeked)); + AudioMixBufAdvance(&MixBuf, cFramesPeeked); + + /* Check that at 0dB the frames came out unharmed. */ + if (memcmp(au16Buf, s_aFrames16S, sizeof(s_aFrames16S)) != 0) + RTTestFailed(hTest, + "0dB test failed\n" + "mismatch: %.*Rhxs\n" + "expected: %.*Rhxs\n", + sizeof(s_aFrames16S), au16Buf, sizeof(s_aFrames16S), s_aFrames16S); + + /* + * 2) Half volume/-6dB attenuation (16 steps down). + */ + PDMAudioVolumeInitFromStereo(&Vol, false, 255 - 16, 255 - 16); + AudioMixBufSetVolume(&MixBuf, &Vol); + + /* Write all the test frames to the mixer buffer: */ + AudioMixBufWrite(&MixBuf, &WriteState, &s_aFrames16S[0], sizeof(s_aFrames16S), 0 /*offDstFrame*/, cBufSize, &cFramesWritten); + RTTESTI_CHECK(cFramesWritten == RT_ELEMENTS(s_aFrames16S) / 2); + AudioMixBufCommit(&MixBuf, cFramesWritten); + + /* Read them back. We should get them back just like we wrote them. */ + AudioMixBufPeek(&MixBuf, 0 /*offSrcFrame*/, cFramesWritten, &cFramesPeeked, &PeekState, au16Buf, sizeof(au16Buf), &cbPeeked); + RTTESTI_CHECK(cFramesPeeked == cFramesWritten); + RTTESTI_CHECK(cbPeeked == PDMAudioPropsFramesToBytes(&Cfg, cFramesPeeked)); + AudioMixBufAdvance(&MixBuf, cFramesPeeked); + + /* Check that at -6dB the sample values are halved. */ + int16_t ai16Expect[sizeof(s_aFrames16S) / 2]; + memcpy(ai16Expect, s_aFrames16S, sizeof(ai16Expect)); + for (uintptr_t i = 0; i < RT_ELEMENTS(ai16Expect); i++) + ai16Expect[i] >>= 1; /* /= 2 - not the same for signed numbers; */ + if (memcmp(au16Buf, ai16Expect, sizeof(ai16Expect)) != 0) + RTTestFailed(hTest, + "-6dB test failed\n" + "mismatch: %.*Rhxs\n" + "expected: %.*Rhxs\n" + "wrote: %.*Rhxs\n", + sizeof(ai16Expect), au16Buf, sizeof(ai16Expect), ai16Expect, sizeof(s_aFrames16S), s_aFrames16S); + + AudioMixBufTerm(&MixBuf); +} + + +int main(int argc, char **argv) +{ + RTR3InitExe(argc, &argv, 0); + + /* + * Initialize IPRT and create the test. + */ + RTTEST hTest; + int rc = RTTestInitAndCreate("tstAudioMixBuffer", &hTest); + if (rc) + return rc; + RTTestBanner(hTest); + + tstBasics(hTest); + tstSimple(hTest); + + /* Run tstConversion for all combinations we have test data. */ + for (unsigned iSrc = 0; iSrc < RT_ELEMENTS(g_aTestSamples); iSrc++) + { + for (unsigned iSrcSigned = 0; iSrcSigned < RT_ELEMENTS(g_aTestSamples[0].apv); iSrcSigned++) + if (g_aTestSamples[iSrc].apv[iSrcSigned]) + for (unsigned cSrcChs = 1; cSrcChs <= 2; cSrcChs++) + for (unsigned iDst = 0; iDst < RT_ELEMENTS(g_aTestSamples); iDst++) + for (unsigned iDstSigned = 0; iDstSigned < RT_ELEMENTS(g_aTestSamples[0].apv); iDstSigned++) + if (g_aTestSamples[iDst].apv[iDstSigned]) + for (unsigned cDstChs = 1; cDstChs <= 2; cDstChs++) + tstConversion(hTest, iSrc * 8, iSrcSigned == 1, cSrcChs, + /*->*/ iDst * 8, iDstSigned == 1, cDstChs); + } + +#if 0 /** @todo rewrite to non-parent/child setup */ + tstDownsampling(hTest, 44100, 22050); + tstDownsampling(hTest, 48000, 44100); + tstDownsampling(hTest, 48000, 22050); + tstDownsampling(hTest, 48000, 11000); +#endif + tstNewPeek(hTest, 48000, 48000); + tstNewPeek(hTest, 48000, 11000); + tstNewPeek(hTest, 48000, 44100); + tstNewPeek(hTest, 44100, 22050); + tstNewPeek(hTest, 44100, 11000); + //tstNewPeek(hTest, 11000, 48000); + //tstNewPeek(hTest, 22050, 44100); + + tstVolume(hTest); + + /* + * Summary + */ + return RTTestSummaryAndDestroy(hTest); +} diff --git a/src/VBox/Devices/Audio/testcase/tstAudioTestService.cpp b/src/VBox/Devices/Audio/testcase/tstAudioTestService.cpp new file mode 100644 index 00000000..c459360c --- /dev/null +++ b/src/VBox/Devices/Audio/testcase/tstAudioTestService.cpp @@ -0,0 +1,178 @@ +/* $Id: tstAudioTestService.cpp $ */ +/** @file + * Audio testcase - Tests for the Audio Test Service (ATS). + */ + +/* + * Copyright (C) 2021-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 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ + +#include <iprt/errcore.h> +#include <iprt/file.h> +#include <iprt/initterm.h> +#include <iprt/mem.h> +#include <iprt/rand.h> +#include <iprt/stream.h> +#include <iprt/string.h> +#include <iprt/test.h> + +#include "../AudioTestService.h" +#include "../AudioTestServiceClient.h" + + +static size_t g_cToRead = _1M; +static size_t g_cbRead = 0; + + +/** @copydoc ATSCALLBACKS::pfnTestSetSendRead */ +static DECLCALLBACK(int) tstTestSetSendReadCallback(void const *pvUser, + const char *pszTag, void *pvBuf, size_t cbBuf, size_t *pcbRead) +{ + RT_NOREF(pvUser, pszTag); + + size_t cbToRead = RT_MIN(g_cToRead - g_cbRead, cbBuf); + if (cbToRead) + { + memset(pvBuf, 0x42, cbToRead); + g_cbRead += cbToRead; + } + + *pcbRead = cbToRead; + + return VINF_SUCCESS; +} + +int main(int argc, char **argv) +{ + RTR3InitExe(argc, &argv, 0); + + /* + * Initialize IPRT and create the test. + */ + RTTEST hTest; + int rc = RTTestInitAndCreate("tstAudioTestService", &hTest); + if (rc) + return rc; + RTTestBanner(hTest); + + ATSCALLBACKS Callbacks; + RT_ZERO(Callbacks); + Callbacks.pfnTestSetSendRead = tstTestSetSendReadCallback; + + ATSSERVER Srv; + rc = AudioTestSvcInit(&Srv, &Callbacks); + RTTEST_CHECK_RC_OK(hTest, rc); + + RTGETOPTUNION Val; + RT_ZERO(Val); + + Val.u32 = ATSCONNMODE_SERVER; + rc = AudioTestSvcHandleOption(&Srv, ATSTCPOPT_CONN_MODE, &Val); + RTTEST_CHECK_RC_OK(hTest, rc); + + ATSCLIENT Client; + + if (RT_SUCCESS(rc)) + { + uint16_t uPort = ATS_TCP_DEF_BIND_PORT_HOST; + + for (unsigned i = 0; i < 64; i++) + { + Val.u16 = uPort; + rc = AudioTestSvcHandleOption(&Srv, ATSTCPOPT_BIND_PORT, &Val); + RTTEST_CHECK_RC_OK(hTest, rc); + + rc = AudioTestSvcStart(&Srv); + if (RT_SUCCESS(rc)) + break; + + RTTestPrintf(hTest, RTTESTLVL_ALWAYS, "Port %RU32 already used\n", uPort); + + /* Use a different port base in case VBox already is running + * with the same service using ATS_TCP_DEF_BIND_PORT_HOST. */ + uPort = ATS_TCP_DEF_BIND_PORT_HOST + RTRandU32Ex(0, 4242); + } + + if (RT_SUCCESS(rc)) + { + RTTestPrintf(hTest, RTTESTLVL_ALWAYS, "Using port %RU32\n", uPort); + + rc = AudioTestSvcClientCreate(&Client); + RTTEST_CHECK_RC_OK(hTest, rc); + + Val.u32 = ATSCONNMODE_CLIENT; + rc = AudioTestSvcClientHandleOption(&Client, ATSTCPOPT_CONN_MODE, &Val); + RTTEST_CHECK_RC_OK(hTest, rc); + + Val.psz = ATS_TCP_DEF_CONNECT_HOST_ADDR_STR; + rc = AudioTestSvcClientHandleOption(&Client, ATSTCPOPT_CONNECT_ADDRESS, &Val); + RTTEST_CHECK_RC_OK(hTest, rc); + + Val.u16 = uPort; + rc = AudioTestSvcClientHandleOption(&Client, ATSTCPOPT_CONNECT_PORT, &Val); + RTTEST_CHECK_RC_OK(hTest, rc); + + rc = AudioTestSvcClientConnect(&Client); + RTTEST_CHECK_RC_OK(hTest, rc); + } + } + + if (RT_SUCCESS(rc)) + { + char szTemp[RTPATH_MAX]; + rc = RTPathTemp(szTemp, sizeof(szTemp)); + RTTEST_CHECK_RC_OK(hTest, rc); + if (RT_SUCCESS(rc)) + { + char szName[RTPATH_MAX]; + RTTESTI_CHECK_RC(rc = RTStrCopy(szName, sizeof(szName), szTemp), VINF_SUCCESS); + RTTESTI_CHECK_RC(rc = RTPathAppend(szName, sizeof(szName), "tstAudioTestService-XXXXX"), VINF_SUCCESS); + if (RT_SUCCESS(rc)) + { + rc = AudioTestSvcClientTestSetDownload(&Client, "ignored", szName); + RTTEST_CHECK_RC_OK(hTest, rc); + + /* ignore rc */ RTFileDelete(szName); + } + } + } + + rc = AudioTestSvcClientDisconnect(&Client); + RTTEST_CHECK_RC_OK(hTest, rc); + + AudioTestSvcClientDestroy(&Client); + + rc = AudioTestSvcStop(&Srv); + RTTEST_CHECK_RC_OK(hTest, rc); + + rc = AudioTestSvcDestroy(&Srv); + RTTEST_CHECK_RC_OK(hTest, rc); + + /* + * Summary + */ + return RTTestSummaryAndDestroy(hTest); +} |