summaryrefslogtreecommitdiffstats
path: root/src/VBox/ValidationKit/utils/audio
diff options
context:
space:
mode:
Diffstat (limited to 'src/VBox/ValidationKit/utils/audio')
-rw-r--r--src/VBox/ValidationKit/utils/audio/Makefile.kmk220
-rw-r--r--src/VBox/ValidationKit/utils/audio/ntPlayToneWaveX.cpp226
-rw-r--r--src/VBox/ValidationKit/utils/audio/readme.txt2
-rw-r--r--src/VBox/ValidationKit/utils/audio/vkat.cpp1649
-rw-r--r--src/VBox/ValidationKit/utils/audio/vkatCmdGeneric.cpp1169
-rw-r--r--src/VBox/ValidationKit/utils/audio/vkatCmdSelfTest.cpp481
-rw-r--r--src/VBox/ValidationKit/utils/audio/vkatCommon.cpp1760
-rw-r--r--src/VBox/ValidationKit/utils/audio/vkatDriverStack.cpp1621
-rw-r--r--src/VBox/ValidationKit/utils/audio/vkatInternal.h547
9 files changed, 7675 insertions, 0 deletions
diff --git a/src/VBox/ValidationKit/utils/audio/Makefile.kmk b/src/VBox/ValidationKit/utils/audio/Makefile.kmk
new file mode 100644
index 00000000..f249189f
--- /dev/null
+++ b/src/VBox/ValidationKit/utils/audio/Makefile.kmk
@@ -0,0 +1,220 @@
+# $Id: Makefile.kmk $
+## @file
+# VirtualBox Validation Kit - Audio Utilities.
+#
+
+#
+# Copyright (C) 2010-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
+#
+
+SUB_DEPTH = ../../../../..
+include $(KBUILD_PATH)/subheader.kmk
+
+#
+# Make sure the ValKit config file is included when the additions build
+# is including just this makefile.
+#
+ifndef VBOX_VALIDATIONKIT_CONFIG_KMK_INCLUDED
+ include $(PATH_ROOT)/src/VBox/ValidationKit/Config.kmk
+endif
+
+
+#
+# Globals.
+#
+VBOX_PATH_SRC_DEVICES = $(PATH_ROOT)/src/VBox/Devices
+VKAT_PATH_AUDIO = $(VBOX_PATH_SRC_DEVICES)/Audio
+
+
+#
+# Append what we build here to PROGRAMS (at the top because it's a bit messy).
+#
+ifn1of ($(KBUILD_TARGET), os2 freebsd netbsd openbsd)
+ if defined(VBOX_ONLY_VALIDATIONKIT) || !defined(VBOX_ONLY_BUILD)
+ PROGRAMS += vkat
+ if defined(VBOX_WITH_HOST_SHIPPING_AUDIO_TEST) && !defined(VBOX_ONLY_BUILD)
+ PROGRAMS += vkathost
+ endif
+ endif
+ if defined(VBOX_WITH_ADDITIONS_SHIPPING_AUDIO_TEST) \
+ && defined(VBOX_WITH_ADDITIONS) \
+ && !defined(VBOX_WITH_ADDITIONS_FROM_BUILD_SERVER) \
+ && (defined(VBOX_ONLY_ADDITIONS) || !defined(VBOX_ONLY_BUILD))
+ PROGRAMS += vkatadd
+ endif
+endif
+
+
+#
+# Utility to play sine wave to Default Audio Device.
+#
+if 0 # Disabled for now; does not work without WinMM.dll import validator files.
+ PROGRAMS.win += ntPlayToneWaveX
+ ntPlayToneWaveX_TEMPLATE = VBoxValidationKitR3
+ ntPlayToneWaveX_SOURCES = ntPlayToneWaveX.cpp
+ ntPlayToneWaveX_LIBS += \
+ WinMM.Lib
+endif
+
+
+#
+# The Validation Kit Audio Test (VKAT) utility.
+#
+vkat_TEMPLATE = VBoxValidationKitR3
+vkat_VBOX_IMPORT_CHECKER.win.x86 = nt4
+vkat_DEFS = VBOX_AUDIO_VKAT IN_VMM_R3 IN_VMM_STATIC
+vkat_INCS = \
+ $(PATH_ROOT)/src/VBox/Devices/build \
+ $(PATH_ROOT)/src/VBox/Devices \
+ $(PATH_ROOT)/src/VBox/Devices/Audio
+vkat_SOURCES = \
+ vkat.cpp \
+ vkatCommon.cpp \
+ vkatCmdGeneric.cpp \
+ vkatDriverStack.cpp \
+ $(VKAT_PATH_AUDIO)/AudioTest.cpp \
+ $(VKAT_PATH_AUDIO)/DrvAudio.cpp \
+ $(VKAT_PATH_AUDIO)/DrvHostAudioNull.cpp \
+ $(VKAT_PATH_AUDIO)/AudioMixer.cpp \
+ $(VKAT_PATH_AUDIO)/AudioMixBuffer.cpp \
+ $(VKAT_PATH_AUDIO)/AudioHlp.cpp
+
+# Debug stuff.
+ifdef VBOX_WITH_AUDIO_DEBUG
+ vkat_DEFS += VBOX_WITH_AUDIO_DEBUG
+ vkat_SOURCES += \
+ $(VKAT_PATH_AUDIO)/DrvHostAudioDebug.cpp
+endif
+
+# Self-test stuff.
+vkat_DEFS += VBOX_WITH_AUDIO_VALIDATIONKIT
+vkat_SOURCES += \
+ vkatCmdSelfTest.cpp \
+ $(VKAT_PATH_AUDIO)/DrvHostAudioValidationKit.cpp \
+ $(VKAT_PATH_AUDIO)/AudioTestService.cpp \
+ $(VKAT_PATH_AUDIO)/AudioTestServiceClient.cpp \
+ $(VKAT_PATH_AUDIO)/AudioTestServiceProtocol.cpp \
+ $(VKAT_PATH_AUDIO)/AudioTestServiceTcp.cpp
+
+ifdef VBOX_WITH_AUDIO_PULSE
+ vkat_DEFS += VBOX_WITH_AUDIO_PULSE
+ vkat_SOURCES += \
+ $(VKAT_PATH_AUDIO)/DrvHostAudioPulseAudioStubs.cpp \
+ $(VKAT_PATH_AUDIO)/DrvHostAudioPulseAudio.cpp
+endif
+
+ifdef VBOX_WITH_AUDIO_ALSA
+ vkat_DEFS += VBOX_WITH_AUDIO_ALSA
+ vkat_SOURCES += \
+ $(VKAT_PATH_AUDIO)/DrvHostAudioAlsa.cpp \
+ $(VKAT_PATH_AUDIO)/DrvHostAudioAlsaStubs.cpp
+endif
+
+ifdef VBOX_WITH_AUDIO_OSS
+ vkat_DEFS += VBOX_WITH_AUDIO_OSS
+ vkat_SOURCES += \
+ $(VKAT_PATH_AUDIO)/DrvHostAudioOss.cpp
+endif
+
+vkat_SOURCES.win += \
+ $(VKAT_PATH_AUDIO)/DrvHostAudioDSound.cpp \
+ $(VKAT_PATH_AUDIO)/DrvHostAudioWasApi.cpp
+ifdef VBOX_WITH_AUDIO_MMNOTIFICATION_CLIENT
+ vkat_DEFS.win += VBOX_WITH_AUDIO_MMNOTIFICATION_CLIENT
+ vkat_SOURCES.win += \
+ $(VKAT_PATH_AUDIO)/DrvHostAudioDSoundMMNotifClient.cpp
+endif
+
+vkat_SOURCES.darwin = \
+ $(VKAT_PATH_AUDIO)/DrvHostAudioCoreAudio.cpp \
+ $(VKAT_PATH_AUDIO)/DrvHostAudioCoreAudioAuth.mm
+vkat_LDFLAGS.darwin = \
+ -framework CoreAudio \
+ -framework AudioUnit \
+ -framework AudioToolbox \
+ -framework Foundation
+ifn1of ($(VBOX_DEF_MACOSX_VERSION_MIN), 10.4 10.5 10.6)
+ vkat_LDFLAGS.darwin += \
+ -framework AVFoundation
+endif
+
+
+#
+# The additions variant of the audio test utility.
+#
+# We name it VBoxAudioTest though, to not clutter up Guest Additions
+# installations with cryptic binaries not sporting 'VBox' as prefix.
+#
+vkatadd_TEMPLATE = VBoxGuestR3Exe
+vkatadd_EXTENDS = vkat
+vkatadd_EXTENDS_BY = appending
+vkatadd_NAME = VBoxAudioTest
+vkatadd_SDKS = VBoxZlibStatic
+vkatadd_LDFLAGS.darwin = -framework IOKit
+vkatadd_LIBS.solaris = m
+
+
+#
+# Build the valkit vkat to bin as VBoxAudioTest, so that it can be shipped with
+# the host installer too.
+#
+# Note: We also need to have this as a signed binary, so don't just copy the
+# vkat binary to bin/ directory but built this as an own binary.
+#
+vkathost_TEMPLATE := VBoxR3Exe
+vkathost_EXTENDS := vkat
+vkathost_INST := $(INST_BIN)
+vkathost_NAME := VBoxAudioTest
+vkathost_SOURCES = \
+ $(vkat_SOURCES) \
+ $(VBOX_PATH_SRC_DEVICES)/build/VBoxDD.d
+vkathost_LIBS = \
+ $(LIB_RUNTIME)
+
+
+if defined(VBOX_WITH_TESTCASES) && !defined(VBOX_ONLY_ADDITIONS) && !defined(VBOX_ONLY_SDK) \
+ && 0 ## @todo r=bird: Disabled because nobody really wants or needs to run this during build other than Andy.
+ ## And more importantly, it breaks the build (os2, bsd*).
+
+ PROGRAMS += tstVkatHostSelftest
+ tstVkatHostSelftest_EXTENDS = vkat
+ tstVkatHostSelftest_EXTENDS_BY = appending
+ tstVkatHostSelftest_INST = $(INST_TESTCASE)
+ tstVkatHostSelftest_DEFS.debug = VBOX_WITH_EF_WRAPS
+
+ TESTING += $(tstVkatHostSelftest_0_OUTDIR)/tstVkatHostSelftest.run
+ $$(tstVkatHostSelftest_0_OUTDIR)/tstVkatHostSelftest.run: $$(tstVkatHostSelftest_1_TARGET)
+ export VKAT_RELEASE_LOG=-all; $(tstVkatHostSelftest_1_TARGET) selftest
+ $(QUIET)$(APPEND) -t "$@" "done"
+
+endif
+
+include $(FILE_KBUILD_SUB_FOOTER)
+
diff --git a/src/VBox/ValidationKit/utils/audio/ntPlayToneWaveX.cpp b/src/VBox/ValidationKit/utils/audio/ntPlayToneWaveX.cpp
new file mode 100644
index 00000000..e07165dc
--- /dev/null
+++ b/src/VBox/ValidationKit/utils/audio/ntPlayToneWaveX.cpp
@@ -0,0 +1,226 @@
+/* $Id: ntPlayToneWaveX.cpp $ */
+/** @file
+ * ????
+ */
+
+/*
+ * Copyright (C) 2012-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/win/windows.h>
+
+#include <iprt/getopt.h>
+#include <iprt/initterm.h>
+#include <iprt/message.h>
+#include <iprt/stream.h>
+#include <iprt/errcore.h>
+
+#define _USE_MATH_DEFINES
+#include <math.h>
+
+
+/*********************************************************************************************************************************
+* Global Variables *
+*********************************************************************************************************************************/
+uint32_t g_cSamplesPerSec = 44100;
+uint32_t g_cSamplesPerPeriod = 100; // 441.0Hz for 44.1kHz
+uint32_t g_cSamplesInBuffer = 4096;
+double g_rdSecDuration = 5.0;
+
+uint32_t g_cbSample; // assuming 16-bit stereo (for now)
+
+HWAVEOUT g_hWaveOut;
+HANDLE g_hWavEvent;
+
+
+int main(int argc, char **argv)
+{
+ int rc = RTR3InitExe(argc, &argv, 0);
+ if (RT_FAILURE(rc))
+ return RTMsgInitFailure(rc);
+
+ static const RTGETOPTDEF s_aOptions[] =
+ {
+ { "--samples-per-sec", 's', RTGETOPT_REQ_UINT32 },
+ { "--period-in-samples", 'p', RTGETOPT_REQ_UINT32 },
+ { "--bufsize-in-samples", 'b', RTGETOPT_REQ_UINT32 },
+ { "--total-duration-in-secs", 'd', RTGETOPT_REQ_UINT32 }
+ };
+
+ RTGETOPTSTATE State;
+ RTGetOptInit(&State, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, 0);
+ RTGETOPTUNION ValueUnion;
+ int chOpt;
+ while ((chOpt = RTGetOpt(&State, &ValueUnion)) != 0)
+ {
+ switch (chOpt)
+ {
+ case 's': g_cSamplesPerSec = ValueUnion.u32; break;
+ case 'p': g_cSamplesPerPeriod = ValueUnion.u32; break;
+ case 'b': g_cSamplesInBuffer = ValueUnion.u32; break;
+ case 'd': g_rdSecDuration = ValueUnion.u32; break;
+ case 'h':
+ RTPrintf("usage: ntPlayToneWaveX.exe\n"
+ "[-s|--samples-per-sec]\n"
+ "[-p|--period-in-samples]\n"
+ "[-b|--bufsize-in-samples]\n"
+ "[-d|--total-duration-in-secs]\n"
+ "\n"
+ "Plays sine tone using ancient waveX API\n");
+ return 0;
+
+ default:
+ return RTGetOptPrintError(chOpt, &ValueUnion);
+ }
+ }
+
+
+ WAVEFORMATEX waveFormatEx = { 0 };
+ MMRESULT mmresult;
+
+ waveFormatEx.wFormatTag = WAVE_FORMAT_PCM;
+ waveFormatEx.nChannels = 2;
+ waveFormatEx.nSamplesPerSec = g_cSamplesPerSec;
+ waveFormatEx.wBitsPerSample = 16;
+ waveFormatEx.nBlockAlign = g_cbSample = waveFormatEx.nChannels * waveFormatEx.wBitsPerSample / 8;
+ waveFormatEx.nAvgBytesPerSec = waveFormatEx.nBlockAlign * waveFormatEx.nSamplesPerSec;
+ waveFormatEx.cbSize = 0;
+
+ g_hWavEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
+ mmresult = waveOutOpen(&g_hWaveOut, WAVE_MAPPER, &waveFormatEx, (DWORD_PTR)g_hWavEvent, NULL, CALLBACK_EVENT);
+
+ if (mmresult != MMSYSERR_NOERROR)
+ {
+ RTMsgError("waveOutOpen failed with 0x%X\n", mmresult);
+ return -1;
+ }
+
+
+ uint32_t ui32SamplesToPlayTotal = (uint32_t)(g_rdSecDuration * g_cSamplesPerSec);
+ uint32_t ui32SamplesToPlay = ui32SamplesToPlayTotal;
+ uint32_t ui32SamplesPlayed = 0;
+ uint32_t ui32SamplesForWavBuf;
+
+ WAVEHDR waveHdr1 = {0}, waveHdr2 = {0}, *pWaveHdr, *pWaveHdrPlaying, *pWaveHdrWaiting;
+ uint32_t i, k;
+ DWORD res;
+
+ int16_t *i16Samples1 = (int16_t *)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, g_cSamplesInBuffer * g_cbSample);
+ int16_t *i16Samples2 = (int16_t *)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, g_cSamplesInBuffer * g_cbSample);
+
+ k = 0; // This is discrete time really!!!
+
+ for (i = 0; i < g_cSamplesInBuffer; i++, k++)
+ {
+ i16Samples1[2 * i] = (uint16_t)(10000.0 * sin(2.0 * M_PI * k / g_cSamplesPerPeriod));
+ i16Samples1[2 * i + 1] = i16Samples1[2 * i];
+ }
+
+ ui32SamplesForWavBuf = min(ui32SamplesToPlay, g_cSamplesInBuffer);
+
+ waveHdr1.lpData = (LPSTR)i16Samples1;
+ waveHdr1.dwBufferLength = ui32SamplesForWavBuf * g_cbSample;
+ waveHdr1.dwFlags = 0;
+ waveHdr1.dwLoops = 0;
+
+ ui32SamplesToPlay -= ui32SamplesForWavBuf;
+ ui32SamplesPlayed += ui32SamplesForWavBuf;
+
+ pWaveHdrPlaying = &waveHdr1;
+
+ mmresult = waveOutPrepareHeader(g_hWaveOut, pWaveHdrPlaying, sizeof(WAVEHDR));
+ mmresult = waveOutWrite(g_hWaveOut, pWaveHdrPlaying, sizeof(WAVEHDR));
+ //RTMsgInfo("waveOutWrite completes with %d\n", mmresult);
+
+ res = WaitForSingleObject(g_hWavEvent, INFINITE);
+ //RTMsgInfo("WaitForSingleObject completes with %d\n\n", res);
+
+ waveHdr2.lpData = (LPSTR)i16Samples2;
+ waveHdr2.dwBufferLength = 0;
+ waveHdr2.dwFlags = 0;
+ waveHdr2.dwLoops = 0;
+
+ pWaveHdrWaiting = &waveHdr2;
+
+ while (ui32SamplesToPlay > 0)
+ {
+ int16_t *i16Samples = (int16_t *)pWaveHdrWaiting->lpData;
+
+ for (i = 0; i < g_cSamplesInBuffer; i++, k++)
+ {
+ i16Samples[2 * i] = (uint16_t)(10000.0 * sin(2.0 * M_PI * k / g_cSamplesPerPeriod));
+ i16Samples[2 * i + 1] = i16Samples[2 * i];
+ }
+
+ ui32SamplesForWavBuf = min(ui32SamplesToPlay, g_cSamplesInBuffer);
+
+ pWaveHdrWaiting->dwBufferLength = ui32SamplesForWavBuf * g_cbSample;
+ pWaveHdrWaiting->dwFlags = 0;
+ pWaveHdrWaiting->dwLoops = 0;
+
+
+ ui32SamplesToPlay -= ui32SamplesForWavBuf;
+ ui32SamplesPlayed += ui32SamplesForWavBuf;
+
+ mmresult = waveOutPrepareHeader(g_hWaveOut, pWaveHdrWaiting, sizeof(WAVEHDR));
+ mmresult = waveOutWrite(g_hWaveOut, pWaveHdrWaiting, sizeof(WAVEHDR));
+ //RTMsgInfo("waveOutWrite completes with %d\n", mmresult);
+
+ res = WaitForSingleObject(g_hWavEvent, INFINITE);
+ //RTMsgInfo("WaitForSingleObject completes with %d\n\n", res);
+
+ mmresult = waveOutUnprepareHeader(g_hWaveOut, pWaveHdrPlaying, sizeof(WAVEHDR));
+ //RTMsgInfo("waveOutUnprepareHeader completes with %d\n", mmresult);
+
+ pWaveHdr = pWaveHdrWaiting;
+ pWaveHdrWaiting = pWaveHdrPlaying;
+ pWaveHdrPlaying = pWaveHdr;
+ }
+
+ while (mmresult = waveOutUnprepareHeader(g_hWaveOut, pWaveHdrPlaying, sizeof(WAVEHDR)))
+ {
+ //Expecting WAVERR_STILLPLAYING
+ //RTMsgInfo("waveOutUnprepareHeader failed with 0x%X\n", mmresult);
+ Sleep(100);
+ }
+
+ if (mmresult == MMSYSERR_NOERROR)
+ {
+ waveOutClose(g_hWaveOut);
+ }
+
+ HeapFree(GetProcessHeap(), 0, i16Samples1);
+ HeapFree(GetProcessHeap(), 0, i16Samples2);
+}
+
diff --git a/src/VBox/ValidationKit/utils/audio/readme.txt b/src/VBox/ValidationKit/utils/audio/readme.txt
new file mode 100644
index 00000000..ba894b5a
--- /dev/null
+++ b/src/VBox/ValidationKit/utils/audio/readme.txt
@@ -0,0 +1,2 @@
+
+See docs/VBoxAudioValidationKitReadMe.txt or docs/VBoxAudioValidationKitReadMe.html.
diff --git a/src/VBox/ValidationKit/utils/audio/vkat.cpp b/src/VBox/ValidationKit/utils/audio/vkat.cpp
new file mode 100644
index 00000000..0ff17e4c
--- /dev/null
+++ b/src/VBox/ValidationKit/utils/audio/vkat.cpp
@@ -0,0 +1,1649 @@
+/* $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;
+}
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 */
+};
+
diff --git a/src/VBox/ValidationKit/utils/audio/vkatCmdSelfTest.cpp b/src/VBox/ValidationKit/utils/audio/vkatCmdSelfTest.cpp
new file mode 100644
index 00000000..3259398d
--- /dev/null
+++ b/src/VBox/ValidationKit/utils/audio/vkatCmdSelfTest.cpp
@@ -0,0 +1,481 @@
+/* $Id: vkatCmdSelfTest.cpp $ */
+/** @file
+ * Validation Kit Audio Test (VKAT) - Self test.
+ *
+ * Self-test which does a complete audio testing framework run without the need
+ * of a VM or other infrastructure, i.e. all required parts are running locally
+ * on the same machine.
+ *
+ * This self-test does the following:
+ * - 1. Creates a separate thread for the guest side VKAT and connects to the ATS instance on
+ * the host side at port 6052 (ATS_TCP_DEF_BIND_PORT_HOST).
+ * - 2. Uses the Validation Kit audio backend, which in turn creates an ATS instance
+ * listening at port 6062 (ATS_TCP_DEF_BIND_PORT_VALKIT).
+ * - 3. Uses the host test environment which creates an ATS instance
+ * listening at port 6052 (ATS_TCP_DEF_BIND_PORT_HOST).
+ * - 4. Executes a complete test run locally (e.g. without any guest (VM) involved).
+ */
+
+/*
+ * 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/ctype.h>
+#include <iprt/errcore.h>
+#include <iprt/getopt.h>
+#include <iprt/message.h>
+#include <iprt/rand.h>
+#include <iprt/test.h>
+
+#include "Audio/AudioHlp.h"
+#include "Audio/AudioTest.h"
+#include "Audio/AudioTestService.h"
+#include "Audio/AudioTestServiceClient.h"
+
+#include "vkatInternal.h"
+
+
+/*********************************************************************************************************************************
+* Internal structures *
+*********************************************************************************************************************************/
+
+/**
+ * Structure for keeping a VKAT self test context.
+ */
+typedef struct SELFTESTCTX
+{
+ /** Common tag for guest and host side. */
+ char szTag[AUDIOTEST_TAG_MAX];
+ /** The driver stack in use. */
+ AUDIOTESTDRVSTACK DrvStack;
+ /** Audio driver to use.
+ * Defaults to the platform's default driver. */
+ PCPDMDRVREG pDrvReg;
+ struct
+ {
+ AUDIOTESTENV TstEnv;
+ /** Where to bind the address of the guest ATS instance to.
+ * Defaults to localhost (127.0.0.1) if empty. */
+ char szAtsAddr[64];
+ /** Port of the guest ATS instance.
+ * Defaults to ATS_ALT_PORT if not set. */
+ uint32_t uAtsPort;
+ } Guest;
+ struct
+ {
+ AUDIOTESTENV TstEnv;
+ /** Address of the guest ATS instance.
+ * Defaults to localhost (127.0.0.1) if not set. */
+ char szGuestAtsAddr[64];
+ /** Port of the guest ATS instance.
+ * Defaults to ATS_DEFAULT_PORT if not set. */
+ uint32_t uGuestAtsPort;
+ /** Address of the Validation Kit audio driver ATS instance.
+ * Defaults to localhost (127.0.0.1) if not set. */
+ char szValKitAtsAddr[64];
+ /** Port of the Validation Kit audio driver ATS instance.
+ * Defaults to ATS_ALT_PORT if not set. */
+ uint32_t uValKitAtsPort;
+ } Host;
+} SELFTESTCTX;
+/** Pointer to a VKAT self test context. */
+typedef SELFTESTCTX *PSELFTESTCTX;
+
+
+/*********************************************************************************************************************************
+* Global Variables *
+*********************************************************************************************************************************/
+
+/** The global self-text context. */
+static SELFTESTCTX g_Ctx;
+
+
+/*********************************************************************************************************************************
+* Driver stack self-test implementation *
+*********************************************************************************************************************************/
+
+/**
+ * Performs a (quick) audio driver stack self test.
+ *
+ * Local only, no guest/host communication involved.
+ *
+ * @returns VBox status code.
+ */
+int AudioTestDriverStackPerformSelftest(void)
+{
+ PCPDMDRVREG pDrvReg = AudioTestGetDefaultBackend();
+
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Testing driver stack started\n");
+
+ AUDIOTESTDRVSTACK DrvStack;
+ int rc = audioTestDriverStackProbe(&DrvStack, pDrvReg,
+ true /* fEnabledIn */, true /* fEnabledOut */, false /* fWithDrvAudio */);
+ RTTEST_CHECK_RC_OK_RET(g_hTest, rc, rc);
+
+ AUDIOTESTIOOPTS IoOpts;
+ audioTestIoOptsInitDefaults(&IoOpts);
+
+ PPDMAUDIOSTREAM pStream;
+ PDMAUDIOSTREAMCFG CfgAcq;
+ rc = audioTestDriverStackStreamCreateOutput(&DrvStack, &IoOpts.Props,
+ IoOpts.cMsBufferSize, IoOpts.cMsPreBuffer, IoOpts.cMsSchedulingHint,
+ &pStream, &CfgAcq);
+ AssertRCReturn(rc, rc);
+
+ rc = audioTestDriverStackStreamEnable(&DrvStack, pStream);
+ RTTEST_CHECK_RC_OK_RET(g_hTest, rc, rc);
+
+ RTTEST_CHECK_RET(g_hTest, audioTestDriverStackStreamIsOkay(&DrvStack, pStream), VERR_AUDIO_STREAM_NOT_READY);
+
+ uint8_t abBuf[_4K];
+ memset(abBuf, 0x42, sizeof(abBuf));
+
+ uint32_t cbWritten;
+ rc = audioTestDriverStackStreamPlay(&DrvStack, pStream, abBuf, sizeof(abBuf), &cbWritten);
+ RTTEST_CHECK_RC_OK_RET(g_hTest, rc, rc);
+ RTTEST_CHECK_RET(g_hTest, cbWritten == sizeof(abBuf), VERR_AUDIO_STREAM_NOT_READY);
+
+ audioTestDriverStackStreamDrain(&DrvStack, pStream, true /* fSync */);
+ audioTestDriverStackStreamDestroy(&DrvStack, pStream);
+
+ audioTestDriverStackDelete(&DrvStack);
+
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Testing driver stack ended with %Rrc\n", rc);
+ return rc;
+}
+
+
+/*********************************************************************************************************************************
+* Self-test implementation *
+*********************************************************************************************************************************/
+
+/**
+ * Thread callback for mocking the guest (VM) side of things.
+ *
+ * @returns VBox status code.
+ * @param hThread Thread handle.
+ * @param pvUser Pointer to user-supplied data.
+ */
+static DECLCALLBACK(int) audioTestSelftestGuestAtsThread(RTTHREAD hThread, void *pvUser)
+{
+ RT_NOREF(hThread);
+ PSELFTESTCTX pCtx = (PSELFTESTCTX)pvUser;
+
+ PAUDIOTESTENV pTstEnvGst = &pCtx->Guest.TstEnv;
+
+ audioTestEnvInit(pTstEnvGst);
+
+ /* Flag the environment for self test mode. */
+ pTstEnvGst->fSelftest = true;
+
+ /* Tweak the address the guest ATS is trying to connect to the host if anything else is specified.
+ * Note: The host also runs on the same host (this self-test is completely self-contained and does not need a VM). */
+ if (!pTstEnvGst->TcpOpts.szConnectAddr[0])
+ RTStrCopy(pTstEnvGst->TcpOpts.szConnectAddr, sizeof(pTstEnvGst->TcpOpts.szConnectAddr), "127.0.0.1");
+
+ /* Generate tag for guest side. */
+ int rc = RTStrCopy(pTstEnvGst->szTag, sizeof(pTstEnvGst->szTag), pCtx->szTag);
+ RTTEST_CHECK_RC_OK_RET(g_hTest, rc, rc);
+
+ rc = AudioTestPathCreateTemp(pTstEnvGst->szPathTemp, sizeof(pTstEnvGst->szPathTemp), "selftest-guest");
+ RTTEST_CHECK_RC_OK_RET(g_hTest, rc, rc);
+
+ rc = AudioTestPathCreateTemp(pTstEnvGst->szPathOut, sizeof(pTstEnvGst->szPathOut), "selftest-out");
+ RTTEST_CHECK_RC_OK_RET(g_hTest, rc, rc);
+
+ pTstEnvGst->enmMode = AUDIOTESTMODE_GUEST;
+
+ rc = audioTestEnvCreate(pTstEnvGst, &pCtx->DrvStack);
+ if (RT_SUCCESS(rc))
+ {
+ RTThreadUserSignal(hThread);
+
+ rc = audioTestWorker(pTstEnvGst);
+ RTTEST_CHECK_RC_OK_RET(g_hTest, rc, rc);
+
+ audioTestEnvDestroy(pTstEnvGst);
+ }
+
+ return rc;
+}
+
+/**
+ * Main function for performing the self test.
+ *
+ * @returns RTEXITCODE
+ * @param pCtx Self test context to use.
+ */
+RTEXITCODE audioTestDoSelftest(PSELFTESTCTX pCtx)
+{
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Running self test ...\n");
+
+ /* Generate a common tag for guest and host side. */
+ int rc = AudioTestGenTag(pCtx->szTag, sizeof(pCtx->szTag));
+ RTTEST_CHECK_RC_OK_RET(g_hTest, rc, RTEXITCODE_FAILURE);
+
+ PAUDIOTESTENV pTstEnvHst = &pCtx->Host.TstEnv;
+
+ audioTestEnvInit(pTstEnvHst);
+
+ /* Flag the environment for self test mode. */
+ pTstEnvHst->fSelftest = true;
+
+ /* One test iteration with a 5s maximum test tone is enough for a (quick) self test. */
+ pTstEnvHst->cIterations = 1;
+ pTstEnvHst->ToneParms.msDuration = RTRandU32Ex(500, RT_MS_5SEC);
+
+ /* Generate tag for host side. */
+ rc = RTStrCopy(pTstEnvHst->szTag, sizeof(pTstEnvHst->szTag), pCtx->szTag);
+ RTTEST_CHECK_RC_OK_RET(g_hTest, rc, RTEXITCODE_FAILURE);
+
+ rc = AudioTestPathCreateTemp(pTstEnvHst->szPathTemp, sizeof(pTstEnvHst->szPathTemp), "selftest-tmp");
+ RTTEST_CHECK_RC_OK_RET(g_hTest, rc, RTEXITCODE_FAILURE);
+
+ rc = AudioTestPathCreateTemp(pTstEnvHst->szPathOut, sizeof(pTstEnvHst->szPathOut), "selftest-out");
+ RTTEST_CHECK_RC_OK_RET(g_hTest, rc, RTEXITCODE_FAILURE);
+
+ /*
+ * Step 1.
+ */
+ RTTHREAD hThreadGstAts = NIL_RTTHREAD;
+
+ bool const fStartGuestAts = RTStrNLen(pCtx->Host.szGuestAtsAddr, sizeof(pCtx->Host.szGuestAtsAddr)) == 0;
+ if (fStartGuestAts)
+ {
+ /* Step 1b. */
+ rc = RTThreadCreate(&hThreadGstAts, audioTestSelftestGuestAtsThread, pCtx, 0, RTTHREADTYPE_IO, RTTHREADFLAGS_WAITABLE,
+ "VKATGstAts");
+ if (RT_SUCCESS(rc))
+ rc = RTThreadUserWait(hThreadGstAts, RT_MS_30SEC);
+ }
+
+ RTThreadSleep(2000); /* Fudge: Wait until guest ATS is up. 2 seconds should be enough (tm). */
+
+ if (RT_SUCCESS(rc))
+ {
+ /*
+ * Steps 2 + 3.
+ */
+ pTstEnvHst->enmMode = AUDIOTESTMODE_HOST;
+
+ rc = audioTestEnvCreate(pTstEnvHst, &pCtx->DrvStack);
+ if (RT_SUCCESS(rc))
+ {
+ /*
+ * Step 4.
+ */
+ rc = audioTestWorker(pTstEnvHst);
+ RTTEST_CHECK_RC_OK(g_hTest, rc);
+
+ audioTestEnvDestroy(pTstEnvHst);
+ }
+ }
+
+ /*
+ * Shutting down.
+ */
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Shutting down self test\n");
+
+ /* If we started the guest ATS ourselves, wait for it to terminate properly. */
+ if (fStartGuestAts)
+ {
+ int rcThread;
+ int rc2 = RTThreadWait(hThreadGstAts, RT_MS_30SEC, &rcThread);
+ if (RT_SUCCESS(rc2))
+ rc2 = rcThread;
+ if (RT_FAILURE(rc2))
+ RTTestFailed(g_hTest, "Shutting down guest ATS failed with %Rrc\n", rc2);
+ if (RT_SUCCESS(rc))
+ rc = rc2;
+ }
+
+ if (RT_FAILURE(rc))
+ RTTestFailed(g_hTest, "Self test failed with %Rrc\n", rc);
+
+ return RT_SUCCESS(rc) ? RTEXITCODE_SUCCESS : RTEXITCODE_FAILURE;
+}
+
+
+/*********************************************************************************************************************************
+* Command: selftest *
+*********************************************************************************************************************************/
+
+/**
+ * Command line parameters for self-test mode.
+ */
+static const RTGETOPTDEF s_aCmdSelftestOptions[] =
+{
+ { "--exclude-all", 'a', RTGETOPT_REQ_NOTHING },
+ { "--backend", 'b', RTGETOPT_REQ_STRING },
+ { "--with-drv-audio", 'd', RTGETOPT_REQ_NOTHING },
+ { "--with-mixer", 'm', RTGETOPT_REQ_NOTHING },
+ { "--exclude", 'e', RTGETOPT_REQ_UINT32 },
+ { "--include", 'i', RTGETOPT_REQ_UINT32 }
+};
+
+/** the 'selftest' command option help. */
+static DECLCALLBACK(const char *) audioTestCmdSelftestHelp(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 'm': return "Use the internal mixing engine explicitly";
+ default: return NULL;
+ }
+}
+
+/**
+ * The 'selftest' command handler.
+ *
+ * @returns Program exit code.
+ * @param pGetState RTGetOpt state.
+ */
+DECLCALLBACK(RTEXITCODE) audioTestCmdSelftestHandler(PRTGETOPTSTATE pGetState)
+{
+ RT_ZERO(g_Ctx);
+
+ audioTestEnvInit(&g_Ctx.Guest.TstEnv);
+ audioTestEnvInit(&g_Ctx.Host.TstEnv);
+
+ AUDIOTESTIOOPTS IoOpts;
+ audioTestIoOptsInitDefaults(&IoOpts);
+
+ /* Argument processing loop: */
+ int rc;
+ RTGETOPTUNION ValueUnion;
+ while ((rc = RTGetOpt(pGetState, &ValueUnion)) != 0)
+ {
+ switch (rc)
+ {
+ case 'a':
+ for (unsigned i = 0; i < g_cTests; i++)
+ g_aTests[i].fExcluded = true;
+ break;
+
+ case 'b':
+ g_Ctx.pDrvReg = AudioTestFindBackendOpt(ValueUnion.psz);
+ if (g_Ctx.pDrvReg == NULL)
+ return RTEXITCODE_SYNTAX;
+ break;
+
+ case 'd':
+ IoOpts.fWithDrvAudio = true;
+ break;
+
+ case 'e':
+ if (ValueUnion.u32 >= g_cTests)
+ return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Invalid test number %u passed to --exclude", ValueUnion.u32);
+ g_aTests[ValueUnion.u32].fExcluded = true;
+ break;
+
+ case 'i':
+ if (ValueUnion.u32 >= g_cTests)
+ return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Invalid test number %u passed to --include", ValueUnion.u32);
+ g_aTests[ValueUnion.u32].fExcluded = false;
+ break;
+
+ case 'm':
+ IoOpts.fWithMixer = true;
+ break;
+
+ AUDIO_TEST_COMMON_OPTION_CASES(ValueUnion, &g_CmdSelfTest);
+
+ default:
+ return RTGetOptPrintError(rc, &ValueUnion);
+ }
+ }
+
+ /* For simplicity both test environments, guest and host, will have the same I/O options.
+ ** @todo Make this indepedent by a prefix, "--[guest|host]-<option>" -> e.g. "--guest-with-drv-audio". */
+ memcpy(&g_Ctx.Guest.TstEnv.IoOpts, &IoOpts, sizeof(AUDIOTESTIOOPTS));
+ memcpy(&g_Ctx.Host.TstEnv.IoOpts, &IoOpts, sizeof(AUDIOTESTIOOPTS));
+
+ rc = AudioTestDriverStackPerformSelftest();
+ if (RT_FAILURE(rc))
+ return RTMsgErrorExit(RTEXITCODE_FAILURE, "Testing driver stack failed: %Rrc\n", rc);
+
+ /* Go with the Validation Kit audio backend if nothing else is specified. */
+ if (g_Ctx.pDrvReg == NULL)
+ g_Ctx.pDrvReg = AudioTestFindBackendOpt("valkit");
+
+ /*
+ * In self-test mode the guest and the host side have to share the same driver stack,
+ * as we don't have any device emulation between the two sides.
+ *
+ * This is necessary to actually get the played/recorded audio to from/to the guest
+ * and host respectively.
+ *
+ * Choosing any other backend than the Validation Kit above *will* break this self-test!
+ */
+ rc = audioTestDriverStackInitEx(&g_Ctx.DrvStack, g_Ctx.pDrvReg,
+ true /* fEnabledIn */, true /* fEnabledOut */, g_Ctx.Host.TstEnv.IoOpts.fWithDrvAudio);
+ if (RT_FAILURE(rc))
+ return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Unable to init driver stack: %Rrc\n", rc);
+
+ /*
+ * Start testing.
+ */
+ RTTestBanner(g_hTest);
+
+ int rc2 = audioTestDoSelftest(&g_Ctx);
+ if (RT_FAILURE(rc2))
+ RTTestFailed(g_hTest, "Self test failed with rc=%Rrc", rc2);
+
+ audioTestDriverStackDelete(&g_Ctx.DrvStack);
+
+ /*
+ * Print summary and exit.
+ */
+ return RTTestSummaryAndDestroy(g_hTest);
+}
+
+/**
+ * Command table entry for 'selftest'.
+ */
+const VKATCMD g_CmdSelfTest =
+{
+ "selftest",
+ audioTestCmdSelftestHandler,
+ "Performs self-tests.",
+ s_aCmdSelftestOptions,
+ RT_ELEMENTS(s_aCmdSelftestOptions),
+ audioTestCmdSelftestHelp,
+ true /* fNeedsTransport */
+};
+
diff --git a/src/VBox/ValidationKit/utils/audio/vkatCommon.cpp b/src/VBox/ValidationKit/utils/audio/vkatCommon.cpp
new file mode 100644
index 00000000..12ea83be
--- /dev/null
+++ b/src/VBox/ValidationKit/utils/audio/vkatCommon.cpp
@@ -0,0 +1,1760 @@
+/* $Id: vkatCommon.cpp $ */
+/** @file
+ * Validation Kit Audio Test (VKAT) - Self test code.
+ */
+
+/*
+ * 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/log.h>
+
+#ifdef VBOX_WITH_AUDIO_ALSA
+# include "DrvHostAudioAlsaStubsMangling.h"
+# include <alsa/asoundlib.h>
+# include <alsa/control.h> /* For device enumeration. */
+# include <alsa/version.h>
+# include "DrvHostAudioAlsaStubs.h"
+#endif
+#ifdef VBOX_WITH_AUDIO_OSS
+# include <errno.h>
+# include <fcntl.h>
+# include <sys/ioctl.h>
+# include <sys/mman.h>
+# include <sys/soundcard.h>
+# include <unistd.h>
+#endif
+#ifdef RT_OS_WINDOWS
+# include <iprt/win/windows.h>
+# include <iprt/win/audioclient.h>
+# include <endpointvolume.h> /* For IAudioEndpointVolume. */
+# include <audiopolicy.h> /* For IAudioSessionManager. */
+# include <AudioSessionTypes.h>
+# include <Mmdeviceapi.h>
+#endif
+
+#include <iprt/ctype.h>
+#include <iprt/dir.h>
+#include <iprt/errcore.h>
+#include <iprt/getopt.h>
+#include <iprt/message.h>
+#include <iprt/rand.h>
+#include <iprt/test.h>
+
+#include "Audio/AudioHlp.h"
+#include "Audio/AudioTest.h"
+#include "Audio/AudioTestService.h"
+#include "Audio/AudioTestServiceClient.h"
+
+#include "vkatInternal.h"
+
+
+/*********************************************************************************************************************************
+* Defined Constants And Macros *
+*********************************************************************************************************************************/
+
+
+/*********************************************************************************************************************************
+* Internal Functions *
+*********************************************************************************************************************************/
+static int audioTestStreamInit(PAUDIOTESTDRVSTACK pDrvStack, PAUDIOTESTSTREAM pStream, PDMAUDIODIR enmDir, PAUDIOTESTIOOPTS pPlayOpt);
+static int audioTestStreamDestroy(PAUDIOTESTDRVSTACK pDrvStack, PAUDIOTESTSTREAM pStream);
+
+
+/*********************************************************************************************************************************
+* Volume handling. *
+*********************************************************************************************************************************/
+
+#ifdef VBOX_WITH_AUDIO_ALSA
+/**
+ * Sets the system's master volume via ALSA, if available.
+ *
+ * @returns VBox status code.
+ * @param uVolPercent Volume (in percent) to set.
+ */
+static int audioTestSetMasterVolumeALSA(unsigned uVolPercent)
+{
+ int rc = audioLoadAlsaLib();
+ if (RT_FAILURE(rc))
+ return rc;
+
+ int err;
+ snd_mixer_t *handle;
+
+# define ALSA_CHECK_RET(a_Exp, a_Text) \
+ if (!(a_Exp)) \
+ { \
+ AssertLogRelMsg(a_Exp, a_Text); \
+ if (handle) \
+ snd_mixer_close(handle); \
+ return VERR_GENERAL_FAILURE; \
+ }
+
+# define ALSA_CHECK_ERR_RET(a_Text) \
+ ALSA_CHECK_RET(err >= 0, a_Text)
+
+ err = snd_mixer_open(&handle, 0 /* Index */);
+ ALSA_CHECK_ERR_RET(("ALSA: Failed to open mixer: %s\n", snd_strerror(err)));
+ err = snd_mixer_attach(handle, "default");
+ ALSA_CHECK_ERR_RET(("ALSA: Failed to attach to default sink: %s\n", snd_strerror(err)));
+ err = snd_mixer_selem_register(handle, NULL, NULL);
+ ALSA_CHECK_ERR_RET(("ALSA: Failed to attach to default sink: %s\n", snd_strerror(err)));
+ err = snd_mixer_load(handle);
+ ALSA_CHECK_ERR_RET(("ALSA: Failed to load mixer: %s\n", snd_strerror(err)));
+
+ snd_mixer_selem_id_t *sid = NULL;
+ snd_mixer_selem_id_alloca(&sid);
+
+ snd_mixer_selem_id_set_index(sid, 0 /* Index */);
+ snd_mixer_selem_id_set_name(sid, "Master");
+
+ snd_mixer_elem_t* elem = snd_mixer_find_selem(handle, sid);
+ ALSA_CHECK_RET(elem != NULL, ("ALSA: Failed to find mixer element: %s\n", snd_strerror(err)));
+
+ long uVolMin, uVolMax;
+ snd_mixer_selem_get_playback_volume_range(elem, &uVolMin, &uVolMax);
+ ALSA_CHECK_ERR_RET(("ALSA: Failed to get playback volume range: %s\n", snd_strerror(err)));
+
+ long const uVol = RT_MIN(uVolPercent, 100) * uVolMax / 100;
+
+ err = snd_mixer_selem_set_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT, uVol);
+ ALSA_CHECK_ERR_RET(("ALSA: Failed to set playback volume left: %s\n", snd_strerror(err)));
+ err = snd_mixer_selem_set_playback_volume(elem, SND_MIXER_SCHN_FRONT_RIGHT, uVol);
+ ALSA_CHECK_ERR_RET(("ALSA: Failed to set playback volume right: %s\n", snd_strerror(err)));
+
+ snd_mixer_close(handle);
+
+ return VINF_SUCCESS;
+
+# undef ALSA_CHECK_RET
+# undef ALSA_CHECK_ERR_RET
+}
+#endif /* VBOX_WITH_AUDIO_ALSA */
+
+#ifdef VBOX_WITH_AUDIO_OSS
+/**
+ * Sets the system's master volume via OSS, if available.
+ *
+ * @returns VBox status code.
+ * @param uVolPercent Volume (in percent) to set.
+ */
+static int audioTestSetMasterVolumeOSS(unsigned uVolPercent)
+{
+ int hFile = open("/dev/dsp", O_WRONLY | O_NONBLOCK, 0);
+ if (hFile == -1)
+ {
+ /* Try opening the mixing device instead. */
+ hFile = open("/dev/mixer", O_RDONLY | O_NONBLOCK, 0);
+ }
+
+ if (hFile != -1)
+ {
+ /* OSS maps 0 (muted) - 100 (max), so just use uVolPercent unmodified here. */
+ uint16_t uVol = RT_MAKE_U16(uVolPercent, uVolPercent);
+ AssertLogRelMsgReturnStmt(ioctl(hFile, SOUND_MIXER_PCM /* SNDCTL_DSP_SETPLAYVOL */, &uVol) >= 0,
+ ("OSS: Failed to set DSP playback volume: %s (%d)\n",
+ strerror(errno), errno), close(hFile), RTErrConvertFromErrno(errno));
+ return VINF_SUCCESS;
+ }
+
+ return VERR_NOT_SUPPORTED;
+}
+#endif /* VBOX_WITH_AUDIO_OSS */
+
+#ifdef RT_OS_WINDOWS
+static int audioTestSetMasterVolumeWASAPI(unsigned uVolPercent)
+{
+ HRESULT hr;
+
+# define WASAPI_CHECK_HR_RET(a_Text) \
+ if (FAILED(hr)) \
+ { \
+ AssertLogRelMsgFailed(a_Text); \
+ return VERR_GENERAL_FAILURE; \
+ }
+
+ hr = CoInitialize(NULL);
+ WASAPI_CHECK_HR_RET(("CoInitialize() failed, hr=%Rhrc", hr));
+ IMMDeviceEnumerator* pIEnumerator = NULL;
+ hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void **)&pIEnumerator);
+ WASAPI_CHECK_HR_RET(("WASAPI: Unable to create IMMDeviceEnumerator, hr=%Rhrc", hr));
+
+ IMMDevice *pIMMDevice = NULL;
+ hr = pIEnumerator->GetDefaultAudioEndpoint(EDataFlow::eRender, ERole::eConsole, &pIMMDevice);
+ WASAPI_CHECK_HR_RET(("WASAPI: Unable to get audio endpoint, hr=%Rhrc", hr));
+ pIEnumerator->Release();
+
+ IAudioEndpointVolume *pIAudioEndpointVolume = NULL;
+ hr = pIMMDevice->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, NULL, (void **)&pIAudioEndpointVolume);
+ WASAPI_CHECK_HR_RET(("WASAPI: Unable to activate audio endpoint volume, hr=%Rhrc", hr));
+ pIMMDevice->Release();
+
+ float dbMin, dbMax, dbInc;
+ hr = pIAudioEndpointVolume->GetVolumeRange(&dbMin, &dbMax, &dbInc);
+ WASAPI_CHECK_HR_RET(("WASAPI: Unable to get volume range, hr=%Rhrc", hr));
+
+ float const dbSteps = (dbMax - dbMin) / dbInc;
+ float const dbStepsPerPercent = (dbSteps * dbInc) / 100;
+ float const dbVol = dbMin + (dbStepsPerPercent * (float(RT_MIN(uVolPercent, 100.0))));
+
+ hr = pIAudioEndpointVolume->SetMasterVolumeLevel(dbVol, NULL);
+ WASAPI_CHECK_HR_RET(("WASAPI: Unable to set master volume level, hr=%Rhrc", hr));
+ pIAudioEndpointVolume->Release();
+
+ return VINF_SUCCESS;
+
+# undef WASAPI_CHECK_HR_RET
+}
+#endif /* RT_OS_WINDOWS */
+
+/**
+ * Sets the system's master volume, if available.
+ *
+ * @returns VBox status code. VERR_NOT_SUPPORTED if not supported.
+ * @param uVolPercent Volume (in percent) to set.
+ */
+int audioTestSetMasterVolume(unsigned uVolPercent)
+{
+ int rc = VINF_SUCCESS;
+
+#ifdef VBOX_WITH_AUDIO_ALSA
+ rc = audioTestSetMasterVolumeALSA(uVolPercent);
+ if (RT_SUCCESS(rc))
+ return rc;
+ /* else try OSS (if available) below. */
+#endif /* VBOX_WITH_AUDIO_ALSA */
+
+#ifdef VBOX_WITH_AUDIO_OSS
+ rc = audioTestSetMasterVolumeOSS(uVolPercent);
+ if (RT_SUCCESS(rc))
+ return rc;
+#endif /* VBOX_WITH_AUDIO_OSS */
+
+#ifdef RT_OS_WINDOWS
+ rc = audioTestSetMasterVolumeWASAPI(uVolPercent);
+ if (RT_SUCCESS(rc))
+ return rc;
+#endif
+
+ RT_NOREF(rc, uVolPercent);
+ /** @todo Port other platforms. */
+ return VERR_NOT_SUPPORTED;
+}
+
+
+/*********************************************************************************************************************************
+* Device enumeration + handling. *
+*********************************************************************************************************************************/
+
+/**
+ * Enumerates audio devices and optionally searches for a specific device.
+ *
+ * @returns VBox status code.
+ * @param pDrvStack Driver stack to use for enumeration.
+ * @param pszDev Device name to search for. Can be NULL if the default device shall be used.
+ * @param ppDev Where to return the pointer of the device enumeration of \a pTstEnv when a
+ * specific device was found.
+ */
+int audioTestDevicesEnumerateAndCheck(PAUDIOTESTDRVSTACK pDrvStack, const char *pszDev, PPDMAUDIOHOSTDEV *ppDev)
+{
+ RTTestSubF(g_hTest, "Enumerating audio devices and checking for device '%s'", pszDev && *pszDev ? pszDev : "[Default]");
+
+ if (!pDrvStack->pIHostAudio->pfnGetDevices)
+ {
+ RTTestSkipped(g_hTest, "Backend does not support device enumeration, skipping");
+ return VINF_NOT_SUPPORTED;
+ }
+
+ Assert(pszDev == NULL || ppDev);
+
+ if (ppDev)
+ *ppDev = NULL;
+
+ int rc = pDrvStack->pIHostAudio->pfnGetDevices(pDrvStack->pIHostAudio, &pDrvStack->DevEnum);
+ if (RT_SUCCESS(rc))
+ {
+ PPDMAUDIOHOSTDEV pDev;
+ RTListForEach(&pDrvStack->DevEnum.LstDevices, pDev, PDMAUDIOHOSTDEV, ListEntry)
+ {
+ char szFlags[PDMAUDIOHOSTDEV_MAX_FLAGS_STRING_LEN];
+ if (pDev->pszId)
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Enum: Device '%s' (ID '%s'):\n", pDev->pszName, pDev->pszId);
+ else
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Enum: Device '%s':\n", pDev->pszName);
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Enum: Usage = %s\n", PDMAudioDirGetName(pDev->enmUsage));
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Enum: Flags = %s\n", PDMAudioHostDevFlagsToString(szFlags, pDev->fFlags));
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Enum: Input channels = %RU8\n", pDev->cMaxInputChannels);
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Enum: Output channels = %RU8\n", pDev->cMaxOutputChannels);
+
+ if ( (pszDev && *pszDev)
+ && !RTStrCmp(pDev->pszName, pszDev))
+ {
+ *ppDev = pDev;
+ }
+ }
+ }
+ else
+ RTTestFailed(g_hTest, "Enumerating audio devices failed with %Rrc", rc);
+
+ if (RT_SUCCESS(rc))
+ {
+ if ( (pszDev && *pszDev)
+ && *ppDev == NULL)
+ {
+ RTTestFailed(g_hTest, "Audio device '%s' not found", pszDev);
+ rc = VERR_NOT_FOUND;
+ }
+ }
+
+ RTTestSubDone(g_hTest);
+ return rc;
+}
+
+static int audioTestStreamInit(PAUDIOTESTDRVSTACK pDrvStack, PAUDIOTESTSTREAM pStream,
+ PDMAUDIODIR enmDir, PAUDIOTESTIOOPTS pIoOpts)
+{
+ int rc;
+
+ if (enmDir == PDMAUDIODIR_IN)
+ rc = audioTestDriverStackStreamCreateInput(pDrvStack, &pIoOpts->Props, pIoOpts->cMsBufferSize,
+ pIoOpts->cMsPreBuffer, pIoOpts->cMsSchedulingHint, &pStream->pStream, &pStream->Cfg);
+ else if (enmDir == PDMAUDIODIR_OUT)
+ rc = audioTestDriverStackStreamCreateOutput(pDrvStack, &pIoOpts->Props, pIoOpts->cMsBufferSize,
+ pIoOpts->cMsPreBuffer, pIoOpts->cMsSchedulingHint, &pStream->pStream, &pStream->Cfg);
+ else
+ rc = VERR_NOT_SUPPORTED;
+
+ if (RT_SUCCESS(rc))
+ {
+ if (!pDrvStack->pIAudioConnector)
+ {
+ pStream->pBackend = &((PAUDIOTESTDRVSTACKSTREAM)pStream->pStream)->Backend;
+ }
+ else
+ pStream->pBackend = NULL;
+
+ /*
+ * Automatically enable the mixer if the PCM properties don't match.
+ */
+ if ( !pIoOpts->fWithMixer
+ && !PDMAudioPropsAreEqual(&pIoOpts->Props, &pStream->Cfg.Props))
+ {
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Enabling stream mixer\n");
+ pIoOpts->fWithMixer = true;
+ }
+
+ rc = AudioTestMixStreamInit(&pStream->Mix, pDrvStack, pStream->pStream,
+ pIoOpts->fWithMixer ? &pIoOpts->Props : NULL, 100 /* ms */); /** @todo Configure mixer buffer? */
+ }
+
+ if (RT_FAILURE(rc))
+ RTTestFailed(g_hTest, "Initializing %s stream failed with %Rrc", enmDir == PDMAUDIODIR_IN ? "input" : "output", rc);
+
+ return rc;
+}
+
+/**
+ * Destroys an audio test stream.
+ *
+ * @returns VBox status code.
+ * @param pDrvStack Driver stack the stream belongs to.
+ * @param pStream Audio stream to destroy.
+ */
+static int audioTestStreamDestroy(PAUDIOTESTDRVSTACK pDrvStack, PAUDIOTESTSTREAM pStream)
+{
+ AssertPtrReturn(pStream, VERR_INVALID_POINTER);
+
+ if (pStream->pStream)
+ {
+ /** @todo Anything else to do here, e.g. test if there are left over samples or some such? */
+
+ audioTestDriverStackStreamDestroy(pDrvStack, pStream->pStream);
+ pStream->pStream = NULL;
+ pStream->pBackend = NULL;
+ }
+
+ AudioTestMixStreamTerm(&pStream->Mix);
+
+ return VINF_SUCCESS;
+}
+
+
+/*********************************************************************************************************************************
+* Test Primitives *
+*********************************************************************************************************************************/
+
+/**
+ * Initializes test tone parameters (partly with random values).
+
+ * @param pToneParms Test tone parameters to initialize.
+ */
+void audioTestToneParmsInit(PAUDIOTESTTONEPARMS pToneParms)
+{
+ RT_BZERO(pToneParms, sizeof(AUDIOTESTTONEPARMS));
+
+ /**
+ * Set default (randomized) test tone parameters if not set explicitly.
+ */
+ pToneParms->dbFreqHz = AudioTestToneGetRandomFreq();
+ pToneParms->msDuration = RTRandU32Ex(200, RT_MS_30SEC);
+ pToneParms->uVolumePercent = 100; /* We always go with maximum volume for now. */
+
+ PDMAudioPropsInit(&pToneParms->Props,
+ 2 /* 16-bit */, true /* fPcmSigned */, 2 /* cPcmChannels */, 44100 /* uPcmHz */);
+}
+
+/**
+ * Initializes I/O options with some sane default values.
+ *
+ * @param pIoOpts I/O options to initialize.
+ */
+void audioTestIoOptsInitDefaults(PAUDIOTESTIOOPTS pIoOpts)
+{
+ RT_BZERO(pIoOpts, sizeof(AUDIOTESTIOOPTS));
+
+ /* Initialize the PCM properties to some sane values. */
+ PDMAudioPropsInit(&pIoOpts->Props,
+ 2 /* 16-bit */, true /* fPcmSigned */, 2 /* cPcmChannels */, 44100 /* uPcmHz */);
+
+ pIoOpts->cMsBufferSize = UINT32_MAX;
+ pIoOpts->cMsPreBuffer = UINT32_MAX;
+ pIoOpts->cMsSchedulingHint = UINT32_MAX;
+ pIoOpts->uVolumePercent = 100; /* Use maximum volume by default. */
+}
+
+#if 0 /* Unused */
+/**
+ * Returns a random scheduling hint (in ms).
+ */
+DECLINLINE(uint32_t) audioTestEnvGetRandomSchedulingHint(void)
+{
+ static const unsigned s_aSchedulingHintsMs[] =
+ {
+ 10,
+ 25,
+ 50,
+ 100,
+ 200,
+ 250
+ };
+
+ return s_aSchedulingHintsMs[RTRandU32Ex(0, RT_ELEMENTS(s_aSchedulingHintsMs) - 1)];
+}
+#endif
+
+/**
+ * Plays a test tone on a specific audio test stream.
+ *
+ * @returns VBox status code.
+ * @param pIoOpts I/O options to use.
+ * @param pTstEnv Test environment to use for running the test.
+ * Optional and can be NULL (for simple playback only).
+ * @param pStream Stream to use for playing the tone.
+ * @param pParms Tone parameters to use.
+ *
+ * @note Blocking function.
+ */
+int audioTestPlayTone(PAUDIOTESTIOOPTS pIoOpts, PAUDIOTESTENV pTstEnv, PAUDIOTESTSTREAM pStream, PAUDIOTESTTONEPARMS pParms)
+{
+ uint32_t const idxTest = (uint8_t)pParms->Hdr.idxTest;
+
+ AUDIOTESTTONE TstTone;
+ AudioTestToneInit(&TstTone, &pStream->Cfg.Props, pParms->dbFreqHz);
+
+ char const *pcszPathOut = NULL;
+ if (pTstEnv)
+ pcszPathOut = pTstEnv->Set.szPathAbs;
+
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Playing test tone (tone frequency is %RU16Hz, %RU32ms, %RU8%% volume)\n",
+ idxTest, (uint16_t)pParms->dbFreqHz, pParms->msDuration, pParms->uVolumePercent);
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Using %RU32ms stream scheduling hint\n",
+ idxTest, pStream->Cfg.Device.cMsSchedulingHint);
+ if (pcszPathOut)
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Writing to '%s'\n", idxTest, pcszPathOut);
+
+ int rc;
+
+ /** @todo Use .WAV here? */
+ AUDIOTESTOBJ Obj;
+ RT_ZERO(Obj); /* Shut up MSVC. */
+ if (pTstEnv)
+ {
+ rc = AudioTestSetObjCreateAndRegister(&pTstEnv->Set, "guest-tone-play.pcm", &Obj);
+ AssertRCReturn(rc, rc);
+ }
+
+ uint8_t const uVolPercent = pIoOpts->uVolumePercent;
+ int rc2 = audioTestSetMasterVolume(uVolPercent);
+ if (RT_FAILURE(rc2))
+ {
+ if (rc2 == VERR_NOT_SUPPORTED)
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Setting system's master volume is not supported on this platform, skipping\n");
+ else
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Setting system's master volume failed with %Rrc\n", rc2);
+ }
+ else
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Set system's master volume to %RU8%%\n", uVolPercent);
+
+ rc = AudioTestMixStreamEnable(&pStream->Mix);
+ if ( RT_SUCCESS(rc)
+ && AudioTestMixStreamIsOkay(&pStream->Mix))
+ {
+ uint32_t cbToWriteTotal = PDMAudioPropsMilliToBytes(&pStream->Cfg.Props, pParms->msDuration);
+ AssertStmt(cbToWriteTotal, rc = VERR_INVALID_PARAMETER);
+ uint32_t cbWrittenTotal = 0;
+
+ /* We play a pre + post beacon before + after the actual test tone.
+ * We always start with the pre beacon. */
+ AUDIOTESTTONEBEACON Beacon;
+ AudioTestBeaconInit(&Beacon, (uint8_t)pParms->Hdr.idxTest, AUDIOTESTTONEBEACONTYPE_PLAY_PRE, &pStream->Cfg.Props);
+
+ uint32_t const cbBeacon = AudioTestBeaconGetSize(&Beacon);
+ if (cbBeacon)
+ {
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Playing 2 x %RU32 bytes pre/post beacons\n",
+ idxTest, cbBeacon);
+
+ if (g_uVerbosity >= 2)
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Playing %s beacon ...\n",
+ idxTest, AudioTestBeaconTypeGetName(Beacon.enmType));
+ }
+
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Playing %RU32 bytes total\n", idxTest, cbToWriteTotal);
+
+ AudioTestObjAddMetadataStr(Obj, "test_id=%04RU32\n", pParms->Hdr.idxTest);
+ AudioTestObjAddMetadataStr(Obj, "beacon_type=%RU32\n", (uint32_t)AudioTestBeaconGetType(&Beacon));
+ AudioTestObjAddMetadataStr(Obj, "beacon_pre_bytes=%RU32\n", cbBeacon);
+ AudioTestObjAddMetadataStr(Obj, "beacon_post_bytes=%RU32\n", cbBeacon);
+ AudioTestObjAddMetadataStr(Obj, "stream_to_write_total_bytes=%RU32\n", cbToWriteTotal);
+ AudioTestObjAddMetadataStr(Obj, "stream_period_size_frames=%RU32\n", pStream->Cfg.Backend.cFramesPeriod);
+ AudioTestObjAddMetadataStr(Obj, "stream_buffer_size_frames=%RU32\n", pStream->Cfg.Backend.cFramesBufferSize);
+ AudioTestObjAddMetadataStr(Obj, "stream_prebuf_size_frames=%RU32\n", pStream->Cfg.Backend.cFramesPreBuffering);
+ /* Note: This mostly is provided by backend (e.g. PulseAudio / ALSA / ++) and
+ * has nothing to do with the device emulation scheduling hint. */
+ AudioTestObjAddMetadataStr(Obj, "device_scheduling_hint_ms=%RU32\n", pStream->Cfg.Device.cMsSchedulingHint);
+
+ PAUDIOTESTDRVMIXSTREAM pMix = &pStream->Mix;
+
+ uint32_t const cbPreBuffer = PDMAudioPropsFramesToBytes(pMix->pProps, pStream->Cfg.Backend.cFramesPreBuffering);
+ uint64_t const nsStarted = RTTimeNanoTS();
+ uint64_t nsDonePreBuffering = 0;
+
+ uint64_t offStream = 0;
+ uint64_t nsTimeout = RT_MS_5MIN_64 * RT_NS_1MS;
+ uint64_t nsLastMsgCantWrite = 0; /* Timestamp (in ns) when the last message of an unwritable stream was shown. */
+ uint64_t nsLastWrite = 0;
+
+ AUDIOTESTSTATE enmState = AUDIOTESTSTATE_PRE;
+ uint8_t abBuf[_16K];
+
+ for (;;)
+ {
+ uint64_t const nsNow = RTTimeNanoTS();
+ if (!nsLastWrite)
+ nsLastWrite = nsNow;
+
+ /* Pace ourselves a little. */
+ if (offStream >= cbPreBuffer)
+ {
+ if (!nsDonePreBuffering)
+ nsDonePreBuffering = nsNow;
+ uint64_t const cNsWritten = PDMAudioPropsBytesToNano64(pMix->pProps, offStream - cbPreBuffer);
+ uint64_t const cNsElapsed = nsNow - nsStarted;
+ if (cNsWritten > cNsElapsed + RT_NS_10MS)
+ RTThreadSleep((cNsWritten - cNsElapsed - RT_NS_10MS / 2) / RT_NS_1MS);
+ }
+
+ uint32_t cbWritten = 0;
+ uint32_t const cbCanWrite = AudioTestMixStreamGetWritable(&pStream->Mix);
+ if (cbCanWrite)
+ {
+ if (g_uVerbosity >= 4)
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Stream is writable with %RU64ms (%RU32 bytes)\n",
+ idxTest, PDMAudioPropsBytesToMilli(pMix->pProps, cbCanWrite), cbCanWrite);
+
+ switch (enmState)
+ {
+ case AUDIOTESTSTATE_PRE:
+ RT_FALL_THROUGH();
+ case AUDIOTESTSTATE_POST:
+ {
+ if (g_uVerbosity >= 4)
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: %RU32 bytes (%RU64ms) beacon data remaining\n",
+ idxTest, AudioTestBeaconGetRemaining(&Beacon),
+ PDMAudioPropsBytesToMilli(&pStream->pStream->Cfg.Props, AudioTestBeaconGetRemaining(&Beacon)));
+
+ bool fGoToNextStage = false;
+
+ if ( AudioTestBeaconGetSize(&Beacon)
+ && !AudioTestBeaconIsComplete(&Beacon))
+ {
+ bool const fStarted = AudioTestBeaconGetRemaining(&Beacon) == AudioTestBeaconGetSize(&Beacon);
+
+ uint32_t const cbBeaconRemaining = AudioTestBeaconGetRemaining(&Beacon);
+ AssertBreakStmt(cbBeaconRemaining, VERR_WRONG_ORDER);
+
+ /* Limit to exactly one beacon (pre or post). */
+ uint32_t const cbToWrite = RT_MIN(sizeof(abBuf), RT_MIN(cbCanWrite, cbBeaconRemaining));
+
+ rc = AudioTestBeaconWrite(&Beacon, abBuf, cbToWrite);
+ if (RT_SUCCESS(rc))
+ {
+ rc = AudioTestMixStreamPlay(&pStream->Mix, abBuf, cbToWrite, &cbWritten);
+ if ( RT_SUCCESS(rc)
+ && pTstEnv)
+ {
+ /* Also write the beacon data to the test object.
+ * Note: We use cbPlayed here instead of cbToPlay to know if the data actually was
+ * reported as being played by the audio stack. */
+ rc = AudioTestObjWrite(Obj, abBuf, cbWritten);
+ }
+ }
+
+ if ( fStarted
+ && g_uVerbosity >= 2)
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Writing %s beacon begin\n",
+ idxTest, AudioTestBeaconTypeGetName(Beacon.enmType));
+ if (AudioTestBeaconIsComplete(&Beacon))
+ {
+ if (g_uVerbosity >= 2)
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Writing %s beacon end\n",
+ idxTest, AudioTestBeaconTypeGetName(Beacon.enmType));
+ fGoToNextStage = true;
+ }
+ }
+ else
+ fGoToNextStage = true;
+
+ if (fGoToNextStage)
+ {
+ if (enmState == AUDIOTESTSTATE_PRE)
+ enmState = AUDIOTESTSTATE_RUN;
+ else if (enmState == AUDIOTESTSTATE_POST)
+ enmState = AUDIOTESTSTATE_DONE;
+ }
+ break;
+ }
+
+ case AUDIOTESTSTATE_RUN:
+ {
+ uint32_t cbToWrite = RT_MIN(sizeof(abBuf), cbCanWrite);
+ cbToWrite = RT_MIN(cbToWrite, cbToWriteTotal - cbWrittenTotal);
+
+ if (g_uVerbosity >= 4)
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS,
+ "Test #%RU32: Playing back %RU32 bytes\n", idxTest, cbToWrite);
+
+ if (cbToWrite)
+ {
+ rc = AudioTestToneGenerate(&TstTone, abBuf, cbToWrite, &cbToWrite);
+ if (RT_SUCCESS(rc))
+ {
+ if (pTstEnv)
+ {
+ /* Write stuff to disk before trying to play it. Helps analysis later. */
+ rc = AudioTestObjWrite(Obj, abBuf, cbToWrite);
+ }
+
+ if (RT_SUCCESS(rc))
+ {
+ rc = AudioTestMixStreamPlay(&pStream->Mix, abBuf, cbToWrite, &cbWritten);
+ if (RT_SUCCESS(rc))
+ {
+ AssertBreakStmt(cbWritten <= cbToWrite, rc = VERR_TOO_MUCH_DATA);
+
+ offStream += cbWritten;
+
+ if (cbWritten != cbToWrite)
+ RTTestFailed(g_hTest, "Test #%RU32: Only played %RU32/%RU32 bytes",
+ idxTest, cbWritten, cbToWrite);
+
+ if (cbWritten)
+ nsLastWrite = nsNow;
+
+ if (g_uVerbosity >= 4)
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS,
+ "Test #%RU32: Played back %RU32 bytes\n", idxTest, cbWritten);
+
+ cbWrittenTotal += cbWritten;
+ }
+ }
+ }
+ }
+
+ if (RT_SUCCESS(rc))
+ {
+ const bool fComplete = cbWrittenTotal >= cbToWriteTotal;
+ if (fComplete)
+ {
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Playing back audio data ended\n", idxTest);
+
+ enmState = AUDIOTESTSTATE_POST;
+
+ /* Re-use the beacon object, but this time it's the post beacon. */
+ AudioTestBeaconInit(&Beacon, (uint8_t)idxTest, AUDIOTESTTONEBEACONTYPE_PLAY_POST,
+ &pStream->Cfg.Props);
+ }
+ }
+ else
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Playing back failed with %Rrc\n", idxTest, rc);
+ break;
+ }
+
+ case AUDIOTESTSTATE_DONE:
+ {
+ /* Handled below. */
+ break;
+ }
+
+ default:
+ AssertFailed();
+ break;
+ }
+
+ if (RT_FAILURE(rc))
+ break;
+
+ if (enmState == AUDIOTESTSTATE_DONE)
+ break;
+
+ nsLastMsgCantWrite = 0;
+ }
+ else if (AudioTestMixStreamIsOkay(&pStream->Mix))
+ {
+ RTMSINTERVAL const msSleep = RT_MIN(RT_MAX(1, pStream->Cfg.Device.cMsSchedulingHint), 256);
+
+ if ( g_uVerbosity >= 3
+ && ( !nsLastMsgCantWrite
+ || (nsNow - nsLastMsgCantWrite) > RT_NS_10SEC)) /* Don't spam the output too much. */
+ {
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Waiting %RU32ms for stream to be writable again (last write %RU64ns ago) ...\n",
+ idxTest, msSleep, nsNow - nsLastWrite);
+ nsLastMsgCantWrite = nsNow;
+ }
+
+ RTThreadSleep(msSleep);
+ }
+ else
+ AssertFailedBreakStmt(rc = VERR_AUDIO_STREAM_NOT_READY);
+
+ /* Fail-safe in case something screwed up while playing back. */
+ uint64_t const cNsElapsed = nsNow - nsStarted;
+ if (cNsElapsed > nsTimeout)
+ {
+ RTTestFailed(g_hTest, "Test #%RU32: Playback took too long (running %RU64 vs. timeout %RU64), aborting\n",
+ idxTest, cNsElapsed, nsTimeout);
+ rc = VERR_TIMEOUT;
+ }
+
+ if (RT_FAILURE(rc))
+ break;
+ } /* for */
+
+ if (cbWrittenTotal != cbToWriteTotal)
+ RTTestFailed(g_hTest, "Test #%RU32: Playback ended unexpectedly (%RU32/%RU32 played)\n",
+ idxTest, cbWrittenTotal, cbToWriteTotal);
+
+ if (RT_SUCCESS(rc))
+ {
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Draining stream ...\n", idxTest);
+ rc = AudioTestMixStreamDrain(&pStream->Mix, true /*fSync*/);
+ }
+ }
+ else
+ rc = VERR_AUDIO_STREAM_NOT_READY;
+
+ if (pTstEnv)
+ {
+ rc2 = AudioTestObjClose(Obj);
+ if (RT_SUCCESS(rc))
+ rc = rc2;
+ }
+
+ if (RT_FAILURE(rc))
+ RTTestFailed(g_hTest, "Test #%RU32: Playing tone failed with %Rrc\n", idxTest, rc);
+
+ return rc;
+}
+
+/**
+ * Records a test tone from a specific audio test stream.
+ *
+ * @returns VBox status code.
+ * @param pIoOpts I/O options to use.
+ * @param pTstEnv Test environment to use for running the test.
+ * @param pStream Stream to use for recording the tone.
+ * @param pParms Tone parameters to use.
+ *
+ * @note Blocking function.
+ */
+static int audioTestRecordTone(PAUDIOTESTIOOPTS pIoOpts, PAUDIOTESTENV pTstEnv, PAUDIOTESTSTREAM pStream, PAUDIOTESTTONEPARMS pParms)
+{
+ uint32_t const idxTest = (uint8_t)pParms->Hdr.idxTest;
+
+ const char *pcszPathOut = pTstEnv->Set.szPathAbs;
+
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Recording test tone (tone frequency is %RU16Hz, %RU32ms)\n",
+ idxTest, (uint16_t)pParms->dbFreqHz, pParms->msDuration);
+ RTTestPrintf(g_hTest, RTTESTLVL_DEBUG, "Test #%RU32: Writing to '%s'\n", idxTest, pcszPathOut);
+
+ /** @todo Use .WAV here? */
+ AUDIOTESTOBJ Obj;
+ int rc = AudioTestSetObjCreateAndRegister(&pTstEnv->Set, "guest-tone-rec.pcm", &Obj);
+ AssertRCReturn(rc, rc);
+
+ PAUDIOTESTDRVMIXSTREAM pMix = &pStream->Mix;
+
+ rc = AudioTestMixStreamEnable(pMix);
+ if (RT_SUCCESS(rc))
+ {
+ uint32_t cbRecTotal = 0; /* Counts everything, including silence / whatever. */
+ uint32_t cbTestToRec = PDMAudioPropsMilliToBytes(&pStream->Cfg.Props, pParms->msDuration);
+ uint32_t cbTestRec = 0;
+
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Recording %RU32 bytes total\n", idxTest, cbTestToRec);
+
+ /* We expect a pre + post beacon before + after the actual test tone.
+ * We always start with the pre beacon. */
+ AUDIOTESTTONEBEACON Beacon;
+ AudioTestBeaconInit(&Beacon, (uint8_t)pParms->Hdr.idxTest, AUDIOTESTTONEBEACONTYPE_PLAY_PRE, &pStream->Cfg.Props);
+
+ uint32_t const cbBeacon = AudioTestBeaconGetSize(&Beacon);
+ if (cbBeacon)
+ {
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Expecting 2 x %RU32 bytes pre/post beacons\n",
+ idxTest, cbBeacon);
+ if (g_uVerbosity >= 2)
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Waiting for %s beacon ...\n",
+ idxTest, AudioTestBeaconTypeGetName(Beacon.enmType));
+ }
+
+ AudioTestObjAddMetadataStr(Obj, "test_id=%04RU32\n", pParms->Hdr.idxTest);
+ AudioTestObjAddMetadataStr(Obj, "beacon_type=%RU32\n", (uint32_t)AudioTestBeaconGetType(&Beacon));
+ AudioTestObjAddMetadataStr(Obj, "beacon_pre_bytes=%RU32\n", cbBeacon);
+ AudioTestObjAddMetadataStr(Obj, "beacon_post_bytes=%RU32\n", cbBeacon);
+ AudioTestObjAddMetadataStr(Obj, "stream_to_record_bytes=%RU32\n", cbTestToRec);
+ AudioTestObjAddMetadataStr(Obj, "stream_buffer_size_ms=%RU32\n", pIoOpts->cMsBufferSize);
+ AudioTestObjAddMetadataStr(Obj, "stream_prebuf_size_ms=%RU32\n", pIoOpts->cMsPreBuffer);
+ /* Note: This mostly is provided by backend (e.g. PulseAudio / ALSA / ++) and
+ * has nothing to do with the device emulation scheduling hint. */
+ AudioTestObjAddMetadataStr(Obj, "device_scheduling_hint_ms=%RU32\n", pIoOpts->cMsSchedulingHint);
+
+ uint8_t abSamples[16384];
+ uint32_t const cbSamplesAligned = PDMAudioPropsFloorBytesToFrame(pMix->pProps, sizeof(abSamples));
+
+ uint64_t const nsStarted = RTTimeNanoTS();
+
+ uint64_t nsTimeout = RT_MS_5MIN_64 * RT_NS_1MS;
+ uint64_t nsLastMsgCantRead = 0; /* Timestamp (in ns) when the last message of an unreadable stream was shown. */
+
+ AUDIOTESTSTATE enmState = AUDIOTESTSTATE_PRE;
+
+ while (!g_fTerminate)
+ {
+ uint64_t const nsNow = RTTimeNanoTS();
+
+ /*
+ * Anything we can read?
+ */
+ uint32_t const cbCanRead = AudioTestMixStreamGetReadable(pMix);
+ if (cbCanRead)
+ {
+ if (g_uVerbosity >= 3)
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Stream is readable with %RU64ms (%RU32 bytes)\n",
+ idxTest, PDMAudioPropsBytesToMilli(pMix->pProps, cbCanRead), cbCanRead);
+
+ uint32_t const cbToRead = RT_MIN(cbCanRead, cbSamplesAligned);
+ uint32_t cbRecorded = 0;
+ rc = AudioTestMixStreamCapture(pMix, abSamples, cbToRead, &cbRecorded);
+ if (RT_SUCCESS(rc))
+ {
+ /* Flag indicating whether the whole block we're going to play is silence or not. */
+ bool const fIsAllSilence = PDMAudioPropsIsBufferSilence(&pStream->pStream->Cfg.Props, abSamples, cbRecorded);
+
+ cbRecTotal += cbRecorded; /* Do a bit of accounting. */
+
+ switch (enmState)
+ {
+ case AUDIOTESTSTATE_PRE:
+ RT_FALL_THROUGH();
+ case AUDIOTESTSTATE_POST:
+ {
+ bool fGoToNextStage = false;
+
+ if ( AudioTestBeaconGetSize(&Beacon)
+ && !AudioTestBeaconIsComplete(&Beacon))
+ {
+ bool const fStarted = AudioTestBeaconGetRemaining(&Beacon) == AudioTestBeaconGetSize(&Beacon);
+
+ size_t uOff;
+ rc = AudioTestBeaconAddConsecutive(&Beacon, abSamples, cbRecorded, &uOff);
+ if (RT_SUCCESS(rc))
+ {
+ /*
+ * When being in the AUDIOTESTSTATE_PRE state, we might get more audio data
+ * than we need for the pre-beacon to complete. In other words, that "more data"
+ * needs to be counted to the actual recorded test tone data then.
+ */
+ if (enmState == AUDIOTESTSTATE_PRE)
+ cbTestRec += cbRecorded - (uint32_t)uOff;
+ }
+
+ if ( fStarted
+ && g_uVerbosity >= 3)
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS,
+ "Test #%RU32: Detection of %s beacon started (%RU32ms recorded so far)\n",
+ idxTest, AudioTestBeaconTypeGetName(Beacon.enmType),
+ PDMAudioPropsBytesToMilli(&pStream->pStream->Cfg.Props, cbRecTotal));
+
+ if (AudioTestBeaconIsComplete(&Beacon))
+ {
+ if (g_uVerbosity >= 2)
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Detected %s beacon\n",
+ idxTest, AudioTestBeaconTypeGetName(Beacon.enmType));
+ fGoToNextStage = true;
+ }
+ }
+ else
+ fGoToNextStage = true;
+
+ if (fGoToNextStage)
+ {
+ if (enmState == AUDIOTESTSTATE_PRE)
+ enmState = AUDIOTESTSTATE_RUN;
+ else if (enmState == AUDIOTESTSTATE_POST)
+ enmState = AUDIOTESTSTATE_DONE;
+ }
+ break;
+ }
+
+ case AUDIOTESTSTATE_RUN:
+ {
+ /* Whether we count all silence as recorded data or not.
+ * Currently we don't, as otherwise consequtively played tones will be cut off in the end. */
+ if (!fIsAllSilence)
+ {
+ uint32_t const cbToAddMax = cbTestToRec - cbTestRec;
+
+ /* Don't read more than we're told to.
+ * After the actual test tone data there might come a post beacon which also
+ * needs to be handled in the AUDIOTESTSTATE_POST state then. */
+ if (cbRecorded > cbToAddMax)
+ cbRecorded = cbToAddMax;
+
+ cbTestRec += cbRecorded;
+ }
+
+ if (cbTestToRec - cbTestRec == 0) /* Done recording the test tone? */
+ {
+ enmState = AUDIOTESTSTATE_POST;
+
+ if (g_uVerbosity >= 2)
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Recording tone data done", idxTest);
+
+ if (AudioTestBeaconGetSize(&Beacon))
+ {
+ /* Re-use the beacon object, but this time it's the post beacon. */
+ AudioTestBeaconInit(&Beacon, (uint8_t)pParms->Hdr.idxTest, AUDIOTESTTONEBEACONTYPE_PLAY_POST,
+ &pStream->Cfg.Props);
+ if (g_uVerbosity >= 2)
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS,
+ "Test #%RU32: Waiting for %s beacon ...",
+ idxTest, AudioTestBeaconTypeGetName(Beacon.enmType));
+ }
+ }
+ break;
+ }
+
+ case AUDIOTESTSTATE_DONE:
+ {
+ /* Nothing to do here. */
+ break;
+ }
+
+ default:
+ AssertFailed();
+ break;
+ }
+ }
+
+ if (cbRecorded)
+ {
+ /* Always write (record) everything, no matter if the current audio contains complete silence or not.
+ * Might be also become handy later if we want to have a look at start/stop timings and so on. */
+ rc = AudioTestObjWrite(Obj, abSamples, cbRecorded);
+ AssertRCBreak(rc);
+ }
+
+ if (enmState == AUDIOTESTSTATE_DONE) /* Bail out when in state "done". */
+ break;
+ }
+ else if (AudioTestMixStreamIsOkay(pMix))
+ {
+ RTMSINTERVAL const msSleep = RT_MIN(RT_MAX(1, pStream->Cfg.Device.cMsSchedulingHint), 256);
+
+ if ( g_uVerbosity >= 3
+ && ( !nsLastMsgCantRead
+ || (nsNow - nsLastMsgCantRead) > RT_NS_10SEC)) /* Don't spam the output too much. */
+ {
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Waiting %RU32ms for stream to be readable again ...\n",
+ idxTest, msSleep);
+ nsLastMsgCantRead = nsNow;
+ }
+
+ RTThreadSleep(msSleep);
+ }
+
+ /* Fail-safe in case something screwed up while playing back. */
+ uint64_t const cNsElapsed = nsNow - nsStarted;
+ if (cNsElapsed > nsTimeout)
+ {
+ RTTestFailed(g_hTest, "Test #%RU32: Recording took too long (running %RU64 vs. timeout %RU64), aborting\n",
+ idxTest, cNsElapsed, nsTimeout);
+ rc = VERR_TIMEOUT;
+ }
+
+ if (RT_FAILURE(rc))
+ break;
+ }
+
+ if (g_uVerbosity >= 2)
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Recorded %RU32 bytes total\n", idxTest, cbRecTotal);
+ if (cbTestRec != cbTestToRec)
+ {
+ RTTestFailed(g_hTest, "Test #%RU32: Recording ended unexpectedly (%RU32/%RU32 recorded)\n",
+ idxTest, cbTestRec, cbTestToRec);
+ rc = VERR_WRONG_ORDER; /** @todo Find a better rc. */
+ }
+
+ if (RT_FAILURE(rc))
+ RTTestFailed(g_hTest, "Test #%RU32: Recording failed (state is '%s')\n", idxTest, AudioTestStateToStr(enmState));
+
+ int rc2 = AudioTestMixStreamDisable(pMix);
+ if (RT_SUCCESS(rc))
+ rc = rc2;
+ }
+
+ int rc2 = AudioTestObjClose(Obj);
+ if (RT_SUCCESS(rc))
+ rc = rc2;
+
+ if (RT_FAILURE(rc))
+ RTTestFailed(g_hTest, "Test #%RU32: Recording tone done failed with %Rrc\n", idxTest, rc);
+
+ return rc;
+}
+
+
+/*********************************************************************************************************************************
+* ATS Callback Implementations *
+*********************************************************************************************************************************/
+
+/** @copydoc ATSCALLBACKS::pfnHowdy
+ *
+ * @note Runs as part of the guest ATS.
+ */
+static DECLCALLBACK(int) audioTestGstAtsHowdyCallback(void const *pvUser)
+{
+ PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser;
+
+ AssertReturn(pCtx->cClients <= UINT8_MAX - 1, VERR_BUFFER_OVERFLOW);
+
+ pCtx->cClients++;
+
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "New client connected, now %RU8 total\n", pCtx->cClients);
+
+ return VINF_SUCCESS;
+}
+
+/** @copydoc ATSCALLBACKS::pfnBye
+ *
+ * @note Runs as part of the guest ATS.
+ */
+static DECLCALLBACK(int) audioTestGstAtsByeCallback(void const *pvUser)
+{
+ PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser;
+
+ AssertReturn(pCtx->cClients, VERR_WRONG_ORDER);
+ pCtx->cClients--;
+
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Client wants to disconnect, %RU8 remaining\n", pCtx->cClients);
+
+ if (0 == pCtx->cClients) /* All clients disconnected? Tear things down. */
+ {
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Last client disconnected, terminating server ...\n");
+ ASMAtomicWriteBool(&g_fTerminate, true);
+ }
+
+ return VINF_SUCCESS;
+}
+
+/** @copydoc ATSCALLBACKS::pfnTestSetBegin
+ *
+ * @note Runs as part of the guest ATS.
+ */
+static DECLCALLBACK(int) audioTestGstAtsTestSetBeginCallback(void const *pvUser, const char *pszTag)
+{
+ PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser;
+ PAUDIOTESTENV pTstEnv = pCtx->pTstEnv;
+
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Got request for beginning test set '%s' in '%s'\n", pszTag, pTstEnv->szPathTemp);
+
+ return AudioTestSetCreate(&pTstEnv->Set, pTstEnv->szPathTemp, pszTag);
+}
+
+/** @copydoc ATSCALLBACKS::pfnTestSetEnd
+ *
+ * @note Runs as part of the guest ATS.
+ */
+static DECLCALLBACK(int) audioTestGstAtsTestSetEndCallback(void const *pvUser, const char *pszTag)
+{
+ PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser;
+ PAUDIOTESTENV pTstEnv = pCtx->pTstEnv;
+
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Got request for ending test set '%s'\n", pszTag);
+
+ /* Pack up everything to be ready for transmission. */
+ return audioTestEnvPrologue(pTstEnv, true /* fPack */, pCtx->szTestSetArchive, sizeof(pCtx->szTestSetArchive));
+}
+
+/** @copydoc ATSCALLBACKS::pfnTonePlay
+ *
+ * @note Runs as part of the guest ATS.
+ */
+static DECLCALLBACK(int) audioTestGstAtsTonePlayCallback(void const *pvUser, PAUDIOTESTTONEPARMS pToneParms)
+{
+ PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser;
+ PAUDIOTESTENV pTstEnv = pCtx->pTstEnv;
+ PAUDIOTESTIOOPTS pIoOpts = &pTstEnv->IoOpts;
+
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Got request for playing test tone #%RU32 (%RU16Hz, %RU32ms) ...\n",
+ pToneParms->Hdr.idxTest, (uint16_t)pToneParms->dbFreqHz, pToneParms->msDuration);
+
+ char szTimeCreated[RTTIME_STR_LEN];
+ RTTimeToString(&pToneParms->Hdr.tsCreated, szTimeCreated, sizeof(szTimeCreated));
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test created (caller UTC): %s\n", szTimeCreated);
+
+ const PAUDIOTESTSTREAM pTstStream = &pTstEnv->aStreams[0]; /** @todo Make this dynamic. */
+
+ int rc = audioTestStreamInit(pTstEnv->pDrvStack, pTstStream, PDMAUDIODIR_OUT, pIoOpts);
+ if (RT_SUCCESS(rc))
+ {
+ AUDIOTESTPARMS TstParms;
+ RT_ZERO(TstParms);
+ TstParms.enmType = AUDIOTESTTYPE_TESTTONE_PLAY;
+ TstParms.enmDir = PDMAUDIODIR_OUT;
+ TstParms.TestTone = *pToneParms;
+
+ PAUDIOTESTENTRY pTst;
+ rc = AudioTestSetTestBegin(&pTstEnv->Set, "Playing test tone", &TstParms, &pTst);
+ if (RT_SUCCESS(rc))
+ {
+ rc = audioTestPlayTone(&pTstEnv->IoOpts, pTstEnv, pTstStream, pToneParms);
+ if (RT_SUCCESS(rc))
+ {
+ AudioTestSetTestDone(pTst);
+ }
+ else
+ AudioTestSetTestFailed(pTst, rc, "Playing tone failed");
+ }
+
+ int rc2 = audioTestStreamDestroy(pTstEnv->pDrvStack, pTstStream);
+ if (RT_SUCCESS(rc))
+ rc = rc2;
+ }
+ else
+ RTTestFailed(g_hTest, "Error creating output stream, rc=%Rrc\n", rc);
+
+ return rc;
+}
+
+/** @copydoc ATSCALLBACKS::pfnToneRecord */
+static DECLCALLBACK(int) audioTestGstAtsToneRecordCallback(void const *pvUser, PAUDIOTESTTONEPARMS pToneParms)
+{
+ PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser;
+ PAUDIOTESTENV pTstEnv = pCtx->pTstEnv;
+ PAUDIOTESTIOOPTS pIoOpts = &pTstEnv->IoOpts;
+
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Got request for recording test tone #%RU32 (%RU32ms) ...\n",
+ pToneParms->Hdr.idxTest, pToneParms->msDuration);
+
+ char szTimeCreated[RTTIME_STR_LEN];
+ RTTimeToString(&pToneParms->Hdr.tsCreated, szTimeCreated, sizeof(szTimeCreated));
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test created (caller UTC): %s\n", szTimeCreated);
+
+ const PAUDIOTESTSTREAM pTstStream = &pTstEnv->aStreams[0]; /** @todo Make this dynamic. */
+
+ int rc = audioTestStreamInit(pTstEnv->pDrvStack, pTstStream, PDMAUDIODIR_IN, pIoOpts);
+ if (RT_SUCCESS(rc))
+ {
+ AUDIOTESTPARMS TstParms;
+ RT_ZERO(TstParms);
+ TstParms.enmType = AUDIOTESTTYPE_TESTTONE_RECORD;
+ TstParms.enmDir = PDMAUDIODIR_IN;
+ TstParms.TestTone = *pToneParms;
+
+ PAUDIOTESTENTRY pTst;
+ rc = AudioTestSetTestBegin(&pTstEnv->Set, "Recording test tone from host", &TstParms, &pTst);
+ if (RT_SUCCESS(rc))
+ {
+ rc = audioTestRecordTone(pIoOpts, pTstEnv, pTstStream, pToneParms);
+ if (RT_SUCCESS(rc))
+ {
+ AudioTestSetTestDone(pTst);
+ }
+ else
+ AudioTestSetTestFailed(pTst, rc, "Recording tone failed");
+ }
+
+ int rc2 = audioTestStreamDestroy(pTstEnv->pDrvStack, pTstStream);
+ if (RT_SUCCESS(rc))
+ rc = rc2;
+ }
+ else
+ RTTestFailed(g_hTest, "Error creating input stream, rc=%Rrc\n", rc);
+
+ return rc;
+}
+
+/** @copydoc ATSCALLBACKS::pfnTestSetSendBegin */
+static DECLCALLBACK(int) audioTestGstAtsTestSetSendBeginCallback(void const *pvUser, const char *pszTag)
+{
+ RT_NOREF(pszTag);
+
+ PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser;
+
+ if (!RTFileExists(pCtx->szTestSetArchive)) /* Has the archive successfully been created yet? */
+ return VERR_WRONG_ORDER;
+
+ int rc = RTFileOpen(&pCtx->hTestSetArchive, pCtx->szTestSetArchive, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_WRITE);
+ if (RT_SUCCESS(rc))
+ {
+ uint64_t uSize;
+ rc = RTFileQuerySize(pCtx->hTestSetArchive, &uSize);
+ if (RT_SUCCESS(rc))
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Sending test set '%s' (%zu bytes)\n", pCtx->szTestSetArchive, uSize);
+ }
+
+ return rc;
+}
+
+/** @copydoc ATSCALLBACKS::pfnTestSetSendRead */
+static DECLCALLBACK(int) audioTestGstAtsTestSetSendReadCallback(void const *pvUser,
+ const char *pszTag, void *pvBuf, size_t cbBuf, size_t *pcbRead)
+{
+ RT_NOREF(pszTag);
+
+ PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser;
+
+ return RTFileRead(pCtx->hTestSetArchive, pvBuf, cbBuf, pcbRead);
+}
+
+/** @copydoc ATSCALLBACKS::pfnTestSetSendEnd */
+static DECLCALLBACK(int) audioTestGstAtsTestSetSendEndCallback(void const *pvUser, const char *pszTag)
+{
+ RT_NOREF(pszTag);
+
+ PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser;
+
+ int rc = RTFileClose(pCtx->hTestSetArchive);
+ if (RT_SUCCESS(rc))
+ {
+ pCtx->hTestSetArchive = NIL_RTFILE;
+ }
+
+ return rc;
+}
+
+
+/*********************************************************************************************************************************
+* Implementation of audio test environment handling *
+*********************************************************************************************************************************/
+
+/**
+ * Connects an ATS client via TCP/IP to a peer.
+ *
+ * @returns VBox status code.
+ * @param pTstEnv Test environment to use.
+ * @param pClient Client to connect.
+ * @param pszWhat Hint of what to connect to where.
+ * @param pTcpOpts Pointer to TCP options to use.
+ */
+int audioTestEnvConnectViaTcp(PAUDIOTESTENV pTstEnv, PATSCLIENT pClient, const char *pszWhat, PAUDIOTESTENVTCPOPTS pTcpOpts)
+{
+ RT_NOREF(pTstEnv);
+
+ RTGETOPTUNION Val;
+ RT_ZERO(Val);
+
+ Val.u32 = pTcpOpts->enmConnMode;
+ int rc = AudioTestSvcClientHandleOption(pClient, ATSTCPOPT_CONN_MODE, &Val);
+ AssertRCReturn(rc, rc);
+
+ if ( pTcpOpts->enmConnMode == ATSCONNMODE_BOTH
+ || pTcpOpts->enmConnMode == ATSCONNMODE_SERVER)
+ {
+ Assert(pTcpOpts->uBindPort); /* Always set by the caller. */
+ Val.u16 = pTcpOpts->uBindPort;
+ rc = AudioTestSvcClientHandleOption(pClient, ATSTCPOPT_BIND_PORT, &Val);
+ AssertRCReturn(rc, rc);
+
+ if (pTcpOpts->szBindAddr[0])
+ {
+ Val.psz = pTcpOpts->szBindAddr;
+ rc = AudioTestSvcClientHandleOption(pClient, ATSTCPOPT_BIND_ADDRESS, &Val);
+ AssertRCReturn(rc, rc);
+ }
+ else
+ {
+ RTTestFailed(g_hTest, "No bind address specified!\n");
+ return VERR_INVALID_PARAMETER;
+ }
+
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Connecting %s by listening as server at %s:%RU32 ...\n",
+ pszWhat, pTcpOpts->szBindAddr, pTcpOpts->uBindPort);
+ }
+
+
+ if ( pTcpOpts->enmConnMode == ATSCONNMODE_BOTH
+ || pTcpOpts->enmConnMode == ATSCONNMODE_CLIENT)
+ {
+ Assert(pTcpOpts->uConnectPort); /* Always set by the caller. */
+ Val.u16 = pTcpOpts->uConnectPort;
+ rc = AudioTestSvcClientHandleOption(pClient, ATSTCPOPT_CONNECT_PORT, &Val);
+ AssertRCReturn(rc, rc);
+
+ if (pTcpOpts->szConnectAddr[0])
+ {
+ Val.psz = pTcpOpts->szConnectAddr;
+ rc = AudioTestSvcClientHandleOption(pClient, ATSTCPOPT_CONNECT_ADDRESS, &Val);
+ AssertRCReturn(rc, rc);
+ }
+ else
+ {
+ RTTestFailed(g_hTest, "No connect address specified!\n");
+ return VERR_INVALID_PARAMETER;
+ }
+
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Connecting %s by connecting as client to %s:%RU32 ...\n",
+ pszWhat, pTcpOpts->szConnectAddr, pTcpOpts->uConnectPort);
+ }
+
+ rc = AudioTestSvcClientConnect(pClient);
+ if (RT_FAILURE(rc))
+ {
+ RTTestFailed(g_hTest, "Connecting %s failed with %Rrc\n", pszWhat, rc);
+ return rc;
+ }
+
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Successfully connected %s\n", pszWhat);
+ return rc;
+}
+
+/**
+ * Configures and starts an ATS TCP/IP server.
+ *
+ * @returns VBox status code.
+ * @param pSrv ATS server instance to configure and start.
+ * @param pCallbacks ATS callback table to use.
+ * @param pszDesc Hint of server type which is being started.
+ * @param pTcpOpts TCP options to use.
+ */
+int audioTestEnvConfigureAndStartTcpServer(PATSSERVER pSrv, PCATSCALLBACKS pCallbacks, const char *pszDesc,
+ PAUDIOTESTENVTCPOPTS pTcpOpts)
+{
+ RTGETOPTUNION Val;
+ RT_ZERO(Val);
+
+ int rc = AudioTestSvcInit(pSrv, pCallbacks);
+ if (RT_FAILURE(rc))
+ return rc;
+
+ Val.u32 = pTcpOpts->enmConnMode;
+ rc = AudioTestSvcHandleOption(pSrv, ATSTCPOPT_CONN_MODE, &Val);
+ AssertRCReturn(rc, rc);
+
+ if ( pTcpOpts->enmConnMode == ATSCONNMODE_BOTH
+ || pTcpOpts->enmConnMode == ATSCONNMODE_SERVER)
+ {
+ Assert(pTcpOpts->uBindPort); /* Always set by the caller. */
+ Val.u16 = pTcpOpts->uBindPort;
+ rc = AudioTestSvcHandleOption(pSrv, ATSTCPOPT_BIND_PORT, &Val);
+ AssertRCReturn(rc, rc);
+
+ if (pTcpOpts->szBindAddr[0])
+ {
+ Val.psz = pTcpOpts->szBindAddr;
+ rc = AudioTestSvcHandleOption(pSrv, ATSTCPOPT_BIND_ADDRESS, &Val);
+ AssertRCReturn(rc, rc);
+ }
+ else
+ {
+ RTTestFailed(g_hTest, "No bind address specified!\n");
+ return VERR_INVALID_PARAMETER;
+ }
+
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Starting server for %s at %s:%RU32 ...\n",
+ pszDesc, pTcpOpts->szBindAddr, pTcpOpts->uBindPort);
+ }
+
+
+ if ( pTcpOpts->enmConnMode == ATSCONNMODE_BOTH
+ || pTcpOpts->enmConnMode == ATSCONNMODE_CLIENT)
+ {
+ Assert(pTcpOpts->uConnectPort); /* Always set by the caller. */
+ Val.u16 = pTcpOpts->uConnectPort;
+ rc = AudioTestSvcHandleOption(pSrv, ATSTCPOPT_CONNECT_PORT, &Val);
+ AssertRCReturn(rc, rc);
+
+ if (pTcpOpts->szConnectAddr[0])
+ {
+ Val.psz = pTcpOpts->szConnectAddr;
+ rc = AudioTestSvcHandleOption(pSrv, ATSTCPOPT_CONNECT_ADDRESS, &Val);
+ AssertRCReturn(rc, rc);
+ }
+ else
+ {
+ RTTestFailed(g_hTest, "No connect address specified!\n");
+ return VERR_INVALID_PARAMETER;
+ }
+
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Starting server for %s by connecting as client to %s:%RU32 ...\n",
+ pszDesc, pTcpOpts->szConnectAddr, pTcpOpts->uConnectPort);
+ }
+
+ if (RT_SUCCESS(rc))
+ {
+ rc = AudioTestSvcStart(pSrv);
+ if (RT_FAILURE(rc))
+ RTTestFailed(g_hTest, "Starting server for %s failed with %Rrc\n", pszDesc, rc);
+ }
+
+ return rc;
+}
+
+/**
+ * Initializes an audio test environment.
+ *
+ * @param pTstEnv Audio test environment to initialize.
+ */
+void audioTestEnvInit(PAUDIOTESTENV pTstEnv)
+{
+ RT_BZERO(pTstEnv, sizeof(AUDIOTESTENV));
+
+ audioTestIoOptsInitDefaults(&pTstEnv->IoOpts);
+ audioTestToneParmsInit(&pTstEnv->ToneParms);
+}
+
+/**
+ * Creates an audio test environment.
+ *
+ * @returns VBox status code.
+ * @param pTstEnv Audio test environment to create.
+ * @param pDrvStack Driver stack to use.
+ */
+int audioTestEnvCreate(PAUDIOTESTENV pTstEnv, PAUDIOTESTDRVSTACK pDrvStack)
+{
+ AssertReturn(PDMAudioPropsAreValid(&pTstEnv->IoOpts.Props), VERR_WRONG_ORDER);
+
+ int rc = VINF_SUCCESS;
+
+ pTstEnv->pDrvStack = pDrvStack;
+
+ /*
+ * Set sane defaults if not already set.
+ */
+ if (!RTStrNLen(pTstEnv->szTag, sizeof(pTstEnv->szTag)))
+ {
+ rc = AudioTestGenTag(pTstEnv->szTag, sizeof(pTstEnv->szTag));
+ AssertRCReturn(rc, rc);
+ }
+
+ if (!RTStrNLen(pTstEnv->szPathTemp, sizeof(pTstEnv->szPathTemp)))
+ {
+ rc = AudioTestPathGetTemp(pTstEnv->szPathTemp, sizeof(pTstEnv->szPathTemp));
+ AssertRCReturn(rc, rc);
+ }
+
+ if (!RTStrNLen(pTstEnv->szPathOut, sizeof(pTstEnv->szPathOut)))
+ {
+ rc = RTPathJoin(pTstEnv->szPathOut, sizeof(pTstEnv->szPathOut), pTstEnv->szPathTemp, "vkat-temp");
+ AssertRCReturn(rc, rc);
+ }
+
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Initializing environment for mode '%s'\n", pTstEnv->enmMode == AUDIOTESTMODE_HOST ? "host" : "guest");
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Using tag '%s'\n", pTstEnv->szTag);
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Output directory is '%s'\n", pTstEnv->szPathOut);
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Temp directory is '%s'\n", pTstEnv->szPathTemp);
+
+ char szPathTemp[RTPATH_MAX];
+ if ( !strlen(pTstEnv->szPathTemp)
+ || !strlen(pTstEnv->szPathOut))
+ rc = RTPathTemp(szPathTemp, sizeof(szPathTemp));
+
+ if ( RT_SUCCESS(rc)
+ && !strlen(pTstEnv->szPathTemp))
+ rc = RTPathJoin(pTstEnv->szPathTemp, sizeof(pTstEnv->szPathTemp), szPathTemp, "vkat-temp");
+
+ if (RT_SUCCESS(rc))
+ {
+ rc = RTDirCreate(pTstEnv->szPathTemp, RTFS_UNIX_IRWXU, 0 /* fFlags */);
+ if (rc == VERR_ALREADY_EXISTS)
+ rc = VINF_SUCCESS;
+ }
+
+ if ( RT_SUCCESS(rc)
+ && !strlen(pTstEnv->szPathOut))
+ rc = RTPathJoin(pTstEnv->szPathOut, sizeof(pTstEnv->szPathOut), szPathTemp, "vkat");
+
+ if (RT_SUCCESS(rc))
+ {
+ rc = RTDirCreate(pTstEnv->szPathOut, RTFS_UNIX_IRWXU, 0 /* fFlags */);
+ if (rc == VERR_ALREADY_EXISTS)
+ rc = VINF_SUCCESS;
+ }
+
+ if (RT_FAILURE(rc))
+ return rc;
+
+ /**
+ * For NAT'ed VMs we use (default):
+ * - client mode (uConnectAddr / uConnectPort) on the guest.
+ * - server mode (uBindAddr / uBindPort) on the host.
+ */
+ if ( !pTstEnv->TcpOpts.szConnectAddr[0]
+ && !pTstEnv->TcpOpts.szBindAddr[0])
+ RTStrCopy(pTstEnv->TcpOpts.szBindAddr, sizeof(pTstEnv->TcpOpts.szBindAddr), "0.0.0.0");
+
+ /*
+ * Determine connection mode based on set variables.
+ */
+ if ( pTstEnv->TcpOpts.szBindAddr[0]
+ && pTstEnv->TcpOpts.szConnectAddr[0])
+ {
+ pTstEnv->TcpOpts.enmConnMode = ATSCONNMODE_BOTH;
+ }
+ else if (pTstEnv->TcpOpts.szBindAddr[0])
+ pTstEnv->TcpOpts.enmConnMode = ATSCONNMODE_SERVER;
+ else /* "Reversed mode", i.e. used for NATed VMs. */
+ pTstEnv->TcpOpts.enmConnMode = ATSCONNMODE_CLIENT;
+
+ /* Set a back reference to the test environment for the callback context. */
+ pTstEnv->CallbackCtx.pTstEnv = pTstEnv;
+
+ ATSCALLBACKS Callbacks;
+ RT_ZERO(Callbacks);
+ Callbacks.pvUser = &pTstEnv->CallbackCtx;
+
+ if (pTstEnv->enmMode == AUDIOTESTMODE_GUEST)
+ {
+ Callbacks.pfnHowdy = audioTestGstAtsHowdyCallback;
+ Callbacks.pfnBye = audioTestGstAtsByeCallback;
+ Callbacks.pfnTestSetBegin = audioTestGstAtsTestSetBeginCallback;
+ Callbacks.pfnTestSetEnd = audioTestGstAtsTestSetEndCallback;
+ Callbacks.pfnTonePlay = audioTestGstAtsTonePlayCallback;
+ Callbacks.pfnToneRecord = audioTestGstAtsToneRecordCallback;
+ Callbacks.pfnTestSetSendBegin = audioTestGstAtsTestSetSendBeginCallback;
+ Callbacks.pfnTestSetSendRead = audioTestGstAtsTestSetSendReadCallback;
+ Callbacks.pfnTestSetSendEnd = audioTestGstAtsTestSetSendEndCallback;
+
+ if (!pTstEnv->TcpOpts.uBindPort)
+ pTstEnv->TcpOpts.uBindPort = ATS_TCP_DEF_BIND_PORT_GUEST;
+
+ if (!pTstEnv->TcpOpts.uConnectPort)
+ pTstEnv->TcpOpts.uConnectPort = ATS_TCP_DEF_CONNECT_PORT_GUEST;
+
+ pTstEnv->pSrv = (PATSSERVER)RTMemAlloc(sizeof(ATSSERVER));
+ AssertPtrReturn(pTstEnv->pSrv, VERR_NO_MEMORY);
+
+ /*
+ * Start the ATS (Audio Test Service) on the guest side.
+ * That service then will perform playback and recording operations on the guest, triggered from the host.
+ *
+ * When running this in self-test mode, that service also can be run on the host if nothing else is specified.
+ * Note that we have to bind to "0.0.0.0" by default so that the host can connect to it.
+ */
+ rc = audioTestEnvConfigureAndStartTcpServer(pTstEnv->pSrv, &Callbacks, "guest", &pTstEnv->TcpOpts);
+ }
+ else /* Host mode */
+ {
+ if (!pTstEnv->TcpOpts.uBindPort)
+ pTstEnv->TcpOpts.uBindPort = ATS_TCP_DEF_BIND_PORT_HOST;
+
+ if (!pTstEnv->TcpOpts.uConnectPort)
+ pTstEnv->TcpOpts.uConnectPort = ATS_TCP_DEF_CONNECT_PORT_HOST_PORT_FWD;
+
+ /**
+ * Note: Don't set pTstEnv->TcpOpts.szTcpConnectAddr by default here, as this specifies what connection mode
+ * (client / server / both) we use on the host.
+ */
+
+ /* We need to start a server on the host so that VMs configured with NAT networking
+ * can connect to it as well. */
+ rc = AudioTestSvcClientCreate(&pTstEnv->u.Host.AtsClGuest);
+ if (RT_SUCCESS(rc))
+ rc = audioTestEnvConnectViaTcp(pTstEnv, &pTstEnv->u.Host.AtsClGuest,
+ "host -> guest", &pTstEnv->TcpOpts);
+ if (RT_SUCCESS(rc))
+ {
+ AUDIOTESTENVTCPOPTS ValKitTcpOpts;
+ RT_ZERO(ValKitTcpOpts);
+
+ /* We only connect as client to the Validation Kit audio driver ATS. */
+ ValKitTcpOpts.enmConnMode = ATSCONNMODE_CLIENT;
+
+ /* For now we ASSUME that the Validation Kit audio driver ATS runs on the same host as VKAT (this binary) runs on. */
+ ValKitTcpOpts.uConnectPort = ATS_TCP_DEF_CONNECT_PORT_VALKIT; /** @todo Make this dynamic. */
+ RTStrCopy(ValKitTcpOpts.szConnectAddr, sizeof(ValKitTcpOpts.szConnectAddr), ATS_TCP_DEF_CONNECT_HOST_ADDR_STR); /** @todo Ditto. */
+
+ rc = AudioTestSvcClientCreate(&pTstEnv->u.Host.AtsClValKit);
+ if (RT_SUCCESS(rc))
+ {
+ rc = audioTestEnvConnectViaTcp(pTstEnv, &pTstEnv->u.Host.AtsClValKit,
+ "host -> valkit", &ValKitTcpOpts);
+ if (RT_FAILURE(rc))
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Unable to connect to the Validation Kit audio driver!\n"
+ "There could be multiple reasons:\n\n"
+ " - Wrong host being used\n"
+ " - VirtualBox host version is too old\n"
+ " - Audio debug mode is not enabled\n"
+ " - Support for Validation Kit audio driver is not included\n"
+ " - Firewall / network configuration problem\n");
+ }
+ }
+ }
+
+ return rc;
+}
+
+/**
+ * Destroys an audio test environment.
+ *
+ * @param pTstEnv Audio test environment to destroy.
+ */
+void audioTestEnvDestroy(PAUDIOTESTENV pTstEnv)
+{
+ if (!pTstEnv)
+ return;
+
+ /* When in host mode, we need to destroy our ATS clients in order to also let
+ * the ATS server(s) know we're going to quit. */
+ if (pTstEnv->enmMode == AUDIOTESTMODE_HOST)
+ {
+ AudioTestSvcClientDestroy(&pTstEnv->u.Host.AtsClValKit);
+ AudioTestSvcClientDestroy(&pTstEnv->u.Host.AtsClGuest);
+ }
+
+ if (pTstEnv->pSrv)
+ {
+ int rc2 = AudioTestSvcDestroy(pTstEnv->pSrv);
+ AssertRC(rc2);
+
+ RTMemFree(pTstEnv->pSrv);
+ pTstEnv->pSrv = NULL;
+ }
+
+ for (unsigned i = 0; i < RT_ELEMENTS(pTstEnv->aStreams); i++)
+ {
+ int rc2 = audioTestStreamDestroy(pTstEnv->pDrvStack, &pTstEnv->aStreams[i]);
+ if (RT_FAILURE(rc2))
+ RTTestFailed(g_hTest, "Stream destruction for stream #%u failed with %Rrc\n", i, rc2);
+ }
+
+ /* Try cleaning up a bit. */
+ RTDirRemove(pTstEnv->szPathTemp);
+ RTDirRemove(pTstEnv->szPathOut);
+
+ pTstEnv->pDrvStack = NULL;
+}
+
+/**
+ * Closes, packs up and destroys a test environment.
+ *
+ * @returns VBox status code.
+ * @param pTstEnv Test environment to handle.
+ * @param fPack Whether to pack the test set up before destroying / wiping it.
+ * @param pszPackFile Where to store the packed test set file on success. Can be NULL if \a fPack is \c false.
+ * @param cbPackFile Size (in bytes) of \a pszPackFile. Can be 0 if \a fPack is \c false.
+ */
+int audioTestEnvPrologue(PAUDIOTESTENV pTstEnv, bool fPack, char *pszPackFile, size_t cbPackFile)
+{
+ /* Close the test set first. */
+ AudioTestSetClose(&pTstEnv->Set);
+
+ int rc = VINF_SUCCESS;
+
+ if (fPack)
+ {
+ /* Before destroying the test environment, pack up the test set so
+ * that it's ready for transmission. */
+ rc = AudioTestSetPack(&pTstEnv->Set, pTstEnv->szPathOut, pszPackFile, cbPackFile);
+ if (RT_SUCCESS(rc))
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test set packed up to '%s'\n", pszPackFile);
+ }
+
+ if (!g_fDrvAudioDebug) /* Don't wipe stuff when debugging. Can be useful for introspecting data. */
+ /* ignore rc */ AudioTestSetWipe(&pTstEnv->Set);
+
+ AudioTestSetDestroy(&pTstEnv->Set);
+
+ if (RT_FAILURE(rc))
+ RTTestFailed(g_hTest, "Test set prologue failed with %Rrc\n", rc);
+
+ return rc;
+}
+
+/**
+ * Initializes an audio test parameters set.
+ *
+ * @param pTstParms Test parameters set to initialize.
+ */
+void audioTestParmsInit(PAUDIOTESTPARMS pTstParms)
+{
+ RT_ZERO(*pTstParms);
+}
+
+/**
+ * Destroys an audio test parameters set.
+ *
+ * @param pTstParms Test parameters set to destroy.
+ */
+void audioTestParmsDestroy(PAUDIOTESTPARMS pTstParms)
+{
+ if (!pTstParms)
+ return;
+
+ return;
+}
+
diff --git a/src/VBox/ValidationKit/utils/audio/vkatDriverStack.cpp b/src/VBox/ValidationKit/utils/audio/vkatDriverStack.cpp
new file mode 100644
index 00000000..89dbb4ae
--- /dev/null
+++ b/src/VBox/ValidationKit/utils/audio/vkatDriverStack.cpp
@@ -0,0 +1,1621 @@
+/* $Id: vkatDriverStack.cpp $ */
+/** @file
+ * Validation Kit Audio Test (VKAT) - Driver stack code.
+ */
+
+/*
+ * 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/log.h>
+
+#include <iprt/errcore.h>
+#include <iprt/message.h>
+#include <iprt/stream.h>
+#include <iprt/string.h>
+#include <iprt/uuid.h>
+#include <iprt/test.h>
+
+
+/**
+ * Internal driver instance data
+ * @note This must be put here as it's needed before pdmdrv.h is included.
+ */
+typedef struct PDMDRVINSINT
+{
+ /** The stack the drive belongs to. */
+ struct AUDIOTESTDRVSTACK *pStack;
+} PDMDRVINSINT;
+#define PDMDRVINSINT_DECLARED
+
+#include "vkatInternal.h"
+#include "VBoxDD.h"
+
+
+
+/*********************************************************************************************************************************
+* Fake PDM Driver Handling. *
+*********************************************************************************************************************************/
+
+/** @name Driver Fakes/Stubs
+ *
+ * @{ */
+
+VMMR3DECL(PCFGMNODE) audioTestDrvHlp_CFGMR3GetChild(PCFGMNODE pNode, const char *pszPath)
+{
+ RT_NOREF(pNode, pszPath);
+ return NULL;
+}
+
+
+VMMR3DECL(int) audioTestDrvHlp_CFGMR3QueryString(PCFGMNODE pNode, const char *pszName, char *pszString, size_t cchString)
+{
+ if (pNode != NULL)
+ {
+ PCPDMDRVREG pDrvReg = (PCPDMDRVREG)pNode;
+ if (g_uVerbosity > 2)
+ RTPrintf("debug: CFGMR3QueryString([%s], %s, %p, %#x)\n", pDrvReg->szName, pszName, pszString, cchString);
+
+ if ( ( strcmp(pDrvReg->szName, "PulseAudio") == 0
+ || strcmp(pDrvReg->szName, "HostAudioWas") == 0)
+ && strcmp(pszName, "VmName") == 0)
+ return RTStrCopy(pszString, cchString, "vkat");
+
+ if ( strcmp(pDrvReg->szName, "HostAudioWas") == 0
+ && strcmp(pszName, "VmUuid") == 0)
+ return RTStrCopy(pszString, cchString, "794c9192-d045-4f28-91ed-46253ac9998e");
+ }
+ else if (g_uVerbosity > 2)
+ RTPrintf("debug: CFGMR3QueryString(%p, %s, %p, %#x)\n", pNode, pszName, pszString, cchString);
+
+ return VERR_CFGM_VALUE_NOT_FOUND;
+}
+
+
+VMMR3DECL(int) audioTestDrvHlp_CFGMR3QueryStringAlloc(PCFGMNODE pNode, const char *pszName, char **ppszString)
+{
+ char szStr[128];
+ int rc = audioTestDrvHlp_CFGMR3QueryString(pNode, pszName, szStr, sizeof(szStr));
+ if (RT_SUCCESS(rc))
+ *ppszString = RTStrDup(szStr);
+
+ return rc;
+}
+
+
+VMMR3DECL(void) audioTestDrvHlp_MMR3HeapFree(PPDMDRVINS pDrvIns, void *pv)
+{
+ RT_NOREF(pDrvIns);
+
+ /* counterpart to CFGMR3QueryStringAlloc */
+ RTStrFree((char *)pv);
+}
+
+
+VMMR3DECL(int) audioTestDrvHlp_CFGMR3QueryStringDef(PCFGMNODE pNode, const char *pszName, char *pszString, size_t cchString, const char *pszDef)
+{
+ PCPDMDRVREG pDrvReg = (PCPDMDRVREG)pNode;
+ if (RT_VALID_PTR(pDrvReg))
+ {
+ const char *pszRet = pszDef;
+ if ( g_pszDrvAudioDebug
+ && strcmp(pDrvReg->szName, "AUDIO") == 0
+ && strcmp(pszName, "DebugPathOut") == 0)
+ pszRet = g_pszDrvAudioDebug;
+
+ int rc = RTStrCopy(pszString, cchString, pszRet);
+
+ if (g_uVerbosity > 2)
+ RTPrintf("debug: CFGMR3QueryStringDef([%s], %s, %p, %#x, %s) -> '%s' + %Rrc\n",
+ pDrvReg->szName, pszName, pszString, cchString, pszDef, pszRet, rc);
+ return rc;
+ }
+
+ if (g_uVerbosity > 2)
+ RTPrintf("debug: CFGMR3QueryStringDef(%p, %s, %p, %#x, %s)\n", pNode, pszName, pszString, cchString, pszDef);
+ return RTStrCopy(pszString, cchString, pszDef);
+}
+
+
+VMMR3DECL(int) audioTestDrvHlp_CFGMR3QueryBoolDef(PCFGMNODE pNode, const char *pszName, bool *pf, bool fDef)
+{
+ PCPDMDRVREG pDrvReg = (PCPDMDRVREG)pNode;
+ if (RT_VALID_PTR(pDrvReg))
+ {
+ *pf = fDef;
+ if ( strcmp(pDrvReg->szName, "AUDIO") == 0
+ && strcmp(pszName, "DebugEnabled") == 0)
+ *pf = g_fDrvAudioDebug;
+
+ if (g_uVerbosity > 2)
+ RTPrintf("debug: CFGMR3QueryBoolDef([%s], %s, %p, %RTbool) -> %RTbool\n", pDrvReg->szName, pszName, pf, fDef, *pf);
+ return VINF_SUCCESS;
+ }
+ *pf = fDef;
+ return VINF_SUCCESS;
+}
+
+
+VMMR3DECL(int) audioTestDrvHlp_CFGMR3QueryU8(PCFGMNODE pNode, const char *pszName, uint8_t *pu8)
+{
+ RT_NOREF(pNode, pszName, pu8);
+ return VERR_CFGM_VALUE_NOT_FOUND;
+}
+
+
+VMMR3DECL(int) audioTestDrvHlp_CFGMR3QueryU32(PCFGMNODE pNode, const char *pszName, uint32_t *pu32)
+{
+ RT_NOREF(pNode, pszName, pu32);
+ return VERR_CFGM_VALUE_NOT_FOUND;
+}
+
+
+VMMR3DECL(int) audioTestDrvHlp_CFGMR3ValidateConfig(PCFGMNODE pNode, const char *pszNode,
+ const char *pszValidValues, const char *pszValidNodes,
+ const char *pszWho, uint32_t uInstance)
+{
+ RT_NOREF(pNode, pszNode, pszValidValues, pszValidNodes, pszWho, uInstance);
+ return VINF_SUCCESS;
+}
+
+/** @} */
+
+/** @name Driver Helper Fakes
+ * @{ */
+
+static DECLCALLBACK(int) audioTestDrvHlp_Attach(PPDMDRVINS pDrvIns, uint32_t fFlags, PPDMIBASE *ppBaseInterface)
+{
+ /* DrvAudio must be allowed to attach the backend driver (paranoid
+ backend drivers may call us to check that nothing is attached). */
+ if (strcmp(pDrvIns->pReg->szName, "AUDIO") == 0)
+ {
+ PAUDIOTESTDRVSTACK pDrvStack = pDrvIns->Internal.s.pStack;
+ AssertReturn(pDrvStack->pDrvBackendIns == NULL, VERR_PDM_DRIVER_ALREADY_ATTACHED);
+
+ if (g_uVerbosity > 1)
+ RTMsgInfo("Attaching backend '%s' to DrvAudio...\n", pDrvStack->pDrvReg->szName);
+ int rc = audioTestDrvConstruct(pDrvStack, pDrvStack->pDrvReg, pDrvIns, &pDrvStack->pDrvBackendIns);
+ if (RT_SUCCESS(rc))
+ {
+ if (ppBaseInterface)
+ *ppBaseInterface = &pDrvStack->pDrvBackendIns->IBase;
+ }
+ else
+ RTMsgError("Failed to attach backend: %Rrc", rc);
+ return rc;
+ }
+ RT_NOREF(fFlags);
+ return VERR_PDM_NO_ATTACHED_DRIVER;
+}
+
+
+static DECLCALLBACK(void) audioTestDrvHlp_STAMRegister(PPDMDRVINS pDrvIns, void *pvSample, STAMTYPE enmType, const char *pszName,
+ STAMUNIT enmUnit, const char *pszDesc)
+{
+ RT_NOREF(pDrvIns, pvSample, enmType, pszName, enmUnit, pszDesc);
+}
+
+
+static DECLCALLBACK(void) audioTestDrvHlp_STAMRegisterF(PPDMDRVINS pDrvIns, void *pvSample, STAMTYPE enmType,
+ STAMVISIBILITY enmVisibility, STAMUNIT enmUnit, const char *pszDesc,
+ const char *pszName, ...)
+{
+ RT_NOREF(pDrvIns, pvSample, enmType, enmVisibility, enmUnit, pszDesc, pszName);
+}
+
+
+static DECLCALLBACK(void) audioTestDrvHlp_STAMRegisterV(PPDMDRVINS pDrvIns, void *pvSample, STAMTYPE enmType,
+ STAMVISIBILITY enmVisibility, STAMUNIT enmUnit, const char *pszDesc,
+ const char *pszName, va_list args)
+{
+ RT_NOREF(pDrvIns, pvSample, enmType, enmVisibility, enmUnit, pszDesc, pszName, args);
+}
+
+
+static DECLCALLBACK(int) audioTestDrvHlp_STAMDeregister(PPDMDRVINS pDrvIns, void *pvSample)
+{
+ RT_NOREF(pDrvIns, pvSample);
+ return VINF_SUCCESS;
+}
+
+
+static DECLCALLBACK(int) audioTestDrvHlp_STAMDeregisterByPrefix(PPDMDRVINS pDrvIns, const char *pszPrefix)
+{
+ RT_NOREF(pDrvIns, pszPrefix);
+ return VINF_SUCCESS;
+}
+
+/**
+ * Get the driver helpers.
+ */
+static const PDMDRVHLPR3 *audioTestFakeGetDrvHlp(void)
+{
+ /*
+ * Note! No initializer for s_DrvHlp (also why it's not a file global).
+ * We do not want to have to update this code every time PDMDRVHLPR3
+ * grows new entries or are otherwise modified. Only when the
+ * entries used by the audio driver changes do we want to change
+ * our code.
+ */
+ static PDMDRVHLPR3 s_DrvHlp;
+ if (s_DrvHlp.u32Version != PDM_DRVHLPR3_VERSION)
+ {
+ s_DrvHlp.u32Version = PDM_DRVHLPR3_VERSION;
+ s_DrvHlp.u32TheEnd = PDM_DRVHLPR3_VERSION;
+ s_DrvHlp.pfnAttach = audioTestDrvHlp_Attach;
+ s_DrvHlp.pfnSTAMRegister = audioTestDrvHlp_STAMRegister;
+ s_DrvHlp.pfnSTAMRegisterF = audioTestDrvHlp_STAMRegisterF;
+ s_DrvHlp.pfnSTAMRegisterV = audioTestDrvHlp_STAMRegisterV;
+ s_DrvHlp.pfnSTAMDeregister = audioTestDrvHlp_STAMDeregister;
+ s_DrvHlp.pfnSTAMDeregisterByPrefix = audioTestDrvHlp_STAMDeregisterByPrefix;
+ s_DrvHlp.pfnCFGMGetChild = audioTestDrvHlp_CFGMR3GetChild;
+ s_DrvHlp.pfnCFGMQueryString = audioTestDrvHlp_CFGMR3QueryString;
+ s_DrvHlp.pfnCFGMQueryStringAlloc = audioTestDrvHlp_CFGMR3QueryStringAlloc;
+ s_DrvHlp.pfnMMHeapFree = audioTestDrvHlp_MMR3HeapFree;
+ s_DrvHlp.pfnCFGMQueryStringDef = audioTestDrvHlp_CFGMR3QueryStringDef;
+ s_DrvHlp.pfnCFGMQueryBoolDef = audioTestDrvHlp_CFGMR3QueryBoolDef;
+ s_DrvHlp.pfnCFGMQueryU8 = audioTestDrvHlp_CFGMR3QueryU8;
+ s_DrvHlp.pfnCFGMQueryU32 = audioTestDrvHlp_CFGMR3QueryU32;
+ s_DrvHlp.pfnCFGMValidateConfig = audioTestDrvHlp_CFGMR3ValidateConfig;
+ }
+ return &s_DrvHlp;
+}
+
+/** @} */
+
+
+/**
+ * Implementation of PDMIBASE::pfnQueryInterface for a fake device above
+ * DrvAudio.
+ */
+static DECLCALLBACK(void *) audioTestFakeDeviceIBaseQueryInterface(PPDMIBASE pInterface, const char *pszIID)
+{
+ PDMIBASE_RETURN_INTERFACE(pszIID, PDMIBASE, pInterface);
+ RTMsgWarning("audioTestFakeDeviceIBaseQueryInterface: Unknown interface: %s\n", pszIID);
+ return NULL;
+}
+
+/** IBase interface for a fake device above DrvAudio. */
+static PDMIBASE g_AudioTestFakeDeviceIBase = { audioTestFakeDeviceIBaseQueryInterface };
+
+
+static DECLCALLBACK(int) audioTestIHostAudioPort_DoOnWorkerThread(PPDMIHOSTAUDIOPORT pInterface, PPDMAUDIOBACKENDSTREAM pStream,
+ uintptr_t uUser, void *pvUser)
+{
+ RT_NOREF(pInterface, pStream, uUser, pvUser);
+ RTMsgWarning("audioTestIHostAudioPort_DoOnWorkerThread was called\n");
+ return VERR_NOT_IMPLEMENTED;
+}
+
+
+DECLCALLBACK(void) audioTestIHostAudioPort_NotifyDeviceChanged(PPDMIHOSTAUDIOPORT pInterface, PDMAUDIODIR enmDir, void *pvUser)
+{
+ RT_NOREF(pInterface, enmDir, pvUser);
+ RTMsgWarning("audioTestIHostAudioPort_NotifyDeviceChanged was called\n");
+}
+
+
+static DECLCALLBACK(void) audioTestIHostAudioPort_StreamNotifyPreparingDeviceSwitch(PPDMIHOSTAUDIOPORT pInterface,
+ PPDMAUDIOBACKENDSTREAM pStream)
+{
+ RT_NOREF(pInterface, pStream);
+ RTMsgWarning("audioTestIHostAudioPort_StreamNotifyPreparingDeviceSwitch was called\n");
+}
+
+
+static DECLCALLBACK(void) audioTestIHostAudioPort_StreamNotifyDeviceChanged(PPDMIHOSTAUDIOPORT pInterface,
+ PPDMAUDIOBACKENDSTREAM pStream, bool fReInit)
+{
+ RT_NOREF(pInterface, pStream, fReInit);
+ RTMsgWarning("audioTestIHostAudioPort_StreamNotifyDeviceChanged was called\n");
+}
+
+
+static DECLCALLBACK(void) audioTestIHostAudioPort_NotifyDevicesChanged(PPDMIHOSTAUDIOPORT pInterface)
+{
+ RT_NOREF(pInterface);
+ RTMsgWarning("audioTestIHostAudioPort_NotifyDevicesChanged was called\n");
+}
+
+
+static PDMIHOSTAUDIOPORT g_AudioTestIHostAudioPort =
+{
+ audioTestIHostAudioPort_DoOnWorkerThread,
+ audioTestIHostAudioPort_NotifyDeviceChanged,
+ audioTestIHostAudioPort_StreamNotifyPreparingDeviceSwitch,
+ audioTestIHostAudioPort_StreamNotifyDeviceChanged,
+ audioTestIHostAudioPort_NotifyDevicesChanged,
+};
+
+
+/**
+ * Implementation of PDMIBASE::pfnQueryInterface for a fake DrvAudio above a
+ * backend.
+ */
+static DECLCALLBACK(void *) audioTestFakeDrvAudioIBaseQueryInterface(PPDMIBASE pInterface, const char *pszIID)
+{
+ PDMIBASE_RETURN_INTERFACE(pszIID, PDMIBASE, pInterface);
+ PDMIBASE_RETURN_INTERFACE(pszIID, PDMIHOSTAUDIOPORT, &g_AudioTestIHostAudioPort);
+ RTMsgWarning("audioTestFakeDrvAudioIBaseQueryInterface: Unknown interface: %s\n", pszIID);
+ return NULL;
+}
+
+
+/** IBase interface for a fake DrvAudio above a lonesome backend. */
+static PDMIBASE g_AudioTestFakeDrvAudioIBase = { audioTestFakeDrvAudioIBaseQueryInterface };
+
+
+
+/**
+ * Constructs a PDM audio driver instance.
+ *
+ * @returns VBox status code.
+ * @param pDrvStack The stack this is associated with.
+ * @param pDrvReg PDM driver registration record to use for construction.
+ * @param pParentDrvIns The parent driver (if any).
+ * @param ppDrvIns Where to return the driver instance structure.
+ */
+int audioTestDrvConstruct(PAUDIOTESTDRVSTACK pDrvStack, PCPDMDRVREG pDrvReg, PPDMDRVINS pParentDrvIns,
+ PPPDMDRVINS ppDrvIns)
+{
+ /* The destruct function must have valid data to work with. */
+ *ppDrvIns = NULL;
+
+ /*
+ * Check registration structure validation (doesn't need to be too
+ * thorough, PDM check it in detail on every VM startup).
+ */
+ AssertPtrReturn(pDrvReg, VERR_INVALID_POINTER);
+ RTMsgInfo("Initializing backend '%s' ...\n", pDrvReg->szName);
+ AssertPtrReturn(pDrvReg->pfnConstruct, VERR_INVALID_PARAMETER);
+
+ /*
+ * Create the instance data structure.
+ */
+ PPDMDRVINS pDrvIns = (PPDMDRVINS)RTMemAllocZVar(RT_UOFFSETOF_DYN(PDMDRVINS, achInstanceData[pDrvReg->cbInstance]));
+ RTTEST_CHECK_RET(g_hTest, pDrvIns, VERR_NO_MEMORY);
+
+ pDrvIns->u32Version = PDM_DRVINS_VERSION;
+ pDrvIns->iInstance = 0;
+ pDrvIns->pHlpR3 = audioTestFakeGetDrvHlp();
+ pDrvIns->pvInstanceDataR3 = &pDrvIns->achInstanceData[0];
+ pDrvIns->pReg = pDrvReg;
+ pDrvIns->pCfg = (PCFGMNODE)pDrvReg;
+ pDrvIns->Internal.s.pStack = pDrvStack;
+ pDrvIns->pUpBase = NULL;
+ pDrvIns->pDownBase = NULL;
+ if (pParentDrvIns)
+ {
+ Assert(pParentDrvIns->pDownBase == NULL);
+ pParentDrvIns->pDownBase = &pDrvIns->IBase;
+ pDrvIns->pUpBase = &pParentDrvIns->IBase;
+ }
+ else if (strcmp(pDrvReg->szName, "AUDIO") == 0)
+ pDrvIns->pUpBase = &g_AudioTestFakeDeviceIBase;
+ else
+ pDrvIns->pUpBase = &g_AudioTestFakeDrvAudioIBase;
+
+ /*
+ * Invoke the constructor.
+ */
+ int rc = pDrvReg->pfnConstruct(pDrvIns, pDrvIns->pCfg, 0 /*fFlags*/);
+ if (RT_SUCCESS(rc))
+ {
+ *ppDrvIns = pDrvIns;
+ return VINF_SUCCESS;
+ }
+
+ if (pDrvReg->pfnDestruct)
+ pDrvReg->pfnDestruct(pDrvIns);
+ RTMemFree(pDrvIns);
+ return rc;
+}
+
+
+/**
+ * Destructs a PDM audio driver instance.
+ *
+ * @param pDrvIns Driver instance to destruct.
+ */
+static void audioTestDrvDestruct(PPDMDRVINS pDrvIns)
+{
+ if (pDrvIns)
+ {
+ Assert(pDrvIns->u32Version == PDM_DRVINS_VERSION);
+
+ if (pDrvIns->pReg->pfnDestruct)
+ pDrvIns->pReg->pfnDestruct(pDrvIns);
+
+ pDrvIns->u32Version = 0;
+ pDrvIns->pReg = NULL;
+ RTMemFree(pDrvIns);
+ }
+}
+
+
+/**
+ * Sends the PDM driver a power off notification.
+ *
+ * @param pDrvIns Driver instance to notify.
+ */
+static void audioTestDrvNotifyPowerOff(PPDMDRVINS pDrvIns)
+{
+ if (pDrvIns)
+ {
+ Assert(pDrvIns->u32Version == PDM_DRVINS_VERSION);
+ if (pDrvIns->pReg->pfnPowerOff)
+ pDrvIns->pReg->pfnPowerOff(pDrvIns);
+ }
+}
+
+
+/**
+ * Deletes a driver stack.
+ *
+ * This will power off and destroy the drivers.
+ */
+void audioTestDriverStackDelete(PAUDIOTESTDRVSTACK pDrvStack)
+{
+ /*
+ * Do power off notifications (top to bottom).
+ */
+ audioTestDrvNotifyPowerOff(pDrvStack->pDrvAudioIns);
+ audioTestDrvNotifyPowerOff(pDrvStack->pDrvBackendIns);
+
+ /*
+ * Drivers are destroyed from bottom to top (closest to the device).
+ */
+ audioTestDrvDestruct(pDrvStack->pDrvBackendIns);
+ pDrvStack->pDrvBackendIns = NULL;
+ pDrvStack->pIHostAudio = NULL;
+
+ audioTestDrvDestruct(pDrvStack->pDrvAudioIns);
+ pDrvStack->pDrvAudioIns = NULL;
+ pDrvStack->pIAudioConnector = NULL;
+
+ PDMAudioHostEnumDelete(&pDrvStack->DevEnum);
+}
+
+
+/**
+ * Initializes a driver stack, extended version.
+ *
+ * @returns VBox status code.
+ * @param pDrvStack The driver stack to initialize.
+ * @param pDrvReg The backend driver to use.
+ * @param fEnabledIn Whether input is enabled or not on creation time.
+ * @param fEnabledOut Whether output is enabled or not on creation time.
+ * @param fWithDrvAudio Whether to include DrvAudio in the stack or not.
+ */
+int audioTestDriverStackInitEx(PAUDIOTESTDRVSTACK pDrvStack, PCPDMDRVREG pDrvReg, bool fEnabledIn, bool fEnabledOut, bool fWithDrvAudio)
+{
+ int rc;
+
+ RT_ZERO(*pDrvStack);
+ pDrvStack->pDrvReg = pDrvReg;
+
+ PDMAudioHostEnumInit(&pDrvStack->DevEnum);
+
+ if (!fWithDrvAudio)
+ rc = audioTestDrvConstruct(pDrvStack, pDrvReg, NULL /*pParentDrvIns*/, &pDrvStack->pDrvBackendIns);
+ else
+ {
+ rc = audioTestDrvConstruct(pDrvStack, &g_DrvAUDIO, NULL /*pParentDrvIns*/, &pDrvStack->pDrvAudioIns);
+ if (RT_SUCCESS(rc))
+ {
+ Assert(pDrvStack->pDrvAudioIns);
+ PPDMIBASE const pIBase = &pDrvStack->pDrvAudioIns->IBase;
+ pDrvStack->pIAudioConnector = (PPDMIAUDIOCONNECTOR)pIBase->pfnQueryInterface(pIBase, PDMIAUDIOCONNECTOR_IID);
+ if (pDrvStack->pIAudioConnector)
+ {
+ /* Both input and output is disabled by default. */
+ if (fEnabledIn)
+ rc = pDrvStack->pIAudioConnector->pfnEnable(pDrvStack->pIAudioConnector, PDMAUDIODIR_IN, true);
+
+ if (RT_SUCCESS(rc))
+ {
+ if (fEnabledOut)
+ rc = pDrvStack->pIAudioConnector->pfnEnable(pDrvStack->pIAudioConnector, PDMAUDIODIR_OUT, true);
+ }
+
+ if (RT_FAILURE(rc))
+ {
+ RTTestFailed(g_hTest, "Failed to enabled input and output: %Rrc", rc);
+ audioTestDriverStackDelete(pDrvStack);
+ }
+ }
+ else
+ {
+ RTTestFailed(g_hTest, "Failed to query PDMIAUDIOCONNECTOR");
+ audioTestDriverStackDelete(pDrvStack);
+ rc = VERR_PDM_MISSING_INTERFACE;
+ }
+ }
+ }
+
+ /*
+ * Get the IHostAudio interface and check that the host driver is working.
+ */
+ if (RT_SUCCESS(rc))
+ {
+ PPDMIBASE const pIBase = &pDrvStack->pDrvBackendIns->IBase;
+ pDrvStack->pIHostAudio = (PPDMIHOSTAUDIO)pIBase->pfnQueryInterface(pIBase, PDMIHOSTAUDIO_IID);
+ if (pDrvStack->pIHostAudio)
+ {
+ PDMAUDIOBACKENDSTS enmStatus = pDrvStack->pIHostAudio->pfnGetStatus(pDrvStack->pIHostAudio, PDMAUDIODIR_OUT);
+ if (enmStatus == PDMAUDIOBACKENDSTS_RUNNING)
+ return VINF_SUCCESS;
+
+ RTTestFailed(g_hTest, "Expected backend status RUNNING, got %d instead", enmStatus);
+ }
+ else
+ RTTestFailed(g_hTest, "Failed to query PDMIHOSTAUDIO for '%s'", pDrvReg->szName);
+ audioTestDriverStackDelete(pDrvStack);
+ }
+
+ return rc;
+}
+
+
+/**
+ * Initializes a driver stack.
+ *
+ * @returns VBox status code.
+ * @param pDrvStack The driver stack to initialize.
+ * @param pDrvReg The backend driver to use.
+ * @param fEnabledIn Whether input is enabled or not on creation time.
+ * @param fEnabledOut Whether output is enabled or not on creation time.
+ * @param fWithDrvAudio Whether to include DrvAudio in the stack or not.
+ */
+int audioTestDriverStackInit(PAUDIOTESTDRVSTACK pDrvStack, PCPDMDRVREG pDrvReg, bool fWithDrvAudio)
+{
+ return audioTestDriverStackInitEx(pDrvStack, pDrvReg, true /* fEnabledIn */, true /* fEnabledOut */, fWithDrvAudio);
+}
+
+/**
+ * Initializes a driver stack by probing all backends in the order of appearance
+ * in the backends description table.
+ *
+ * @returns VBox status code.
+ * @param pDrvStack The driver stack to initialize.
+ * @param pDrvReg The backend driver to use.
+ * @param fEnabledIn Whether input is enabled or not on creation time.
+ * @param fEnabledOut Whether output is enabled or not on creation time.
+ * @param fWithDrvAudio Whether to include DrvAudio in the stack or not.
+ */
+int audioTestDriverStackProbe(PAUDIOTESTDRVSTACK pDrvStack, PCPDMDRVREG pDrvReg, bool fEnabledIn, bool fEnabledOut, bool fWithDrvAudio)
+{
+ int rc = VERR_IPE_UNINITIALIZED_STATUS; /* Shut up MSVC. */
+
+ for (size_t i = 0; i < g_cBackends; i++)
+ {
+ pDrvReg = g_aBackends[i].pDrvReg;
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Probing for backend '%s' ...\n", g_aBackends[i].pszName);
+
+ rc = audioTestDriverStackInitEx(pDrvStack, pDrvReg, fEnabledIn, fEnabledOut, fWithDrvAudio); /** @todo Make in/out configurable, too. */
+ if (RT_SUCCESS(rc))
+ {
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Probing backend '%s' successful\n", g_aBackends[i].pszName);
+ return rc;
+ }
+
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Probing backend '%s' failed with %Rrc, trying next one\n",
+ g_aBackends[i].pszName, rc);
+ continue;
+ }
+
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Probing all backends failed\n");
+ return rc;
+}
+
+/**
+ * Wrapper around PDMIHOSTAUDIO::pfnSetDevice.
+ */
+int audioTestDriverStackSetDevice(PAUDIOTESTDRVSTACK pDrvStack, PDMAUDIODIR enmDir, const char *pszDevId)
+{
+ int rc;
+ if ( pDrvStack->pIHostAudio
+ && pDrvStack->pIHostAudio->pfnSetDevice)
+ rc = pDrvStack->pIHostAudio->pfnSetDevice(pDrvStack->pIHostAudio, enmDir, pszDevId);
+ else if (!pszDevId || *pszDevId)
+ rc = VINF_SUCCESS;
+ else
+ rc = VERR_INVALID_FUNCTION;
+ return rc;
+}
+
+
+/**
+ * Common stream creation code.
+ *
+ * @returns VBox status code.
+ * @param pDrvStack The audio driver stack to create it via.
+ * @param pCfgReq The requested config.
+ * @param ppStream Where to return the stream pointer on success.
+ * @param pCfgAcq Where to return the actual (well, not necessarily when
+ * using DrvAudio, but probably the same) stream config on
+ * success (not used as input).
+ */
+static int audioTestDriverStackStreamCreate(PAUDIOTESTDRVSTACK pDrvStack, PCPDMAUDIOSTREAMCFG pCfgReq,
+ PPDMAUDIOSTREAM *ppStream, PPDMAUDIOSTREAMCFG pCfgAcq)
+{
+ char szTmp[PDMAUDIOSTRMCFGTOSTRING_MAX + 16];
+ int rc;
+ *ppStream = NULL;
+
+ if (pDrvStack->pIAudioConnector)
+ {
+ /*
+ * DrvAudio does most of the work here.
+ */
+ rc = pDrvStack->pIAudioConnector->pfnStreamCreate(pDrvStack->pIAudioConnector, 0 /*fFlags*/, pCfgReq, ppStream);
+ if (RT_SUCCESS(rc))
+ {
+ *pCfgAcq = (*ppStream)->Cfg;
+ RTMsgInfo("Created backend stream: %s\n", PDMAudioStrmCfgToString(pCfgReq, szTmp, sizeof(szTmp)));
+ return rc;
+ }
+ /* else: Don't set RTTestFailed(...) here, as test boxes (servers) don't have any audio hardware.
+ * Caller has check the rc then. */
+ }
+ else
+ {
+ /*
+ * Get the config so we can see how big the PDMAUDIOBACKENDSTREAM
+ * structure actually is for this backend.
+ */
+ PDMAUDIOBACKENDCFG BackendCfg;
+ rc = pDrvStack->pIHostAudio->pfnGetConfig(pDrvStack->pIHostAudio, &BackendCfg);
+ if (RT_SUCCESS(rc))
+ {
+ if (BackendCfg.cbStream >= sizeof(PDMAUDIOBACKENDSTREAM))
+ {
+ /*
+ * Allocate and initialize the stream.
+ */
+ uint32_t const cbStream = sizeof(AUDIOTESTDRVSTACKSTREAM) - sizeof(PDMAUDIOBACKENDSTREAM) + BackendCfg.cbStream;
+ PAUDIOTESTDRVSTACKSTREAM pStreamAt = (PAUDIOTESTDRVSTACKSTREAM)RTMemAllocZVar(cbStream);
+ if (pStreamAt)
+ {
+ pStreamAt->Core.uMagic = PDMAUDIOSTREAM_MAGIC;
+ pStreamAt->Core.Cfg = *pCfgReq;
+ pStreamAt->Core.cbBackend = cbStream;
+
+ pStreamAt->Backend.uMagic = PDMAUDIOBACKENDSTREAM_MAGIC;
+ pStreamAt->Backend.pStream = &pStreamAt->Core;
+
+ /*
+ * Call the backend to create the stream.
+ */
+ rc = pDrvStack->pIHostAudio->pfnStreamCreate(pDrvStack->pIHostAudio, &pStreamAt->Backend,
+ pCfgReq, &pStreamAt->Core.Cfg);
+ if (RT_SUCCESS(rc))
+ {
+ if (g_uVerbosity > 1)
+ RTMsgInfo("Created backend stream: %s\n",
+ PDMAudioStrmCfgToString(&pStreamAt->Core.Cfg, szTmp, sizeof(szTmp)));
+
+ /* Return if stream is ready: */
+ if (rc == VINF_SUCCESS)
+ {
+ *ppStream = &pStreamAt->Core;
+ *pCfgAcq = pStreamAt->Core.Cfg;
+ return VINF_SUCCESS;
+ }
+ if (rc == VINF_AUDIO_STREAM_ASYNC_INIT_NEEDED)
+ {
+ /*
+ * Do async init right here and now.
+ */
+ rc = pDrvStack->pIHostAudio->pfnStreamInitAsync(pDrvStack->pIHostAudio, &pStreamAt->Backend,
+ false /*fDestroyed*/);
+ if (RT_SUCCESS(rc))
+ {
+ *ppStream = &pStreamAt->Core;
+ *pCfgAcq = pStreamAt->Core.Cfg;
+ return VINF_SUCCESS;
+ }
+
+ RTTestFailed(g_hTest, "pfnStreamInitAsync failed: %Rrc\n", rc);
+ }
+ else
+ {
+ RTTestFailed(g_hTest, "pfnStreamCreate returned unexpected info status: %Rrc", rc);
+ rc = VERR_IPE_UNEXPECTED_INFO_STATUS;
+ }
+ pDrvStack->pIHostAudio->pfnStreamDestroy(pDrvStack->pIHostAudio, &pStreamAt->Backend, true /*fImmediate*/);
+ }
+ /* else: Don't set RTTestFailed(...) here, as test boxes (servers) don't have any audio hardware.
+ * Caller has check the rc then. */
+ }
+ else
+ {
+ RTTestFailed(g_hTest, "Out of memory!\n");
+ rc = VERR_NO_MEMORY;
+ }
+ RTMemFree(pStreamAt);
+ }
+ else
+ {
+ RTTestFailed(g_hTest, "cbStream=%#x is too small, min %#zx!\n", BackendCfg.cbStream, sizeof(PDMAUDIOBACKENDSTREAM));
+ rc = VERR_OUT_OF_RANGE;
+ }
+ }
+ else
+ RTTestFailed(g_hTest, "pfnGetConfig failed: %Rrc\n", rc);
+ }
+ return rc;
+}
+
+
+/**
+ * Creates an output stream.
+ *
+ * @returns VBox status code.
+ * @param pDrvStack The audio driver stack to create it via.
+ * @param pProps The audio properties to use.
+ * @param cMsBufferSize The buffer size in milliseconds.
+ * @param cMsPreBuffer The pre-buffering amount in milliseconds.
+ * @param cMsSchedulingHint The scheduling hint in milliseconds.
+ * @param ppStream Where to return the stream pointer on success.
+ * @param pCfgAcq Where to return the actual (well, not
+ * necessarily when using DrvAudio, but probably
+ * the same) stream config on success (not used as
+ * input).
+ */
+int audioTestDriverStackStreamCreateOutput(PAUDIOTESTDRVSTACK pDrvStack, PCPDMAUDIOPCMPROPS pProps,
+ uint32_t cMsBufferSize, uint32_t cMsPreBuffer, uint32_t cMsSchedulingHint,
+ PPDMAUDIOSTREAM *ppStream, PPDMAUDIOSTREAMCFG pCfgAcq)
+{
+ /*
+ * Calculate the stream config.
+ */
+ PDMAUDIOSTREAMCFG CfgReq;
+ int rc = PDMAudioStrmCfgInitWithProps(&CfgReq, pProps);
+ AssertRC(rc);
+ CfgReq.enmDir = PDMAUDIODIR_OUT;
+ CfgReq.enmPath = PDMAUDIOPATH_OUT_FRONT;
+ CfgReq.Device.cMsSchedulingHint = cMsSchedulingHint == UINT32_MAX || cMsSchedulingHint == 0
+ ? 10 : cMsSchedulingHint;
+ if (pDrvStack->pIAudioConnector && (cMsBufferSize == UINT32_MAX || cMsBufferSize == 0))
+ CfgReq.Backend.cFramesBufferSize = 0; /* DrvAudio picks the default */
+ else
+ CfgReq.Backend.cFramesBufferSize = PDMAudioPropsMilliToFrames(pProps,
+ cMsBufferSize == UINT32_MAX || cMsBufferSize == 0
+ ? 300 : cMsBufferSize);
+ if (cMsPreBuffer == UINT32_MAX)
+ CfgReq.Backend.cFramesPreBuffering = pDrvStack->pIAudioConnector ? UINT32_MAX /*DrvAudo picks the default */
+ : CfgReq.Backend.cFramesBufferSize * 2 / 3;
+ else
+ CfgReq.Backend.cFramesPreBuffering = PDMAudioPropsMilliToFrames(pProps, cMsPreBuffer);
+ if ( CfgReq.Backend.cFramesPreBuffering >= CfgReq.Backend.cFramesBufferSize + 16
+ && !pDrvStack->pIAudioConnector /*DrvAudio deals with it*/ )
+ {
+ RTMsgWarning("Cannot pre-buffer %#x frames with only %#x frames of buffer!",
+ CfgReq.Backend.cFramesPreBuffering, CfgReq.Backend.cFramesBufferSize);
+ CfgReq.Backend.cFramesPreBuffering = CfgReq.Backend.cFramesBufferSize > 16
+ ? CfgReq.Backend.cFramesBufferSize - 16 : 0;
+ }
+
+ static uint32_t s_idxStream = 0;
+ uint32_t const idxStream = s_idxStream++;
+ RTStrPrintf(CfgReq.szName, sizeof(CfgReq.szName), "out-%u", idxStream);
+
+ /*
+ * Call common code to do the actual work.
+ */
+ return audioTestDriverStackStreamCreate(pDrvStack, &CfgReq, ppStream, pCfgAcq);
+}
+
+
+/**
+ * Creates an input stream.
+ *
+ * @returns VBox status code.
+ * @param pDrvStack The audio driver stack to create it via.
+ * @param pProps The audio properties to use.
+ * @param cMsBufferSize The buffer size in milliseconds.
+ * @param cMsPreBuffer The pre-buffering amount in milliseconds.
+ * @param cMsSchedulingHint The scheduling hint in milliseconds.
+ * @param ppStream Where to return the stream pointer on success.
+ * @param pCfgAcq Where to return the actual (well, not
+ * necessarily when using DrvAudio, but probably
+ * the same) stream config on success (not used as
+ * input).
+ */
+int audioTestDriverStackStreamCreateInput(PAUDIOTESTDRVSTACK pDrvStack, PCPDMAUDIOPCMPROPS pProps,
+ uint32_t cMsBufferSize, uint32_t cMsPreBuffer, uint32_t cMsSchedulingHint,
+ PPDMAUDIOSTREAM *ppStream, PPDMAUDIOSTREAMCFG pCfgAcq)
+{
+ /*
+ * Calculate the stream config.
+ */
+ PDMAUDIOSTREAMCFG CfgReq;
+ int rc = PDMAudioStrmCfgInitWithProps(&CfgReq, pProps);
+ AssertRC(rc);
+ CfgReq.enmDir = PDMAUDIODIR_IN;
+ CfgReq.enmPath = PDMAUDIOPATH_IN_LINE;
+ CfgReq.Device.cMsSchedulingHint = cMsSchedulingHint == UINT32_MAX || cMsSchedulingHint == 0
+ ? 10 : cMsSchedulingHint;
+ if (pDrvStack->pIAudioConnector && (cMsBufferSize == UINT32_MAX || cMsBufferSize == 0))
+ CfgReq.Backend.cFramesBufferSize = 0; /* DrvAudio picks the default */
+ else
+ CfgReq.Backend.cFramesBufferSize = PDMAudioPropsMilliToFrames(pProps,
+ cMsBufferSize == UINT32_MAX || cMsBufferSize == 0
+ ? 300 : cMsBufferSize);
+ if (cMsPreBuffer == UINT32_MAX)
+ CfgReq.Backend.cFramesPreBuffering = pDrvStack->pIAudioConnector ? UINT32_MAX /*DrvAudio picks the default */
+ : CfgReq.Backend.cFramesBufferSize / 2;
+ else
+ CfgReq.Backend.cFramesPreBuffering = PDMAudioPropsMilliToFrames(pProps, cMsPreBuffer);
+ if ( CfgReq.Backend.cFramesPreBuffering >= CfgReq.Backend.cFramesBufferSize + 16 /** @todo way to little */
+ && !pDrvStack->pIAudioConnector /*DrvAudio deals with it*/ )
+ {
+ RTMsgWarning("Cannot pre-buffer %#x frames with only %#x frames of buffer!",
+ CfgReq.Backend.cFramesPreBuffering, CfgReq.Backend.cFramesBufferSize);
+ CfgReq.Backend.cFramesPreBuffering = CfgReq.Backend.cFramesBufferSize > 16
+ ? CfgReq.Backend.cFramesBufferSize - 16 : 0;
+ }
+
+ static uint32_t s_idxStream = 0;
+ uint32_t const idxStream = s_idxStream++;
+ RTStrPrintf(CfgReq.szName, sizeof(CfgReq.szName), "in-%u", idxStream);
+
+ /*
+ * Call common code to do the actual work.
+ */
+ return audioTestDriverStackStreamCreate(pDrvStack, &CfgReq, ppStream, pCfgAcq);
+}
+
+
+/**
+ * Destroys a stream.
+ *
+ * @param pDrvStack Driver stack the stream to destroy is assigned to.
+ * @param pStream Stream to destroy. Pointer will be NULL (invalid) after successful return.
+ */
+void audioTestDriverStackStreamDestroy(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream)
+{
+ if (!pStream)
+ return;
+
+ if (pDrvStack->pIAudioConnector)
+ {
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Destroying stream '%s' (IAudioConnector) ...\n", pStream->Cfg.szName);
+ int rc = pDrvStack->pIAudioConnector->pfnStreamDestroy(pDrvStack->pIAudioConnector, pStream, true /*fImmediate*/);
+ if (RT_FAILURE(rc))
+ RTTestFailed(g_hTest, "pfnStreamDestroy failed: %Rrc", rc);
+ }
+ else
+ {
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Destroying stream '%s' (IHostAudio) ...\n", pStream->Cfg.szName);
+ PAUDIOTESTDRVSTACKSTREAM pStreamAt = (PAUDIOTESTDRVSTACKSTREAM)pStream;
+ int rc = pDrvStack->pIHostAudio->pfnStreamDestroy(pDrvStack->pIHostAudio, &pStreamAt->Backend, true /*fImmediate*/);
+ if (RT_SUCCESS(rc))
+ {
+ pStreamAt->Core.uMagic = ~PDMAUDIOSTREAM_MAGIC;
+ pStreamAt->Backend.uMagic = ~PDMAUDIOBACKENDSTREAM_MAGIC;
+
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Destroying stream '%s' done\n", pStream->Cfg.szName);
+
+ RTMemFree(pStreamAt);
+
+ pStreamAt = NULL;
+ pStream = NULL;
+ }
+ else
+ RTTestFailed(g_hTest, "PDMIHOSTAUDIO::pfnStreamDestroy failed: %Rrc", rc);
+ }
+}
+
+
+/**
+ * Enables a stream.
+ */
+int audioTestDriverStackStreamEnable(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream)
+{
+ int rc;
+ if (pDrvStack->pIAudioConnector)
+ {
+ rc = pDrvStack->pIAudioConnector->pfnStreamControl(pDrvStack->pIAudioConnector, pStream, PDMAUDIOSTREAMCMD_ENABLE);
+ if (RT_FAILURE(rc))
+ RTTestFailed(g_hTest, "pfnStreamControl/ENABLE failed: %Rrc", rc);
+ }
+ else
+ {
+ PAUDIOTESTDRVSTACKSTREAM pStreamAt = (PAUDIOTESTDRVSTACKSTREAM)pStream;
+ rc = pDrvStack->pIHostAudio->pfnStreamEnable(pDrvStack->pIHostAudio, &pStreamAt->Backend);
+ if (RT_FAILURE(rc))
+ RTTestFailed(g_hTest, "PDMIHOSTAUDIO::pfnStreamEnable failed: %Rrc", rc);
+ }
+ return rc;
+}
+
+
+/**
+ * Disables a stream.
+ */
+int AudioTestDriverStackStreamDisable(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream)
+{
+ int rc;
+ if (pDrvStack->pIAudioConnector)
+ {
+ rc = pDrvStack->pIAudioConnector->pfnStreamControl(pDrvStack->pIAudioConnector, pStream, PDMAUDIOSTREAMCMD_DISABLE);
+ if (RT_FAILURE(rc))
+ RTTestFailed(g_hTest, "pfnStreamControl/DISABLE failed: %Rrc", rc);
+ }
+ else
+ {
+ PAUDIOTESTDRVSTACKSTREAM pStreamAt = (PAUDIOTESTDRVSTACKSTREAM)pStream;
+ rc = pDrvStack->pIHostAudio->pfnStreamDisable(pDrvStack->pIHostAudio, &pStreamAt->Backend);
+ if (RT_FAILURE(rc))
+ RTTestFailed(g_hTest, "PDMIHOSTAUDIO::pfnStreamDisable failed: %Rrc", rc);
+ }
+ return rc;
+}
+
+
+/**
+ * Drains an output stream.
+ */
+int audioTestDriverStackStreamDrain(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream, bool fSync)
+{
+ int rc;
+ if (pDrvStack->pIAudioConnector)
+ {
+ /*
+ * Issue the drain request.
+ */
+ rc = pDrvStack->pIAudioConnector->pfnStreamControl(pDrvStack->pIAudioConnector, pStream, PDMAUDIOSTREAMCMD_DRAIN);
+ if (RT_SUCCESS(rc) && fSync)
+ {
+ /*
+ * This is a synchronous drain, so wait for the driver to change state to inactive.
+ */
+ PDMAUDIOSTREAMSTATE enmState;
+ while ( (enmState = pDrvStack->pIAudioConnector->pfnStreamGetState(pDrvStack->pIAudioConnector, pStream))
+ >= PDMAUDIOSTREAMSTATE_ENABLED)
+ {
+ RTThreadSleep(2);
+ rc = pDrvStack->pIAudioConnector->pfnStreamIterate(pDrvStack->pIAudioConnector, pStream);
+ if (RT_FAILURE(rc))
+ {
+ RTTestFailed(g_hTest, "pfnStreamIterate/DRAIN failed: %Rrc", rc);
+ break;
+ }
+ }
+ if (enmState != PDMAUDIOSTREAMSTATE_INACTIVE)
+ {
+ RTTestFailed(g_hTest, "Stream state not INACTIVE after draining: %s", PDMAudioStreamStateGetName(enmState));
+ rc = VERR_AUDIO_STREAM_NOT_READY;
+ }
+ }
+ else if (RT_FAILURE(rc))
+ RTTestFailed(g_hTest, "pfnStreamControl/ENABLE failed: %Rrc", rc);
+ }
+ else
+ {
+ /*
+ * Issue the drain request.
+ */
+ PAUDIOTESTDRVSTACKSTREAM pStreamAt = (PAUDIOTESTDRVSTACKSTREAM)pStream;
+ rc = pDrvStack->pIHostAudio->pfnStreamDrain(pDrvStack->pIHostAudio, &pStreamAt->Backend);
+ if (RT_SUCCESS(rc) && fSync)
+ {
+ RTMSINTERVAL const msTimeout = RT_MS_5MIN; /* 5 minutes should be really enough for draining our stuff. */
+ uint64_t const tsStart = RTTimeMilliTS();
+
+ /*
+ * This is a synchronous drain, so wait for the driver to change state to inactive.
+ */
+ PDMHOSTAUDIOSTREAMSTATE enmHostState;
+ while ( (enmHostState = pDrvStack->pIHostAudio->pfnStreamGetState(pDrvStack->pIHostAudio, &pStreamAt->Backend))
+ == PDMHOSTAUDIOSTREAMSTATE_DRAINING)
+ {
+ RTThreadSleep(2);
+ uint32_t cbWritten = UINT32_MAX;
+ rc = pDrvStack->pIHostAudio->pfnStreamPlay(pDrvStack->pIHostAudio, &pStreamAt->Backend,
+ NULL /*pvBuf*/, 0 /*cbBuf*/, &cbWritten);
+ if (RT_FAILURE(rc))
+ {
+ RTTestFailed(g_hTest, "pfnStreamPlay/DRAIN failed: %Rrc", rc);
+ break;
+ }
+ if (cbWritten != 0)
+ {
+ RTTestFailed(g_hTest, "pfnStreamPlay/DRAIN did not set cbWritten to zero: %#x", cbWritten);
+ rc = VERR_MISSING;
+ break;
+ }
+
+ /* Fail-safe for audio stacks and/or implementations which mess up draining.
+ *
+ * Note: On some testboxes draining never seems to finish and thus is getting aborted, no clue why.
+ * The test result in the end still could be correct, although the actual draining problem
+ * needs to be investigated further.
+ *
+ * So don't make this (and the stream state check below) an error for now and just warn about it.
+ *
+ ** @todo Investigate draining issues on testboxes.
+ */
+ if (RTTimeMilliTS() - tsStart > msTimeout)
+ {
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS,
+ "Warning: Draining stream took too long (timeout is %RU32ms), giving up", msTimeout);
+ break;
+ }
+ }
+ if (enmHostState != PDMHOSTAUDIOSTREAMSTATE_OKAY)
+ RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS,
+ "Warning: Stream state not OKAY after draining: %s", PDMHostAudioStreamStateGetName(enmHostState));
+ }
+ else if (RT_FAILURE(rc))
+ RTTestFailed(g_hTest, "PDMIHOSTAUDIO::pfnStreamControl/ENABLE failed: %Rrc", rc);
+ }
+ return rc;
+}
+
+
+/**
+ * Checks if the stream is okay.
+ * @returns true if okay, false if not.
+ */
+bool audioTestDriverStackStreamIsOkay(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream)
+{
+ /*
+ * Get the stream status and check if it means is okay or not.
+ */
+ bool fRc = false;
+ if (pDrvStack->pIAudioConnector)
+ {
+ PDMAUDIOSTREAMSTATE enmState = pDrvStack->pIAudioConnector->pfnStreamGetState(pDrvStack->pIAudioConnector, pStream);
+ switch (enmState)
+ {
+ case PDMAUDIOSTREAMSTATE_NOT_WORKING:
+ case PDMAUDIOSTREAMSTATE_NEED_REINIT:
+ break;
+ case PDMAUDIOSTREAMSTATE_INACTIVE:
+ case PDMAUDIOSTREAMSTATE_ENABLED:
+ case PDMAUDIOSTREAMSTATE_ENABLED_READABLE:
+ case PDMAUDIOSTREAMSTATE_ENABLED_WRITABLE:
+ fRc = true;
+ break;
+ /* no default */
+ case PDMAUDIOSTREAMSTATE_INVALID:
+ case PDMAUDIOSTREAMSTATE_END:
+ case PDMAUDIOSTREAMSTATE_32BIT_HACK:
+ break;
+ }
+ }
+ else
+ {
+ PAUDIOTESTDRVSTACKSTREAM pStreamAt = (PAUDIOTESTDRVSTACKSTREAM)pStream;
+ PDMHOSTAUDIOSTREAMSTATE enmHostState = pDrvStack->pIHostAudio->pfnStreamGetState(pDrvStack->pIHostAudio,
+ &pStreamAt->Backend);
+ switch (enmHostState)
+ {
+ case PDMHOSTAUDIOSTREAMSTATE_INITIALIZING:
+ case PDMHOSTAUDIOSTREAMSTATE_NOT_WORKING:
+ break;
+ case PDMHOSTAUDIOSTREAMSTATE_OKAY:
+ case PDMHOSTAUDIOSTREAMSTATE_DRAINING:
+ case PDMHOSTAUDIOSTREAMSTATE_INACTIVE:
+ fRc = true;
+ break;
+ /* no default */
+ case PDMHOSTAUDIOSTREAMSTATE_INVALID:
+ case PDMHOSTAUDIOSTREAMSTATE_END:
+ case PDMHOSTAUDIOSTREAMSTATE_32BIT_HACK:
+ break;
+ }
+ }
+ return fRc;
+}
+
+
+/**
+ * Gets the number of bytes it's currently possible to write to the stream.
+ */
+uint32_t audioTestDriverStackStreamGetWritable(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream)
+{
+ uint32_t cbWritable;
+ if (pDrvStack->pIAudioConnector)
+ cbWritable = pDrvStack->pIAudioConnector->pfnStreamGetWritable(pDrvStack->pIAudioConnector, pStream);
+ else
+ {
+ PAUDIOTESTDRVSTACKSTREAM pStreamAt = (PAUDIOTESTDRVSTACKSTREAM)pStream;
+ cbWritable = pDrvStack->pIHostAudio->pfnStreamGetWritable(pDrvStack->pIHostAudio, &pStreamAt->Backend);
+ }
+ return cbWritable;
+}
+
+
+/**
+ * Tries to play the @a cbBuf bytes of samples in @a pvBuf.
+ */
+int audioTestDriverStackStreamPlay(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream,
+ void const *pvBuf, uint32_t cbBuf, uint32_t *pcbPlayed)
+{
+ int rc;
+ if (pDrvStack->pIAudioConnector)
+ {
+ rc = pDrvStack->pIAudioConnector->pfnStreamPlay(pDrvStack->pIAudioConnector, pStream, pvBuf, cbBuf, pcbPlayed);
+ if (RT_FAILURE(rc))
+ RTTestFailed(g_hTest, "pfnStreamPlay(,,,%#x,) failed: %Rrc", cbBuf, rc);
+ }
+ else
+ {
+ PAUDIOTESTDRVSTACKSTREAM pStreamAt = (PAUDIOTESTDRVSTACKSTREAM)pStream;
+ rc = pDrvStack->pIHostAudio->pfnStreamPlay(pDrvStack->pIHostAudio, &pStreamAt->Backend, pvBuf, cbBuf, pcbPlayed);
+ if (RT_FAILURE(rc))
+ RTTestFailed(g_hTest, "PDMIHOSTAUDIO::pfnStreamPlay(,,,%#x,) failed: %Rrc", cbBuf, rc);
+ }
+ return rc;
+}
+
+
+/**
+ * Gets the number of bytes it's currently possible to write to the stream.
+ */
+uint32_t audioTestDriverStackStreamGetReadable(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream)
+{
+ uint32_t cbReadable;
+ if (pDrvStack->pIAudioConnector)
+ cbReadable = pDrvStack->pIAudioConnector->pfnStreamGetReadable(pDrvStack->pIAudioConnector, pStream);
+ else
+ {
+ PAUDIOTESTDRVSTACKSTREAM pStreamAt = (PAUDIOTESTDRVSTACKSTREAM)pStream;
+ cbReadable = pDrvStack->pIHostAudio->pfnStreamGetReadable(pDrvStack->pIHostAudio, &pStreamAt->Backend);
+ }
+ return cbReadable;
+}
+
+
+/**
+ * Tries to capture @a cbBuf bytes of samples in @a pvBuf.
+ */
+int audioTestDriverStackStreamCapture(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream,
+ void *pvBuf, uint32_t cbBuf, uint32_t *pcbCaptured)
+{
+ int rc;
+ if (pDrvStack->pIAudioConnector)
+ {
+ rc = pDrvStack->pIAudioConnector->pfnStreamCapture(pDrvStack->pIAudioConnector, pStream, pvBuf, cbBuf, pcbCaptured);
+ if (RT_FAILURE(rc))
+ RTTestFailed(g_hTest, "pfnStreamCapture(,,,%#x,) failed: %Rrc", cbBuf, rc);
+ }
+ else
+ {
+ PAUDIOTESTDRVSTACKSTREAM pStreamAt = (PAUDIOTESTDRVSTACKSTREAM)pStream;
+ rc = pDrvStack->pIHostAudio->pfnStreamCapture(pDrvStack->pIHostAudio, &pStreamAt->Backend, pvBuf, cbBuf, pcbCaptured);
+ if (RT_FAILURE(rc))
+ RTTestFailed(g_hTest, "PDMIHOSTAUDIO::pfnStreamCapture(,,,%#x,) failed: %Rrc", cbBuf, rc);
+ }
+ return rc;
+}
+
+
+/*********************************************************************************************************************************
+* Mixed streams *
+*********************************************************************************************************************************/
+
+/**
+ * Initializing mixing for a stream.
+ *
+ * This can be used as a do-nothing wrapper for the stack.
+ *
+ * @returns VBox status code.
+ * @param pMix The mixing state.
+ * @param pStream The stream to mix to/from.
+ * @param pProps The mixer properties. Pass NULL for no mixing, just
+ * wrap the driver stack functionality.
+ * @param cMsBuffer The buffer size.
+ */
+int AudioTestMixStreamInit(PAUDIOTESTDRVMIXSTREAM pMix, PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream,
+ PCPDMAUDIOPCMPROPS pProps, uint32_t cMsBuffer)
+{
+ RT_ZERO(*pMix);
+
+ AssertReturn(pDrvStack, VERR_INVALID_PARAMETER);
+ AssertReturn(pStream, VERR_INVALID_PARAMETER);
+
+ pMix->pDrvStack = pDrvStack;
+ pMix->pStream = pStream;
+ if (!pProps)
+ {
+ pMix->pProps = &pStream->Cfg.Props;
+ return VINF_SUCCESS;
+ }
+
+ /*
+ * Okay, we're doing mixing so we need to set up the mixer buffer
+ * and associated states.
+ */
+ pMix->fDoMixing = true;
+ int rc = AudioMixBufInit(&pMix->MixBuf, "mixer", pProps, PDMAudioPropsMilliToFrames(pProps, cMsBuffer));
+ if (RT_SUCCESS(rc))
+ {
+ pMix->pProps = &pMix->MixBuf.Props;
+
+ if (pStream->Cfg.enmDir == PDMAUDIODIR_IN)
+ {
+ rc = AudioMixBufInitPeekState(&pMix->MixBuf, &pMix->PeekState, &pMix->MixBuf.Props);
+ if (RT_SUCCESS(rc))
+ {
+ rc = AudioMixBufInitWriteState(&pMix->MixBuf, &pMix->WriteState, &pStream->Cfg.Props);
+ if (RT_SUCCESS(rc))
+ return rc;
+ }
+ }
+ else if (pStream->Cfg.enmDir == PDMAUDIODIR_OUT)
+ {
+ rc = AudioMixBufInitWriteState(&pMix->MixBuf, &pMix->WriteState, &pMix->MixBuf.Props);
+ if (RT_SUCCESS(rc))
+ {
+ rc = AudioMixBufInitPeekState(&pMix->MixBuf, &pMix->PeekState, &pStream->Cfg.Props);
+ if (RT_SUCCESS(rc))
+ return rc;
+ }
+ }
+ else
+ {
+ RTTestFailed(g_hTest, "Bogus stream direction!");
+ rc = VERR_INVALID_STATE;
+ }
+ }
+ else
+ RTTestFailed(g_hTest, "AudioMixBufInit failed: %Rrc", rc);
+ RT_ZERO(*pMix);
+ return rc;
+}
+
+
+/**
+ * Terminate mixing (leaves the stream untouched).
+ *
+ * @param pMix The mixing state.
+ */
+void AudioTestMixStreamTerm(PAUDIOTESTDRVMIXSTREAM pMix)
+{
+ if (pMix->fDoMixing)
+ {
+ AudioMixBufTerm(&pMix->MixBuf);
+ pMix->pStream = NULL;
+ }
+ RT_ZERO(*pMix);
+}
+
+
+/**
+ * Worker that transports data between the mixer buffer and the drivers.
+ *
+ * @returns VBox status code.
+ * @param pMix The mixer stream setup to do transfers for.
+ */
+static int audioTestMixStreamTransfer(PAUDIOTESTDRVMIXSTREAM pMix)
+{
+ uint8_t abBuf[16384];
+ if (pMix->pStream->Cfg.enmDir == PDMAUDIODIR_IN)
+ {
+ /*
+ * Try fill up the mixer buffer as much as possible.
+ *
+ * Slight fun part is that we have to calculate conversion
+ * ratio and be rather pessimistic about it.
+ */
+ uint32_t const cbBuf = PDMAudioPropsFloorBytesToFrame(&pMix->pStream->Cfg.Props, sizeof(abBuf));
+ for (;;)
+ {
+ /*
+ * Figure out how much we can move in this iteration.
+ */
+ uint32_t cDstFrames = AudioMixBufFree(&pMix->MixBuf);
+ if (!cDstFrames)
+ break;
+
+ uint32_t cbReadable = audioTestDriverStackStreamGetReadable(pMix->pDrvStack, pMix->pStream);
+ if (!cbReadable)
+ break;
+
+ uint32_t cbToRead;
+ if (PDMAudioPropsHz(&pMix->pStream->Cfg.Props) == PDMAudioPropsHz(&pMix->MixBuf.Props))
+ cbToRead = PDMAudioPropsFramesToBytes(&pMix->pStream->Cfg.Props, cDstFrames);
+ else
+ cbToRead = PDMAudioPropsFramesToBytes(&pMix->pStream->Cfg.Props,
+ (uint64_t)cDstFrames * PDMAudioPropsHz(&pMix->pStream->Cfg.Props)
+ / PDMAudioPropsHz(&pMix->MixBuf.Props));
+ cbToRead = RT_MIN(cbToRead, RT_MIN(cbReadable, cbBuf));
+ if (!cbToRead)
+ break;
+
+ /*
+ * Get the data.
+ */
+ uint32_t cbCaptured = 0;
+ int rc = audioTestDriverStackStreamCapture(pMix->pDrvStack, pMix->pStream, abBuf, cbToRead, &cbCaptured);
+ if (RT_FAILURE(rc))
+ return rc;
+ Assert(cbCaptured == cbToRead);
+ AssertBreak(cbCaptured > 0);
+
+ /*
+ * Feed it to the mixer.
+ */
+ uint32_t cDstFramesWritten = 0;
+ if ((abBuf[0] >> 4) & 1) /* some cheap random */
+ AudioMixBufWrite(&pMix->MixBuf, &pMix->WriteState, abBuf, cbCaptured,
+ 0 /*offDstFrame*/, cDstFrames, &cDstFramesWritten);
+ else
+ {
+ AudioMixBufSilence(&pMix->MixBuf, &pMix->WriteState, 0 /*offFrame*/, cDstFrames);
+ AudioMixBufBlend(&pMix->MixBuf, &pMix->WriteState, abBuf, cbCaptured,
+ 0 /*offDstFrame*/, cDstFrames, &cDstFramesWritten);
+ }
+ AudioMixBufCommit(&pMix->MixBuf, cDstFramesWritten);
+ }
+ }
+ else
+ {
+ /*
+ * The goal here is to empty the mixer buffer by transfering all
+ * the data to the drivers.
+ */
+ uint32_t const cbBuf = PDMAudioPropsFloorBytesToFrame(&pMix->MixBuf.Props, sizeof(abBuf));
+ for (;;)
+ {
+ uint32_t cFrames = AudioMixBufUsed(&pMix->MixBuf);
+ if (!cFrames)
+ break;
+
+ uint32_t cbWritable = audioTestDriverStackStreamGetWritable(pMix->pDrvStack, pMix->pStream);
+ if (!cbWritable)
+ break;
+
+ uint32_t cSrcFramesPeeked;
+ uint32_t cbDstPeeked;
+ AudioMixBufPeek(&pMix->MixBuf, 0 /*offSrcFrame*/, cFrames, &cSrcFramesPeeked,
+ &pMix->PeekState, abBuf, RT_MIN(cbBuf, cbWritable), &cbDstPeeked);
+ AudioMixBufAdvance(&pMix->MixBuf, cSrcFramesPeeked);
+
+ if (!cbDstPeeked)
+ break;
+
+ uint32_t offBuf = 0;
+ while (offBuf < cbDstPeeked)
+ {
+ uint32_t cbPlayed = 0;
+ int rc = audioTestDriverStackStreamPlay(pMix->pDrvStack, pMix->pStream,
+ &abBuf[offBuf], cbDstPeeked - offBuf, &cbPlayed);
+ if (RT_FAILURE(rc))
+ return rc;
+ if (!cbPlayed)
+ RTThreadSleep(1);
+ offBuf += cbPlayed;
+ }
+ }
+ }
+ return VINF_SUCCESS;
+}
+
+
+/**
+ * Same as audioTestDriverStackStreamEnable.
+ */
+int AudioTestMixStreamEnable(PAUDIOTESTDRVMIXSTREAM pMix)
+{
+ return audioTestDriverStackStreamEnable(pMix->pDrvStack, pMix->pStream);
+}
+
+
+/**
+ * Same as audioTestDriverStackStreamDrain.
+ */
+int AudioTestMixStreamDrain(PAUDIOTESTDRVMIXSTREAM pMix, bool fSync)
+{
+ /*
+ * If we're mixing, we must first make sure the buffer is empty.
+ */
+ if (pMix->fDoMixing)
+ {
+ audioTestMixStreamTransfer(pMix);
+ while (AudioMixBufUsed(&pMix->MixBuf) > 0)
+ {
+ RTThreadSleep(1);
+ audioTestMixStreamTransfer(pMix);
+ }
+ }
+
+ /*
+ * Then we do the regular work.
+ */
+ return audioTestDriverStackStreamDrain(pMix->pDrvStack, pMix->pStream, fSync);
+}
+
+/**
+ * Same as audioTestDriverStackStreamDisable.
+ */
+int AudioTestMixStreamDisable(PAUDIOTESTDRVMIXSTREAM pMix)
+{
+ return AudioTestDriverStackStreamDisable(pMix->pDrvStack, pMix->pStream);
+}
+
+
+/**
+ * Same as audioTestDriverStackStreamIsOkay.
+ */
+bool AudioTestMixStreamIsOkay(PAUDIOTESTDRVMIXSTREAM pMix)
+{
+ return audioTestDriverStackStreamIsOkay(pMix->pDrvStack, pMix->pStream);
+}
+
+
+/**
+ * Same as audioTestDriverStackStreamGetWritable
+ */
+uint32_t AudioTestMixStreamGetWritable(PAUDIOTESTDRVMIXSTREAM pMix)
+{
+ if (!pMix->fDoMixing)
+ return audioTestDriverStackStreamGetWritable(pMix->pDrvStack, pMix->pStream);
+ uint32_t cbRet = AudioMixBufFreeBytes(&pMix->MixBuf);
+ if (!cbRet)
+ {
+ audioTestMixStreamTransfer(pMix);
+ cbRet = AudioMixBufFreeBytes(&pMix->MixBuf);
+ }
+ return cbRet;
+}
+
+
+
+
+/**
+ * Same as audioTestDriverStackStreamPlay.
+ */
+int AudioTestMixStreamPlay(PAUDIOTESTDRVMIXSTREAM pMix, void const *pvBuf, uint32_t cbBuf, uint32_t *pcbPlayed)
+{
+ if (!pMix->fDoMixing)
+ return audioTestDriverStackStreamPlay(pMix->pDrvStack, pMix->pStream, pvBuf, cbBuf, pcbPlayed);
+
+ *pcbPlayed = 0;
+
+ int rc = audioTestMixStreamTransfer(pMix);
+ if (RT_FAILURE(rc))
+ return rc;
+
+ uint32_t const cbFrame = PDMAudioPropsFrameSize(&pMix->MixBuf.Props);
+ while (cbBuf >= cbFrame)
+ {
+ uint32_t const cFrames = AudioMixBufFree(&pMix->MixBuf);
+ if (!cFrames)
+ break;
+ uint32_t cbToWrite = PDMAudioPropsFramesToBytes(&pMix->MixBuf.Props, cFrames);
+ cbToWrite = RT_MIN(cbToWrite, cbBuf);
+ cbToWrite = PDMAudioPropsFloorBytesToFrame(&pMix->MixBuf.Props, cbToWrite);
+
+ uint32_t cFramesWritten = 0;
+ AudioMixBufWrite(&pMix->MixBuf, &pMix->WriteState, pvBuf, cbToWrite, 0 /*offDstFrame*/, cFrames, &cFramesWritten);
+ Assert(cFramesWritten == PDMAudioPropsBytesToFrames(&pMix->MixBuf.Props, cbToWrite));
+ AudioMixBufCommit(&pMix->MixBuf, cFramesWritten);
+
+ *pcbPlayed += cbToWrite;
+ cbBuf -= cbToWrite;
+ pvBuf = (uint8_t const *)pvBuf + cbToWrite;
+
+ rc = audioTestMixStreamTransfer(pMix);
+ if (RT_FAILURE(rc))
+ return *pcbPlayed ? VINF_SUCCESS : rc;
+ }
+
+ return VINF_SUCCESS;
+}
+
+
+/**
+ * Same as audioTestDriverStackStreamGetReadable
+ */
+uint32_t AudioTestMixStreamGetReadable(PAUDIOTESTDRVMIXSTREAM pMix)
+{
+ if (!pMix->fDoMixing)
+ return audioTestDriverStackStreamGetReadable(pMix->pDrvStack, pMix->pStream);
+
+ audioTestMixStreamTransfer(pMix);
+ uint32_t cbRet = AudioMixBufUsedBytes(&pMix->MixBuf);
+ return cbRet;
+}
+
+
+
+
+/**
+ * Same as audioTestDriverStackStreamCapture.
+ */
+int AudioTestMixStreamCapture(PAUDIOTESTDRVMIXSTREAM pMix, void *pvBuf, uint32_t cbBuf, uint32_t *pcbCaptured)
+{
+ if (!pMix->fDoMixing)
+ return audioTestDriverStackStreamCapture(pMix->pDrvStack, pMix->pStream, pvBuf, cbBuf, pcbCaptured);
+
+ *pcbCaptured = 0;
+
+ int rc = audioTestMixStreamTransfer(pMix);
+ if (RT_FAILURE(rc))
+ return rc;
+
+ uint32_t const cbFrame = PDMAudioPropsFrameSize(&pMix->MixBuf.Props);
+ while (cbBuf >= cbFrame)
+ {
+ uint32_t const cFrames = AudioMixBufUsed(&pMix->MixBuf);
+ if (!cFrames)
+ break;
+ uint32_t cbToRead = PDMAudioPropsFramesToBytes(&pMix->MixBuf.Props, cFrames);
+ cbToRead = RT_MIN(cbToRead, cbBuf);
+ cbToRead = PDMAudioPropsFloorBytesToFrame(&pMix->MixBuf.Props, cbToRead);
+
+ uint32_t cFramesPeeked = 0;
+ uint32_t cbPeeked = 0;
+ AudioMixBufPeek(&pMix->MixBuf, 0 /*offSrcFrame*/, cFrames, &cFramesPeeked, &pMix->PeekState, pvBuf, cbToRead, &cbPeeked);
+ Assert(cFramesPeeked == PDMAudioPropsBytesToFrames(&pMix->MixBuf.Props, cbPeeked));
+ AudioMixBufAdvance(&pMix->MixBuf, cFramesPeeked);
+
+ *pcbCaptured += cbToRead;
+ cbBuf -= cbToRead;
+ pvBuf = (uint8_t *)pvBuf + cbToRead;
+
+ rc = audioTestMixStreamTransfer(pMix);
+ if (RT_FAILURE(rc))
+ return *pcbCaptured ? VINF_SUCCESS : rc;
+ }
+
+ return VINF_SUCCESS;
+}
+
+/**
+ * Sets the volume of a mixing stream.
+ *
+ * @param pMix Mixing stream to set volume for.
+ * @param uVolumePercent Volume to set (in percent, 0-100).
+ */
+void AudioTestMixStreamSetVolume(PAUDIOTESTDRVMIXSTREAM pMix, uint8_t uVolumePercent)
+{
+ AssertReturnVoid(pMix->fDoMixing);
+
+ uint8_t const uVol = (PDMAUDIO_VOLUME_MAX / 100) * uVolumePercent;
+
+ PDMAUDIOVOLUME Vol;
+ RT_ZERO(Vol);
+ for (size_t i = 0; i < RT_ELEMENTS(Vol.auChannels); i++)
+ Vol.auChannels[i] = uVol;
+ AudioMixBufSetVolume(&pMix->MixBuf, &Vol);
+}
+
diff --git a/src/VBox/ValidationKit/utils/audio/vkatInternal.h b/src/VBox/ValidationKit/utils/audio/vkatInternal.h
new file mode 100644
index 00000000..aed2ff6c
--- /dev/null
+++ b/src/VBox/ValidationKit/utils/audio/vkatInternal.h
@@ -0,0 +1,547 @@
+/* $Id: vkatInternal.h $ */
+/** @file
+ * VKAT - Internal header file for common definitions + structs.
+ */
+
+/*
+ * 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
+ */
+
+#ifndef VBOX_INCLUDED_SRC_audio_vkatInternal_h
+#define VBOX_INCLUDED_SRC_audio_vkatInternal_h
+#ifndef RT_WITHOUT_PRAGMA_ONCE
+# pragma once
+#endif
+
+
+/*********************************************************************************************************************************
+* Header Files *
+*********************************************************************************************************************************/
+#include <iprt/getopt.h>
+
+#include <VBox/vmm/pdmdrv.h>
+#include <VBox/vmm/pdmaudioinline.h>
+#include <VBox/vmm/pdmaudiohostenuminline.h>
+
+#include "Audio/AudioMixBuffer.h"
+#include "Audio/AudioTest.h"
+#include "Audio/AudioTestService.h"
+#include "Audio/AudioTestServiceClient.h"
+
+#include "VBoxDD.h"
+
+
+/*********************************************************************************************************************************
+* Structures and Typedefs *
+*********************************************************************************************************************************/
+/**
+ * Audio driver stack.
+ *
+ * This can be just be backend driver alone or DrvAudio with a backend.
+ * @todo add automatic resampling via mixer so we can test more of the audio
+ * stack used by the device emulations.
+ */
+typedef struct AUDIOTESTDRVSTACK
+{
+ /** The device registration record for the backend. */
+ PCPDMDRVREG pDrvReg;
+ /** The backend driver instance. */
+ PPDMDRVINS pDrvBackendIns;
+ /** The backend's audio interface. */
+ PPDMIHOSTAUDIO pIHostAudio;
+
+ /** The DrvAudio instance. */
+ PPDMDRVINS pDrvAudioIns;
+ /** This is NULL if we don't use DrvAudio. */
+ PPDMIAUDIOCONNECTOR pIAudioConnector;
+
+ /** The current (last) audio device enumeration to use. */
+ PDMAUDIOHOSTENUM DevEnum;
+} AUDIOTESTDRVSTACK;
+/** Pointer to an audio driver stack. */
+typedef AUDIOTESTDRVSTACK *PAUDIOTESTDRVSTACK;
+
+/**
+ * Backend-only stream structure.
+ */
+typedef struct AUDIOTESTDRVSTACKSTREAM
+{
+ /** The public stream data. */
+ PDMAUDIOSTREAM Core;
+ /** The backend data (variable size). */
+ PDMAUDIOBACKENDSTREAM Backend;
+} AUDIOTESTDRVSTACKSTREAM;
+/** Pointer to a backend-only stream structure. */
+typedef AUDIOTESTDRVSTACKSTREAM *PAUDIOTESTDRVSTACKSTREAM;
+
+/**
+ * Mixer setup for a stream.
+ */
+typedef struct AUDIOTESTDRVMIXSTREAM
+{
+ /** Pointer to the driver stack. */
+ PAUDIOTESTDRVSTACK pDrvStack;
+ /** Pointer to the stream. */
+ PPDMAUDIOSTREAM pStream;
+ /** Properties to use. */
+ PCPDMAUDIOPCMPROPS pProps;
+ /** Set if we're mixing or just passing thru to the driver stack. */
+ bool fDoMixing;
+ /** Mixer buffer. */
+ AUDIOMIXBUF MixBuf;
+ /** Write state. */
+ AUDIOMIXBUFWRITESTATE WriteState;
+ /** Peek state. */
+ AUDIOMIXBUFPEEKSTATE PeekState;
+} AUDIOTESTDRVMIXSTREAM;
+/** Pointer to mixer setup for a stream. */
+typedef AUDIOTESTDRVMIXSTREAM *PAUDIOTESTDRVMIXSTREAM;
+
+/**
+ * Enumeration specifying the current audio test mode.
+ */
+typedef enum AUDIOTESTMODE
+{
+ /** Unknown mode. */
+ AUDIOTESTMODE_UNKNOWN = 0,
+ /** VKAT is running on the guest side. */
+ AUDIOTESTMODE_GUEST,
+ /** VKAT is running on the host side. */
+ AUDIOTESTMODE_HOST
+} AUDIOTESTMODE;
+
+struct AUDIOTESTENV;
+/** Pointer a audio test environment. */
+typedef AUDIOTESTENV *PAUDIOTESTENV;
+
+struct AUDIOTESTDESC;
+/** Pointer a audio test descriptor. */
+typedef AUDIOTESTDESC *PAUDIOTESTDESC;
+
+/**
+ * Callback to set up the test parameters for a specific test.
+ *
+ * @returns IPRT status code.
+ * @retval VINF_SUCCESS if setting the parameters up succeeded. Any other error code
+ * otherwise indicating the kind of error.
+ * @param pszTest Test name.
+ * @param pTstParmsAcq The audio test parameters to set up.
+ */
+typedef DECLCALLBACKTYPE(int, FNAUDIOTESTSETUP,(PAUDIOTESTENV pTstEnv, PAUDIOTESTDESC pTstDesc, PAUDIOTESTPARMS pTstParmsAcq, void **ppvCtx));
+/** Pointer to an audio test setup callback. */
+typedef FNAUDIOTESTSETUP *PFNAUDIOTESTSETUP;
+
+typedef DECLCALLBACKTYPE(int, FNAUDIOTESTEXEC,(PAUDIOTESTENV pTstEnv, void *pvCtx, PAUDIOTESTPARMS pTstParms));
+/** Pointer to an audio test exec callback. */
+typedef FNAUDIOTESTEXEC *PFNAUDIOTESTEXEC;
+
+typedef DECLCALLBACKTYPE(int, FNAUDIOTESTDESTROY,(PAUDIOTESTENV pTstEnv, void *pvCtx));
+/** Pointer to an audio test destroy callback. */
+typedef FNAUDIOTESTDESTROY *PFNAUDIOTESTDESTROY;
+
+/**
+ * Structure for keeping an audio test audio stream.
+ */
+typedef struct AUDIOTESTSTREAM
+{
+ /** The PDM stream. */
+ PPDMAUDIOSTREAM pStream;
+ /** The backend stream. */
+ PPDMAUDIOBACKENDSTREAM pBackend;
+ /** The stream config. */
+ PDMAUDIOSTREAMCFG Cfg;
+ /** Associated mixing stream. Optional. */
+ AUDIOTESTDRVMIXSTREAM Mix;
+} AUDIOTESTSTREAM;
+/** Pointer to audio test stream. */
+typedef AUDIOTESTSTREAM *PAUDIOTESTSTREAM;
+
+/** Maximum audio streams a test environment can handle. */
+#define AUDIOTESTENV_MAX_STREAMS 8
+
+/**
+ * Structure for keeping TCP/IP-specific options.
+ */
+typedef struct AUDIOTESTENVTCPOPTS
+{
+ /** Connection mode(s) to use. */
+ ATSCONNMODE enmConnMode;
+ /** Bind address (server mode). When empty, "0.0.0.0" (any host) will be used. */
+ char szBindAddr[128];
+ /** Bind port (server mode). */
+ uint16_t uBindPort;
+ /** Connection address (client mode). */
+ char szConnectAddr[128];
+ /** Connection port (client mode). */
+ uint16_t uConnectPort;
+} AUDIOTESTENVTCPOPTS;
+/** Pointer to audio test TCP options. */
+typedef AUDIOTESTENVTCPOPTS *PAUDIOTESTENVTCPOPTS;
+
+/**
+ * Structure holding additional I/O options.
+ */
+typedef struct AUDIOTESTIOOPTS
+{
+ /** Whether to use the audio connector or not. */
+ bool fWithDrvAudio;
+ /** Whether to use a mixing buffer or not. */
+ bool fWithMixer;
+ /** Buffer size (in ms). */
+ uint32_t cMsBufferSize;
+ /** Pre-buffering size (in ms). */
+ uint32_t cMsPreBuffer;
+ /** Scheduling (in ms). */
+ uint32_t cMsSchedulingHint;
+ /** Audio vlume to use (in percent). */
+ uint8_t uVolumePercent;
+ /** PCM audio properties to use. */
+ PDMAUDIOPCMPROPS Props;
+} AUDIOTESTIOOPTS;
+/** Pointer to additional playback options. */
+typedef AUDIOTESTIOOPTS *PAUDIOTESTIOOPTS;
+
+/**
+ * Structure for keeping a user context for the test service callbacks.
+ */
+typedef struct ATSCALLBACKCTX
+{
+ /** The test environment bound to this context. */
+ PAUDIOTESTENV pTstEnv;
+ /** Absolute path to the packed up test set archive.
+ * Keep it simple for now and only support one (open) archive at a time. */
+ char szTestSetArchive[RTPATH_MAX];
+ /** File handle to the (opened) test set archive for reading. */
+ RTFILE hTestSetArchive;
+ /** Number of currently connected clients. */
+ uint8_t cClients;
+} ATSCALLBACKCTX;
+typedef ATSCALLBACKCTX *PATSCALLBACKCTX;
+
+/**
+ * Audio test environment parameters.
+ *
+ * This is global to all tests defined.
+ */
+typedef struct AUDIOTESTENV
+{
+ /** Audio testing mode. */
+ AUDIOTESTMODE enmMode;
+ /** Whether self test mode is active or not. */
+ bool fSelftest;
+ /** Whether skip the actual verification or not. */
+ bool fSkipVerify;
+ /** Name of the audio device to use.
+ * If empty the default audio device will be used. */
+ char szDev[128];
+ /** Zero-based index of current test (will be increased for every run test). */
+ uint32_t idxTest;
+ /** Number of iterations for *all* tests specified.
+ * When set to 0 (default), a random value (see specific test) will be chosen. */
+ uint32_t cIterations;
+ /** I/O options to use. */
+ AUDIOTESTIOOPTS IoOpts;
+ /** Test tone parameters to use. */
+ AUDIOTESTTONEPARMS ToneParms;
+ /** Output path for storing the test environment's final test files. */
+ char szTag[AUDIOTEST_TAG_MAX];
+ /** Output path for storing the test environment's final test files. */
+ char szPathOut[RTPATH_MAX];
+ /** Temporary path for this test environment. */
+ char szPathTemp[RTPATH_MAX];
+ /** Pointer to audio test driver stack to use. */
+ PAUDIOTESTDRVSTACK pDrvStack;
+ /** Audio stream. */
+ AUDIOTESTSTREAM aStreams[AUDIOTESTENV_MAX_STREAMS];
+ /** The audio test set to use. */
+ AUDIOTESTSET Set;
+ /** TCP options to use for ATS. */
+ AUDIOTESTENVTCPOPTS TcpOpts;
+ /** ATS server instance to use.
+ * NULL if not in use. */
+ PATSSERVER pSrv;
+ /** ATS callback context to use. */
+ ATSCALLBACKCTX CallbackCtx;
+ union
+ {
+ struct
+ {
+ /** Client connected to the ATS on the guest side. */
+ ATSCLIENT AtsClGuest;
+ /** Path to the guest's test set downloaded to the host. */
+ char szPathTestSetGuest[RTPATH_MAX];
+ /** Client connected to the Validation Kit audio driver ATS. */
+ ATSCLIENT AtsClValKit;
+ /** Path to the Validation Kit audio driver's test set downloaded to the host. */
+ char szPathTestSetValKit[RTPATH_MAX];
+ } Host;
+ } u;
+} AUDIOTESTENV;
+
+/**
+ * Audio test descriptor.
+ */
+typedef struct AUDIOTESTDESC
+{
+ /** (Sort of) Descriptive test name. */
+ const char *pszName;
+ /** Flag whether the test is excluded. */
+ bool fExcluded;
+ /** The setup callback. */
+ PFNAUDIOTESTSETUP pfnSetup;
+ /** The exec callback. */
+ PFNAUDIOTESTEXEC pfnExec;
+ /** The destruction callback. */
+ PFNAUDIOTESTDESTROY pfnDestroy;
+} AUDIOTESTDESC;
+
+/**
+ * Backend description.
+ */
+typedef struct AUDIOTESTBACKENDDESC
+{
+ /** The driver registration structure. */
+ PCPDMDRVREG pDrvReg;
+ /** The backend name.
+ * Aliases are implemented by having multiple entries for the same backend. */
+ const char *pszName;
+} AUDIOTESTBACKENDDESC;
+
+/**
+ * VKAT command table entry.
+ */
+typedef struct VKATCMD
+{
+ /** The command name. */
+ const char *pszCommand;
+ /** The command handler. */
+ DECLCALLBACKMEMBER(RTEXITCODE, pfnHandler,(PRTGETOPTSTATE pGetState));
+
+ /** Command description. */
+ const char *pszDesc;
+ /** Options array. */
+ PCRTGETOPTDEF paOptions;
+ /** Number of options in the option array. */
+ size_t cOptions;
+ /** Gets help for an option. */
+ DECLCALLBACKMEMBER(const char *, pfnOptionHelp,(PCRTGETOPTDEF pOpt));
+ /** Flag indicating if the command needs the ATS transport layer.
+ * Needed for command line parsing. */
+ bool fNeedsTransport;
+} VKATCMD;
+/** Pointer to a const VKAT command entry. */
+typedef VKATCMD const *PCVKATCMD;
+
+
+/*********************************************************************************************************************************
+* Global Variables *
+*********************************************************************************************************************************/
+/** Terminate ASAP if set. Set on Ctrl-C. */
+extern bool volatile g_fTerminate;
+/** The release logger. */
+extern PRTLOGGER g_pRelLogger;
+
+/** The test handle. */
+extern RTTEST g_hTest;
+/** The current verbosity level. */
+extern unsigned g_uVerbosity;
+/** DrvAudio: Enable debug (or not). */
+extern bool g_fDrvAudioDebug;
+/** DrvAudio: The debug output path. */
+extern const char *g_pszDrvAudioDebug;
+
+extern const VKATCMD g_CmdTest;
+extern const VKATCMD g_CmdVerify;
+extern const VKATCMD g_CmdBackends;
+extern const VKATCMD g_CmdEnum;
+extern const VKATCMD g_CmdPlay;
+extern const VKATCMD g_CmdRec;
+extern const VKATCMD g_CmdSelfTest;
+
+
+extern AUDIOTESTDESC g_aTests[];
+extern unsigned g_cTests;
+
+extern AUDIOTESTBACKENDDESC const g_aBackends[];
+extern unsigned g_cBackends;
+
+
+/*********************************************************************************************************************************
+* Prototypes *
+*********************************************************************************************************************************/
+
+/** @name Command line handlers
+ * @{ */
+RTEXITCODE audioTestUsage(PRTSTREAM pStrm, PCVKATCMD pOnlyCmd);
+RTEXITCODE audioTestVersion(void);
+void audioTestShowLogo(PRTSTREAM pStream);
+/** @} */
+
+/** @name Driver stack
+ * @{ */
+int AudioTestDriverStackPerformSelftest(void);
+
+void audioTestDriverStackDelete(PAUDIOTESTDRVSTACK pDrvStack);
+int audioTestDriverStackInitEx(PAUDIOTESTDRVSTACK pDrvStack, PCPDMDRVREG pDrvReg, bool fEnabledIn, bool fEnabledOut, bool fWithDrvAudio);
+int audioTestDriverStackInit(PAUDIOTESTDRVSTACK pDrvStack, PCPDMDRVREG pDrvReg, bool fWithDrvAudio);
+int audioTestDriverStackProbe(PAUDIOTESTDRVSTACK pDrvStack, PCPDMDRVREG pDrvReg, bool fEnabledIn, bool fEnabledOut, bool fWithDrvAudio);
+int audioTestDriverStackSetDevice(PAUDIOTESTDRVSTACK pDrvStack, PDMAUDIODIR enmDir, const char *pszDevId);
+/** @} */
+
+/** @name Driver
+ * @{ */
+int audioTestDrvConstruct(PAUDIOTESTDRVSTACK pDrvStack, PCPDMDRVREG pDrvReg, PPDMDRVINS pParentDrvIns, PPPDMDRVINS ppDrvIns);
+/** @} */
+
+/** @name Driver stack stream
+ * @{ */
+int audioTestDriverStackStreamCreateInput(PAUDIOTESTDRVSTACK pDrvStack, PCPDMAUDIOPCMPROPS pProps,
+ uint32_t cMsBufferSize, uint32_t cMsPreBuffer, uint32_t cMsSchedulingHint,
+ PPDMAUDIOSTREAM *ppStream, PPDMAUDIOSTREAMCFG pCfgAcq);
+int audioTestDriverStackStreamCreateOutput(PAUDIOTESTDRVSTACK pDrvStack, PCPDMAUDIOPCMPROPS pProps,
+ uint32_t cMsBufferSize, uint32_t cMsPreBuffer, uint32_t cMsSchedulingHint,
+ PPDMAUDIOSTREAM *ppStream, PPDMAUDIOSTREAMCFG pCfgAcq);
+void audioTestDriverStackStreamDestroy(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream);
+int audioTestDriverStackStreamDrain(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream, bool fSync);
+int audioTestDriverStackStreamEnable(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream);
+int AudioTestDriverStackStreamDisable(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream);
+bool audioTestDriverStackStreamIsOkay(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream);
+uint32_t audioTestDriverStackStreamGetWritable(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream);
+int audioTestDriverStackStreamPlay(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream, void const *pvBuf,
+ uint32_t cbBuf, uint32_t *pcbPlayed);
+uint32_t audioTestDriverStackStreamGetReadable(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream);
+int audioTestDriverStackStreamCapture(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream,
+ void *pvBuf, uint32_t cbBuf, uint32_t *pcbCaptured);
+/** @} */
+
+/** @name Backend handling
+ * @{ */
+PCPDMDRVREG AudioTestGetDefaultBackend(void);
+PCPDMDRVREG AudioTestFindBackendOpt(const char *pszBackend);
+/** @} */
+
+/** @name Mixing stream
+ * @{ */
+int AudioTestMixStreamInit(PAUDIOTESTDRVMIXSTREAM pMix, PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream,
+ PCPDMAUDIOPCMPROPS pProps, uint32_t cMsBuffer);
+void AudioTestMixStreamTerm(PAUDIOTESTDRVMIXSTREAM pMix);
+int AudioTestMixStreamEnable(PAUDIOTESTDRVMIXSTREAM pMix);
+int AudioTestMixStreamDrain(PAUDIOTESTDRVMIXSTREAM pMix, bool fSync);
+int AudioTestMixStreamDisable(PAUDIOTESTDRVMIXSTREAM pMix);
+bool AudioTestMixStreamIsOkay(PAUDIOTESTDRVMIXSTREAM pMix);
+uint32_t AudioTestMixStreamGetWritable(PAUDIOTESTDRVMIXSTREAM pMix);
+int AudioTestMixStreamPlay(PAUDIOTESTDRVMIXSTREAM pMix, void const *pvBuf, uint32_t cbBuf, uint32_t *pcbPlayed);
+uint32_t AudioTestMixStreamGetReadable(PAUDIOTESTDRVMIXSTREAM pMix);
+int AudioTestMixStreamCapture(PAUDIOTESTDRVMIXSTREAM pMix, void *pvBuf, uint32_t cbBuf, uint32_t *pcbCaptured);
+void AudioTestMixStreamSetVolume(PAUDIOTESTDRVMIXSTREAM pMix, uint8_t uVolumePercent);
+/** @} */
+
+/** @name Device handling
+ * @{ */
+int audioTestDeviceOpen(PPDMAUDIOHOSTDEV pDev);
+int audioTestDeviceClose(PPDMAUDIOHOSTDEV pDev);
+
+int audioTestDevicesEnumerateAndCheck(PAUDIOTESTDRVSTACK pDrvStack, const char *pszDev, PPDMAUDIOHOSTDEV *ppDev);
+/** @} */
+
+/** @name ATS routines
+ * @{ */
+int audioTestEnvConnectToValKitAts(PAUDIOTESTENV pTstEnv,
+ const char *pszHostTcpAddr, uint32_t uHostTcpPort);
+/** @} */
+
+/** @name Test environment handling
+ * @{ */
+void audioTestEnvInit(PAUDIOTESTENV pTstEnv);
+int audioTestEnvCreate(PAUDIOTESTENV pTstEnv, PAUDIOTESTDRVSTACK pDrvStack);
+void audioTestEnvDestroy(PAUDIOTESTENV pTstEnv);
+int audioTestEnvPrologue(PAUDIOTESTENV pTstEnv, bool fPack, char *pszPackFile, size_t cbPackFile);
+
+void audioTestParmsInit(PAUDIOTESTPARMS pTstParms);
+void audioTestParmsDestroy(PAUDIOTESTPARMS pTstParms);
+/** @} */
+
+int audioTestWorker(PAUDIOTESTENV pTstEnv);
+
+/** @todo Test tone handling */
+int audioTestPlayTone(PAUDIOTESTIOOPTS pIoOpts, PAUDIOTESTENV pTstEnv, PAUDIOTESTSTREAM pStream, PAUDIOTESTTONEPARMS pParms);
+void audioTestToneParmsInit(PAUDIOTESTTONEPARMS pToneParms);
+/** @} */
+
+void audioTestIoOptsInitDefaults(PAUDIOTESTIOOPTS pIoOpts);
+
+
+/*********************************************************************************************************************************
+* Common command line stuff *
+*********************************************************************************************************************************/
+
+/**
+ * Common long options values.
+ */
+enum
+{
+ AUDIO_TEST_OPT_CMN_DAEMONIZE = 256,
+ AUDIO_TEST_OPT_CMN_DAEMONIZED,
+ AUDIO_TEST_OPT_CMN_DEBUG_AUDIO_ENABLE,
+ AUDIO_TEST_OPT_CMN_DEBUG_AUDIO_PATH
+};
+
+/** For use in the option switch to handle common options. */
+#define AUDIO_TEST_COMMON_OPTION_CASES(a_ValueUnion, a_pCmd) \
+ case 'q': \
+ g_uVerbosity = 0; \
+ if (g_pRelLogger) \
+ RTLogGroupSettings(g_pRelLogger, "all=0 all.e"); \
+ break; \
+ \
+ case 'v': \
+ /* No-op here, has been handled by main() already. */ /** @todo r-bird: -q works, so -v must too! */ \
+ break; \
+ \
+ case 'V': \
+ return audioTestVersion(); \
+ \
+ case 'h': \
+ return audioTestUsage(g_pStdOut, a_pCmd); \
+ \
+ case AUDIO_TEST_OPT_CMN_DEBUG_AUDIO_ENABLE: \
+ g_fDrvAudioDebug = true; \
+ break; \
+ \
+ case AUDIO_TEST_OPT_CMN_DEBUG_AUDIO_PATH: \
+ g_pszDrvAudioDebug = (a_ValueUnion).psz; \
+ break; \
+ case AUDIO_TEST_OPT_CMN_DAEMONIZE: \
+ break; \
+ case AUDIO_TEST_OPT_CMN_DAEMONIZED: \
+ break;
+
+#endif /* !VBOX_INCLUDED_SRC_audio_vkatInternal_h */
+