/* $Id: vkat.cpp $ */
/** @file
 * Validation Kit Audio Test (VKAT) utility for testing and validating the audio stack.
 */

/*
 * Copyright (C) 2021-2023 Oracle and/or its affiliates.
 *
 * This file is part of VirtualBox base platform packages, as
 * available from https://www.virtualbox.org.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation, in version 3 of the
 * License.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <https://www.gnu.org/licenses>.
 *
 * The contents of this file may alternatively be used under the terms
 * of the Common Development and Distribution License Version 1.0
 * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
 * in the VirtualBox distribution, in which case the provisions of the
 * CDDL are applicable instead of those of the GPL.
 *
 * You may elect to license modified versions of this file under the
 * terms and conditions of either the GPL or the CDDL or both.
 *
 * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
 */


/*********************************************************************************************************************************
*   Header Files                                                                                                                 *
*********************************************************************************************************************************/
#define LOG_GROUP LOG_GROUP_AUDIO_TEST

#include <iprt/buildconfig.h>
#include <iprt/ctype.h>
#include <iprt/dir.h>
#include <iprt/errcore.h>
#include <iprt/file.h>
#include <iprt/initterm.h>
#include <iprt/getopt.h>
#include <iprt/message.h>
#include <iprt/path.h>
#include <iprt/process.h>
#include <iprt/rand.h>
#include <iprt/stream.h>
#include <iprt/string.h>
#include <iprt/test.h>

#include <package-generated.h>
#include "product-generated.h"

#include <VBox/version.h>
#include <VBox/log.h>

#ifdef RT_OS_WINDOWS
# include <iprt/win/windows.h> /* for CoInitializeEx and SetConsoleCtrlHandler */
#else
# include <signal.h>
#endif

#include "vkatInternal.h"


/*********************************************************************************************************************************
*   Internal Functions                                                                                                           *
*********************************************************************************************************************************/
static int audioVerifyOne(const char *pszPathSetA, const char *pszPathSetB, PAUDIOTESTVERIFYOPTS pOpts);


/*********************************************************************************************************************************
*   Global Variables                                                                                                             *
*********************************************************************************************************************************/
/**
 * Backends description table.
 *
 * @note The first backend in the array is the default one for the platform.
 */
AUDIOTESTBACKENDDESC const g_aBackends[] =
{
#ifdef VBOX_WITH_AUDIO_PULSE
    {   &g_DrvHostPulseAudio,         "pulseaudio" },
    {   &g_DrvHostPulseAudio,         "pulse" },
    {   &g_DrvHostPulseAudio,         "pa" },
#endif
/*
 * Note: ALSA has to come second so that PulseAudio above always is the default on Linux-y OSes
 *       -- most distros are using an ALSA plugin for PulseAudio nowadays.
 *       However, some of these configurations do not seem to work by default (can't create audio streams).
 *
 *       If PulseAudio is not available, the (optional) probing ("--probe-backends") will choose the "pure" ALSA stack instead then.
 */
#if defined(VBOX_WITH_AUDIO_ALSA) && defined(RT_OS_LINUX)
    {   &g_DrvHostALSAAudio,          "alsa" },
#endif
#ifdef VBOX_WITH_AUDIO_OSS
    {   &g_DrvHostOSSAudio,           "oss" },
#endif
#if defined(RT_OS_DARWIN)
    {   &g_DrvHostCoreAudio,          "coreaudio" },
    {   &g_DrvHostCoreAudio,          "core" },
    {   &g_DrvHostCoreAudio,          "ca" },
#endif
#if defined(RT_OS_WINDOWS)
    {   &g_DrvHostAudioWas,           "wasapi" },
    {   &g_DrvHostAudioWas,           "was" },
    {   &g_DrvHostDSound,             "directsound" },
    {   &g_DrvHostDSound,             "dsound" },
    {   &g_DrvHostDSound,             "ds" },
#endif
#ifdef VBOX_WITH_AUDIO_DEBUG
    {   &g_DrvHostDebugAudio,         "debug" },
#endif
    {   &g_DrvHostValidationKitAudio, "valkit" }
};
AssertCompile(sizeof(g_aBackends) > 0 /* port me */);
/** Number of backends defined. */
unsigned g_cBackends = RT_ELEMENTS(g_aBackends);

/**
 * Long option values for the 'test' command.
 */
enum
{
    VKAT_TEST_OPT_COUNT = 900,
    VKAT_TEST_OPT_DEV,
    VKAT_TEST_OPT_GUEST_ATS_ADDR,
    VKAT_TEST_OPT_GUEST_ATS_PORT,
    VKAT_TEST_OPT_HOST_ATS_ADDR,
    VKAT_TEST_OPT_HOST_ATS_PORT,
    VKAT_TEST_OPT_MODE,
    VKAT_TEST_OPT_NO_AUDIO_OK,
    VKAT_TEST_OPT_NO_VERIFY,
    VKAT_TEST_OPT_OUTDIR,
    VKAT_TEST_OPT_PAUSE,
    VKAT_TEST_OPT_PCM_HZ,
    VKAT_TEST_OPT_PCM_BIT,
    VKAT_TEST_OPT_PCM_CHAN,
    VKAT_TEST_OPT_PCM_SIGNED,
    VKAT_TEST_OPT_PROBE_BACKENDS,
    VKAT_TEST_OPT_TAG,
    VKAT_TEST_OPT_TEMPDIR,
    VKAT_TEST_OPT_VOL,
    VKAT_TEST_OPT_TCP_BIND_ADDRESS,
    VKAT_TEST_OPT_TCP_BIND_PORT,
    VKAT_TEST_OPT_TCP_CONNECT_ADDRESS,
    VKAT_TEST_OPT_TCP_CONNECT_PORT,
    VKAT_TEST_OPT_TONE_DURATION_MS,
    VKAT_TEST_OPT_TONE_VOL_PERCENT
};

/**
 * Long option values for the 'verify' command.
 */
enum
{
    VKAT_VERIFY_OPT_MAX_DIFF_COUNT = 900,
    VKAT_VERIFY_OPT_MAX_DIFF_PERCENT,
    VKAT_VERIFY_OPT_MAX_SIZE_PERCENT,
    VKAT_VERIFY_OPT_NORMALIZE
};

/**
 * Common command line parameters.
 */
static const RTGETOPTDEF g_aCmdCommonOptions[] =
{
    { "--quiet",            'q',                                        RTGETOPT_REQ_NOTHING },
    { "--verbose",          'v',                                        RTGETOPT_REQ_NOTHING },
    { "--daemonize",        AUDIO_TEST_OPT_CMN_DAEMONIZE,               RTGETOPT_REQ_NOTHING },
    { "--daemonized",       AUDIO_TEST_OPT_CMN_DAEMONIZED,              RTGETOPT_REQ_NOTHING },
    { "--debug-audio",      AUDIO_TEST_OPT_CMN_DEBUG_AUDIO_ENABLE,      RTGETOPT_REQ_NOTHING },
    { "--debug-audio-path", AUDIO_TEST_OPT_CMN_DEBUG_AUDIO_PATH,        RTGETOPT_REQ_STRING  },
};

/**
 * Command line parameters for test mode.
 */
static const RTGETOPTDEF g_aCmdTestOptions[] =
{
    { "--backend",           'b',                               RTGETOPT_REQ_STRING  },
    { "--drvaudio",          'd',                               RTGETOPT_REQ_NOTHING },
    { "--exclude",           'e',                               RTGETOPT_REQ_UINT32  },
    { "--exclude-all",       'a',                               RTGETOPT_REQ_NOTHING },
    { "--guest-ats-addr",    VKAT_TEST_OPT_GUEST_ATS_ADDR,      RTGETOPT_REQ_STRING  },
    { "--guest-ats-port",    VKAT_TEST_OPT_GUEST_ATS_PORT,      RTGETOPT_REQ_UINT32  },
    { "--host-ats-address",  VKAT_TEST_OPT_HOST_ATS_ADDR,       RTGETOPT_REQ_STRING  },
    { "--host-ats-port",     VKAT_TEST_OPT_HOST_ATS_PORT,       RTGETOPT_REQ_UINT32  },
    { "--include",           'i',                               RTGETOPT_REQ_UINT32  },
    { "--outdir",            VKAT_TEST_OPT_OUTDIR,              RTGETOPT_REQ_STRING  },
    { "--count",             VKAT_TEST_OPT_COUNT,               RTGETOPT_REQ_UINT32  },
    { "--device",            VKAT_TEST_OPT_DEV,                 RTGETOPT_REQ_STRING  },
    { "--pause",             VKAT_TEST_OPT_PAUSE,               RTGETOPT_REQ_UINT32  },
    { "--pcm-bit",           VKAT_TEST_OPT_PCM_BIT,             RTGETOPT_REQ_UINT8   },
    { "--pcm-chan",          VKAT_TEST_OPT_PCM_CHAN,            RTGETOPT_REQ_UINT8   },
    { "--pcm-hz",            VKAT_TEST_OPT_PCM_HZ,              RTGETOPT_REQ_UINT16  },
    { "--pcm-signed",        VKAT_TEST_OPT_PCM_SIGNED,          RTGETOPT_REQ_BOOL    },
    { "--probe-backends",    VKAT_TEST_OPT_PROBE_BACKENDS,      RTGETOPT_REQ_NOTHING },
    { "--mode",              VKAT_TEST_OPT_MODE,                RTGETOPT_REQ_STRING  },
    { "--no-audio-ok",       VKAT_TEST_OPT_NO_AUDIO_OK,         RTGETOPT_REQ_NOTHING },
    { "--no-verify",         VKAT_TEST_OPT_NO_VERIFY,           RTGETOPT_REQ_NOTHING },
    { "--tag",               VKAT_TEST_OPT_TAG,                 RTGETOPT_REQ_STRING  },
    { "--tempdir",           VKAT_TEST_OPT_TEMPDIR,             RTGETOPT_REQ_STRING  },
    { "--vol",               VKAT_TEST_OPT_VOL,                 RTGETOPT_REQ_UINT8   },
    { "--tcp-bind-addr",     VKAT_TEST_OPT_TCP_BIND_ADDRESS,    RTGETOPT_REQ_STRING  },
    { "--tcp-bind-port",     VKAT_TEST_OPT_TCP_BIND_PORT,       RTGETOPT_REQ_UINT16  },
    { "--tcp-connect-addr",  VKAT_TEST_OPT_TCP_CONNECT_ADDRESS, RTGETOPT_REQ_STRING  },
    { "--tcp-connect-port",  VKAT_TEST_OPT_TCP_CONNECT_PORT,    RTGETOPT_REQ_UINT16  },
    { "--tone-duration",     VKAT_TEST_OPT_TONE_DURATION_MS,    RTGETOPT_REQ_UINT32  },
    { "--tone-vol",          VKAT_TEST_OPT_TONE_VOL_PERCENT,    RTGETOPT_REQ_UINT8   }
};

/**
 * Command line parameters for verification mode.
 */
static const RTGETOPTDEF g_aCmdVerifyOptions[] =
{
    { "--max-diff-count",      VKAT_VERIFY_OPT_MAX_DIFF_COUNT,     RTGETOPT_REQ_UINT32 },
    { "--max-diff-percent",    VKAT_VERIFY_OPT_MAX_DIFF_PERCENT,   RTGETOPT_REQ_UINT8  },
    { "--max-size-percent",    VKAT_VERIFY_OPT_MAX_SIZE_PERCENT,   RTGETOPT_REQ_UINT8  },
    { "--normalize",           VKAT_VERIFY_OPT_NORMALIZE,          RTGETOPT_REQ_BOOL   }
};

/** Terminate ASAP if set.  Set on Ctrl-C. */
bool volatile    g_fTerminate = false;
/** The release logger. */
PRTLOGGER        g_pRelLogger = NULL;
/** The test handle. */
RTTEST           g_hTest;
/** The current verbosity level. */
unsigned         g_uVerbosity = 0;
/** DrvAudio: Enable debug (or not). */
bool             g_fDrvAudioDebug = false;
/** DrvAudio: The debug output path. */
const char      *g_pszDrvAudioDebug = NULL;


/**
 * Get default backend.
 */
PCPDMDRVREG AudioTestGetDefaultBackend(void)
{
    return g_aBackends[0].pDrvReg;
}


/**
 * Helper for handling --backend options.
 *
 * @returns Pointer to the specified backend, NULL if not found (error
 *          displayed).
 * @param   pszBackend      The backend option value.
 */
PCPDMDRVREG AudioTestFindBackendOpt(const char *pszBackend)
{
    for (uintptr_t i = 0; i < RT_ELEMENTS(g_aBackends); i++)
        if (   strcmp(pszBackend, g_aBackends[i].pszName) == 0
            || strcmp(pszBackend, g_aBackends[i].pDrvReg->szName) == 0)
            return g_aBackends[i].pDrvReg;
    RTMsgError("Unknown backend: '%s'\n\n", pszBackend);
    RTPrintf("Supported backend values are: ");
    for (uintptr_t i = 0; i < RT_ELEMENTS(g_aBackends); i++)
    {
        if (i > 0)
            RTPrintf(", ");
        RTPrintf(g_aBackends[i].pszName);
    }
    RTPrintf("\n");
    return NULL;
}


/*********************************************************************************************************************************
*   Test callbacks                                                                                                               *
*********************************************************************************************************************************/

/**
 * @copydoc FNAUDIOTESTSETUP
 */
static DECLCALLBACK(int) audioTestPlayToneSetup(PAUDIOTESTENV pTstEnv, PAUDIOTESTDESC pTstDesc, PAUDIOTESTPARMS pTstParmsAcq, void **ppvCtx)
{
    RT_NOREF(pTstDesc, ppvCtx);

    int rc = VINF_SUCCESS;

    if (strlen(pTstEnv->szDev))
    {
        rc = audioTestDriverStackSetDevice(pTstEnv->pDrvStack, PDMAUDIODIR_OUT, pTstEnv->szDev);
        if (RT_FAILURE(rc))
            return rc;
    }

    pTstParmsAcq->enmType     = AUDIOTESTTYPE_TESTTONE_PLAY;
    pTstParmsAcq->enmDir      = PDMAUDIODIR_OUT;

    pTstParmsAcq->TestTone    = pTstEnv->ToneParms;

    pTstParmsAcq->TestTone.Hdr.idxTest = pTstEnv->idxTest; /* Assign unique test ID. */

    return rc;
}

/**
 * @copydoc FNAUDIOTESTEXEC
 */
static DECLCALLBACK(int) audioTestPlayToneExec(PAUDIOTESTENV pTstEnv, void *pvCtx, PAUDIOTESTPARMS pTstParms)
{
    RT_NOREF(pvCtx);

    int rc = VINF_SUCCESS;

    PAUDIOTESTTONEPARMS const pToneParms = &pTstParms->TestTone;

    uint32_t const idxTest = pToneParms->Hdr.idxTest;

    RTTIMESPEC NowTimeSpec;
    RTTimeExplode(&pToneParms->Hdr.tsCreated, RTTimeNow(&NowTimeSpec));

    RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Playing test tone (%RU16Hz, %RU32ms)\n",
                 idxTest, (uint16_t)pToneParms->dbFreqHz, pToneParms->msDuration);

    /*
     * 1. Arm the (host) ValKit ATS with the recording parameters.
     */
    RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS,
                 "Test #%RU32: Telling ValKit audio driver on host to record new tone ...\n", idxTest);

    rc = AudioTestSvcClientToneRecord(&pTstEnv->u.Host.AtsClValKit, pToneParms);
    if (RT_SUCCESS(rc))
    {
        /* Give the Validaiton Kit audio driver on the host a bit of time to register / arming the new test. */
        RTThreadSleep(5000); /* Fudge factor. */

        /*
         * 2. Tell VKAT on guest  to start playback.
         */
        RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Telling VKAT on guest to play tone ...\n", idxTest);

        rc = AudioTestSvcClientTonePlay(&pTstEnv->u.Host.AtsClGuest, pToneParms);
        if (RT_FAILURE(rc))
            RTTestFailed(g_hTest, "Test #%RU32: AudioTestSvcClientTonePlay() failed with %Rrc\n", idxTest, rc);
    }
    else
        RTTestFailed(g_hTest, "Test #%RU32: AudioTestSvcClientToneRecord() failed with %Rrc\n", idxTest, rc);

    if (RT_SUCCESS(rc))
    {
        RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Playing tone done\n", idxTest);

        /* Give the audio stack a random amount of time for draining data before the next iteration. */
        if (pTstEnv->cIterations > 1)
            RTThreadSleep(RTRandU32Ex(2000, 5000)); /** @todo Implement some dedicated ATS command for this? */
    }

    if (RT_FAILURE(rc))
        RTTestFailed(g_hTest, "Test #%RU32: Playing test tone failed with %Rrc\n", idxTest, rc);

    return rc;
}

/**
 * @copydoc FNAUDIOTESTDESTROY
 */
static DECLCALLBACK(int) audioTestPlayToneDestroy(PAUDIOTESTENV pTstEnv, void *pvCtx)
{
    RT_NOREF(pTstEnv, pvCtx);

    return VINF_SUCCESS;
}

/**
 * @copydoc FNAUDIOTESTSETUP
 */
static DECLCALLBACK(int) audioTestRecordToneSetup(PAUDIOTESTENV pTstEnv, PAUDIOTESTDESC pTstDesc, PAUDIOTESTPARMS pTstParmsAcq, void **ppvCtx)
{
    RT_NOREF(pTstDesc, ppvCtx);

    int rc = VINF_SUCCESS;

    if (strlen(pTstEnv->szDev))
    {
        rc = audioTestDriverStackSetDevice(pTstEnv->pDrvStack, PDMAUDIODIR_IN, pTstEnv->szDev);
        if (RT_FAILURE(rc))
            return rc;
    }

    pTstParmsAcq->enmType     = AUDIOTESTTYPE_TESTTONE_RECORD;
    pTstParmsAcq->enmDir      = PDMAUDIODIR_IN;

    pTstParmsAcq->TestTone    = pTstEnv->ToneParms;

    pTstParmsAcq->TestTone.Hdr.idxTest = pTstEnv->idxTest; /* Assign unique test ID. */

    return rc;
}

/**
 * @copydoc FNAUDIOTESTEXEC
 */
static DECLCALLBACK(int) audioTestRecordToneExec(PAUDIOTESTENV pTstEnv, void *pvCtx, PAUDIOTESTPARMS pTstParms)
{
    RT_NOREF(pvCtx);

    int rc = VINF_SUCCESS;

    PAUDIOTESTTONEPARMS const pToneParms = &pTstParms->TestTone;

    uint32_t const idxTest = pToneParms->Hdr.idxTest;

    RTTIMESPEC NowTimeSpec;
    RTTimeExplode(&pToneParms->Hdr.tsCreated, RTTimeNow(&NowTimeSpec));

    RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Recording test tone (%RU16Hz, %RU32ms)\n",
                 idxTest, (uint16_t)pToneParms->dbFreqHz, pToneParms->msDuration);

    /*
     * 1. Arm the (host) ValKit ATS with the playback parameters.
     */
    RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS,
                 "Test #%RU32: Telling ValKit audio driver on host to inject recording data ...\n", idxTest);

    rc = AudioTestSvcClientTonePlay(&pTstEnv->u.Host.AtsClValKit, &pTstParms->TestTone);
    if (RT_SUCCESS(rc))
    {
        /*
         * 2. Tell the guest ATS to start recording.
         */
        RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Telling VKAT on guest to record audio ...\n", idxTest);

        rc = AudioTestSvcClientToneRecord(&pTstEnv->u.Host.AtsClGuest, &pTstParms->TestTone);
        if (RT_FAILURE(rc))
            RTTestFailed(g_hTest, "Test #%RU32: AudioTestSvcClientToneRecord() failed with %Rrc\n", idxTest, rc);
    }
    else
        RTTestFailed(g_hTest, "Test #%RU32: AudioTestSvcClientTonePlay() failed with %Rrc\n", idxTest, rc);

    if (RT_SUCCESS(rc))
    {
        /* Wait a bit to let the left over audio bits being processed. */
        if (pTstEnv->cIterations > 1)
            RTThreadSleep(RTRandU32Ex(2000, 5000)); /** @todo Implement some dedicated ATS command for this? */
    }

    if (RT_FAILURE(rc))
        RTTestFailed(g_hTest, "Test #%RU32: Recording test tone failed with %Rrc\n", idxTest, rc);

    return rc;
}

/**
 * @copydoc FNAUDIOTESTDESTROY
 */
static DECLCALLBACK(int) audioTestRecordToneDestroy(PAUDIOTESTENV pTstEnv, void *pvCtx)
{
    RT_NOREF(pTstEnv, pvCtx);

    return VINF_SUCCESS;
}


/*********************************************************************************************************************************
*   Test execution                                                                                                               *
*********************************************************************************************************************************/

/** Test definition table. */
AUDIOTESTDESC g_aTests[] =
{
    /* pszTest      fExcluded      pfnSetup */
    { "PlayTone",   false,         audioTestPlayToneSetup,       audioTestPlayToneExec,      audioTestPlayToneDestroy },
    { "RecordTone", false,         audioTestRecordToneSetup,     audioTestRecordToneExec,    audioTestRecordToneDestroy }
};
/** Number of tests defined. */
unsigned g_cTests = RT_ELEMENTS(g_aTests);

/**
 * Runs one specific audio test.
 *
 * @returns VBox status code.
 * @param   pTstEnv             Test environment to use for running the test.
 * @param   pTstDesc            Test to run.
 */
static int audioTestOne(PAUDIOTESTENV pTstEnv, PAUDIOTESTDESC pTstDesc)
{
    int rc = VINF_SUCCESS;

    AUDIOTESTPARMS TstParms;
    audioTestParmsInit(&TstParms);

    RTTestSub(g_hTest, pTstDesc->pszName);

    if (pTstDesc->fExcluded)
    {
        RTTestSkipped(g_hTest, "Test #%RU32 is excluded from list, skipping", pTstEnv->idxTest);
        return VINF_SUCCESS;
    }

    pTstEnv->cIterations = pTstEnv->cIterations == 0 ? RTRandU32Ex(1, 10) : pTstEnv->cIterations;

    RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32 (%RU32 iterations total)\n", pTstEnv->idxTest, pTstEnv->cIterations);

    void *pvCtx = NULL; /* Test-specific opaque context. Optional and can be NULL. */

    AssertPtr(pTstDesc->pfnExec);
    for (uint32_t i = 0; i < pTstEnv->cIterations; i++)
    {
        int rc2;

        if (pTstDesc->pfnSetup)
        {
            rc2 = pTstDesc->pfnSetup(pTstEnv, pTstDesc, &TstParms, &pvCtx);
            if (RT_FAILURE(rc2))
                RTTestFailed(g_hTest, "Test #%RU32 setup failed with %Rrc\n", pTstEnv->idxTest, rc2);
        }
        else
            rc2 = VINF_SUCCESS;

        if (RT_SUCCESS(rc2))
        {
            AssertPtrBreakStmt(pTstDesc->pfnExec, VERR_INVALID_POINTER);
            rc2 = pTstDesc->pfnExec(pTstEnv, pvCtx, &TstParms);
            if (RT_FAILURE(rc2))
                RTTestFailed(g_hTest, "Test #%RU32 execution failed with %Rrc\n", pTstEnv->idxTest, rc2);
        }

        if (pTstDesc->pfnDestroy)
        {
            rc2 = pTstDesc->pfnDestroy(pTstEnv, pvCtx);
            if (RT_FAILURE(rc2))
                RTTestFailed(g_hTest, "Test #%RU32 destruction failed with %Rrc\n", pTstEnv->idxTest, rc2);
        }

        if (RT_SUCCESS(rc))
            rc = rc2;

        /* Keep going. */
        pTstEnv->idxTest++;
    }

    RTTestSubDone(g_hTest);

    audioTestParmsDestroy(&TstParms);

    return rc;
}

/**
 * Runs all specified tests in a row.
 *
 * @returns VBox status code.
 * @param   pTstEnv             Test environment to use for running all tests.
 */
int audioTestWorker(PAUDIOTESTENV pTstEnv)
{
    int rc = VINF_SUCCESS;

    if (pTstEnv->enmMode == AUDIOTESTMODE_GUEST)
    {
        RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Guest ATS running\n");

        while (!g_fTerminate)
            RTThreadSleep(100);

        RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Shutting down guest ATS ...\n");

        int rc2 = AudioTestSvcStop(pTstEnv->pSrv);
        if (RT_SUCCESS(rc))
            rc = rc2;

        RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Guest ATS shutdown complete\n");
    }
    else if (pTstEnv->enmMode == AUDIOTESTMODE_HOST)
    {
        RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Using tag '%s'\n", pTstEnv->szTag);

        RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Telling ValKit audio driver on host to begin a new test set ...\n");
        rc = AudioTestSvcClientTestSetBegin(&pTstEnv->u.Host.AtsClValKit, pTstEnv->szTag);
        if (RT_SUCCESS(rc))
        {
            RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Telling VKAT on guest to begin a new test set ...\n");
            rc = AudioTestSvcClientTestSetBegin(&pTstEnv->u.Host.AtsClGuest, pTstEnv->szTag);
            if (RT_FAILURE(rc))
                RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS,
                             "Beginning test set on guest failed with %Rrc\n", rc);
        }
        else
            RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS,
                         "Beginning test set on host (Validation Kit audio driver) failed with %Rrc\n", rc);

        if (RT_SUCCESS(rc))
        {
            for (unsigned i = 0; i < RT_ELEMENTS(g_aTests); i++)
            {
                int rc2 = audioTestOne(pTstEnv, &g_aTests[i]);
                if (RT_SUCCESS(rc))
                    rc = rc2;

                if (g_fTerminate)
                    break;
            }

            if (RT_SUCCESS(rc))
            {
                /** @todo Fudge! */
                RTMSINTERVAL const msWait = RTRandU32Ex(RT_MS_1SEC, RT_MS_5SEC);
                RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS,
                             "Waiting %RU32ms to let guest and the audio stack process remaining data  ...\n", msWait);
                RTThreadSleep(msWait);
            }

            RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Ending test set on guest ...\n");
            int rc2 = AudioTestSvcClientTestSetEnd(&pTstEnv->u.Host.AtsClGuest, pTstEnv->szTag);
            if (RT_FAILURE(rc2))
            {
                RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Ending test set on guest failed with %Rrc\n", rc2);
                if (RT_SUCCESS(rc))
                    rc = rc2;
            }

            RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Ending test set on host (Validation Kit audio driver) ...\n");
            rc2 = AudioTestSvcClientTestSetEnd(&pTstEnv->u.Host.AtsClValKit, pTstEnv->szTag);
            if (RT_FAILURE(rc2))
            {
                RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS,
                             "Ending test set on host (Validation Kit audio driver) failed with %Rrc\n", rc2);
                if (RT_SUCCESS(rc))
                    rc = rc2;
            }

            if (   !g_fTerminate
                && RT_SUCCESS(rc))
            {
                /*
                 * Download guest + Validation Kit audio driver test sets to our output directory.
                 */
                char szFileName[RTPATH_MAX];
                if (RTStrPrintf2(szFileName, sizeof(szFileName), "%s-guest.tar.gz", pTstEnv->szTag))
                {
                    rc = RTPathJoin(pTstEnv->u.Host.szPathTestSetGuest, sizeof(pTstEnv->u.Host.szPathTestSetGuest),
                                    pTstEnv->szPathOut, szFileName);
                    if (RT_SUCCESS(rc))
                    {
                        if (RTStrPrintf2(szFileName, sizeof(szFileName), "%s-host.tar.gz", pTstEnv->szTag))
                        {
                            rc = RTPathJoin(pTstEnv->u.Host.szPathTestSetValKit, sizeof(pTstEnv->u.Host.szPathTestSetValKit),
                                            pTstEnv->szPathOut, szFileName);
                        }
                        else
                            rc = VERR_BUFFER_OVERFLOW;
                    }
                    else
                        rc = VERR_BUFFER_OVERFLOW;

                    if (RT_SUCCESS(rc))
                    {
                        RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Downloading guest test set to '%s'\n",
                                     pTstEnv->u.Host.szPathTestSetGuest);
                        rc = AudioTestSvcClientTestSetDownload(&pTstEnv->u.Host.AtsClGuest,
                                                               pTstEnv->szTag, pTstEnv->u.Host.szPathTestSetGuest);
                    }

                    if (RT_SUCCESS(rc))
                    {
                        RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Downloading host test set to '%s'\n",
                                     pTstEnv->u.Host.szPathTestSetValKit);
                        rc = AudioTestSvcClientTestSetDownload(&pTstEnv->u.Host.AtsClValKit,
                                                               pTstEnv->szTag, pTstEnv->u.Host.szPathTestSetValKit);
                    }
                }
                else
                    rc = VERR_BUFFER_OVERFLOW;

                if (   RT_SUCCESS(rc)
                    && !pTstEnv->fSkipVerify)
                {
                    rc = audioVerifyOne(pTstEnv->u.Host.szPathTestSetGuest, pTstEnv->u.Host.szPathTestSetValKit, NULL /* pOpts */);
                }
                else
                    RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Verification skipped\n");

                if (!pTstEnv->fSkipVerify)
                {
                    RTFileDelete(pTstEnv->u.Host.szPathTestSetGuest);
                    RTFileDelete(pTstEnv->u.Host.szPathTestSetValKit);
                }
                else
                    RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Leaving test set files behind\n");
            }
        }
    }
    else
        rc = VERR_NOT_IMPLEMENTED;

    /* Clean up. */
    RTDirRemove(pTstEnv->szPathTemp);
    RTDirRemove(pTstEnv->szPathOut);

    if (RT_FAILURE(rc))
        RTTestFailed(g_hTest, "Test worker failed with %Rrc", rc);

    return rc;
}

/** Option help for the 'test' command.   */
static DECLCALLBACK(const char *) audioTestCmdTestHelp(PCRTGETOPTDEF pOpt)
{
    switch (pOpt->iShort)
    {
        case 'a':                               return "Exclude all tests from the list (useful to enable single tests later with --include)";
        case 'b':                               return "The audio backend to use";
        case 'd':                               return "Go via DrvAudio instead of directly interfacing with the backend";
        case 'e':                               return "Exclude the given test id from the list";
        case 'i':                               return "Include the given test id in the list";
        case VKAT_TEST_OPT_COUNT:               return "Number of test iterations to perform for selected tests\n"
                                                       "    Default: random number";
        case VKAT_TEST_OPT_DEV:                 return "Name of the input/output device to use\n"
                                                       "    Default: default device";
        case VKAT_TEST_OPT_TONE_DURATION_MS:    return "Test tone duration to play / record (ms)\n"
                                                       "    Default: random duration";
        case VKAT_TEST_OPT_TONE_VOL_PERCENT:    return "Test tone volume (percent)\n"
                                                       "    Default: 100";
        case VKAT_TEST_OPT_GUEST_ATS_ADDR:      return "Address of guest ATS to connect to\n"
                                                       "    Default: " ATS_TCP_DEF_CONNECT_GUEST_STR;
        case VKAT_TEST_OPT_GUEST_ATS_PORT:      return "Port of guest ATS to connect to (needs NAT port forwarding)\n"
                                                       "    Default: 6042"; /* ATS_TCP_DEF_CONNECT_PORT_GUEST */
        case VKAT_TEST_OPT_HOST_ATS_ADDR:       return "Address of host ATS to connect to\n"
                                                       "    Default: " ATS_TCP_DEF_CONNECT_HOST_ADDR_STR;
        case VKAT_TEST_OPT_HOST_ATS_PORT:       return "Port of host ATS to connect to\n"
                                                       "    Default: 6052"; /* ATS_TCP_DEF_BIND_PORT_VALKIT */
        case VKAT_TEST_OPT_MODE:                return "Test mode to use when running the tests\n"
                                                        "    Available modes:\n"
                                                        "        guest: Run as a guest-side ATS\n"
                                                        "        host:  Run as a host-side ATS";
        case VKAT_TEST_OPT_NO_AUDIO_OK:         return "Enables running without any found audio hardware (e.g. servers)";
        case VKAT_TEST_OPT_NO_VERIFY:           return "Skips the verification step";
        case VKAT_TEST_OPT_OUTDIR:              return "Output directory to use";
        case VKAT_TEST_OPT_PAUSE:               return "Not yet implemented";
        case VKAT_TEST_OPT_PCM_HZ:              return "PCM Hertz (Hz) rate to use\n"
                                                       "    Default: 44100";
        case VKAT_TEST_OPT_PCM_BIT:             return "PCM sample bits (i.e. 16) to use\n"
                                                       "    Default: 16";
        case VKAT_TEST_OPT_PCM_CHAN:            return "PCM channels to use\n"
                                                       "    Default: 2";
        case VKAT_TEST_OPT_PCM_SIGNED:          return "PCM samples to use (signed = true, unsigned = false)\n"
                                                       "    Default: true";
        case VKAT_TEST_OPT_PROBE_BACKENDS:      return "Probes all (available) backends until a working one is found";
        case VKAT_TEST_OPT_TAG:                 return "Test set tag to use";
        case VKAT_TEST_OPT_TEMPDIR:             return "Temporary directory to use";
        case VKAT_TEST_OPT_VOL:                 return "Audio volume (percent) to use";
        case VKAT_TEST_OPT_TCP_BIND_ADDRESS:    return "TCP address listening to (server mode)";
        case VKAT_TEST_OPT_TCP_BIND_PORT:       return "TCP port listening to (server mode)";
        case VKAT_TEST_OPT_TCP_CONNECT_ADDRESS: return "TCP address to connect to (client mode)";
        case VKAT_TEST_OPT_TCP_CONNECT_PORT:    return "TCP port to connect to (client mode)";
        default:
            break;
    }
    return NULL;
}

/**
 * Main (entry) function for the testing functionality of VKAT.
 *
 * @returns Program exit code.
 * @param   pGetState   RTGetOpt state.
 */
static DECLCALLBACK(RTEXITCODE) audioTestMain(PRTGETOPTSTATE pGetState)
{
    AUDIOTESTENV TstEnv;
    audioTestEnvInit(&TstEnv);

    int         rc;

    PCPDMDRVREG pDrvReg        = AudioTestGetDefaultBackend();
    uint8_t     cPcmSampleBit  = 0;
    uint8_t     cPcmChannels   = 0;
    uint32_t    uPcmHz         = 0;
    bool        fPcmSigned     = true;
    bool        fProbeBackends = false;
    bool        fNoAudioOk     = false;

    const char *pszGuestTcpAddr  = NULL;
    uint16_t    uGuestTcpPort    = ATS_TCP_DEF_BIND_PORT_GUEST;
    const char *pszValKitTcpAddr = NULL;
    uint16_t    uValKitTcpPort   = ATS_TCP_DEF_BIND_PORT_VALKIT;

    int           ch;
    RTGETOPTUNION ValueUnion;
    while ((ch = RTGetOpt(pGetState, &ValueUnion)))
    {
        switch (ch)
        {
            case 'a':
                for (unsigned i = 0; i < RT_ELEMENTS(g_aTests); i++)
                    g_aTests[i].fExcluded = true;
                break;

            case 'b':
                pDrvReg = AudioTestFindBackendOpt(ValueUnion.psz);
                if (pDrvReg == NULL)
                    return RTEXITCODE_SYNTAX;
                break;

            case 'd':
                TstEnv.IoOpts.fWithDrvAudio = true;
                break;

            case 'e':
                if (ValueUnion.u32 >= RT_ELEMENTS(g_aTests))
                    return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Invalid test number %u passed to --exclude", ValueUnion.u32);
                g_aTests[ValueUnion.u32].fExcluded = true;
                break;

            case VKAT_TEST_OPT_GUEST_ATS_ADDR:
                pszGuestTcpAddr = ValueUnion.psz;
                break;

            case VKAT_TEST_OPT_GUEST_ATS_PORT:
                uGuestTcpPort = ValueUnion.u32;
                break;

            case VKAT_TEST_OPT_HOST_ATS_ADDR:
                pszValKitTcpAddr = ValueUnion.psz;
                break;

            case VKAT_TEST_OPT_HOST_ATS_PORT:
                uValKitTcpPort = ValueUnion.u32;
                break;

            case VKAT_TEST_OPT_MODE:
                if (TstEnv.enmMode != AUDIOTESTMODE_UNKNOWN)
                    return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Test mode (guest / host) already specified");
                TstEnv.enmMode = RTStrICmp(ValueUnion.psz, "guest") == 0 ? AUDIOTESTMODE_GUEST : AUDIOTESTMODE_HOST;
                break;

            case VKAT_TEST_OPT_NO_AUDIO_OK:
                fNoAudioOk = true;
                break;

            case VKAT_TEST_OPT_NO_VERIFY:
                TstEnv.fSkipVerify = true;
                break;

            case 'i':
                if (ValueUnion.u32 >= RT_ELEMENTS(g_aTests))
                    return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Invalid test number %u passed to --include", ValueUnion.u32);
                g_aTests[ValueUnion.u32].fExcluded = false;
                break;

            case VKAT_TEST_OPT_COUNT:
                TstEnv.cIterations = ValueUnion.u32;
                break;

            case VKAT_TEST_OPT_DEV:
                rc = RTStrCopy(TstEnv.szDev, sizeof(TstEnv.szDev), ValueUnion.psz);
                if (RT_FAILURE(rc))
                    return RTMsgErrorExitFailure("Failed to copy out device: %Rrc", rc);
                break;

            case VKAT_TEST_OPT_TONE_DURATION_MS:
                TstEnv.ToneParms.msDuration = ValueUnion.u32;
                break;

            case VKAT_TEST_OPT_TONE_VOL_PERCENT:
                TstEnv.ToneParms.uVolumePercent = ValueUnion.u8;
                break;

            case VKAT_TEST_OPT_PAUSE:
                return RTMsgErrorExitFailure("Not yet implemented!");

            case VKAT_TEST_OPT_OUTDIR:
                rc = RTStrCopy(TstEnv.szPathOut, sizeof(TstEnv.szPathOut), ValueUnion.psz);
                if (RT_FAILURE(rc))
                    return RTMsgErrorExitFailure("Failed to copy out directory: %Rrc", rc);
                break;

            case VKAT_TEST_OPT_PCM_BIT:
                cPcmSampleBit = ValueUnion.u8;
                break;

            case VKAT_TEST_OPT_PCM_CHAN:
                cPcmChannels = ValueUnion.u8;
                break;

            case VKAT_TEST_OPT_PCM_HZ:
                uPcmHz = ValueUnion.u32;
                break;

            case VKAT_TEST_OPT_PCM_SIGNED:
                fPcmSigned = ValueUnion.f;
                break;

            case VKAT_TEST_OPT_PROBE_BACKENDS:
                fProbeBackends = true;
                break;

            case VKAT_TEST_OPT_TAG:
                rc = RTStrCopy(TstEnv.szTag, sizeof(TstEnv.szTag), ValueUnion.psz);
                if (RT_FAILURE(rc))
                    return RTMsgErrorExit(RTEXITCODE_FAILURE, "Tag invalid, rc=%Rrc", rc);
                break;

            case VKAT_TEST_OPT_TEMPDIR:
                rc = RTStrCopy(TstEnv.szPathTemp, sizeof(TstEnv.szPathTemp), ValueUnion.psz);
                if (RT_FAILURE(rc))
                    return RTMsgErrorExit(RTEXITCODE_FAILURE, "Temp dir invalid, rc=%Rrc", rc);
                break;

            case VKAT_TEST_OPT_VOL:
                TstEnv.IoOpts.uVolumePercent = ValueUnion.u8;
                break;

            case VKAT_TEST_OPT_TCP_BIND_ADDRESS:
                rc = RTStrCopy(TstEnv.TcpOpts.szBindAddr, sizeof(TstEnv.TcpOpts.szBindAddr), ValueUnion.psz);
                if (RT_FAILURE(rc))
                    return RTMsgErrorExit(RTEXITCODE_FAILURE, "Bind address invalid, rc=%Rrc", rc);
                break;

            case VKAT_TEST_OPT_TCP_BIND_PORT:
                TstEnv.TcpOpts.uBindPort = ValueUnion.u16;
                break;

            case VKAT_TEST_OPT_TCP_CONNECT_ADDRESS:
                rc = RTStrCopy(TstEnv.TcpOpts.szConnectAddr, sizeof(TstEnv.TcpOpts.szConnectAddr), ValueUnion.psz);
                if (RT_FAILURE(rc))
                    return RTMsgErrorExit(RTEXITCODE_FAILURE, "Connect address invalid, rc=%Rrc", rc);
                break;

            case VKAT_TEST_OPT_TCP_CONNECT_PORT:
                TstEnv.TcpOpts.uConnectPort = ValueUnion.u16;
                break;

            AUDIO_TEST_COMMON_OPTION_CASES(ValueUnion, &g_CmdTest);

            default:
                return RTGetOptPrintError(ch, &ValueUnion);
        }
    }

    /*
     * Start testing.
     */
    RTTestBanner(g_hTest);

    if (TstEnv.enmMode == AUDIOTESTMODE_UNKNOWN)
        return RTMsgErrorExit(RTEXITCODE_SYNTAX, "No test mode (--mode) specified!\n");

    /* Validate TCP options. */
    if (   TstEnv.TcpOpts.szBindAddr[0]
        && TstEnv.TcpOpts.szConnectAddr[0])
        return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Only one TCP connection mode (connect as client *or* bind as server) can be specified) at a time!\n");

    /* Set new (override standard) I/O PCM properties if set by the user. */
    if (   cPcmSampleBit
        || cPcmChannels
        || uPcmHz)
    {
        PDMAudioPropsInit(&TstEnv.IoOpts.Props,
                          cPcmSampleBit ? cPcmSampleBit / 2 : 2 /* 16-bit */, fPcmSigned /* fSigned */,
                          cPcmChannels  ? cPcmChannels      : 2 /* Stereo */, uPcmHz ? uPcmHz : 44100);
    }

    /* Do this first before everything else below. */
    rc = AudioTestDriverStackPerformSelftest();
    if (RT_FAILURE(rc))
    {
        if (!fNoAudioOk)
            return RTMsgErrorExit(RTEXITCODE_FAILURE, "Testing driver stack failed: %Rrc\n", rc);
        RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS,
                     "Warning: Testing driver stack not possible (%Rrc), but --no-audio-ok was specified. Running on a server without audio hardware?\n", rc);
    }

    AUDIOTESTDRVSTACK DrvStack;
    if (fProbeBackends)
        rc = audioTestDriverStackProbe(&DrvStack, pDrvReg,
                                       true /* fEnabledIn */, true /* fEnabledOut */, TstEnv.IoOpts.fWithDrvAudio); /** @todo Make in/out configurable, too. */
    else
        rc = audioTestDriverStackInitEx(&DrvStack, pDrvReg,
                                        true /* fEnabledIn */, true /* fEnabledOut */, TstEnv.IoOpts.fWithDrvAudio); /** @todo Make in/out configurable, too. */
    if (RT_FAILURE(rc))
    {
        if (!fNoAudioOk)
            return RTMsgErrorExit(RTEXITCODE_FAILURE, "Unable to init driver stack: %Rrc\n", rc);
        RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS,
                     "Warning: Initializing driver stack not possible (%Rrc), but --no-audio-ok was specified. Running on a server without audio hardware?\n", rc);
    }

    PPDMAUDIOHOSTDEV pDev;
    rc = audioTestDevicesEnumerateAndCheck(&DrvStack, TstEnv.szDev, &pDev);
    if (RT_FAILURE(rc))
    {
        if (!fNoAudioOk)
            return RTMsgErrorExit(RTEXITCODE_FAILURE, "Enumerating device(s) failed: %Rrc\n", rc);
    }

    /* For now all tests have the same test environment and driver stack. */
    rc = audioTestEnvCreate(&TstEnv, &DrvStack);
    if (RT_SUCCESS(rc))
        rc = audioTestWorker(&TstEnv);

    audioTestEnvDestroy(&TstEnv);
    audioTestDriverStackDelete(&DrvStack);

    if (RT_FAILURE(rc)) /* Let us know that something went wrong in case we forgot to mention it. */
        RTTestFailed(g_hTest, "Testing failed with %Rrc\n", rc);

    /*
     * Print summary and exit.
     */
    return RTTestSummaryAndDestroy(g_hTest);
}


const VKATCMD g_CmdTest =
{
    "test",
    audioTestMain,
    "Runs audio tests and creates an audio test set.",
    g_aCmdTestOptions,
    RT_ELEMENTS(g_aCmdTestOptions),
    audioTestCmdTestHelp,
    true /* fNeedsTransport */
};


/*********************************************************************************************************************************
*   Command: verify                                                                                                              *
*********************************************************************************************************************************/

static int audioVerifyOpenTestSet(const char *pszPathSet, PAUDIOTESTSET pSet)
{
    int rc;

    char szPathExtracted[RTPATH_MAX];

    RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Opening test set '%s'\n", pszPathSet);

    const bool fPacked = AudioTestSetIsPacked(pszPathSet);

    if (fPacked)
    {
        RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test set is an archive and needs to be unpacked\n");

        if (!RTFileExists(pszPathSet))
        {
            RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test set '%s' does not exist\n", pszPathSet);
            rc = VERR_FILE_NOT_FOUND;
        }
        else
            rc = VINF_SUCCESS;

        if (RT_SUCCESS(rc))
        {
            char szPathTemp[RTPATH_MAX];
            rc = RTPathTemp(szPathTemp, sizeof(szPathTemp));
            if (RT_SUCCESS(rc))
            {
                RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Using temporary directory '%s'\n", szPathTemp);

                rc = RTPathJoin(szPathExtracted, sizeof(szPathExtracted), szPathTemp, "vkat-testset-XXXX");
                if (RT_SUCCESS(rc))
                {
                    rc = RTDirCreateTemp(szPathExtracted, 0755);
                    if (RT_SUCCESS(rc))
                    {
                        RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Unpacking archive to '%s'\n", szPathExtracted);
                        rc = AudioTestSetUnpack(pszPathSet, szPathExtracted);
                        if (RT_SUCCESS(rc))
                            RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Archive successfully unpacked\n");
                    }
                }
            }
        }
    }
    else
        rc = VINF_SUCCESS;

    if (RT_SUCCESS(rc))
        rc = AudioTestSetOpen(pSet, fPacked ? szPathExtracted : pszPathSet);

    if (RT_FAILURE(rc))
        RTTestFailed(g_hTest, "Unable to open / unpack test set archive: %Rrc", rc);

    return rc;
}

/**
 * Verifies one test set pair.
 *
 * @returns VBox status code.
 * @param   pszPathSetA         Absolute path to test set A.
 * @param   pszPathSetB         Absolute path to test set B.
 * @param   pOpts               Verification options to use. Optional.
 *                              When NULL, the (very strict) defaults will be used.
 */
static int audioVerifyOne(const char *pszPathSetA, const char *pszPathSetB, PAUDIOTESTVERIFYOPTS pOpts)
{
    RTTestSubF(g_hTest, "Verifying");
    RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Verifying test set '%s' with test set '%s'\n", pszPathSetA, pszPathSetB);

    AUDIOTESTSET SetA, SetB;
    int rc = audioVerifyOpenTestSet(pszPathSetA, &SetA);
    if (RT_SUCCESS(rc))
    {
        rc = audioVerifyOpenTestSet(pszPathSetB, &SetB);
        if (RT_SUCCESS(rc))
        {
            AUDIOTESTERRORDESC errDesc;
            if (pOpts)
                rc = AudioTestSetVerifyEx(&SetA, &SetB, pOpts, &errDesc);
            else
                rc = AudioTestSetVerify(&SetA, &SetB, &errDesc);
            if (RT_SUCCESS(rc))
            {
                uint32_t const cErr = AudioTestErrorDescCount(&errDesc);
                RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "%RU32 errors occurred while verifying\n", cErr);

                /** @todo Use some AudioTestErrorXXX API for enumeration here later. */
                PAUDIOTESTERRORENTRY pErrEntry;
                RTListForEach(&errDesc.List, pErrEntry, AUDIOTESTERRORENTRY, Node)
                {
                    if (RT_FAILURE(pErrEntry->rc))
                        RTTestFailed(g_hTest, "%s\n", pErrEntry->szDesc);
                    else
                        RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "%s\n", pErrEntry->szDesc);
                }

                if (cErr == 0)
                    RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Verification successful\n");

                AudioTestErrorDescDestroy(&errDesc);
            }
            else
                RTTestFailed(g_hTest, "Verification failed with %Rrc", rc);

#ifdef DEBUG
            if (g_fDrvAudioDebug)
                RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS,
                             "\n"
                             "Use the following command line to re-run verification in the debugger:\n"
                             "gdb --args ./VBoxAudioTest -vvvv --debug-audio verify \"%s\" \"%s\"\n",
                             SetA.szPathAbs, SetB.szPathAbs);
#endif
            if (!g_fDrvAudioDebug) /* Don't wipe stuff when debugging. Can be useful for introspecting data. */
                AudioTestSetWipe(&SetB);
            AudioTestSetClose(&SetB);
        }

        if (!g_fDrvAudioDebug) /* Ditto. */
            AudioTestSetWipe(&SetA);
        AudioTestSetClose(&SetA);
    }

    RTTestSubDone(g_hTest);

    return rc;
}

/** Option help for the 'verify' command. */
static DECLCALLBACK(const char *) audioTestCmdVerifyHelp(PCRTGETOPTDEF pOpt)
{
    switch (pOpt->iShort)
    {
        case VKAT_VERIFY_OPT_MAX_DIFF_COUNT:    return "Specifies the maximum number of differences\n"
                                                       "    Default: 0 (strict)";
        case VKAT_VERIFY_OPT_MAX_DIFF_PERCENT:  return "Specifies the maximum difference (percent)\n"
                                                       "    Default: 0 (strict)";
        case VKAT_VERIFY_OPT_MAX_SIZE_PERCENT:  return "Specifies the maximum size difference (percent)\n"
                                                       "    Default: 1 (strict)";
        case VKAT_VERIFY_OPT_NORMALIZE:         return "Enables / disables audio data normalization\n"
                                                       "    Default: false";
        default:
            break;
    }
    return NULL;
}

/**
 * Main (entry) function for the verification functionality of VKAT.
 *
 * @returns Program exit code.
 * @param   pGetState   RTGetOpt state.
 */
static DECLCALLBACK(RTEXITCODE) audioVerifyMain(PRTGETOPTSTATE pGetState)
{
    /*
     * Parse options and process arguments.
     */
    const char *apszSets[2] = { NULL, NULL };
    unsigned    iTestSet    = 0;

    AUDIOTESTVERIFYOPTS Opts;
    AudioTestSetVerifyOptsInit(&Opts);

    int           ch;
    RTGETOPTUNION ValueUnion;
    while ((ch = RTGetOpt(pGetState, &ValueUnion)))
    {
        switch (ch)
        {
            case VKAT_VERIFY_OPT_MAX_DIFF_COUNT:
                Opts.cMaxDiff = ValueUnion.u32;
                break;

            case VKAT_VERIFY_OPT_MAX_DIFF_PERCENT:
                Opts.uMaxDiffPercent = ValueUnion.u8;
                break;

            case VKAT_VERIFY_OPT_MAX_SIZE_PERCENT:
                Opts.uMaxSizePercent = ValueUnion.u8;
                break;

            case VKAT_VERIFY_OPT_NORMALIZE:
                Opts.fNormalize = ValueUnion.f;
                break;

            case VINF_GETOPT_NOT_OPTION:
                if (iTestSet == 0)
                    RTTestBanner(g_hTest);
                if (iTestSet >= RT_ELEMENTS(apszSets))
                    return RTMsgErrorExitFailure("Only two test sets can be verified at one time");
                apszSets[iTestSet++] = ValueUnion.psz;
                break;

            AUDIO_TEST_COMMON_OPTION_CASES(ValueUnion, &g_CmdVerify);

            default:
                return RTGetOptPrintError(ch, &ValueUnion);
        }
    }

    if (!iTestSet)
        return RTMsgErrorExitFailure("At least one test set must be specified");

    int rc = VINF_SUCCESS;

    /*
     * If only test set A is given, default to the current directory
     * for test set B.
     */
    char szDirCur[RTPATH_MAX];
    if (iTestSet == 1)
    {
        rc = RTPathGetCurrent(szDirCur, sizeof(szDirCur));
        if (RT_SUCCESS(rc))
            apszSets[1] = szDirCur;
        else
            RTTestFailed(g_hTest, "Failed to retrieve current directory: %Rrc", rc);
    }

    if (RT_SUCCESS(rc))
        audioVerifyOne(apszSets[0], apszSets[1], &Opts);

    /*
     * Print summary and exit.
     */
    return RTTestSummaryAndDestroy(g_hTest);
}


const VKATCMD g_CmdVerify =
{
    "verify",
    audioVerifyMain,
    "Verifies a formerly created audio test set.",
    g_aCmdVerifyOptions,
    RT_ELEMENTS(g_aCmdVerifyOptions),
    audioTestCmdVerifyHelp,
    false /* fNeedsTransport */
};


/*********************************************************************************************************************************
*   Main                                                                                                                         *
*********************************************************************************************************************************/

/**
 * Ctrl-C signal handler.
 *
 * This just sets g_fTerminate and hope it will be noticed soon.
 *
 * On non-Windows it restores the SIGINT action to default, so that a second
 * Ctrl-C will have the normal effect (just in case the code doesn't respond to
 * g_fTerminate).
 */
#ifdef RT_OS_WINDOWS
static BOOL CALLBACK audioTestConsoleCtrlHandler(DWORD dwCtrlType) RT_NOEXCEPT
{
    if (dwCtrlType != CTRL_C_EVENT && dwCtrlType != CTRL_BREAK_EVENT)
        return false;
    RTPrintf(dwCtrlType == CTRL_C_EVENT ? "Ctrl-C!\n" : "Ctrl-Break!\n");

    ASMAtomicWriteBool(&g_fTerminate, true);

    return true;
}
#else
static void audioTestSignalHandler(int iSig) RT_NOEXCEPT
{
    Assert(iSig == SIGINT); RT_NOREF(iSig);
    RTPrintf("Ctrl-C!\n");

    ASMAtomicWriteBool(&g_fTerminate, true);

    signal(SIGINT, SIG_DFL);
}
#endif

/**
 * Commands.
 */
static const VKATCMD * const g_apCommands[] =
{
    &g_CmdTest,
    &g_CmdVerify,
    &g_CmdBackends,
    &g_CmdEnum,
    &g_CmdPlay,
    &g_CmdRec,
    &g_CmdSelfTest
};

/**
 * Shows tool usage text.
 */
RTEXITCODE audioTestUsage(PRTSTREAM pStrm, PCVKATCMD pOnlyCmd)
{
    RTStrmPrintf(pStrm, "usage: %s [global options] <command> [command-options]\n", RTProcShortName());
    RTStrmPrintf(pStrm,
                 "\n"
                 "Global Options:\n"
                 "  --debug-audio\n"
                 "    Enables (DrvAudio) debugging\n"
                 "  --debug-audio-path=<path>\n"
                 "    Tells DrvAudio where to put its debug output (wav-files)\n"
                 "  -q, --quiet\n"
                 "    Sets verbosity to zero\n"
                 "  -v, --verbose\n"
                 "    Increase verbosity\n"
                 "  -V, --version\n"
                 "    Displays version\n"
                 "  -h, -?, --help\n"
                 "    Displays help\n"
                 );

    for (uintptr_t iCmd = 0; iCmd < RT_ELEMENTS(g_apCommands); iCmd++)
    {
        PCVKATCMD const pCmd = g_apCommands[iCmd];
        if (!pOnlyCmd || pCmd == pOnlyCmd)
        {
            RTStrmPrintf(pStrm,
                         "\n"
                         "Command '%s':\n"
                         "    %s\n"
                         "Options for '%s':\n",
                         pCmd->pszCommand, pCmd->pszDesc, pCmd->pszCommand);
            PCRTGETOPTDEF const paOptions = pCmd->paOptions;
            for (unsigned i = 0; i < pCmd->cOptions; i++)
            {
                if (RT_C_IS_PRINT(paOptions[i].iShort))
                    RTStrmPrintf(pStrm, "  -%c, %s\n", paOptions[i].iShort, paOptions[i].pszLong);
                else
                    RTStrmPrintf(pStrm, "  %s\n", paOptions[i].pszLong);

                const char *pszHelp = NULL;
                if (pCmd->pfnOptionHelp)
                    pszHelp = pCmd->pfnOptionHelp(&paOptions[i]);
                if (pszHelp)
                    RTStrmPrintf(pStrm, "    %s\n", pszHelp);
            }

            if (pCmd->fNeedsTransport)
                for (uintptr_t iTx = 0; iTx < g_cTransports; iTx++)
                    g_apTransports[iTx]->pfnUsage(pStrm);
        }
    }

    return RTEXITCODE_SUCCESS;
}

/**
 * Lists the commands and their descriptions.
 */
static RTEXITCODE audioTestListCommands(PRTSTREAM pStrm)
{
    RTStrmPrintf(pStrm, "Commands:\n");
    for (uintptr_t iCmd = 0; iCmd < RT_ELEMENTS(g_apCommands); iCmd++)
        RTStrmPrintf(pStrm, "%8s - %s\n", g_apCommands[iCmd]->pszCommand, g_apCommands[iCmd]->pszDesc);
    return RTEXITCODE_SUCCESS;
}

/**
 * Shows tool version.
 */
RTEXITCODE audioTestVersion(void)
{
    RTPrintf("%s\n", RTBldCfgRevisionStr());
    return RTEXITCODE_SUCCESS;
}

/**
 * Shows the logo.
 *
 * @param   pStream             Output stream to show logo on.
 */
void audioTestShowLogo(PRTSTREAM pStream)
{
    RTStrmPrintf(pStream, VBOX_PRODUCT " VKAT (Validation Kit Audio Test) Version " VBOX_VERSION_STRING " - r%s\n"
                 "Copyright (C) " VBOX_C_YEAR " " VBOX_VENDOR "\n\n", RTBldCfgRevisionStr());
}

int main(int argc, char **argv)
{
    /*
     * Init IPRT.
     */
    int rc = RTR3InitExe(argc, &argv, 0);
    if (RT_FAILURE(rc))
        return RTMsgInitFailure(rc);

    /*
     * Handle special command line options which need parsing before
     * everything else.
     */
    /** @todo r=bird: this isn't at all syntactically sane, because you don't know
     * how to parse past the command (can almost be done safely thought, since
     * you've got the option definitions for every command at hand).  So, if someone
     * wants to play a file named "-v.wav", you'll incorrectly take that as two 'v'
     * options. The parsing has to stop when you get to the command, i.e. first
     * VINF_GETOPT_NOT_OPTION or anything that isn't a common option. Daemonizing
     * when for instance encountering an invalid command, is not correct.
     *
     * Btw. you MUST however process the 'q' option in parallel to 'v' here, they
     * are oposites.  For instance '-vqvvv' is supposed to give you level 3 logging,
     * not quiet!  So, either you process both 'v' and 'q' here, or you pospone them
     * (better option).
     */
    /** @todo r=bird: Is the daemonizing needed? The testcase doesn't seem to use
     *        it... If you don't need it, drop it as it make the parsing complex
     *        and illogical.  The --daemonized / --damonize options should be
     *        required to before the command, then okay.  */
    bool fDaemonize  = false;
    bool fDaemonized = false;

    RTGETOPTSTATE GetState;
    rc = RTGetOptInit(&GetState, argc, argv, g_aCmdCommonOptions,
                      RT_ELEMENTS(g_aCmdCommonOptions), 1 /*idxFirst*/, 0 /*fFlags - must not sort! */);
    AssertRCReturn(rc, RTEXITCODE_INIT);

    int           ch;
    RTGETOPTUNION ValueUnion;
    while ((ch = RTGetOpt(&GetState, &ValueUnion)) != 0)
    {
        switch (ch)
        {
            case AUDIO_TEST_OPT_CMN_DAEMONIZE:
                fDaemonize = true;
                break;

            case AUDIO_TEST_OPT_CMN_DAEMONIZED:
                fDaemonized = true;
                break;

            /* Has to be defined here and not in AUDIO_TEST_COMMON_OPTION_CASES, to get the logger
             * configured before the specific command handlers down below come into play. */
            case 'v':
                g_uVerbosity++;
                break;

            default:
                break;
        }
    }

    /** @todo add something to suppress this stuff.   */
    audioTestShowLogo(g_pStdOut);

    if (fDaemonize)
    {
        if (!fDaemonized)
        {
            rc = RTProcDaemonize(argv, "--daemonized");
            if (RT_FAILURE(rc))
                return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTProcDaemonize() failed with %Rrc\n", rc);

            RTMsgInfo("Starting in background (daemonizing) ...");
            return RTEXITCODE_SUCCESS;
        }
        /* else continue running in background. */
    }

    /*
     * Init test and globals.
     * Note: Needs to be done *after* daemonizing, otherwise the child will fail!
     */
    rc = RTTestCreate("AudioTest", &g_hTest);
    if (RT_FAILURE(rc))
        return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTTestCreate() failed with %Rrc\n", rc);

#ifdef RT_OS_WINDOWS
    HRESULT hrc = CoInitializeEx(NULL /*pReserved*/, COINIT_MULTITHREADED | COINIT_SPEED_OVER_MEMORY | COINIT_DISABLE_OLE1DDE);
    if (FAILED(hrc))
        RTMsgWarning("CoInitializeEx failed: %#x", hrc);
#endif

    /*
     * Configure release logging to go to stdout.
     */
    RTUINT fFlags = RTLOGFLAGS_PREFIX_THREAD | RTLOGFLAGS_PREFIX_TIME_PROG;
#if defined(RT_OS_WINDOWS) || defined(RT_OS_OS2)
    fFlags |= RTLOGFLAGS_USECRLF;
#endif
    static const char * const s_apszLogGroups[] = VBOX_LOGGROUP_NAMES;
    rc = RTLogCreate(&g_pRelLogger, fFlags, "all.e.l", "VKAT_RELEASE_LOG",
                     RT_ELEMENTS(s_apszLogGroups), s_apszLogGroups, RTLOGDEST_STDOUT, NULL /*"vkat-release.log"*/);
    if (RT_SUCCESS(rc))
    {
        RTLogRelSetDefaultInstance(g_pRelLogger);
        if (g_uVerbosity)
        {
            RTMsgInfo("Setting verbosity logging to level %u\n", g_uVerbosity);
            switch (g_uVerbosity) /* Not very elegant, but has to do it for now. */
            {
                case 1:
                    rc = RTLogGroupSettings(g_pRelLogger,
                                            "drv_audio.e.l+drv_host_audio.e.l+"
                                            "audio_mixer.e.l+audio_test.e.l");
                    break;

                case 2:
                    rc = RTLogGroupSettings(g_pRelLogger,
                                            "drv_audio.e.l.l2+drv_host_audio.e.l.l2+"
                                            "audio_mixer.e.l.l2+audio_test.e.l.l2");
                    break;

                case 3:
                    rc = RTLogGroupSettings(g_pRelLogger,
                                            "drv_audio.e.l.l2.l3+drv_host_audio.e.l.l2.l3+"
                                            "audio_mixer.e.l.l2.l3+audio_test.e.l.l2.l3");
                    break;

                case 4:
                    RT_FALL_THROUGH();
                default:
                    rc = RTLogGroupSettings(g_pRelLogger,
                                            "drv_audio.e.l.l2.l3.l4.f+drv_host_audio.e.l.l2.l3.l4.f+"
                                            "audio_mixer.e.l.l2.l3.l4.f+audio_test.e.l.l2.l3.l4.f");
                    break;
            }
            if (RT_FAILURE(rc))
                RTMsgError("Setting debug logging failed, rc=%Rrc\n", rc);
        }
    }
    else
        RTMsgWarning("Failed to create release logger: %Rrc", rc);

    /*
     * Install a Ctrl-C signal handler.
     */
#ifdef RT_OS_WINDOWS
    SetConsoleCtrlHandler(audioTestConsoleCtrlHandler, TRUE);
#else
    struct sigaction sa;
    RT_ZERO(sa);
    sa.sa_handler = audioTestSignalHandler;
    sigaction(SIGINT, &sa, NULL);
#endif

    /*
     * Process common options.
     */
    RT_ZERO(GetState);
    rc = RTGetOptInit(&GetState, argc, argv, g_aCmdCommonOptions,
                      RT_ELEMENTS(g_aCmdCommonOptions), 1 /*idxFirst*/, 0 /*fFlags - must not sort! */);
    AssertRCReturn(rc, RTEXITCODE_INIT);

    while ((ch = RTGetOpt(&GetState, &ValueUnion)) != 0)
    {
        switch (ch)
        {
            AUDIO_TEST_COMMON_OPTION_CASES(ValueUnion, NULL);

            case VINF_GETOPT_NOT_OPTION:
            {
                for (uintptr_t iCmd = 0; iCmd < RT_ELEMENTS(g_apCommands); iCmd++)
                {
                    PCVKATCMD const pCmd = g_apCommands[iCmd];
                    if (strcmp(ValueUnion.psz, pCmd->pszCommand) == 0)
                    {
                        /* Count the combined option definitions:  */
                        size_t cCombinedOptions  = pCmd->cOptions + RT_ELEMENTS(g_aCmdCommonOptions);
                        if (pCmd->fNeedsTransport)
                            for (uintptr_t iTx = 0; iTx < g_cTransports; iTx++)
                                cCombinedOptions += g_apTransports[iTx]->cOpts;

                        /* Combine the option definitions: */
                        PRTGETOPTDEF paCombinedOptions = (PRTGETOPTDEF)RTMemAlloc(cCombinedOptions * sizeof(RTGETOPTDEF));
                        if (paCombinedOptions)
                        {
                            uint32_t idxOpts = 0;
                            memcpy(paCombinedOptions, g_aCmdCommonOptions, sizeof(g_aCmdCommonOptions));
                            idxOpts += RT_ELEMENTS(g_aCmdCommonOptions);

                            memcpy(&paCombinedOptions[idxOpts], pCmd->paOptions, pCmd->cOptions * sizeof(RTGETOPTDEF));
                            idxOpts += (uint32_t)pCmd->cOptions;

                            if (pCmd->fNeedsTransport)
                                for (uintptr_t iTx = 0; iTx < g_cTransports; iTx++)
                                {
                                    memcpy(&paCombinedOptions[idxOpts],
                                           g_apTransports[iTx]->paOpts, g_apTransports[iTx]->cOpts * sizeof(RTGETOPTDEF));
                                    idxOpts += (uint32_t)g_apTransports[iTx]->cOpts;
                                }

                            /* Re-initialize the option getter state and pass it to the command handler. */
                            rc = RTGetOptInit(&GetState, argc, argv, paCombinedOptions, cCombinedOptions,
                                              GetState.iNext /*idxFirst*/, RTGETOPTINIT_FLAGS_OPTS_FIRST);
                            if (RT_SUCCESS(rc))
                            {
                                RTEXITCODE rcExit = pCmd->pfnHandler(&GetState);
                                RTMemFree(paCombinedOptions);
                                return rcExit;
                            }
                            RTMemFree(paCombinedOptions);
                            return RTMsgErrorExitFailure("RTGetOptInit failed for '%s': %Rrc", ValueUnion.psz, rc);
                        }
                        return RTMsgErrorExitFailure("Out of memory!");
                    }
                }
                RTMsgError("Unknown command '%s'!\n", ValueUnion.psz);
                audioTestListCommands(g_pStdErr);
                return RTEXITCODE_SYNTAX;
            }

            default:
                return RTGetOptPrintError(ch, &ValueUnion);
        }
    }

    RTMsgError("No command specified!\n");
    audioTestListCommands(g_pStdErr);
    return RTEXITCODE_SYNTAX;
}
