From 3f7773f83bda59aa897b23ce7d9386cd2226732c Mon Sep 17 00:00:00 2001 From: Alibek Omarov Date: Tue, 22 Feb 2022 09:34:28 +0300 Subject: [PATCH] engine: client: rewrite VOX subsystem, fix spaces being taken into sentences, add tests for parser, remove dead code --- engine/client/s_vox.c | 916 +++++++++++++++++++----------------------- engine/client/vox.h | 13 +- engine/common/host.c | 3 + engine/common/tests.h | 1 + 4 files changed, 424 insertions(+), 509 deletions(-) diff --git a/engine/client/s_vox.c b/engine/client/s_vox.c index b421fe35..f2060257 100644 --- a/engine/client/s_vox.c +++ b/engine/client/s_vox.c @@ -16,679 +16,597 @@ GNU General Public License for more details. #include "common.h" #include "sound.h" #include "const.h" +#include "sequence.h" #include -sentence_t g_Sentences[MAX_SENTENCES]; -static uint g_numSentences; -static char *rgpparseword[CVOXWORDMAX]; // array of pointers to parsed words -static char voxperiod[] = "_period"; // vocal pause -static char voxcomma[] = "_comma"; // vocal pause +static int cszrawsentences = 0; +static char *rgpszrawsentence[CVOXFILESENTENCEMAX]; +static char *voxperiod = "_period", *voxcomma = "_comma"; -static int IsNextWord( const char c ) +// return number of samples mixed +int VOX_MixDataToDevice( channel_t *pchan, int sampleCount, int outputRate, int outputOffset ) { - if( c == '.' || c == ',' || c == ' ' || c == '(' ) - return 1; - return 0; -} + // save this to compute total output + int startingOffset = outputOffset; -static int IsSkipSpace( const char c ) -{ - if( c == ',' || c == '.' || c == ' ' ) - return 1; - return 0; + if( !pchan->currentWord ) + return 0; + + while( sampleCount > 0 && pchan->currentWord ) + { + int timeCompress = pchan->words[pchan->wordIndex].timecompress; + int outputCount = S_MixDataToDevice( pchan, sampleCount, outputRate, outputOffset, timeCompress ); + + outputOffset += outputCount; + sampleCount -= outputCount; + + // if we finished load a next word + if( pchan->currentWord->finished ) + { + VOX_FreeWord( pchan ); + pchan->wordIndex++; + VOX_LoadWord( pchan ); + + if( pchan->currentWord ) + { + pchan->sfx = pchan->words[pchan->wordIndex].sfx; + } + } + } + return outputOffset - startingOffset; } -static int IsWhiteSpace( const char space ) +void VOX_LoadWord( channel_t *ch ) { - if( space == ' ' || space == '\t' || space == '\r' || space == '\n' ) - return 1; - return 0; + const voxword_t *word = &ch->words[ch->wordIndex]; + wavdata_t *data; + int start, end, samples; + + if( !word->sfx ) + return; + + data = S_LoadSound( word->sfx ); + + if( !data ) + return; + + ch->currentWord = &ch->pMixer; + ch->currentWord->pData = data; + + samples = data->samples; + start = word->start; + end = word->end; + + if( end <= start ) end = 0; + + if( start ) + S_SetSampleStart( ch, data, start * 0.01f * samples ); + + if( end ) + S_SetSampleEnd( ch, data, end * 0.01f * samples ); } -static int IsCommandChar( const char c ) +void VOX_FreeWord( channel_t *ch ) { - if( c == 'v' || c == 'p' || c == 's' || c == 'e' || c == 't' ) - return 1; - return 0; + voxword_t *word = &ch->words[ch->wordIndex]; + + ch->currentWord = NULL; + memset( &ch->pMixer, 0, sizeof( ch->pMixer )); + + if( !word->sfx && !word->fKeepCached ) + return; + + FS_FreeSound( word->sfx->cache ); + word->sfx->cache = NULL; + word->sfx = NULL; } -static int IsDelimitChar( const char c ) +void VOX_SetChanVol( channel_t *ch ) { - if( c == '(' || c == ')' ) - return 1; - return 0; + voxword_t *word; + if( !ch->currentWord ) + return; + + word = &ch->words[ch->wordIndex]; + + if( word->volume == 100 ) + return; + + ch->leftvol = ch->leftvol * word->volume * 0.01f; + ch->rightvol = ch->rightvol * word->volume * 0.01f; } -static char *ScanForwardUntil( char *string, const char scan ) +float VOX_ModifyPitch( channel_t *ch, float pitch ) { - while( string[0] ) - { - if( string[0] == scan ) - return string; - string++; - } - return string; + voxword_t *word; + if( !ch->currentWord ) + return pitch; + + word = &ch->words[ch->wordIndex]; + + if( word->pitch < 0 ) + return pitch; + + pitch += ( word->pitch - PITCH_NORM ) * 0.01f; + + return pitch; } -// backwards scan psz for last '/' -// return substring in szpath null terminated -// if '/' not found, return 'vox/' -static char *VOX_GetDirectory( char *szpath, char *psz ) +static const char *VOX_GetDirectory( char *szpath, const char *psz, int nsize ) { - char c; - int cb = 0, len; - char *p; + const char *p; + int len; - len = Q_strlen( psz ); - p = psz + len - 1; + // search / backwards + p = Q_strrchr( psz, '/' ); - // scan backwards until first '/' or start of string - c = *p; - while( p > psz && c != '/' ) + if( !p ) { - c = *( --p ); - cb++; + Q_strcpy( szpath, "vox/" ); + return psz; } - if( c != '/' ) + len = p - psz + 1; + + if( len > nsize ) { - // didn't find '/', return default directory - Q_strcpy( szpath, "vox/" ); - return psz; + Con_Printf( "VOX_GetDirectory: invalid directory in: %s\n", psz ); + return NULL; } - cb = len - cb; - memcpy( szpath, psz, cb ); - szpath[cb] = 0; + memcpy( szpath, psz, len ); + szpath[len] = 0; return p + 1; } -// scan g_Sentences, looking for pszin sentence name -// return pointer to sentence data if found, null if not -// CONSIDER: if we have a large number of sentences, should -// CONSIDER: sort strings in g_Sentences and do binary search. -char *VOX_LookupString( const char *pSentenceName, int *psentencenum ) +static const char *VOX_LookupString( const char *pszin ) { - int i; - - if( Q_isdigit( pSentenceName ) && (i = Q_atoi( pSentenceName )) < g_numSentences ) - { - if( psentencenum ) *psentencenum = i; - return (g_Sentences[i].pName + Q_strlen( g_Sentences[i].pName ) + 1 ); - } + int i = -1, len; + const char *c; - for( i = 0; i < g_numSentences; i++ ) + // check if we are a CSCZ or immediate sentence + if( *pszin == '#' ) { - if( !Q_stricmp( pSentenceName, g_Sentences[i].pName )) + // Q_atoi is too smart and allows negative values + // so check with Q_isdigit beforehand + if( Q_isdigit( pszin + 1 )) { - if( psentencenum ) *psentencenum = i; - return (g_Sentences[i].pName + Q_strlen( g_Sentences[i].pName ) + 1 ); + sentenceEntry_s *sentenceEntry; + i = Q_atoi( pszin + 1 ); + if(( sentenceEntry = Sequence_GetSentenceByIndex( i ))) + return sentenceEntry->data; + } + else + { + // immediate sentence, probably coming from "speak" command + return pszin + 1; } } - return NULL; -} - -// parse a null terminated string of text into component words, with -// pointers to each word stored in rgpparseword -// note: this code actually alters the passed in string! -char **VOX_ParseString( char *psz ) -{ - int i, fdone = 0; - char c, *p = psz; - - memset( rgpparseword, 0, sizeof( char* ) * CVOXWORDMAX ); - - if( !psz ) return NULL; - - i = 0; - rgpparseword[i++] = psz; - - while( !fdone && i < CVOXWORDMAX ) + // check if we received an index + if( Q_isdigit( pszin )) { - // scan up to next word - c = *p; - while( c && !IsNextWord( c )) - c = *(++p); + i = Q_atoi( pszin ); - // if '(' then scan for matching ')' - if( c == '(' ) - { - p = ScanForwardUntil( p, ')' ); - c = *(++p); - if( !c ) fdone = 1; - } + if( i >= cszrawsentences ) + i = -1; + } - if( fdone || !c ) + // last hope: find it in sentences array + if( i == -1 ) + { + for( i = 0; i < cszrawsentences; i++ ) { - fdone = 1; + if( !Q_stricmp( pszin, rgpszrawsentence[i] )) + break; } - else - { - // if . or , insert pause into rgpparseword, - // unless this is the last character - if(( c == '.' || c == ',' ) && *(p+1) != '\n' && *(p+1) != '\r' && *(p+1) != 0 ) - { - if( c == '.' ) rgpparseword[i++] = voxperiod; - else rgpparseword[i++] = voxcomma; - - if( i >= CVOXWORDMAX ) - break; - } + } - // null terminate substring - *p++ = 0; + // not found, exit + if( i == cszrawsentences ) + return NULL; - // skip whitespace - c = *p; - while( c && IsSkipSpace( c )) - c = *(++p); + len = Q_strlen( rgpszrawsentence[i] ); - if( !c ) fdone = 1; - else rgpparseword[i++] = p; - } - } + c = &rgpszrawsentence[i][len + 1]; + for( ; *c == ' ' || *c == '\t'; c++ ); - return rgpparseword; + return c; } -float VOX_GetVolumeScale( channel_t *pchan ) +static int VOX_ParseString( char *psz, char *rgpparseword[CVOXWORDMAX] ) { - if( pchan->currentWord ) + int i = 0; + + if( !psz ) + return i; + + rgpparseword[i++] = psz; + + while( i < CVOXWORDMAX ) { - if ( pchan->words[pchan->wordIndex].volume ) + // skip to next word + for( ; *psz && + *psz != ' ' && + *psz != '.' && + *psz != ','; psz++ ); + + // skip anything in between ( and ) + if( *psz == '(' ) { - float volume = pchan->words[pchan->wordIndex].volume * 0.01f; - if( volume < 1.0f ) return volume; + for( ; *psz && *psz != ')'; psz++ ); + psz++; } - } - return 1.0f; -} + if( !*psz ) + return i; -void VOX_SetChanVol( channel_t *ch ) -{ - float scale; + // . and , are special but if not end of string + if(( *psz == '.' || *psz == ',' ) && + psz[1] != '\n' && psz[1] != '\r' && psz[1] != '\0' ) + { + if( *psz == '.' ) + rgpparseword[i++] = voxperiod; + else rgpparseword[i++] = voxcomma; - if( !ch->currentWord ) - return; + if( i >= CVOXWORDMAX ) + return i; + } - scale = VOX_GetVolumeScale( ch ); - if( scale == 1.0f ) return; + *psz++ = 0; - ch->rightvol = (int)(ch->rightvol * scale); - ch->leftvol = (int)(ch->leftvol * scale); -} + for( ; *psz && ( *psz == '.' || *psz == ' ' || *psz == ',' ); + psz++ ); -float VOX_ModifyPitch( channel_t *ch, float pitch ) -{ - if( ch->currentWord ) - { - if( ch->words[ch->wordIndex].pitch > 0 ) - { - pitch += ( ch->words[ch->wordIndex].pitch - PITCH_NORM ) * 0.01f; - } + if( !*psz ) + return i; + + rgpparseword[i++] = psz; } - return pitch; + return i; } -//=============================================================================== -// Get any pitch, volume, start, end params into voxword -// and null out trailing format characters -// Format: -// someword(v100 p110 s10 e20) -// -// v is volume, 0% to n% -// p is pitch shift up 0% to n% -// s is start wave offset % -// e is end wave offset % -// t is timecompression % -// -// pass fFirst == 1 if this is the first string in sentence -// returns 1 if valid string, 0 if parameter block only. -// -// If a ( xxx ) parameter block does not directly follow a word, -// then that 'default' parameter block will be used as the default value -// for all following words. Default parameter values are reset -// by another 'default' parameter block. Default parameter values -// for a single word are overridden for that word if it has a parameter block. -// -//=============================================================================== -int VOX_ParseWordParams( char *psz, voxword_t *pvoxword, int fFirst ) +static qboolean VOX_ParseWordParams( char *psz, voxword_t *pvoxword, qboolean fFirst ) { - char *pszsave = psz; - char c, ct, sznum[8]; - static voxword_t voxwordDefault; - int i; + int len, i; + char sznum[8], *pszsave = psz; + static voxword_t voxwordDefault; - // init to defaults if this is the first word in string. if( fFirst ) { + voxwordDefault.fKeepCached = 0; voxwordDefault.pitch = -1; voxwordDefault.volume = 100; voxwordDefault.start = 0; voxwordDefault.end = 100; - voxwordDefault.fKeepCached = 0; voxwordDefault.timecompress = 0; } *pvoxword = voxwordDefault; - // look at next to last char to see if we have a - // valid format: - c = *( psz + Q_strlen( psz ) - 1 ); - - // no formatting, return - if( c != ')' ) return 1; + len = Q_strlen( psz ); - // scan forward to first '(' - c = *psz; - while( !IsDelimitChar( c )) - c = *(++psz); + if( len == 0 ) + return false; - // bogus formatting - if( c == ')' ) return 0; + // no special params + if( psz[len-1] != ')' ) + return true; - // null terminate - *psz = 0; - ct = *(++psz); + for( ; *psz != '(' && *psz != ')'; psz++ ); - while( 1 ) - { - // scan until we hit a character in the commandSet - while( ct && !IsCommandChar( ct )) - ct = *(++psz); + // invalid syntax + if( *psz == ')' ) + return false; - if( ct == ')' ) - break; + // split filename and params + *psz++ = '\0'; - memset( sznum, 0, sizeof( sznum )); - i = 0; + for( ;; ) + { + char command; + + // find command + for( ; *psz && + *psz != 'v' && + *psz != 'p' && + *psz != 's' && + *psz != 'e' && + *psz != 't'; psz++ ) + { + if( *psz == ')' ) + break; + } - c = *(++psz); + command = *psz++; - if( !isdigit( c )) + if( !isdigit( *psz )) break; - // read number - while( isdigit( c ) && i < sizeof( sznum ) - 1 ) - { - sznum[i++] = c; - c = *(++psz); - } + memset( sznum, 0, sizeof( sznum )); + for( i = 0; i < sizeof( sznum ) - 1 && isdigit( *psz ); i++, psz++ ) + sznum[i] = *psz; - // get value of number i = Q_atoi( sznum ); - - switch( ct ) + switch( command ) { - case 'v': pvoxword->volume = i; break; + case 'e': pvoxword->end = i; break; case 'p': pvoxword->pitch = i; break; case 's': pvoxword->start = i; break; - case 'e': pvoxword->end = i; break; case 't': pvoxword->timecompress = i; break; + case 'v': pvoxword->volume = i; break; } - - ct = c; } - // if the string has zero length, this was an isolated - // parameter block. Set default voxword to these - // values + // no actual word but new defaults if( Q_strlen( pszsave ) == 0 ) { voxwordDefault = *pvoxword; - return 0; + return false; } - return 1; + return true; } -void VOX_LoadWord( channel_t *pchan ) +void VOX_LoadSound( channel_t *ch, const char *pszin ) { - if( pchan->words[pchan->wordIndex].sfx ) - { - wavdata_t *pSource = S_LoadSound( pchan->words[pchan->wordIndex].sfx ); - - if( pSource ) - { - int start = pchan->words[pchan->wordIndex].start; - int end = pchan->words[pchan->wordIndex].end; - - // apply mixer - pchan->currentWord = &pchan->pMixer; - pchan->currentWord->pData = pSource; + char buffer[512], szpath[32], pathbuffer[64]; + char *rgpparseword[CVOXWORDMAX]; + const char *psz; + int i, j; - // don't allow overlapped ranges - if( end <= start ) end = 0; + if( !pszin ) + return; - if( start || end ) - { - int sampleCount = pSource->samples; + memset( buffer, 0, sizeof( buffer )); + memset( rgpparseword, 0, sizeof( rgpparseword )); - if( start ) - { - S_SetSampleStart( pchan, pSource, (int)(sampleCount * 0.01f * start)); - } + psz = VOX_LookupString( pszin ); - if( end ) - { - S_SetSampleEnd( pchan, pSource, (int)(sampleCount * 0.01f * end)); - } - } - } + if( !psz ) + { + Con_Printf( "VOX_LoadSound: no sentence named %s\n", pszin ); + return; } -} -void VOX_FreeWord( channel_t *pchan ) -{ - pchan->currentWord = NULL; // sentence is finished - memset( &pchan->pMixer, 0, sizeof( pchan->pMixer )); + psz = VOX_GetDirectory( szpath, psz, sizeof( szpath )); - // release unused sounds - if( pchan->words[pchan->wordIndex].sfx ) + if( !psz ) { - // If this wave wasn't precached by the game code - if( !pchan->words[pchan->wordIndex].fKeepCached ) - { - FS_FreeSound( pchan->words[pchan->wordIndex].sfx->cache ); - pchan->words[pchan->wordIndex].sfx->cache = NULL; - pchan->words[pchan->wordIndex].sfx = NULL; - } + Con_Printf( "VOX_LoadSound: failed getting directory for %s\n", pszin ); + return; } -} - -void VOX_LoadFirstWord( channel_t *pchan, voxword_t *pwords ) -{ - int i = 0; - // copy each pointer in the sfx temp array into the - // sentence array, and set the channel to point to the - // sentence array - while( pwords[i].sfx != NULL ) + if( Q_strlen( psz ) >= sizeof( buffer ) ) { - pchan->words[i] = pwords[i]; - i++; + Con_Printf( "VOX_LoadSound: sentence is too long %s", psz ); + return; } - pchan->words[i].sfx = NULL; - pchan->wordIndex = 0; - VOX_LoadWord( pchan ); -} + Q_strncpy( buffer, psz, sizeof( buffer )); + VOX_ParseString( buffer, rgpparseword ); -// return number of samples mixed -int VOX_MixDataToDevice( channel_t *pchan, int sampleCount, int outputRate, int outputOffset ) -{ - // save this to compute total output - int startingOffset = outputOffset; - - if( !pchan->currentWord ) - return 0; - - while( sampleCount > 0 && pchan->currentWord ) + j = 0; + for( i = 0; rgpparseword[i]; i++ ) { - int timeCompress = pchan->words[pchan->wordIndex].timecompress; - int outputCount = S_MixDataToDevice( pchan, sampleCount, outputRate, outputOffset, timeCompress ); + if( !VOX_ParseWordParams( rgpparseword[i], &ch->words[j], i == 0 )) + continue; - outputOffset += outputCount; - sampleCount -= outputCount; + Q_snprintf( pathbuffer, sizeof( pathbuffer ), "%s%s.wav", szpath, rgpparseword[i] ); - // if we finished load a next word - if( pchan->currentWord->finished ) - { - VOX_FreeWord( pchan ); - pchan->wordIndex++; - VOX_LoadWord( pchan ); + ch->words[j].sfx = S_FindName( pathbuffer, &ch->words[j].fKeepCached ); - if( pchan->currentWord ) - { - pchan->sfx = pchan->words[pchan->wordIndex].sfx; - } - } + j++; } - return outputOffset - startingOffset; + + ch->words[j].sfx = NULL; + ch->sfx = ch->words[0].sfx; + ch->wordIndex = 0; + ch->isSentence = true; + + VOX_LoadWord( ch ); } -// link all sounds in sentence, start playing first word. -void VOX_LoadSound( channel_t *pchan, const char *pszin ) +static void VOX_ReadSentenceFile_( byte *buf, fs_offset_t size ) { - char buffer[512]; - int i, cword; - char pathbuffer[64]; - char szpath[32]; - voxword_t rgvoxword[CVOXWORDMAX]; - char *psz; - - if( !pszin || !*pszin ) - return; - - memset( rgvoxword, 0, sizeof( voxword_t ) * CVOXWORDMAX ); - memset( buffer, 0, sizeof( buffer )); + char *p, *last; - // lookup actual string in g_Sentences, - // set pointer to string data - if( pszin[0] == '#' ) - psz = (char *)pszin + 1; - else - psz = VOX_LookupString( pszin, NULL ); + p = (char *)buf; + last = p + size; - if( !psz ) + while( p < last ) { - Con_DPrintf( S_ERROR "VOX_LoadSound: no such sentence %s\n", pszin ); - return; - } + char *name = NULL, *value = NULL; - // get directory from string, advance psz - psz = VOX_GetDirectory( szpath, psz ); + if( cszrawsentences >= CVOXFILESENTENCEMAX ) + break; - if( Q_strlen( psz ) > sizeof( buffer ) - 1 ) - { - Con_Printf( S_ERROR "VOX_LoadSound: sentence is too long %s\n", psz ); - return; - } + for( ; p < last && ( *p == '\n' || *p == '\r' || *p == '\t' || *p == ' ' ); + p++ ); - // copy into buffer - Q_strcpy( buffer, psz ); - psz = buffer; + if( *p != '/' ) + { + name = p; - // parse sentence (also inserts null terminators between words) - VOX_ParseString( psz ); + for( ; p < last && *p != ' ' && *p != '\t' ; p++ ); - // for each word in the sentence, construct the filename, - // lookup the sfx and save each pointer in a temp array + if( p < last ) + *p++ = 0; - i = 0; - cword = 0; - while( rgpparseword[i] ) - { - // Get any pitch, volume, start, end params into voxword - if( VOX_ParseWordParams( rgpparseword[i], &rgvoxword[cword], i == 0 )) - { - // this is a valid word (as opposed to a parameter block) - Q_strcpy( pathbuffer, szpath ); - Q_strncat( pathbuffer, rgpparseword[i], sizeof( pathbuffer )); - Q_strncat( pathbuffer, ".wav", sizeof( pathbuffer )); - - // find name, if already in cache, mark voxword - // so we don't discard when word is done playing - rgvoxword[cword].sfx = S_FindName( pathbuffer, &( rgvoxword[cword].fKeepCached )); - cword++; + value = p; } - i++; - } - VOX_LoadFirstWord( pchan, rgvoxword ); + for( ; p < last && *p != '\n' && *p != '\r'; p++ ); - pchan->isSentence = true; - pchan->sfx = rgvoxword[0].sfx; -} + if( p < last ) + *p++ = 0; -//----------------------------------------------------------------------------- -// Purpose: Take a NULL terminated sentence, and parse any commands contained in -// {}. The string is rewritten in place with those commands removed. -// -// Input : *pSentenceData - sentence data to be modified in place -// sentenceIndex - global sentence table index for any data that is -// parsed out -//----------------------------------------------------------------------------- -void VOX_ParseLineCommands( char *pSentenceData, int sentenceIndex ) -{ - char tempBuffer[512]; - char *pNext, *pStart; - int length, tempBufferPos = 0; + if( name ) + { + int index = cszrawsentences; + int size = strlen( name ) + strlen( value ) + 2; - if( !pSentenceData ) - return; + rgpszrawsentence[index] = Mem_Malloc( host.mempool, size ); + memcpy( rgpszrawsentence[index], name, size ); + rgpszrawsentence[index][size - 1] = 0; + cszrawsentences++; + } + } +} - pStart = pSentenceData; +static void VOX_ReadSentenceFile( char *path ) +{ + byte *buf; + fs_offset_t size; - while( *pSentenceData ) - { - pNext = ScanForwardUntil( pSentenceData, '{' ); + VOX_Shutdown(); - // find length of "good" portion of the string (not a {} command) - length = pNext - pSentenceData; - if( tempBufferPos + length > sizeof( tempBuffer )) - { - Con_Printf( S_ERROR "Sentence too long (max length %lu characters)\n", sizeof(tempBuffer) - 1 ); - return; - } + buf = FS_LoadFile( path, &size, false ); + if( !buf ) return; - // Copy good string to temp buffer - memcpy( tempBuffer + tempBufferPos, pSentenceData, length ); + VOX_ReadSentenceFile_( buf, size ); - // move the copy position - tempBufferPos += length; + Mem_Free( buf ); +} - pSentenceData = pNext; +void VOX_Init( void ) +{ + VOX_ReadSentenceFile( DEFAULT_SOUNDPATH "sentences.txt" ); +} - // skip ahead of the opening brace - if( *pSentenceData ) pSentenceData++; +void VOX_Shutdown( void ) +{ + int i; - // skip whitespace - while( *pSentenceData && *pSentenceData <= 32 ) - pSentenceData++; + for( i = 0; i < cszrawsentences; i++ ) + Mem_Free( rgpszrawsentence[i] ); - // simple comparison of string commands: - switch( Q_tolower( *pSentenceData )) - { - case 'l': - // all commands starting with the letter 'l' here - if( !Q_strnicmp( pSentenceData, "len", 3 )) - { - g_Sentences[sentenceIndex].length = Q_atof( pSentenceData + 3 ); - } - break; - case 0: - default: - break; - } + cszrawsentences = 0; +} - pSentenceData = ScanForwardUntil( pSentenceData, '}' ); +#if XASH_ENGINE_TESTS +#include "tests.h" - // skip the closing brace - if( *pSentenceData ) pSentenceData++; +static void Test_VOX_GetDirectory( void ) +{ + const char *data[] = + { + "", "", "vox/", + "bark bark", "bark bark", "vox/", + "barney/meow", "meow", "barney/" - // skip trailing whitespace - while( *pSentenceData && *pSentenceData <= 32 ) - pSentenceData++; - } + }; + int i; - if( tempBufferPos < sizeof( tempBuffer )) + for( i = 0; i < sizeof( data ) / sizeof( data[0] ); i += 3 ) { - // terminate cleaned up copy - tempBuffer[tempBufferPos] = 0; + string szpath; + const char *p = VOX_GetDirectory( szpath, data[i+0], sizeof( szpath )); - // copy it over the original data - Q_strcpy( pStart, tempBuffer ); + TASSERT_STR( p, data[i+1] ); + TASSERT_STR( szpath, data[i+2] ); } } -// Load sentence file into memory, insert null terminators to -// delimit sentence name/sentence pairs. Keep pointer to each -// sentence name so we can search later. -void VOX_ReadSentenceFile( const char *psentenceFileName ) +static void Test_VOX_LookupString( void ) { - char c, *pch, *pFileData; - char *pchlast, *pSentenceData; - fs_offset_t fileSize; + int i; + const char *p, *data[] = + { + "0", "123", + "3", "SPAAACE", + "-2", NULL, + "404", NULL, + "not found", NULL, + "exactmatch", "123", + "caseinsensitive", "456", + "SentenceWithTabs", "789", + "SentenceWithSpaces", "SPAAACE", + }; + + VOX_Shutdown(); + + rgpszrawsentence[cszrawsentences++] = "exactmatch\000123"; + rgpszrawsentence[cszrawsentences++] = "CaseInsensitive\000456"; + rgpszrawsentence[cszrawsentences++] = "SentenceWithTabs\0\t\t\t789"; + rgpszrawsentence[cszrawsentences++] = "SentenceWithSpaces\0 SPAAACE"; + rgpszrawsentence[cszrawsentences++] = "SentenceWithTabsAndSpaces\0\t \t\t MEOW"; + + for( i = 0; i < sizeof( data ) / sizeof( data[0] ); i += 2 ) + { + p = VOX_LookupString( data[i] ); - // load file - pFileData = (char *)FS_LoadFile( psentenceFileName, &fileSize, false ); - if( !pFileData ) return; // this game just doesn't used vox sound system + TASSERT_STR( p, data[i+1] ); + } - pch = pFileData; - pchlast = pch + fileSize; + cszrawsentences = 0; +} - while( pch < pchlast ) +static void Test_VOX_ParseString( void ) +{ + char *rgpparseword[CVOXWORDMAX]; + const char *data[] = { - if( g_numSentences >= MAX_SENTENCES ) - { - Con_Printf( S_ERROR "VOX_Init: too many sentences specified, max is %d\n", MAX_SENTENCES ); - break; - } - - // only process this pass on sentences - pSentenceData = NULL; - - // skip newline, cr, tab, space + "(p100) my ass is, heavy!(p80) clik.", + "(p100)", "my", "ass", "is", "_comma", "heavy!(p80)", "clik", NULL, + "freeman...", + "freeman", "_period", NULL, + }; + int i = 0; + + while( i < sizeof( data ) / sizeof( data[0] )) + { + char buffer[4096]; + int wordcount, j = 0; + Q_strncpy( buffer, data[i], sizeof( buffer )); + wordcount = VOX_ParseString( buffer, rgpparseword ); - c = *pch; - while( pch < pchlast && IsWhiteSpace( c )) - c = *(++pch); + i++; - // skip entire line if first char is / - if( *pch != '/' ) + while( data[i] ) { - sentence_t *pSentence = &g_Sentences[g_numSentences++]; - - pSentence->pName = pch; - pSentence->length = 0; - - // scan forward to first space, insert null terminator - // after sentence name - - c = *pch; - while( pch < pchlast && c != ' ' ) - c = *(++pch); - - if( pch < pchlast ) - *pch++ = 0; - - // a sentence may have some line commands, make an extra pass - pSentenceData = pch; + TASSERT_STR( data[i], rgpparseword[j] ); + i++; + j++; } - // scan forward to end of sentence or eof - while( pch < pchlast && pch[0] != '\n' && pch[0] != '\r' ) - pch++; - - // insert null terminator - if( pch < pchlast ) *pch++ = 0; - - // If we have some sentence data, parse out any line commands - if( pSentenceData && pSentenceData < pchlast ) - { - int index = g_numSentences - 1; + TASSERT( j == wordcount ); - // the current sentence has an index of count-1 - VOX_ParseLineCommands( pSentenceData, index ); - } + i++; } } -void VOX_Init( void ) +static void Test_VOX_ParseWordParams( void ) { - memset( g_Sentences, 0, sizeof( g_Sentences )); - g_numSentences = 0; - - VOX_ReadSentenceFile( DEFAULT_SOUNDPATH "sentences.txt" ); + string buffer; + qboolean ret; + voxword_t word; + + Q_strncpy( buffer, "heavy!(p80)", sizeof( buffer )); + ret = VOX_ParseWordParams( buffer, &word, true ); + TASSERT( word.pitch == 80 ); + TASSERT( ret ); + + Q_strncpy( buffer, "(p105)", sizeof( buffer )); + ret = VOX_ParseWordParams( buffer, &word, false ); + TASSERT( word.pitch == 105 ); + TASSERT( !ret ); + + Q_strncpy( buffer, "quiet(v50)", sizeof( buffer )); + ret = VOX_ParseWordParams( buffer, &word, false ); + TASSERT( word.pitch == 105 ); // defaulted + TASSERT( word.volume == 50 ); + TASSERT( !ret ); } - -void VOX_Shutdown( void ) +void Test_RunVOX( void ) { - g_numSentences = 0; + TRUN( Test_VOX_GetDirectory() ); + TRUN( Test_VOX_LookupString() ); + TRUN( Test_VOX_ParseString() ); + TRUN( Test_VOX_ParseWordParams() ); } + +#endif /* XASH_ENGINE_TESTS */ diff --git a/engine/client/vox.h b/engine/client/vox.h index 8e6e557b..56967959 100644 --- a/engine/client/vox.h +++ b/engine/client/vox.h @@ -16,9 +16,9 @@ GNU General Public License for more details. #ifndef VOX_H #define VOX_H -#define CVOXWORDMAX 64 -#define CVOXZEROSCANMAX 255 // scan up to this many samples for next zero crossing -#define MAX_SENTENCES 4096 +#define CVOXWORDMAX 64 +#define CVOXFILESENTENCEMAX 4096 + #define SENTENCE_INDEX -99999 // unique sentence index typedef struct voxword_s @@ -34,13 +34,6 @@ typedef struct voxword_s sfx_t *sfx; // name and cache pointer } voxword_t; - -typedef struct -{ - char *pName; - float length; -} sentence_t; - struct channel_s; void VOX_LoadWord( struct channel_s *pchan ); void VOX_FreeWord( struct channel_s *pchan ); diff --git a/engine/common/host.c b/engine/common/host.c index ec7c95a0..3b3b1f8b 100644 --- a/engine/common/host.c +++ b/engine/common/host.c @@ -826,6 +826,9 @@ static void Host_RunTests( int stage ) break; case 1: // after FS load Test_RunImagelib(); +#if !XASH_DEDICATED + Test_RunVOX(); +#endif Msg( "Done! %d passed, %d failed\n", tests_stats.passed, tests_stats.failed ); Sys_Quit(); } diff --git a/engine/common/tests.h b/engine/common/tests.h index 3ff6e030..e58f20a5 100644 --- a/engine/common/tests.h +++ b/engine/common/tests.h @@ -36,6 +36,7 @@ void Test_RunCommon( void ); void Test_RunCmd( void ); void Test_RunCvar( void ); void Test_RunCon( void ); +void Test_RunVOX( void ); #endif