|
|
#include <windows.h>
#include <stdlib.h>
#include <mmsystem.h>
#include <mmreg.h>
#include <msacm.h>
#include <stdlib.h>
#include <stdio.h>
#define TSSND_NATIVE_BITSPERSAMPLE 16
#define TSSND_NATIVE_CHANNELS 2
#define TSSND_NATIVE_SAMPLERATE 22050
#define TSSND_NATIVE_BLOCKALIGN ((TSSND_NATIVE_BITSPERSAMPLE * \
TSSND_NATIVE_CHANNELS) / 8) #define TSSND_NATIVE_AVGBYTESPERSEC (TSSND_NATIVE_BLOCKALIGN * \
TSSND_NATIVE_SAMPLERATE)
#define TSSND_SAMPLESPERBLOCK 8192
//
// Defines
//
#undef ASSERT
#ifdef DBG
#define TRC _DebugMessage
#define ASSERT(_x_) if (!(_x_)) \
{ TRC(FATAL, "ASSERT failed, line %d, file %s\n", \ __LINE__, __FILE__); DebugBreak(); } #else // !DBG
#define TRC
#define ASSERT
#endif // !DBG
#define TSMALLOC( _x_ ) malloc( _x_ )
#define TSFREE( _x_ ) free( _x_ )
#ifndef G723MAGICWORD1
#define G723MAGICWORD1 0xf7329ace
#endif
#ifndef G723MAGICWORD2
#define G723MAGICWORD2 0xacdeaea2
#endif
#ifndef VOXWARE_KEY
#define VOXWARE_KEY "35243410-F7340C0668-CD78867B74DAD857-AC71429AD8CAFCB5-E4E1A99E7FFD-371"
#endif
#ifndef WMAUDIO_KEY
#define WMAUDIO_KEY "F6DC9830-BC79-11d2-A9D0-006097926036"
#endif
#ifndef WMAUDIO_DEC_KEY
#define WMAUDIO_DEC_KEY "1A0F78F0-EC8A-11d2-BBBE-006008320064"
#endif
#define WAVE_FORMAT_WMAUDIO2 0x161
const CHAR *ALV = "TSSNDD::ALV - "; const CHAR *INF = "TSSNDD::INF - "; const CHAR *WRN = "TSSNDD::WRN - "; const CHAR *ERR = "TSSNDD::ERR - "; const CHAR *FATAL = "TSSNDD::FATAL - ";
typedef struct _VCSNDFORMATLIST { struct _VCSNDFORMATLIST *pNext; HACMDRIVERID hacmDriverId; WAVEFORMATEX Format; // additional data for the format
} VCSNDFORMATLIST, *PVCSNDFORMATLIST;
#ifdef _WIN32
#include <pshpack1.h>
#else
#ifndef RC_INVOKED
#pragma pack(1)
#endif
#endif
typedef struct wmaudio2waveformat_tag { WAVEFORMATEX wfx; DWORD dwSamplesPerBlock; // only counting "new" samples "= half of what will be used due to overlapping
WORD wEncodeOptions; DWORD dwSuperBlockAlign; // the big size... should be multiples of wfx.nBlockAlign.
} WMAUDIO2WAVEFORMAT;
typedef struct msg723waveformat_tag { WAVEFORMATEX wfx; WORD wConfigWord; DWORD dwCodeword1; DWORD dwCodeword2; } MSG723WAVEFORMAT;
typedef struct intelg723waveformat_tag { WAVEFORMATEX wfx; WORD wConfigWord; DWORD dwCodeword1; DWORD dwCodeword2; } INTELG723WAVEFORMAT;
typedef struct tagVOXACM_WAVEFORMATEX { WAVEFORMATEX wfx; DWORD dwCodecId; DWORD dwMode; char szKey[72]; } VOXACM_WAVEFORMATEX, *PVOXACM_WAVEFORMATEX, FAR *LPVOXACM_WAVEFORMATEX;
#ifdef _WIN32
#include <poppack.h>
#else
#ifndef RC_INVOKED
#pragma pack()
#endif
#endif
/////////////////////////////////////////////////////////////////////
//
// Tracing
//
/////////////////////////////////////////////////////////////////////
VOID _cdecl _DebugMessage( LPCSTR szLevel, LPCSTR szFormat, ... ) { CHAR szBuffer[256]; va_list arglist;
if (szLevel == ALV) return;
va_start (arglist, szFormat); _vsnprintf (szBuffer, sizeof(szBuffer), szFormat, arglist); va_end (arglist);
// printf( "%s:%s", szLevel, szBuffer );
OutputDebugStringA(szLevel); OutputDebugStringA(szBuffer); }
/*
* Function: * _VCSmdFindSuggestedConverter * * Description: * Searches for intermidiate converter * */ BOOL _VCSndFindSuggestedConverter( HACMDRIVERID hadid, LPWAVEFORMATEX pDestFormat, LPWAVEFORMATEX pInterrimFmt ) { BOOL rv = FALSE; MMRESULT mmres; HACMDRIVER hacmDriver = NULL; HACMSTREAM hacmStream = NULL;
ASSERT( NULL != pDestFormat ); ASSERT( NULL != hadid ); ASSERT( NULL != pInterrimFmt );
//
// first, open the destination acm driver
//
mmres = acmDriverOpen(&hacmDriver, hadid, 0); if ( MMSYSERR_NOERROR != mmres ) { TRC(ERR, "_VCSndFindSuggestedConverter: can't " "open the acm driver: %d\n", mmres); goto exitpt; }
//
// first probe with the native format
// if it passes, we don't need intermidiate
// format converter
//
pInterrimFmt->wFormatTag = WAVE_FORMAT_PCM; pInterrimFmt->nChannels = TSSND_NATIVE_CHANNELS; pInterrimFmt->nSamplesPerSec = TSSND_NATIVE_SAMPLERATE; pInterrimFmt->nAvgBytesPerSec = TSSND_NATIVE_AVGBYTESPERSEC; pInterrimFmt->nBlockAlign = TSSND_NATIVE_BLOCKALIGN; pInterrimFmt->wBitsPerSample = TSSND_NATIVE_BITSPERSAMPLE; pInterrimFmt->cbSize = 0;
mmres = acmStreamOpen( &hacmStream, hacmDriver, pInterrimFmt, pDestFormat, NULL, // filter
0, // callback
0, // dwinstance
ACM_STREAMOPENF_NONREALTIME );
if ( MMSYSERR_NOERROR == mmres ) { //
// format is supported
//
rv = TRUE; goto exitpt; } else { TRC(ALV, "_VCSndFindSuggestedConverter: format is not supported\n"); }
//
// find a suggested intermidiate PCM format
//
mmres = acmFormatSuggest( hacmDriver, pDestFormat, pInterrimFmt, sizeof( *pInterrimFmt ), ACM_FORMATSUGGESTF_WFORMATTAG );
if ( MMSYSERR_NOERROR != mmres ) { TRC(ALV, "_VCSndFindSuggestedConverter: can't find " "interrim format: %d\n", mmres); goto exitpt; }
if ( 16 != pInterrimFmt->wBitsPerSample || ( 1 != pInterrimFmt->nChannels && 2 != pInterrimFmt->nChannels) || ( 8000 != pInterrimFmt->nSamplesPerSec && 11025 != pInterrimFmt->nSamplesPerSec && 12000 != pInterrimFmt->nSamplesPerSec && 16000 != pInterrimFmt->nSamplesPerSec && 22050 != pInterrimFmt->nSamplesPerSec) ) { TRC(ALV, "_VCSndFindSuggestedConverter: not supported " "interrim format. Details:\n"); TRC(ALV, "Channels - %d\n", pInterrimFmt->nChannels); TRC(ALV, "SamplesPerSec - %d\n", pInterrimFmt->nSamplesPerSec); TRC(ALV, "AvgBytesPerSec - %d\n", pInterrimFmt->nAvgBytesPerSec); TRC(ALV, "BlockAlign - %d\n", pInterrimFmt->nBlockAlign); TRC(ALV, "BitsPerSample - %d\n", pInterrimFmt->wBitsPerSample); goto exitpt; }
if ( 1 == pInterrimFmt->nChannels ) { switch ( pInterrimFmt->nSamplesPerSec ) { case 8000: case 11025: case 12000: case 16000: case 22050: break; default: ASSERT( 0 ); } } else { switch ( pInterrimFmt->nSamplesPerSec ) { case 8000: case 11025: case 12000: case 16000: case 22050: break; default: ASSERT( 0 ); } }
//
// probe with this format
//
mmres = acmStreamOpen( &hacmStream, hacmDriver, pInterrimFmt, pDestFormat, NULL, // filter
0, // callback
0, // dwinstance
ACM_STREAMOPENF_NONREALTIME );
if ( MMSYSERR_NOERROR != mmres ) { TRC(ALV, "_VCSndFindSuggestedConverter: probing the suggested " "format failed: %d\n", mmres); goto exitpt; }
TRC(ALV, "_VCSndFindSuggestedConverter: found intermidiate PCM format\n"); TRC(ALV, "Channels - %d\n", pInterrimFmt->nChannels); TRC(ALV, "SamplesPerSec - %d\n", pInterrimFmt->nSamplesPerSec); TRC(ALV, "AvgBytesPerSec - %d\n", pInterrimFmt->nAvgBytesPerSec); TRC(ALV, "BlockAlign - %d\n", pInterrimFmt->nBlockAlign); TRC(ALV, "BitsPerSample - %d\n", pInterrimFmt->wBitsPerSample);
rv = TRUE;
exitpt: if ( NULL != hacmStream ) acmStreamClose( hacmStream, 0 );
if ( NULL != hacmDriver ) acmDriverClose( hacmDriver, 0 );
return rv; }
/*
* Function: * _VCSndOrderFormatList * * Description: * Order all formats in descendant order * */ VOID _VCSndOrderFormatList( PVCSNDFORMATLIST *ppFormatList, DWORD *pdwNum ) { PVCSNDFORMATLIST pFormatList; PVCSNDFORMATLIST pLessThan; PVCSNDFORMATLIST pPrev; PVCSNDFORMATLIST pNext; PVCSNDFORMATLIST pIter; PVCSNDFORMATLIST pIter2; DWORD dwNum = 0;
ASSERT ( NULL != ppFormatList );
pFormatList = *ppFormatList; pLessThan = NULL;
//
// fill both lists
//
pIter = pFormatList; while ( NULL != pIter ) { pNext = pIter->pNext; pIter->pNext = NULL;
//
// descending order
//
pIter2 = pLessThan; pPrev = NULL; while ( NULL != pIter2 && pIter2->Format.nAvgBytesPerSec > pIter->Format.nAvgBytesPerSec ) { pPrev = pIter2; pIter2 = pIter2->pNext; }
pIter->pNext = pIter2; if ( NULL == pPrev ) pLessThan = pIter; else pPrev->pNext = pIter;
pIter = pNext; dwNum ++; }
*ppFormatList = pLessThan;
if ( NULL != pdwNum ) *pdwNum = dwNum; }
//
// puts code licensing codes into the header
//
BOOL _VCSndFixHeader( PWAVEFORMATEX pFmt, PWAVEFORMATEX *ppNewFmt ) { BOOL rv = FALSE;
*ppNewFmt = NULL; switch (pFmt->wFormatTag) { case WAVE_FORMAT_MSG723: ASSERT(pFmt->cbSize == 10); ((MSG723WAVEFORMAT *) pFmt)->dwCodeword1 = G723MAGICWORD1; ((MSG723WAVEFORMAT *) pFmt)->dwCodeword2 = G723MAGICWORD2;
rv = TRUE; break;
case WAVE_FORMAT_MSRT24: //
// assume call control will take care of the other
// params ?
//
ASSERT(pFmt->cbSize == 80); strncpy(((VOXACM_WAVEFORMATEX *) pFmt)->szKey, VOXWARE_KEY, 80);
rv = TRUE; break;
case WAVE_FORMAT_WMAUDIO2: if ( ((WMAUDIO2WAVEFORMAT *)pFmt)->dwSamplesPerBlock > TSSND_SAMPLESPERBLOCK ) { //
// block is too big, too high latency
//
break; } ASSERT( pFmt->cbSize == sizeof( WMAUDIO2WAVEFORMAT ) - sizeof( WAVEFORMATEX )); *ppNewFmt = TSMALLOC( sizeof( WMAUDIO2WAVEFORMAT ) + sizeof( WMAUDIO_KEY )); if ( NULL == *ppNewFmt ) { break; } memcpy( *ppNewFmt, pFmt, sizeof( WMAUDIO2WAVEFORMAT )); strncpy((CHAR *)(((WMAUDIO2WAVEFORMAT *) *ppNewFmt) + 1), WMAUDIO_KEY, sizeof( WMAUDIO_KEY )); (*ppNewFmt)->cbSize += sizeof( WMAUDIO_KEY ); rv = TRUE; break; default: rv = TRUE; }
return rv;
}
/*
* Function: * acmFormatEnumCallback * * Description: * All formats enumerator * */ BOOL CALLBACK acmFormatEnumCallback( HACMDRIVERID hadid, LPACMFORMATDETAILS pAcmFormatDetails, DWORD_PTR dwInstance, DWORD fdwSupport ) { PVCSNDFORMATLIST *ppFormatList; PWAVEFORMATEX pEntry, pFixedEntry = NULL;
ASSERT(0 != dwInstance); ASSERT(NULL != pAcmFormatDetails); ASSERT(NULL != pAcmFormatDetails->pwfx);
if ( 0 == dwInstance || NULL == pAcmFormatDetails || NULL == pAcmFormatDetails->pwfx ) {
TRC( ERR, "acmFormatEnumCallback: Invalid parameters\n" ); goto exitpt; }
ppFormatList = (PVCSNDFORMATLIST *)dwInstance;
if (( 0 != ( fdwSupport & ACMDRIVERDETAILS_SUPPORTF_CODEC ) || 0 != ( fdwSupport & ACMDRIVERDETAILS_SUPPORTF_CONVERTER )) && pAcmFormatDetails->pwfx->nAvgBytesPerSec < TSSND_NATIVE_AVGBYTESPERSEC ) { //
// this codec should be good, save it in the list
// keep the list sorted in descended order
//
PVCSNDFORMATLIST pIter; PVCSNDFORMATLIST pPrev; PVCSNDFORMATLIST pNewEntry; WAVEFORMATEX WaveFormat; // dummy parameter
DWORD itemsize;
if ( WAVE_FORMAT_PCM == pAcmFormatDetails->pwfx->wFormatTag || !_VCSndFixHeader(pAcmFormatDetails->pwfx, &pFixedEntry ) ) { TRC(ALV, "acmFormatEnumCallback: unsupported format, " "don't use it\n"); goto exitpt; }
pEntry = ( NULL == pFixedEntry )?pAcmFormatDetails->pwfx:pFixedEntry;
if (!_VCSndFindSuggestedConverter( hadid, pEntry, &WaveFormat )) { TRC(ALV, "acmFormatEnumCallback: unsupported format, " "don't use it\n"); goto exitpt; }
TRC(ALV, "acmFormatEnumCallback: codec found %S (%d b/s)\n", pAcmFormatDetails->szFormat, pEntry->nAvgBytesPerSec);
itemsize = sizeof( *pNewEntry ) + pEntry->cbSize; pNewEntry = (PVCSNDFORMATLIST) TSMALLOC( itemsize );
if (NULL == pNewEntry) { TRC(ERR, "acmFormatEnumCallback: can't allocate %d bytes\n", itemsize); goto exitpt; }
memcpy( &pNewEntry->Format, pEntry, sizeof (pNewEntry->Format) + pEntry->cbSize ); pNewEntry->hacmDriverId = hadid;
pNewEntry->pNext = *ppFormatList; *ppFormatList = pNewEntry;
}
exitpt:
if ( NULL != pFixedEntry ) { TSFREE( pFixedEntry ); }
return TRUE; }
//
// returns true if this codec is shipped with windows
// because we are testing only the these
//
BOOL AllowThisCodec( HACMDRIVERID hadid ) { ACMDRIVERDETAILS Details; BOOL rv = FALSE;
static DWORD AllowedCodecs[][2] = { MM_INTEL, 503, MM_MICROSOFT, MM_MSFT_ACM_IMAADPCM, MM_FRAUNHOFER_IIS, 12, MM_MICROSOFT, 90, MM_MICROSOFT, MM_MSFT_ACM_MSADPCM, MM_MICROSOFT, 39, MM_MICROSOFT, MM_MSFT_ACM_G711, MM_MICROSOFT, 82, MM_MICROSOFT, MM_MSFT_ACM_GSM610, MM_SIPROLAB, 1, MM_DSP_GROUP, 1, MM_MICROSOFT, MM_MSFT_ACM_PCM };
RtlZeroMemory( &Details, sizeof( Details )); Details.cbStruct = sizeof( Details );
if ( MMSYSERR_NOERROR == acmDriverDetails( hadid, &Details, 0 )) { //
// Is this one known
//
DWORD count;
for ( count = 0; count < sizeof( AllowedCodecs ) / (2 * sizeof( DWORD )); count ++ ) { if ( Details.wMid == AllowedCodecs[count][0] && Details.wPid == AllowedCodecs[count][1] ) { rv = TRUE; goto exitpt; } } }
exitpt: if ( rv ) TRC( ALV, "ACMDRV: +++++++++++++++++++++ CODEC ALLOWED +++++++++++++++++++++++\n" ); else TRC( ALV, "ACMDRV: ------------------- CODEC DISALLOWED ----------------------\n" );
TRC( ALV, "ACMDRV: Mid: %d\n", Details.wMid ); TRC( ALV, "ACMDRV: Pid: %d\n", Details.wPid ); TRC( ALV, "ACMDRV: ShortName: %S\n", Details.szShortName ); TRC( ALV, "ACMDRV: LongName: %S\n", Details.szLongName ); TRC( ALV, "ACMDRV: Copyright: %S\n", Details.szLicensing ); TRC( ALV, "ACMDRV: Features: %S\n", Details.szFeatures );
return rv; } /*
* Function: * acmDriverEnumCallback * * Description: * All drivers enumerator * */ BOOL CALLBACK acmDriverEnumCallback( HACMDRIVERID hadid, DWORD_PTR dwInstance, DWORD fdwSupport ) { PVCSNDFORMATLIST *ppFormatList; MMRESULT mmres;
ASSERT(dwInstance);
ppFormatList = (PVCSNDFORMATLIST *)dwInstance;
if ( (0 != ( fdwSupport & ACMDRIVERDETAILS_SUPPORTF_CODEC ) || 0 != ( fdwSupport & ACMDRIVERDETAILS_SUPPORTF_CONVERTER )) && AllowThisCodec(hadid) ) { //
// a codec found
//
HACMDRIVER had;
mmres = acmDriverOpen(&had, hadid, 0); if (MMSYSERR_NOERROR == mmres) { PWAVEFORMATEX pWaveFormat; ACMFORMATDETAILS AcmFormatDetails; DWORD dwMaxFormatSize;
//
// first find the max size for the format
//
mmres = acmMetrics( (HACMOBJ)had, ACM_METRIC_MAX_SIZE_FORMAT, (LPVOID)&dwMaxFormatSize);
if (MMSYSERR_NOERROR != mmres || dwMaxFormatSize < sizeof( *pWaveFormat ))
dwMaxFormatSize = sizeof( *pWaveFormat );
//
// Allocate the format structure
//
__try { pWaveFormat = (PWAVEFORMATEX) _alloca ( dwMaxFormatSize ); } __except ( EXCEPTION_EXECUTE_HANDLER ) { pWaveFormat = NULL; }
if ( NULL == pWaveFormat ) { TRC(ERR, "acmDriverEnumCallback: alloca failed for %d bytes\n", dwMaxFormatSize); goto close_acm_driver; }
//
// clear the extra format data
//
memset( pWaveFormat + 1, 0, dwMaxFormatSize - sizeof( *pWaveFormat )); //
// create the format to convert from
//
pWaveFormat->wFormatTag = WAVE_FORMAT_PCM; pWaveFormat->nChannels = TSSND_NATIVE_CHANNELS; pWaveFormat->nSamplesPerSec = TSSND_NATIVE_SAMPLERATE; pWaveFormat->nAvgBytesPerSec = TSSND_NATIVE_AVGBYTESPERSEC; pWaveFormat->nBlockAlign = TSSND_NATIVE_BLOCKALIGN; pWaveFormat->wBitsPerSample = TSSND_NATIVE_BITSPERSAMPLE; pWaveFormat->cbSize = 0;
AcmFormatDetails.cbStruct = sizeof( AcmFormatDetails ); AcmFormatDetails.dwFormatIndex= 0; AcmFormatDetails.dwFormatTag = WAVE_FORMAT_PCM; AcmFormatDetails.fdwSupport = 0; AcmFormatDetails.pwfx = pWaveFormat; AcmFormatDetails.cbwfx = dwMaxFormatSize;
//
// enum all formats supported by this driver
//
mmres = acmFormatEnum( had, &AcmFormatDetails, acmFormatEnumCallback, (DWORD_PTR)ppFormatList, 0 //ACM_FORMATENUMF_CONVERT
);
if (MMSYSERR_NOERROR != mmres) { TRC(ERR, "acmDriverEnumCallback: acmFormatEnum failed %d\n", mmres); }
close_acm_driver: acmDriverClose(had, 0); } else TRC(ALV, "acmDriverEnumCallback: acmDriverOpen failed: %d\n", mmres); }
//
// continue to the next driver
//
return TRUE; }
/*
* Function: * VCSndEnumAllCodecFormats * * Description: * Creates a list of all codecs/formats * */ BOOL VCSndEnumAllCodecFormats( PVCSNDFORMATLIST *ppFormatList, DWORD *pdwNumberOfFormats ) { BOOL rv = FALSE; PVCSNDFORMATLIST pIter; PVCSNDFORMATLIST pPrev; PVCSNDFORMATLIST pNext; MMRESULT mmres; DWORD dwNum = 0;
ASSERT( ppFormatList ); ASSERT( pdwNumberOfFormats );
*ppFormatList = NULL;
mmres = acmDriverEnum( acmDriverEnumCallback, (DWORD_PTR)ppFormatList, 0 );
if (NULL == *ppFormatList) { TRC(WRN, "VCSndEnumAllCodecFormats: acmDriverEnum failed: %d\n", mmres);
goto exitpt; }
_VCSndOrderFormatList( ppFormatList, &dwNum );
pIter = *ppFormatList; //
// number of formats is passed as UINT16, delete all after those
//
if ( dwNum > 0xffff ) { DWORD dwLimit = 0xfffe;
while ( 0 != dwLimit ) { pIter = pIter->pNext; dwLimit --; }
pNext = pIter->pNext; pIter->pNext = NULL; pIter = pNext; while( NULL != pIter ) { pNext = pIter->pNext; TSFREE( pNext ); pIter = pNext; }
dwNum = 0xffff; }
rv = TRUE;
exitpt: if (!rv) { //
// in case of error free the allocated list of formats
//
pIter = *ppFormatList; while( NULL != pIter ) { PVCSNDFORMATLIST pNext = pIter->pNext;
TSFREE( pIter );
pIter = pNext; }
*ppFormatList = NULL;
}
*pdwNumberOfFormats = dwNum;
return rv; }
int _cdecl wmain( void ) { PVCSNDFORMATLIST pFormatList = NULL; DWORD dwNumberOfFormats = 0;
printf( "// use dumpcod.c to generate this table\n" ); printf( "//\n" ); printf( "// FormatTag | Channels | SamplesPerSec | AvgBytesPerSec | BlockAlign | BitsPerSamepl | ExtraInfo\n" ); printf( "// ================================================================================================\n" ); printf( "//\n" ); printf( "BYTE KnownFormats[] = {\n" );
VCSndEnumAllCodecFormats( &pFormatList, &dwNumberOfFormats ); for ( ;pFormatList != NULL; pFormatList = pFormatList->pNext ) { PWAVEFORMATEX pSndFmt = &(pFormatList->Format); UINT i;
printf( "// %.3d, %.2d, %.5d, %.5d, %.3d, %.2d\n", pSndFmt->wFormatTag, pSndFmt->nChannels, pSndFmt->nSamplesPerSec, pSndFmt->nAvgBytesPerSec, pSndFmt->nBlockAlign, pSndFmt->wBitsPerSample);
for ( i = 0; i < sizeof( WAVEFORMATEX ); i ++ ) { printf( "0x%02x", ((PBYTE)pSndFmt)[i]); if ( i + 1 < sizeof( WAVEFORMATEX ) || pSndFmt->cbSize ) { printf( ", " ); } } for ( i = 0; i < pSndFmt->cbSize; i++ ) { printf( "0x%02x", (((PBYTE)pSndFmt) + sizeof( WAVEFORMATEX ))[i]); if ( i + 1 < pSndFmt->cbSize ) { printf( ", " ); } } if ( NULL != pFormatList->pNext ) { printf ( ",\n" ); } else { printf( " };\n" ); } }
return 0;
}
|