summaryrefslogtreecommitdiffstats
path: root/src/VBox/ValidationKit/utils/audio/vkatCmdGeneric.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/VBox/ValidationKit/utils/audio/vkatCmdGeneric.cpp')
-rw-r--r--src/VBox/ValidationKit/utils/audio/vkatCmdGeneric.cpp1169
1 files changed, 1169 insertions, 0 deletions
diff --git a/src/VBox/ValidationKit/utils/audio/vkatCmdGeneric.cpp b/src/VBox/ValidationKit/utils/audio/vkatCmdGeneric.cpp
new file mode 100644
index 00000000..18eb9487
--- /dev/null
+++ b/src/VBox/ValidationKit/utils/audio/vkatCmdGeneric.cpp
@@ -0,0 +1,1169 @@
+/* $Id: vkatCmdGeneric.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 *
+*********************************************************************************************************************************/
+#include <iprt/errcore.h>
+#include <iprt/message.h>
+#include <iprt/rand.h>
+#include <iprt/test.h>
+
+#include "vkatInternal.h"
+
+
+/*********************************************************************************************************************************
+* Command: backends *
+*********************************************************************************************************************************/
+
+/**
+ * Options for 'backends'.
+ */
+static const RTGETOPTDEF g_aCmdBackendsOptions[] =
+{
+ { "--dummy", 'd', RTGETOPT_REQ_NOTHING }, /* just a placeholder */
+};
+
+
+/** The 'backends' command option help. */
+static DECLCALLBACK(const char *) audioTestCmdBackendsHelp(PCRTGETOPTDEF pOpt)
+{
+ RT_NOREF(pOpt);
+ return NULL;
+}
+
+/**
+ * The 'backends' command handler.
+ *
+ * @returns Program exit code.
+ * @param pGetState RTGetOpt state.
+ */
+static DECLCALLBACK(RTEXITCODE) audioTestCmdBackendsHandler(PRTGETOPTSTATE pGetState)
+{
+ /*
+ * Parse options.
+ */
+ int ch;
+ RTGETOPTUNION ValueUnion;
+ while ((ch = RTGetOpt(pGetState, &ValueUnion)) != 0)
+ {
+ switch (ch)
+ {
+ AUDIO_TEST_COMMON_OPTION_CASES(ValueUnion, &g_CmdBackends);
+
+ default:
+ return RTGetOptPrintError(ch, &ValueUnion);
+ }
+ }
+
+ /*
+ * List the backends.
+ */
+ RTPrintf("Backends (%u):\n", g_cBackends);
+ for (size_t i = 0; i < g_cBackends; i++)
+ RTPrintf(" %12s - %s\n", g_aBackends[i].pszName, g_aBackends[i].pDrvReg->pszDescription);
+
+ return RTEXITCODE_SUCCESS;
+}
+
+
+/**
+ * Command table entry for 'backends'.
+ */
+const VKATCMD g_CmdBackends =
+{
+ /* .pszCommand = */ "backends",
+ /* .pfnHandler = */ audioTestCmdBackendsHandler,
+ /* .pszDesc = */ "Lists the compiled in audio backends.",
+ /* .paOptions = */ g_aCmdBackendsOptions,
+ /* .cOptions = */ 0 /*RT_ELEMENTS(g_aCmdBackendsOptions)*/,
+ /* .pfnOptionHelp = */ audioTestCmdBackendsHelp,
+ /* .fNeedsTransport = */ false
+};
+
+
+/*********************************************************************************************************************************
+* Command: enum *
+*********************************************************************************************************************************/
+
+
+
+/**
+ * Long option values for the 'enum' command.
+ */
+enum
+{
+ VKAT_ENUM_OPT_PROBE_BACKENDS = 900
+};
+
+/**
+ * Options for 'enum'.
+ */
+static const RTGETOPTDEF g_aCmdEnumOptions[] =
+{
+ { "--backend", 'b', RTGETOPT_REQ_STRING },
+ { "--probe-backends", VKAT_ENUM_OPT_PROBE_BACKENDS, RTGETOPT_REQ_NOTHING }
+};
+
+
+/** The 'enum' command option help. */
+static DECLCALLBACK(const char *) audioTestCmdEnumHelp(PCRTGETOPTDEF pOpt)
+{
+ switch (pOpt->iShort)
+ {
+ case 'b': return "The audio backend to use";
+ case VKAT_ENUM_OPT_PROBE_BACKENDS: return "Probes all (available) backends until a working one is found";
+ default: return NULL;
+ }
+}
+
+/**
+ * The 'enum' command handler.
+ *
+ * @returns Program exit code.
+ * @param pGetState RTGetOpt state.
+ */
+static DECLCALLBACK(RTEXITCODE) audioTestCmdEnumHandler(PRTGETOPTSTATE pGetState)
+{
+ /*
+ * Parse options.
+ */
+ /* Option values: */
+ PCPDMDRVREG pDrvReg = AudioTestGetDefaultBackend();
+ bool fProbeBackends = false;
+
+ /* Argument processing loop: */
+ int ch;
+ RTGETOPTUNION ValueUnion;
+ while ((ch = RTGetOpt(pGetState, &ValueUnion)) != 0)
+ {
+ switch (ch)
+ {
+ case 'b':
+ pDrvReg = AudioTestFindBackendOpt(ValueUnion.psz);
+ if (pDrvReg == NULL)
+ return RTEXITCODE_SYNTAX;
+ break;
+
+ case VKAT_ENUM_OPT_PROBE_BACKENDS:
+ fProbeBackends = true;
+ break;
+
+ AUDIO_TEST_COMMON_OPTION_CASES(ValueUnion, &g_CmdEnum);
+
+ default:
+ return RTGetOptPrintError(ch, &ValueUnion);
+ }
+ }
+
+ int rc;
+
+ AUDIOTESTDRVSTACK DrvStack;
+ if (fProbeBackends)
+ rc = audioTestDriverStackProbe(&DrvStack, pDrvReg,
+ true /* fEnabledIn */, true /* fEnabledOut */, false /* fWithDrvAudio */);
+ else
+ rc = audioTestDriverStackInitEx(&DrvStack, pDrvReg,
+ true /* fEnabledIn */, true /* fEnabledOut */, false /* fWithDrvAudio */);
+ if (RT_FAILURE(rc))
+ return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Unable to init driver stack: %Rrc\n", rc);
+
+ /*
+ * Do the enumeration.
+ */
+ RTEXITCODE rcExit = RTEXITCODE_FAILURE;
+
+ if (DrvStack.pIHostAudio->pfnGetDevices)
+ {
+ PDMAUDIOHOSTENUM Enum;
+ rc = DrvStack.pIHostAudio->pfnGetDevices(DrvStack.pIHostAudio, &Enum);
+ if (RT_SUCCESS(rc))
+ {
+ RTPrintf("Found %u device%s\n", Enum.cDevices, Enum.cDevices != 1 ? "s" : "");
+
+ PPDMAUDIOHOSTDEV pHostDev;
+ RTListForEach(&Enum.LstDevices, pHostDev, PDMAUDIOHOSTDEV, ListEntry)
+ {
+ RTPrintf("\nDevice \"%s\":\n", pHostDev->pszName);
+
+ char szFlags[PDMAUDIOHOSTDEV_MAX_FLAGS_STRING_LEN];
+ if (pHostDev->cMaxInputChannels && !pHostDev->cMaxOutputChannels && pHostDev->enmUsage == PDMAUDIODIR_IN)
+ RTPrintf(" Input: max %u channels (%s)\n",
+ pHostDev->cMaxInputChannels, PDMAudioHostDevFlagsToString(szFlags, pHostDev->fFlags));
+ else if (!pHostDev->cMaxInputChannels && pHostDev->cMaxOutputChannels && pHostDev->enmUsage == PDMAUDIODIR_OUT)
+ RTPrintf(" Output: max %u channels (%s)\n",
+ pHostDev->cMaxOutputChannels, PDMAudioHostDevFlagsToString(szFlags, pHostDev->fFlags));
+ else
+ RTPrintf(" %s: max %u output channels, max %u input channels (%s)\n",
+ PDMAudioDirGetName(pHostDev->enmUsage), pHostDev->cMaxOutputChannels,
+ pHostDev->cMaxInputChannels, PDMAudioHostDevFlagsToString(szFlags, pHostDev->fFlags));
+
+ if (pHostDev->pszId && *pHostDev->pszId)
+ RTPrintf(" ID: \"%s\"\n", pHostDev->pszId);
+ }
+
+ PDMAudioHostEnumDelete(&Enum);
+ }
+ else
+ rcExit = RTMsgErrorExitFailure("Enumeration failed: %Rrc\n", rc);
+ }
+ else
+ rcExit = RTMsgErrorExitFailure("Enumeration not supported by backend '%s'\n", pDrvReg->szName);
+ audioTestDriverStackDelete(&DrvStack);
+
+ return RTEXITCODE_SUCCESS;
+}
+
+
+/**
+ * Command table entry for 'enum'.
+ */
+const VKATCMD g_CmdEnum =
+{
+ "enum",
+ audioTestCmdEnumHandler,
+ "Enumerates audio devices.",
+ g_aCmdEnumOptions,
+ RT_ELEMENTS(g_aCmdEnumOptions),
+ audioTestCmdEnumHelp,
+ false /* fNeedsTransport */
+};
+
+
+
+
+/*********************************************************************************************************************************
+* Command: play *
+*********************************************************************************************************************************/
+
+/**
+ * Worker for audioTestPlayOne implementing the play loop.
+ */
+static RTEXITCODE audioTestPlayOneInner(PAUDIOTESTDRVMIXSTREAM pMix, PAUDIOTESTWAVEFILE pWaveFile,
+ PCPDMAUDIOSTREAMCFG pCfgAcq, const char *pszFile)
+{
+ uint32_t const cbPreBuffer = PDMAudioPropsFramesToBytes(pMix->pProps, pCfgAcq->Backend.cFramesPreBuffering);
+ uint64_t const nsStarted = RTTimeNanoTS();
+ uint64_t nsDonePreBuffering = 0;
+
+ /*
+ * Transfer data as quickly as we're allowed.
+ */
+ uint8_t abSamples[16384];
+ uint32_t const cbSamplesAligned = PDMAudioPropsFloorBytesToFrame(pMix->pProps, sizeof(abSamples));
+ uint64_t offStream = 0;
+ while (!g_fTerminate)
+ {
+ /* Read a chunk from the wave file. */
+ size_t cbSamples = 0;
+ int rc = AudioTestWaveFileRead(pWaveFile, abSamples, cbSamplesAligned, &cbSamples);
+ if (RT_SUCCESS(rc) && cbSamples > 0)
+ {
+ /* Pace ourselves a little. */
+ if (offStream >= cbPreBuffer)
+ {
+ if (!nsDonePreBuffering)
+ nsDonePreBuffering = RTTimeNanoTS();
+ uint64_t const cNsWritten = PDMAudioPropsBytesToNano64(pMix->pProps, offStream - cbPreBuffer);
+ uint64_t const cNsElapsed = RTTimeNanoTS() - nsStarted;
+ if (cNsWritten > cNsElapsed + RT_NS_10MS)
+ RTThreadSleep((cNsWritten - cNsElapsed - RT_NS_10MS / 2) / RT_NS_1MS);
+ }
+
+ /* Transfer the data to the audio stream. */
+ for (uint32_t offSamples = 0; offSamples < cbSamples;)
+ {
+ uint32_t const cbCanWrite = AudioTestMixStreamGetWritable(pMix);
+ if (cbCanWrite > 0)
+ {
+ uint32_t const cbToPlay = RT_MIN(cbCanWrite, (uint32_t)cbSamples - offSamples);
+ uint32_t cbPlayed = 0;
+ rc = AudioTestMixStreamPlay(pMix, &abSamples[offSamples], cbToPlay, &cbPlayed);
+ if (RT_SUCCESS(rc))
+ {
+ if (cbPlayed)
+ {
+ offSamples += cbPlayed;
+ offStream += cbPlayed;
+ }
+ else
+ return RTMsgErrorExitFailure("Played zero bytes - %#x bytes reported playable!\n", cbCanWrite);
+ }
+ else
+ return RTMsgErrorExitFailure("Failed to play %#x bytes: %Rrc\n", cbToPlay, rc);
+ }
+ else if (AudioTestMixStreamIsOkay(pMix))
+ RTThreadSleep(RT_MIN(RT_MAX(1, pCfgAcq->Device.cMsSchedulingHint), 256));
+ else
+ return RTMsgErrorExitFailure("Stream is not okay!\n");
+ }
+ }
+ else if (RT_SUCCESS(rc) && cbSamples == 0)
+ break;
+ else
+ return RTMsgErrorExitFailure("Error reading wav file '%s': %Rrc", pszFile, rc);
+ }
+
+ /*
+ * Drain the stream.
+ */
+ if (g_uVerbosity > 0)
+ RTMsgInfo("%'RU64 ns: Draining...\n", RTTimeNanoTS() - nsStarted);
+ int rc = AudioTestMixStreamDrain(pMix, true /*fSync*/);
+ if (RT_SUCCESS(rc))
+ {
+ if (g_uVerbosity > 0)
+ RTMsgInfo("%'RU64 ns: Done\n", RTTimeNanoTS() - nsStarted);
+ }
+ else
+ return RTMsgErrorExitFailure("Draining failed: %Rrc", rc);
+
+ return RTEXITCODE_SUCCESS;
+}
+
+/**
+ * Worker for audioTestCmdPlayHandler that plays one file.
+ */
+static RTEXITCODE audioTestPlayOne(const char *pszFile, PCPDMDRVREG pDrvReg, const char *pszDevId,
+ PAUDIOTESTIOOPTS pIoOpts)
+{
+ char szTmp[128];
+
+ /*
+ * First we must open the file and determin the format.
+ */
+ RTERRINFOSTATIC ErrInfo;
+ AUDIOTESTWAVEFILE WaveFile;
+ int rc = AudioTestWaveFileOpen(pszFile, &WaveFile, RTErrInfoInitStatic(&ErrInfo));
+ if (RT_FAILURE(rc))
+ return RTMsgErrorExitFailure("Failed to open '%s': %Rrc%#RTeim", pszFile, rc, &ErrInfo.Core);
+
+ if (g_uVerbosity > 0)
+ {
+ RTMsgInfo("Opened '%s' for playing\n", pszFile);
+ RTMsgInfo("Format: %s\n", PDMAudioPropsToString(&WaveFile.Props, szTmp, sizeof(szTmp)));
+ RTMsgInfo("Size: %'RU32 bytes / %#RX32 / %'RU32 frames / %'RU64 ns\n",
+ WaveFile.cbSamples, WaveFile.cbSamples,
+ PDMAudioPropsBytesToFrames(&WaveFile.Props, WaveFile.cbSamples),
+ PDMAudioPropsBytesToNano(&WaveFile.Props, WaveFile.cbSamples));
+ }
+
+ /*
+ * Construct the driver stack.
+ */
+ RTEXITCODE rcExit = RTEXITCODE_FAILURE;
+ AUDIOTESTDRVSTACK DrvStack;
+ rc = audioTestDriverStackInit(&DrvStack, pDrvReg, pIoOpts->fWithDrvAudio);
+ if (RT_SUCCESS(rc))
+ {
+ /*
+ * Set the output device if one is specified.
+ */
+ rc = audioTestDriverStackSetDevice(&DrvStack, PDMAUDIODIR_OUT, pszDevId);
+ if (RT_SUCCESS(rc))
+ {
+ /*
+ * Open a stream for the output.
+ */
+ uint8_t const cChannels = PDMAudioPropsChannels(&pIoOpts->Props);
+
+ PDMAUDIOPCMPROPS ReqProps = WaveFile.Props;
+ if (cChannels != 0 && PDMAudioPropsChannels(&ReqProps) != cChannels)
+ PDMAudioPropsSetChannels(&ReqProps, cChannels);
+
+ uint8_t const cbSample = PDMAudioPropsSampleSize(&pIoOpts->Props);
+ if (cbSample != 0)
+ PDMAudioPropsSetSampleSize(&ReqProps, cbSample);
+
+ uint32_t const uHz = PDMAudioPropsHz(&pIoOpts->Props);
+ if (uHz != 0)
+ ReqProps.uHz = uHz;
+
+ PDMAUDIOSTREAMCFG CfgAcq;
+ PPDMAUDIOSTREAM pStream = NULL;
+ rc = audioTestDriverStackStreamCreateOutput(&DrvStack, &ReqProps, pIoOpts->cMsBufferSize,
+ pIoOpts->cMsPreBuffer, pIoOpts->cMsSchedulingHint, &pStream, &CfgAcq);
+ if (RT_SUCCESS(rc))
+ {
+ /*
+ * Automatically enable the mixer if the wave file and the
+ * output parameters doesn't match.
+ */
+ if ( !pIoOpts->fWithMixer
+ && ( !PDMAudioPropsAreEqual(&WaveFile.Props, &pStream->Cfg.Props)
+ || pIoOpts->uVolumePercent != 100)
+ )
+ {
+ RTMsgInfo("Enabling the mixer buffer.\n");
+ pIoOpts->fWithMixer = true;
+ }
+
+ /*
+ * Create a mixer wrapper. This is just a thin wrapper if fWithMixer
+ * is false, otherwise it's doing mixing, resampling and recoding.
+ */
+ AUDIOTESTDRVMIXSTREAM Mix;
+ rc = AudioTestMixStreamInit(&Mix, &DrvStack, pStream, pIoOpts->fWithMixer ? &WaveFile.Props : NULL, 100 /*ms*/);
+ if (RT_SUCCESS(rc))
+ {
+ if (g_uVerbosity > 0)
+ RTMsgInfo("Stream: %s cbBackend=%#RX32%s\n",
+ PDMAudioPropsToString(&pStream->Cfg.Props, szTmp, sizeof(szTmp)),
+ pStream->cbBackend, pIoOpts->fWithMixer ? " mixed" : "");
+
+ if (pIoOpts->fWithMixer)
+ AudioTestMixStreamSetVolume(&Mix, pIoOpts->uVolumePercent);
+
+ /*
+ * Enable the stream and start playing.
+ */
+ rc = AudioTestMixStreamEnable(&Mix);
+ if (RT_SUCCESS(rc))
+ rcExit = audioTestPlayOneInner(&Mix, &WaveFile, &CfgAcq, pszFile);
+ else
+ rcExit = RTMsgErrorExitFailure("Enabling the output stream failed: %Rrc", rc);
+
+ /*
+ * Clean up.
+ */
+ AudioTestMixStreamTerm(&Mix);
+ }
+ audioTestDriverStackStreamDestroy(&DrvStack, pStream);
+ pStream = NULL;
+ }
+ else
+ rcExit = RTMsgErrorExitFailure("Creating output stream failed: %Rrc", rc);
+ }
+ else
+ rcExit = RTMsgErrorExitFailure("Failed to set output device to '%s': %Rrc", pszDevId, rc);
+ audioTestDriverStackDelete(&DrvStack);
+ }
+ else
+ rcExit = RTMsgErrorExitFailure("Driver stack construction failed: %Rrc", rc);
+ AudioTestWaveFileClose(&WaveFile);
+ return rcExit;
+}
+
+/**
+ * Worker for audioTestCmdPlayHandler that plays one test tone.
+ */
+static RTEXITCODE audioTestPlayTestToneOne(PAUDIOTESTTONEPARMS pToneParms,
+ PCPDMDRVREG pDrvReg, const char *pszDevId,
+ PAUDIOTESTIOOPTS pIoOpts)
+{
+ char szTmp[128];
+
+ AUDIOTESTSTREAM TstStream;
+ RT_ZERO(TstStream);
+
+ /*
+ * Construct the driver stack.
+ */
+ RTEXITCODE rcExit = RTEXITCODE_FAILURE;
+ AUDIOTESTDRVSTACK DrvStack;
+ int rc = audioTestDriverStackInit(&DrvStack, pDrvReg, pIoOpts->fWithDrvAudio);
+ if (RT_SUCCESS(rc))
+ {
+ /*
+ * Set the output device if one is specified.
+ */
+ rc = audioTestDriverStackSetDevice(&DrvStack, PDMAUDIODIR_OUT, pszDevId);
+ if (RT_SUCCESS(rc))
+ {
+ /*
+ * Open a stream for the output.
+ */
+ uint8_t const cChannels = PDMAudioPropsChannels(&pIoOpts->Props);
+
+ PDMAUDIOPCMPROPS ReqProps = pToneParms->Props;
+ if (cChannels != 0 && PDMAudioPropsChannels(&ReqProps) != cChannels)
+ PDMAudioPropsSetChannels(&ReqProps, cChannels);
+
+ uint8_t const cbSample = PDMAudioPropsSampleSize(&pIoOpts->Props);
+ if (cbSample != 0)
+ PDMAudioPropsSetSampleSize(&ReqProps, cbSample);
+
+ uint32_t const uHz = PDMAudioPropsHz(&pIoOpts->Props);
+ if (uHz != 0)
+ ReqProps.uHz = uHz;
+
+ rc = audioTestDriverStackStreamCreateOutput(&DrvStack, &ReqProps, pIoOpts->cMsBufferSize,
+ pIoOpts->cMsPreBuffer, pIoOpts->cMsSchedulingHint, &TstStream.pStream, &TstStream.Cfg);
+ if (RT_SUCCESS(rc))
+ {
+ /*
+ * Automatically enable the mixer if the wave file and the
+ * output parameters doesn't match.
+ */
+ if ( !pIoOpts->fWithMixer
+ && ( !PDMAudioPropsAreEqual(&pToneParms->Props, &TstStream.pStream->Cfg.Props)
+ || pToneParms->uVolumePercent != 100)
+ )
+ {
+ RTMsgInfo("Enabling the mixer buffer.\n");
+ pIoOpts->fWithMixer = true;
+ }
+
+ /*
+ * Create a mixer wrapper. This is just a thin wrapper if fWithMixer
+ * is false, otherwise it's doing mixing, resampling and recoding.
+ */
+ rc = AudioTestMixStreamInit(&TstStream.Mix, &DrvStack, TstStream.pStream,
+ pIoOpts->fWithMixer ? &pToneParms->Props : NULL, 100 /*ms*/);
+ if (RT_SUCCESS(rc))
+ {
+ if (g_uVerbosity > 0)
+ RTMsgInfo("Stream: %s cbBackend=%#RX32%s\n",
+ PDMAudioPropsToString(&TstStream.pStream->Cfg.Props, szTmp, sizeof(szTmp)),
+ TstStream.pStream->cbBackend, pIoOpts->fWithMixer ? " mixed" : "");
+
+ /*
+ * Enable the stream and start playing.
+ */
+ rc = AudioTestMixStreamEnable(&TstStream.Mix);
+ if (RT_SUCCESS(rc))
+ {
+ if (pIoOpts->fWithMixer)
+ AudioTestMixStreamSetVolume(&TstStream.Mix, pToneParms->uVolumePercent);
+
+ rc = audioTestPlayTone(pIoOpts, NULL /* pTstEnv */, &TstStream, pToneParms);
+ if (RT_SUCCESS(rc))
+ rcExit = RTEXITCODE_SUCCESS;
+ }
+ else
+ rcExit = RTMsgErrorExitFailure("Enabling the output stream failed: %Rrc", rc);
+
+ /*
+ * Clean up.
+ */
+ AudioTestMixStreamTerm(&TstStream.Mix);
+ }
+ audioTestDriverStackStreamDestroy(&DrvStack, TstStream.pStream);
+ TstStream.pStream = NULL;
+ }
+ else
+ rcExit = RTMsgErrorExitFailure("Creating output stream failed: %Rrc", rc);
+ }
+ else
+ rcExit = RTMsgErrorExitFailure("Failed to set output device to '%s': %Rrc", pszDevId, rc);
+ audioTestDriverStackDelete(&DrvStack);
+ }
+ else
+ rcExit = RTMsgErrorExitFailure("Driver stack construction failed: %Rrc", rc);
+ return rcExit;
+}
+
+
+/**
+ * Long option values for the 'play' command.
+ */
+enum
+{
+ VKAT_PLAY_OPT_TONE_DUR = 900,
+ VKAT_PLAY_OPT_TONE_FREQ,
+ VKAT_PLAY_OPT_TONE_VOL,
+ VKAT_PLAY_OPT_VOL
+};
+
+
+/**
+ * Options for 'play'.
+ */
+static const RTGETOPTDEF g_aCmdPlayOptions[] =
+{
+ { "--backend", 'b', RTGETOPT_REQ_STRING },
+ { "--channels", 'c', RTGETOPT_REQ_UINT8 },
+ { "--hz", 'f', RTGETOPT_REQ_UINT32 },
+ { "--frequency", 'f', RTGETOPT_REQ_UINT32 },
+ { "--sample-size", 'z', RTGETOPT_REQ_UINT8 },
+ { "--test-tone", 't', RTGETOPT_REQ_NOTHING },
+ { "--tone-dur", VKAT_PLAY_OPT_TONE_DUR, RTGETOPT_REQ_UINT32 },
+ { "--tone-freq", VKAT_PLAY_OPT_TONE_FREQ, RTGETOPT_REQ_UINT32 },
+ { "--tone-vol", VKAT_PLAY_OPT_TONE_VOL, RTGETOPT_REQ_UINT32 },
+ { "--output-device", 'o', RTGETOPT_REQ_STRING },
+ { "--with-drv-audio", 'd', RTGETOPT_REQ_NOTHING },
+ { "--with-mixer", 'm', RTGETOPT_REQ_NOTHING },
+ { "--vol", VKAT_PLAY_OPT_VOL, RTGETOPT_REQ_UINT8 }
+};
+
+
+/** The 'play' command option help. */
+static DECLCALLBACK(const char *) audioTestCmdPlayHelp(PCRTGETOPTDEF pOpt)
+{
+ switch (pOpt->iShort)
+ {
+ case 'b': return "The audio backend to use";
+ case 'c': return "Number of backend output channels";
+ case 'd': return "Go via DrvAudio instead of directly interfacing with the backend";
+ case 'f': return "Output frequency (Hz)";
+ case 'z': return "Output sample size (bits)";
+ case 't': return "Plays a test tone. Can be specified multiple times";
+ case 'm': return "Go via the mixer";
+ case 'o': return "The ID of the output device to use";
+ case VKAT_PLAY_OPT_TONE_DUR: return "Test tone duration (ms)";
+ case VKAT_PLAY_OPT_TONE_FREQ: return "Test tone frequency (Hz)";
+ case VKAT_PLAY_OPT_TONE_VOL: return "Test tone volume (percent)";
+ case VKAT_PLAY_OPT_VOL: return "Playback volume (percent)";
+ default: return NULL;
+ }
+}
+
+
+/**
+ * The 'play' command handler.
+ *
+ * @returns Program exit code.
+ * @param pGetState RTGetOpt state.
+ */
+static DECLCALLBACK(RTEXITCODE) audioTestCmdPlayHandler(PRTGETOPTSTATE pGetState)
+{
+ /* Option values: */
+ PCPDMDRVREG pDrvReg = AudioTestGetDefaultBackend();
+ const char *pszDevId = NULL;
+ uint32_t cTestTones = 0;
+ uint8_t cbSample = 0;
+ uint8_t cChannels = 0;
+ uint32_t uHz = 0;
+
+ AUDIOTESTIOOPTS IoOpts;
+ audioTestIoOptsInitDefaults(&IoOpts);
+
+ AUDIOTESTTONEPARMS ToneParms;
+ audioTestToneParmsInit(&ToneParms);
+
+ /* Argument processing loop: */
+ int ch;
+ RTGETOPTUNION ValueUnion;
+ while ((ch = RTGetOpt(pGetState, &ValueUnion)) != 0)
+ {
+ switch (ch)
+ {
+ case 'b':
+ pDrvReg = AudioTestFindBackendOpt(ValueUnion.psz);
+ if (pDrvReg == NULL)
+ return RTEXITCODE_SYNTAX;
+ break;
+
+ case 'c':
+ cChannels = ValueUnion.u8;
+ break;
+
+ case 'd':
+ IoOpts.fWithDrvAudio = true;
+ break;
+
+ case 'f':
+ uHz = ValueUnion.u32;
+ break;
+
+ case 'm':
+ IoOpts.fWithMixer = true;
+ break;
+
+ case 'o':
+ pszDevId = ValueUnion.psz;
+ break;
+
+ case 't':
+ cTestTones++;
+ break;
+
+ case 'z':
+ cbSample = ValueUnion.u8 / 8;
+ break;
+
+ case VKAT_PLAY_OPT_TONE_DUR:
+ ToneParms.msDuration = ValueUnion.u32;
+ break;
+
+ case VKAT_PLAY_OPT_TONE_FREQ:
+ ToneParms.dbFreqHz = ValueUnion.u32;
+ break;
+
+ case VKAT_PLAY_OPT_TONE_VOL:
+ ToneParms.uVolumePercent = ValueUnion.u8;
+ if (ToneParms.uVolumePercent > 100)
+ return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Invalid tonevolume (0-100)");
+ break;
+
+ case VKAT_PLAY_OPT_VOL:
+ IoOpts.uVolumePercent = ValueUnion.u8;
+ if (IoOpts.uVolumePercent > 100)
+ return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Invalid playback volume (0-100)");
+ break;
+
+ case VINF_GETOPT_NOT_OPTION:
+ {
+ if (cTestTones)
+ return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Playing test tones (-t) cannot be combined with playing files");
+
+ /* Set new (override standard) I/O PCM properties if set by the user. */
+ PDMAudioPropsInit(&IoOpts.Props,
+ cbSample ? cbSample : 2 /* 16-bit */, true /* fSigned */,
+ cChannels ? cChannels : 2 /* Stereo */, uHz ? uHz : 44100);
+
+ RTEXITCODE rcExit = audioTestPlayOne(ValueUnion.psz, pDrvReg, pszDevId, &IoOpts);
+ if (rcExit != RTEXITCODE_SUCCESS)
+ return rcExit;
+ break;
+ }
+
+ AUDIO_TEST_COMMON_OPTION_CASES(ValueUnion, &g_CmdPlay);
+
+ default:
+ return RTGetOptPrintError(ch, &ValueUnion);
+ }
+ }
+
+ while (cTestTones--)
+ {
+ /* Use some sane defaults if no PCM props are set by the user. */
+ PDMAudioPropsInit(&ToneParms.Props,
+ cbSample ? cbSample : 2 /* 16-bit */, true /* fSigned */,
+ cChannels ? cChannels : 2 /* Stereo */, uHz ? uHz : 44100);
+
+ RTEXITCODE rcExit = audioTestPlayTestToneOne(&ToneParms, pDrvReg, pszDevId, &IoOpts);
+ if (rcExit != RTEXITCODE_SUCCESS)
+ return rcExit;
+ }
+
+ return RTEXITCODE_SUCCESS;
+}
+
+
+/**
+ * Command table entry for 'play'.
+ */
+const VKATCMD g_CmdPlay =
+{
+ "play",
+ audioTestCmdPlayHandler,
+ "Plays one or more wave files.",
+ g_aCmdPlayOptions,
+ RT_ELEMENTS(g_aCmdPlayOptions),
+ audioTestCmdPlayHelp,
+ false /* fNeedsTransport */
+};
+
+
+/*********************************************************************************************************************************
+* Command: rec *
+*********************************************************************************************************************************/
+
+/**
+ * Worker for audioTestRecOne implementing the recording loop.
+ */
+static RTEXITCODE audioTestRecOneInner(PAUDIOTESTDRVMIXSTREAM pMix, PAUDIOTESTWAVEFILE pWaveFile,
+ PCPDMAUDIOSTREAMCFG pCfgAcq, uint64_t cMaxFrames, const char *pszFile)
+{
+ int rc;
+ uint64_t const nsStarted = RTTimeNanoTS();
+
+ /*
+ * Transfer data as quickly as we're allowed.
+ */
+ uint8_t abSamples[16384];
+ uint32_t const cbSamplesAligned = PDMAudioPropsFloorBytesToFrame(pMix->pProps, sizeof(abSamples));
+ uint64_t cFramesCapturedTotal = 0;
+ while (!g_fTerminate && cFramesCapturedTotal < cMaxFrames)
+ {
+ /*
+ * Anything we can read?
+ */
+ uint32_t const cbCanRead = AudioTestMixStreamGetReadable(pMix);
+ if (cbCanRead)
+ {
+ uint32_t const cbToRead = RT_MIN(cbCanRead, cbSamplesAligned);
+ uint32_t cbCaptured = 0;
+ rc = AudioTestMixStreamCapture(pMix, abSamples, cbToRead, &cbCaptured);
+ if (RT_SUCCESS(rc))
+ {
+ if (cbCaptured)
+ {
+ uint32_t cFramesCaptured = PDMAudioPropsBytesToFrames(pMix->pProps, cbCaptured);
+ if (cFramesCaptured + cFramesCaptured < cMaxFrames)
+ { /* likely */ }
+ else
+ {
+ cFramesCaptured = cMaxFrames - cFramesCaptured;
+ cbCaptured = PDMAudioPropsFramesToBytes(pMix->pProps, cFramesCaptured);
+ }
+
+ rc = AudioTestWaveFileWrite(pWaveFile, abSamples, cbCaptured);
+ if (RT_SUCCESS(rc))
+ cFramesCapturedTotal += cFramesCaptured;
+ else
+ return RTMsgErrorExitFailure("Error writing to '%s': %Rrc", pszFile, rc);
+ }
+ else
+ return RTMsgErrorExitFailure("Captured zero bytes - %#x bytes reported readable!\n", cbCanRead);
+ }
+ else
+ return RTMsgErrorExitFailure("Failed to capture %#x bytes: %Rrc (%#x available)\n", cbToRead, rc, cbCanRead);
+ }
+ else if (AudioTestMixStreamIsOkay(pMix))
+ RTThreadSleep(RT_MIN(RT_MAX(1, pCfgAcq->Device.cMsSchedulingHint), 256));
+ else
+ return RTMsgErrorExitFailure("Stream is not okay!\n");
+ }
+
+ /*
+ * Disable the stream.
+ */
+ rc = AudioTestMixStreamDisable(pMix);
+ if (RT_SUCCESS(rc) && g_uVerbosity > 0)
+ RTMsgInfo("%'RU64 ns: Stopped after recording %RU64 frames%s\n", RTTimeNanoTS() - nsStarted, cFramesCapturedTotal,
+ g_fTerminate ? " - Ctrl-C" : ".");
+ else if (RT_FAILURE(rc))
+ return RTMsgErrorExitFailure("Disabling stream failed: %Rrc", rc);
+
+ return RTEXITCODE_SUCCESS;
+}
+
+
+/**
+ * Worker for audioTestCmdRecHandler that recs one file.
+ */
+static RTEXITCODE audioTestRecOne(const char *pszFile, uint8_t cWaveChannels, uint8_t cbWaveSample, uint32_t uWaveHz,
+ PCPDMDRVREG pDrvReg, const char *pszDevId, PAUDIOTESTIOOPTS pIoOpts,
+ uint64_t cMaxFrames, uint64_t cNsMaxDuration)
+{
+ /*
+ * Construct the driver stack.
+ */
+ RTEXITCODE rcExit = RTEXITCODE_FAILURE;
+ AUDIOTESTDRVSTACK DrvStack;
+ int rc = audioTestDriverStackInit(&DrvStack, pDrvReg, pIoOpts->fWithDrvAudio);
+ if (RT_SUCCESS(rc))
+ {
+ /*
+ * Set the input device if one is specified.
+ */
+ rc = audioTestDriverStackSetDevice(&DrvStack, PDMAUDIODIR_IN, pszDevId);
+ if (RT_SUCCESS(rc))
+ {
+ /*
+ * Create an input stream.
+ */
+ PDMAUDIOPCMPROPS ReqProps;
+ PDMAudioPropsInit(&ReqProps,
+ pIoOpts->Props.cbSampleX ? pIoOpts->Props.cbSampleX : cbWaveSample ? cbWaveSample : 2,
+ pIoOpts->Props.fSigned,
+ pIoOpts->Props.cChannelsX ? pIoOpts->Props.cChannelsX : cWaveChannels ? cWaveChannels : 2,
+ pIoOpts->Props.uHz ? pIoOpts->Props.uHz : uWaveHz ? uWaveHz : 44100);
+
+ PDMAUDIOSTREAMCFG CfgAcq;
+ PPDMAUDIOSTREAM pStream = NULL;
+ rc = audioTestDriverStackStreamCreateInput(&DrvStack, &ReqProps, pIoOpts->cMsBufferSize,
+ pIoOpts->cMsPreBuffer, pIoOpts->cMsSchedulingHint, &pStream, &CfgAcq);
+ if (RT_SUCCESS(rc))
+ {
+ /*
+ * Determine the wave file properties. If it differs from the stream
+ * properties, make sure the mixer is enabled.
+ */
+ PDMAUDIOPCMPROPS WaveProps;
+ PDMAudioPropsInit(&WaveProps,
+ cbWaveSample ? cbWaveSample : PDMAudioPropsSampleSize(&CfgAcq.Props),
+ true /*fSigned*/,
+ cWaveChannels ? cWaveChannels : PDMAudioPropsChannels(&CfgAcq.Props),
+ uWaveHz ? uWaveHz : PDMAudioPropsHz(&CfgAcq.Props));
+ if (!pIoOpts->fWithMixer && !PDMAudioPropsAreEqual(&WaveProps, &CfgAcq.Props))
+ {
+ RTMsgInfo("Enabling the mixer buffer.\n");
+ pIoOpts->fWithMixer = true;
+ }
+
+ /* Console the max duration into frames now that we've got the wave file format. */
+ if (cMaxFrames != UINT64_MAX && cNsMaxDuration != UINT64_MAX)
+ {
+ uint64_t cMaxFrames2 = PDMAudioPropsNanoToBytes64(&WaveProps, cNsMaxDuration);
+ cMaxFrames = RT_MAX(cMaxFrames, cMaxFrames2);
+ }
+ else if (cNsMaxDuration != UINT64_MAX)
+ cMaxFrames = PDMAudioPropsNanoToBytes64(&WaveProps, cNsMaxDuration);
+
+ /*
+ * Create a mixer wrapper. This is just a thin wrapper if fWithMixer
+ * is false, otherwise it's doing mixing, resampling and recoding.
+ */
+ AUDIOTESTDRVMIXSTREAM Mix;
+ rc = AudioTestMixStreamInit(&Mix, &DrvStack, pStream, pIoOpts->fWithMixer ? &WaveProps : NULL, 100 /*ms*/);
+ if (RT_SUCCESS(rc))
+ {
+ char szTmp[128];
+ if (g_uVerbosity > 0)
+ RTMsgInfo("Stream: %s cbBackend=%#RX32%s\n",
+ PDMAudioPropsToString(&pStream->Cfg.Props, szTmp, sizeof(szTmp)),
+ pStream->cbBackend, pIoOpts->fWithMixer ? " mixed" : "");
+
+ /*
+ * Open the wave output file.
+ */
+ AUDIOTESTWAVEFILE WaveFile;
+ RTERRINFOSTATIC ErrInfo;
+ rc = AudioTestWaveFileCreate(pszFile, &WaveProps, &WaveFile, RTErrInfoInitStatic(&ErrInfo));
+ if (RT_SUCCESS(rc))
+ {
+ if (g_uVerbosity > 0)
+ {
+ RTMsgInfo("Opened '%s' for playing\n", pszFile);
+ RTMsgInfo("Format: %s\n", PDMAudioPropsToString(&WaveFile.Props, szTmp, sizeof(szTmp)));
+ }
+
+ /*
+ * Enable the stream and start recording.
+ */
+ rc = AudioTestMixStreamEnable(&Mix);
+ if (RT_SUCCESS(rc))
+ rcExit = audioTestRecOneInner(&Mix, &WaveFile, &CfgAcq, cMaxFrames, pszFile);
+ else
+ rcExit = RTMsgErrorExitFailure("Enabling the input stream failed: %Rrc", rc);
+ if (rcExit != RTEXITCODE_SUCCESS)
+ AudioTestMixStreamDisable(&Mix);
+
+ /*
+ * Clean up.
+ */
+ rc = AudioTestWaveFileClose(&WaveFile);
+ if (RT_FAILURE(rc))
+ rcExit = RTMsgErrorExitFailure("Error closing '%s': %Rrc", pszFile, rc);
+ }
+ else
+ rcExit = RTMsgErrorExitFailure("Failed to open '%s': %Rrc%#RTeim", pszFile, rc, &ErrInfo.Core.pszMsg);
+
+ AudioTestMixStreamTerm(&Mix);
+ }
+ audioTestDriverStackStreamDestroy(&DrvStack, pStream);
+ pStream = NULL;
+ }
+ else
+ rcExit = RTMsgErrorExitFailure("Creating output stream failed: %Rrc", rc);
+ }
+ else
+ rcExit = RTMsgErrorExitFailure("Failed to set output device to '%s': %Rrc", pszDevId, rc);
+ audioTestDriverStackDelete(&DrvStack);
+ }
+ else
+ rcExit = RTMsgErrorExitFailure("Driver stack construction failed: %Rrc", rc);
+ return rcExit;
+}
+
+
+/**
+ * Options for 'rec'.
+ */
+static const RTGETOPTDEF g_aCmdRecOptions[] =
+{
+ { "--backend", 'b', RTGETOPT_REQ_STRING },
+ { "--channels", 'c', RTGETOPT_REQ_UINT8 },
+ { "--hz", 'f', RTGETOPT_REQ_UINT32 },
+ { "--frequency", 'f', RTGETOPT_REQ_UINT32 },
+ { "--sample-size", 'z', RTGETOPT_REQ_UINT8 },
+ { "--input-device", 'i', RTGETOPT_REQ_STRING },
+ { "--wav-channels", 'C', RTGETOPT_REQ_UINT8 },
+ { "--wav-hz", 'F', RTGETOPT_REQ_UINT32 },
+ { "--wav-frequency", 'F', RTGETOPT_REQ_UINT32 },
+ { "--wav-sample-size", 'Z', RTGETOPT_REQ_UINT8 },
+ { "--with-drv-audio", 'd', RTGETOPT_REQ_NOTHING },
+ { "--with-mixer", 'm', RTGETOPT_REQ_NOTHING },
+ { "--max-frames", 'r', RTGETOPT_REQ_UINT64 },
+ { "--max-sec", 's', RTGETOPT_REQ_UINT64 },
+ { "--max-seconds", 's', RTGETOPT_REQ_UINT64 },
+ { "--max-ms", 't', RTGETOPT_REQ_UINT64 },
+ { "--max-milliseconds", 't', RTGETOPT_REQ_UINT64 },
+ { "--max-ns", 'T', RTGETOPT_REQ_UINT64 },
+ { "--max-nanoseconds", 'T', RTGETOPT_REQ_UINT64 },
+};
+
+
+/** The 'rec' command option help. */
+static DECLCALLBACK(const char *) audioTestCmdRecHelp(PCRTGETOPTDEF pOpt)
+{
+ switch (pOpt->iShort)
+ {
+ case 'b': return "The audio backend to use.";
+ case 'c': return "Number of backend input channels";
+ case 'C': return "Number of wave-file channels";
+ case 'd': return "Go via DrvAudio instead of directly interfacing with the backend.";
+ case 'f': return "Input frequency (Hz)";
+ case 'F': return "Wave-file frequency (Hz)";
+ case 'z': return "Input sample size (bits)";
+ case 'Z': return "Wave-file sample size (bits)";
+ case 'm': return "Go via the mixer.";
+ case 'i': return "The ID of the input device to use.";
+ case 'r': return "Max recording duration in frames.";
+ case 's': return "Max recording duration in seconds.";
+ case 't': return "Max recording duration in milliseconds.";
+ case 'T': return "Max recording duration in nanoseconds.";
+ default: return NULL;
+ }
+}
+
+
+/**
+ * The 'rec' command handler.
+ *
+ * @returns Program exit code.
+ * @param pGetState RTGetOpt state.
+ */
+static DECLCALLBACK(RTEXITCODE) audioTestCmdRecHandler(PRTGETOPTSTATE pGetState)
+{
+ /* Option values: */
+ PCPDMDRVREG pDrvReg = AudioTestGetDefaultBackend();
+ const char *pszDevId = NULL;
+ uint8_t cbSample = 0;
+ uint8_t cChannels = 0;
+ uint32_t uHz = 0;
+ uint8_t cbWaveSample = 0;
+ uint8_t cWaveChannels = 0;
+ uint32_t uWaveHz = 0;
+ uint64_t cMaxFrames = UINT64_MAX;
+ uint64_t cNsMaxDuration = UINT64_MAX;
+
+ AUDIOTESTIOOPTS IoOpts;
+ audioTestIoOptsInitDefaults(&IoOpts);
+
+ /* Argument processing loop: */
+ int ch;
+ RTGETOPTUNION ValueUnion;
+ while ((ch = RTGetOpt(pGetState, &ValueUnion)) != 0)
+ {
+ switch (ch)
+ {
+ case 'b':
+ pDrvReg = AudioTestFindBackendOpt(ValueUnion.psz);
+ if (pDrvReg == NULL)
+ return RTEXITCODE_SYNTAX;
+ break;
+
+ case 'c':
+ cChannels = ValueUnion.u8;
+ break;
+
+ case 'C':
+ cWaveChannels = ValueUnion.u8;
+ break;
+
+ case 'd':
+ IoOpts.fWithDrvAudio = true;
+ break;
+
+ case 'f':
+ uHz = ValueUnion.u32;
+ break;
+
+ case 'F':
+ uWaveHz = ValueUnion.u32;
+ break;
+
+ case 'i':
+ pszDevId = ValueUnion.psz;
+ break;
+
+ case 'm':
+ IoOpts.fWithMixer = true;
+ break;
+
+ case 'r':
+ cMaxFrames = ValueUnion.u64;
+ break;
+
+ case 's':
+ cNsMaxDuration = ValueUnion.u64 >= UINT64_MAX / RT_NS_1SEC ? UINT64_MAX : ValueUnion.u64 * RT_NS_1SEC;
+ break;
+
+ case 't':
+ cNsMaxDuration = ValueUnion.u64 >= UINT64_MAX / RT_NS_1MS ? UINT64_MAX : ValueUnion.u64 * RT_NS_1MS;
+ break;
+
+ case 'T':
+ cNsMaxDuration = ValueUnion.u64;
+ break;
+
+ case 'z':
+ cbSample = ValueUnion.u8 / 8;
+ break;
+
+ case 'Z':
+ cbWaveSample = ValueUnion.u8 / 8;
+ break;
+
+ case VINF_GETOPT_NOT_OPTION:
+ {
+ if ( cbSample
+ || cChannels
+ || uHz)
+ {
+ /* Set new (override standard) I/O PCM properties if set by the user. */
+ PDMAudioPropsInit(&IoOpts.Props,
+ cbSample ? cbSample : 2 /* 16-bit */, true /* fSigned */,
+ cChannels ? cChannels : 2 /* Stereo */, uHz ? uHz : 44100);
+ }
+
+ RTEXITCODE rcExit = audioTestRecOne(ValueUnion.psz, cWaveChannels, cbWaveSample, uWaveHz,
+ pDrvReg, pszDevId, &IoOpts,
+ cMaxFrames, cNsMaxDuration);
+ if (rcExit != RTEXITCODE_SUCCESS)
+ return rcExit;
+ break;
+ }
+
+ AUDIO_TEST_COMMON_OPTION_CASES(ValueUnion, &g_CmdRec);
+
+ default:
+ return RTGetOptPrintError(ch, &ValueUnion);
+ }
+ }
+ return RTEXITCODE_SUCCESS;
+}
+
+
+/**
+ * Command table entry for 'rec'.
+ */
+const VKATCMD g_CmdRec =
+{
+ "rec",
+ audioTestCmdRecHandler,
+ "Records audio to a wave file.",
+ g_aCmdRecOptions,
+ RT_ELEMENTS(g_aCmdRecOptions),
+ audioTestCmdRecHelp,
+ false /* fNeedsTransport */
+};
+