463 lines
16 KiB
C
463 lines
16 KiB
C
/* $Id: pdmaudiohostenuminline.h $ */
|
|
/** @file
|
|
* PDM - Audio Helpers for host audio device enumeration, Inlined Code. (DEV,++)
|
|
*
|
|
* This is all inlined because it's too tedious to create a couple libraries to
|
|
* contain it all (same bad excuse as for intnetinline.h & pdmnetinline.h).
|
|
*/
|
|
|
|
/*
|
|
* Copyright (C) 2006-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_vmm_pdmaudiohostenuminline_h
|
|
#define VBOX_INCLUDED_vmm_pdmaudiohostenuminline_h
|
|
#ifndef RT_WITHOUT_PRAGMA_ONCE
|
|
# pragma once
|
|
#endif
|
|
|
|
|
|
/*********************************************************************************************************************************
|
|
* Header Files *
|
|
*********************************************************************************************************************************/
|
|
#include <VBox/err.h>
|
|
#include <VBox/log.h>
|
|
#include <VBox/vmm/pdmaudioifs.h>
|
|
#include <VBox/vmm/pdmaudioinline.h>
|
|
|
|
#include <iprt/assert.h>
|
|
#include <iprt/mem.h>
|
|
#include <iprt/string.h>
|
|
|
|
|
|
/** @defgroup grp_pdm_audio_host_enum_inline The PDM Host Audio Enumeration Helper APIs
|
|
* @ingroup grp_pdm
|
|
* @{
|
|
*/
|
|
|
|
|
|
/**
|
|
* Allocates a host audio device for an enumeration result.
|
|
*
|
|
* @returns Newly allocated audio device, or NULL on failure.
|
|
* @param cb The total device structure size. This must be at least the
|
|
* size of PDMAUDIOHOSTDEV. The idea is that the caller extends
|
|
* the PDMAUDIOHOSTDEV structure and appends additional data
|
|
* after it in its private structure.
|
|
* @param cbName The number of bytes to allocate for the name field
|
|
* (including the terminator). Pass zero if RTStrAlloc and
|
|
* friends will be used.
|
|
* @param cbId The number of bytes to allocate for the ID field. Pass
|
|
* zero if RTStrAlloc and friends will be used.
|
|
*/
|
|
DECLINLINE(PPDMAUDIOHOSTDEV) PDMAudioHostDevAlloc(size_t cb, size_t cbName, size_t cbId)
|
|
{
|
|
AssertReturn(cb >= sizeof(PDMAUDIOHOSTDEV), NULL);
|
|
AssertReturn(cb < _4M, NULL);
|
|
AssertReturn(cbName < _4K, NULL);
|
|
AssertReturn(cbId < _16K, NULL);
|
|
|
|
PPDMAUDIOHOSTDEV pDev = (PPDMAUDIOHOSTDEV)RTMemAllocZ(RT_ALIGN_Z(cb + cbName + cbId, 64));
|
|
if (pDev)
|
|
{
|
|
pDev->uMagic = PDMAUDIOHOSTDEV_MAGIC;
|
|
pDev->cbSelf = (uint32_t)cb;
|
|
RTListInit(&pDev->ListEntry);
|
|
if (cbName)
|
|
pDev->pszName = (char *)pDev + cb;
|
|
if (cbId)
|
|
pDev->pszId = (char *)pDev + cb + cbName;
|
|
}
|
|
return pDev;
|
|
}
|
|
|
|
/**
|
|
* Frees a host audio device allocated by PDMAudioHostDevAlloc.
|
|
*
|
|
* @param pDev The device to free. NULL is ignored.
|
|
*/
|
|
DECLINLINE(void) PDMAudioHostDevFree(PPDMAUDIOHOSTDEV pDev)
|
|
{
|
|
if (pDev)
|
|
{
|
|
Assert(pDev->uMagic == PDMAUDIOHOSTDEV_MAGIC);
|
|
pDev->uMagic = ~PDMAUDIOHOSTDEV_MAGIC;
|
|
pDev->cbSelf = 0;
|
|
|
|
if (pDev->fFlags & PDMAUDIOHOSTDEV_F_NAME_ALLOC)
|
|
{
|
|
RTStrFree(pDev->pszName);
|
|
pDev->pszName = NULL;
|
|
}
|
|
|
|
if (pDev->fFlags & PDMAUDIOHOSTDEV_F_ID_ALLOC)
|
|
{
|
|
RTStrFree(pDev->pszId);
|
|
pDev->pszId = NULL;
|
|
}
|
|
|
|
RTMemFree(pDev);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Duplicates a host audio device enumeration entry.
|
|
*
|
|
* @returns Duplicated audio device entry on success, or NULL on failure.
|
|
* @param pDev The audio device enum entry to duplicate.
|
|
* @param fOnlyCoreData
|
|
*/
|
|
DECLINLINE(PPDMAUDIOHOSTDEV) PDMAudioHostDevDup(PCPDMAUDIOHOSTDEV pDev, bool fOnlyCoreData)
|
|
{
|
|
AssertPtrReturn(pDev, NULL);
|
|
Assert(pDev->uMagic == PDMAUDIOHOSTDEV_MAGIC);
|
|
Assert(fOnlyCoreData || !(pDev->fFlags & PDMAUDIOHOSTDEV_F_NO_DUP));
|
|
|
|
uint32_t cbToDup = fOnlyCoreData ? sizeof(PDMAUDIOHOSTDEV) : pDev->cbSelf;
|
|
AssertReturn(cbToDup >= sizeof(*pDev), NULL);
|
|
|
|
PPDMAUDIOHOSTDEV pDevDup = PDMAudioHostDevAlloc(cbToDup, 0, 0);
|
|
if (pDevDup)
|
|
{
|
|
memcpy(pDevDup, pDev, cbToDup);
|
|
RTListInit(&pDevDup->ListEntry);
|
|
pDevDup->cbSelf = cbToDup;
|
|
|
|
if (pDev->pszName)
|
|
{
|
|
uintptr_t off;
|
|
if ( (pDevDup->fFlags & PDMAUDIOHOSTDEV_F_NAME_ALLOC)
|
|
|| (off = (uintptr_t)pDev->pszName - (uintptr_t)pDev) >= pDevDup->cbSelf)
|
|
{
|
|
pDevDup->fFlags |= PDMAUDIOHOSTDEV_F_NAME_ALLOC;
|
|
pDevDup->pszName = RTStrDup(pDev->pszName);
|
|
AssertReturnStmt(pDevDup->pszName, PDMAudioHostDevFree(pDevDup), NULL);
|
|
}
|
|
else
|
|
pDevDup->pszName = (char *)pDevDup + off;
|
|
}
|
|
|
|
if (pDev->pszId)
|
|
{
|
|
uintptr_t off;
|
|
if ( (pDevDup->fFlags & PDMAUDIOHOSTDEV_F_ID_ALLOC)
|
|
|| (off = (uintptr_t)pDev->pszId - (uintptr_t)pDev) >= pDevDup->cbSelf)
|
|
{
|
|
pDevDup->fFlags |= PDMAUDIOHOSTDEV_F_ID_ALLOC;
|
|
pDevDup->pszId = RTStrDup(pDev->pszId);
|
|
AssertReturnStmt(pDevDup->pszId, PDMAudioHostDevFree(pDevDup), NULL);
|
|
}
|
|
else
|
|
pDevDup->pszId = (char *)pDevDup + off;
|
|
}
|
|
}
|
|
|
|
return pDevDup;
|
|
}
|
|
|
|
/**
|
|
* Initializes a host audio device enumeration.
|
|
*
|
|
* @param pDevEnm The enumeration to initialize.
|
|
*/
|
|
DECLINLINE(void) PDMAudioHostEnumInit(PPDMAUDIOHOSTENUM pDevEnm)
|
|
{
|
|
AssertPtr(pDevEnm);
|
|
|
|
pDevEnm->uMagic = PDMAUDIOHOSTENUM_MAGIC;
|
|
pDevEnm->cDevices = 0;
|
|
RTListInit(&pDevEnm->LstDevices);
|
|
}
|
|
|
|
/**
|
|
* Deletes the host audio device enumeration and frees all device entries
|
|
* associated with it.
|
|
*
|
|
* The user must call PDMAudioHostEnumInit again to use it again.
|
|
*
|
|
* @param pDevEnm The host audio device enumeration to delete.
|
|
*/
|
|
DECLINLINE(void) PDMAudioHostEnumDelete(PPDMAUDIOHOSTENUM pDevEnm)
|
|
{
|
|
if (pDevEnm)
|
|
{
|
|
AssertPtr(pDevEnm);
|
|
AssertReturnVoid(pDevEnm->uMagic == PDMAUDIOHOSTENUM_MAGIC);
|
|
|
|
PPDMAUDIOHOSTDEV pDev, pDevNext;
|
|
RTListForEachSafe(&pDevEnm->LstDevices, pDev, pDevNext, PDMAUDIOHOSTDEV, ListEntry)
|
|
{
|
|
RTListNodeRemove(&pDev->ListEntry);
|
|
|
|
PDMAudioHostDevFree(pDev);
|
|
|
|
pDevEnm->cDevices--;
|
|
}
|
|
|
|
/* Sanity. */
|
|
Assert(RTListIsEmpty(&pDevEnm->LstDevices));
|
|
Assert(pDevEnm->cDevices == 0);
|
|
|
|
pDevEnm->uMagic = ~PDMAUDIOHOSTENUM_MAGIC;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds an audio device to a device enumeration.
|
|
*
|
|
* @param pDevEnm Device enumeration to add device to.
|
|
* @param pDev Device to add. The pointer will be owned by the device enumeration then.
|
|
*/
|
|
DECLINLINE(void) PDMAudioHostEnumAppend(PPDMAUDIOHOSTENUM pDevEnm, PPDMAUDIOHOSTDEV pDev)
|
|
{
|
|
AssertPtr(pDevEnm);
|
|
AssertPtr(pDev);
|
|
Assert(pDevEnm->uMagic == PDMAUDIOHOSTENUM_MAGIC);
|
|
|
|
RTListAppend(&pDevEnm->LstDevices, &pDev->ListEntry);
|
|
pDevEnm->cDevices++;
|
|
}
|
|
|
|
/**
|
|
* Appends copies of matching host device entries from one to another enumeration.
|
|
*
|
|
* @returns VBox status code.
|
|
* @param pDstDevEnm The target to append copies of matching device to.
|
|
* @param pSrcDevEnm The source to copy matching devices from.
|
|
* @param enmUsage The usage to match for copying.
|
|
* Use PDMAUDIODIR_INVALID to match all entries.
|
|
* @param fOnlyCoreData Set this to only copy the PDMAUDIOHOSTDEV part.
|
|
* Careful with passing @c false here as not all
|
|
* backends have data that can be copied.
|
|
*/
|
|
DECLINLINE(int) PDMAudioHostEnumCopy(PPDMAUDIOHOSTENUM pDstDevEnm, PCPDMAUDIOHOSTENUM pSrcDevEnm,
|
|
PDMAUDIODIR enmUsage, bool fOnlyCoreData)
|
|
{
|
|
AssertPtrReturn(pDstDevEnm, VERR_INVALID_POINTER);
|
|
AssertReturn(pDstDevEnm->uMagic == PDMAUDIOHOSTENUM_MAGIC, VERR_WRONG_ORDER);
|
|
|
|
AssertPtrReturn(pSrcDevEnm, VERR_INVALID_POINTER);
|
|
AssertReturn(pSrcDevEnm->uMagic == PDMAUDIOHOSTENUM_MAGIC, VERR_WRONG_ORDER);
|
|
|
|
PPDMAUDIOHOSTDEV pSrcDev;
|
|
RTListForEach(&pSrcDevEnm->LstDevices, pSrcDev, PDMAUDIOHOSTDEV, ListEntry)
|
|
{
|
|
if ( enmUsage == pSrcDev->enmUsage
|
|
|| enmUsage == PDMAUDIODIR_INVALID /*all*/)
|
|
{
|
|
PPDMAUDIOHOSTDEV pDstDev = PDMAudioHostDevDup(pSrcDev, fOnlyCoreData);
|
|
AssertReturn(pDstDev, VERR_NO_MEMORY);
|
|
|
|
PDMAudioHostEnumAppend(pDstDevEnm, pDstDev);
|
|
}
|
|
}
|
|
|
|
return VINF_SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* Moves all the device entries from one enumeration to another, destroying the
|
|
* former.
|
|
*
|
|
* @returns VBox status code.
|
|
* @param pDstDevEnm The target to put move @a pSrcDevEnm to. This
|
|
* does not need to be initialized, but if it is it
|
|
* must not have any device entries.
|
|
* @param pSrcDevEnm The source to move from. This will be empty
|
|
* upon successful return.
|
|
*/
|
|
DECLINLINE(int) PDMAudioHostEnumMove(PPDMAUDIOHOSTENUM pDstDevEnm, PPDMAUDIOHOSTENUM pSrcDevEnm)
|
|
{
|
|
AssertPtrReturn(pDstDevEnm, VERR_INVALID_POINTER);
|
|
AssertReturn(pDstDevEnm->uMagic != PDMAUDIOHOSTENUM_MAGIC || pDstDevEnm->cDevices == 0, VERR_WRONG_ORDER);
|
|
|
|
AssertPtrReturn(pSrcDevEnm, VERR_INVALID_POINTER);
|
|
AssertReturn(pSrcDevEnm->uMagic == PDMAUDIOHOSTENUM_MAGIC, VERR_WRONG_ORDER);
|
|
|
|
pDstDevEnm->uMagic = PDMAUDIOHOSTENUM_MAGIC;
|
|
RTListInit(&pDstDevEnm->LstDevices);
|
|
pDstDevEnm->cDevices = pSrcDevEnm->cDevices;
|
|
if (pSrcDevEnm->cDevices)
|
|
{
|
|
PPDMAUDIOHOSTDEV pCur;
|
|
while ((pCur = RTListRemoveFirst(&pSrcDevEnm->LstDevices, PDMAUDIOHOSTDEV, ListEntry)) != NULL)
|
|
RTListAppend(&pDstDevEnm->LstDevices, &pCur->ListEntry);
|
|
}
|
|
return VINF_SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* Get the default device with the given usage.
|
|
*
|
|
* This assumes that only one default device per usage is set, if there should
|
|
* be more than one, the first one is returned.
|
|
*
|
|
* @returns Default device if found, or NULL if not.
|
|
* @param pDevEnm Device enumeration to get default device for.
|
|
* @param enmUsage Usage to get default device for.
|
|
* Pass PDMAUDIODIR_INVALID to get the first device with
|
|
* either PDMAUDIOHOSTDEV_F_DEFAULT_OUT or
|
|
* PDMAUDIOHOSTDEV_F_DEFAULT_IN set.
|
|
*/
|
|
DECLINLINE(PPDMAUDIOHOSTDEV) PDMAudioHostEnumGetDefault(PCPDMAUDIOHOSTENUM pDevEnm, PDMAUDIODIR enmUsage)
|
|
{
|
|
AssertPtrReturn(pDevEnm, NULL);
|
|
AssertReturn(pDevEnm->uMagic == PDMAUDIOHOSTENUM_MAGIC, NULL);
|
|
|
|
Assert(enmUsage == PDMAUDIODIR_IN || enmUsage == PDMAUDIODIR_OUT || enmUsage == PDMAUDIODIR_INVALID);
|
|
uint32_t const fFlags = enmUsage == PDMAUDIODIR_IN ? PDMAUDIOHOSTDEV_F_DEFAULT_IN
|
|
: enmUsage == PDMAUDIODIR_OUT ? PDMAUDIOHOSTDEV_F_DEFAULT_OUT
|
|
: enmUsage == PDMAUDIODIR_INVALID ? PDMAUDIOHOSTDEV_F_DEFAULT_IN | PDMAUDIOHOSTDEV_F_DEFAULT_OUT
|
|
: 0;
|
|
|
|
PPDMAUDIOHOSTDEV pDev;
|
|
RTListForEach(&pDevEnm->LstDevices, pDev, PDMAUDIOHOSTDEV, ListEntry)
|
|
{
|
|
if (pDev->fFlags & fFlags)
|
|
{
|
|
Assert(pDev->enmUsage == enmUsage || pDev->enmUsage == PDMAUDIODIR_DUPLEX || enmUsage == PDMAUDIODIR_INVALID);
|
|
return pDev;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Get the number of device with the given usage.
|
|
*
|
|
* @returns Number of matching devices.
|
|
* @param pDevEnm Device enumeration to get default device for.
|
|
* @param enmUsage Usage to count devices for.
|
|
* Pass PDMAUDIODIR_INVALID to get the total number of devices.
|
|
*/
|
|
DECLINLINE(uint32_t) PDMAudioHostEnumCountMatching(PCPDMAUDIOHOSTENUM pDevEnm, PDMAUDIODIR enmUsage)
|
|
{
|
|
AssertPtrReturn(pDevEnm, 0);
|
|
AssertReturn(pDevEnm->uMagic == PDMAUDIOHOSTENUM_MAGIC, 0);
|
|
|
|
if (enmUsage == PDMAUDIODIR_INVALID)
|
|
return pDevEnm->cDevices;
|
|
|
|
uint32_t cDevs = 0;
|
|
PPDMAUDIOHOSTDEV pDev;
|
|
RTListForEach(&pDevEnm->LstDevices, pDev, PDMAUDIOHOSTDEV, ListEntry)
|
|
{
|
|
if (enmUsage == pDev->enmUsage)
|
|
cDevs++;
|
|
}
|
|
|
|
return cDevs;
|
|
}
|
|
|
|
/** The max string length for all PDMAUDIOHOSTDEV_F_XXX.
|
|
* @sa PDMAudioHostDevFlagsToString */
|
|
#define PDMAUDIOHOSTDEV_MAX_FLAGS_STRING_LEN sizeof("DEFAULT_OUT DEFAULT_IN HOTPLUG BUGGY IGNORE LOCKED DEAD NAME_ALLOC ID_ALLOC NO_DUP ")
|
|
|
|
/**
|
|
* Converts an audio device flags to a string.
|
|
*
|
|
* @returns
|
|
* @param pszDst Destination buffer with a size of at least
|
|
* PDMAUDIOHOSTDEV_MAX_FLAGS_STRING_LEN bytes (including
|
|
* the string terminator).
|
|
* @param fFlags Audio flags (PDMAUDIOHOSTDEV_F_XXX) to convert.
|
|
*/
|
|
DECLINLINE(const char *) PDMAudioHostDevFlagsToString(char pszDst[PDMAUDIOHOSTDEV_MAX_FLAGS_STRING_LEN], uint32_t fFlags)
|
|
{
|
|
static const struct { const char *pszMnemonic; uint32_t cchMnemonic; uint32_t fFlag; } s_aFlags[] =
|
|
{
|
|
{ RT_STR_TUPLE("DEFAULT_OUT "), PDMAUDIOHOSTDEV_F_DEFAULT_OUT },
|
|
{ RT_STR_TUPLE("DEFAULT_IN "), PDMAUDIOHOSTDEV_F_DEFAULT_IN },
|
|
{ RT_STR_TUPLE("HOTPLUG "), PDMAUDIOHOSTDEV_F_HOTPLUG },
|
|
{ RT_STR_TUPLE("BUGGY "), PDMAUDIOHOSTDEV_F_BUGGY },
|
|
{ RT_STR_TUPLE("IGNORE "), PDMAUDIOHOSTDEV_F_IGNORE },
|
|
{ RT_STR_TUPLE("LOCKED "), PDMAUDIOHOSTDEV_F_LOCKED },
|
|
{ RT_STR_TUPLE("DEAD "), PDMAUDIOHOSTDEV_F_DEAD },
|
|
{ RT_STR_TUPLE("NAME_ALLOC "), PDMAUDIOHOSTDEV_F_NAME_ALLOC },
|
|
{ RT_STR_TUPLE("ID_ALLOC "), PDMAUDIOHOSTDEV_F_ID_ALLOC },
|
|
{ RT_STR_TUPLE("NO_DUP "), PDMAUDIOHOSTDEV_F_NO_DUP },
|
|
};
|
|
size_t offDst = 0;
|
|
for (uint32_t i = 0; i < RT_ELEMENTS(s_aFlags); i++)
|
|
if (fFlags & s_aFlags[i].fFlag)
|
|
{
|
|
fFlags &= ~s_aFlags[i].fFlag;
|
|
memcpy(&pszDst[offDst], s_aFlags[i].pszMnemonic, s_aFlags[i].cchMnemonic);
|
|
offDst += s_aFlags[i].cchMnemonic;
|
|
}
|
|
Assert(fFlags == 0);
|
|
Assert(offDst < PDMAUDIOHOSTDEV_MAX_FLAGS_STRING_LEN);
|
|
|
|
if (offDst)
|
|
pszDst[offDst - 1] = '\0';
|
|
else
|
|
memcpy(pszDst, "NONE", sizeof("NONE"));
|
|
return pszDst;
|
|
}
|
|
|
|
/**
|
|
* Logs an audio device enumeration.
|
|
*
|
|
* @param pDevEnm Device enumeration to log.
|
|
* @param pszDesc Logging description (prefix).
|
|
*/
|
|
DECLINLINE(void) PDMAudioHostEnumLog(PCPDMAUDIOHOSTENUM pDevEnm, const char *pszDesc)
|
|
{
|
|
#ifdef LOG_ENABLED
|
|
AssertPtrReturnVoid(pDevEnm);
|
|
AssertPtrReturnVoid(pszDesc);
|
|
AssertReturnVoid(pDevEnm->uMagic == PDMAUDIOHOSTENUM_MAGIC);
|
|
|
|
if (LogIsEnabled())
|
|
{
|
|
LogFunc(("%s: %RU32 devices\n", pszDesc, pDevEnm->cDevices));
|
|
|
|
PPDMAUDIOHOSTDEV pDev;
|
|
RTListForEach(&pDevEnm->LstDevices, pDev, PDMAUDIOHOSTDEV, ListEntry)
|
|
{
|
|
char szFlags[PDMAUDIOHOSTDEV_MAX_FLAGS_STRING_LEN];
|
|
LogFunc(("Device '%s':\n", pDev->pszName));
|
|
LogFunc((" ID = %s\n", pDev->pszId ? pDev->pszId : "<none>"));
|
|
LogFunc((" Usage = %s\n", PDMAudioDirGetName(pDev->enmUsage)));
|
|
LogFunc((" Flags = %s\n", PDMAudioHostDevFlagsToString(szFlags, pDev->fFlags)));
|
|
LogFunc((" Input channels = %RU8\n", pDev->cMaxInputChannels));
|
|
LogFunc((" Output channels = %RU8\n", pDev->cMaxOutputChannels));
|
|
LogFunc((" cbExtra = %RU32 bytes\n", pDev->cbSelf - sizeof(PDMAUDIOHOSTDEV)));
|
|
}
|
|
}
|
|
#else
|
|
RT_NOREF(pDevEnm, pszDesc);
|
|
#endif
|
|
}
|
|
|
|
/** @} */
|
|
|
|
#endif /* !VBOX_INCLUDED_vmm_pdmaudiohostenuminline_h */
|