diff options
Diffstat (limited to '')
-rw-r--r-- | src/VBox/ValidationKit/utils/audio/Makefile.kmk | 220 | ||||
-rw-r--r-- | src/VBox/ValidationKit/utils/audio/ntPlayToneWaveX.cpp | 226 | ||||
-rw-r--r-- | src/VBox/ValidationKit/utils/audio/readme.txt | 2 | ||||
-rw-r--r-- | src/VBox/ValidationKit/utils/audio/vkat.cpp | 1649 | ||||
-rw-r--r-- | src/VBox/ValidationKit/utils/audio/vkatCmdGeneric.cpp | 1169 | ||||
-rw-r--r-- | src/VBox/ValidationKit/utils/audio/vkatCmdSelfTest.cpp | 481 | ||||
-rw-r--r-- | src/VBox/ValidationKit/utils/audio/vkatCommon.cpp | 1760 | ||||
-rw-r--r-- | src/VBox/ValidationKit/utils/audio/vkatDriverStack.cpp | 1621 | ||||
-rw-r--r-- | src/VBox/ValidationKit/utils/audio/vkatInternal.h | 547 |
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 */ + |