diff options
Diffstat (limited to 'src/VBox/Devices/Audio/AudioTest.cpp')
-rw-r--r-- | src/VBox/Devices/Audio/AudioTest.cpp | 3581 |
1 files changed, 3581 insertions, 0 deletions
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); +} + |