//======================================================================= // Copyright XashXT Group 2010 © // s_vox.c - npc sentences //======================================================================= #include "sound.h" #include "const.h" 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 IsNextWord( char c ) { if( c == '.' || c == ',' || c == ' ' || c == '(' ) return 1; return 0; } static int IsSkipSpace( char c ) { if( c == ',' || c == '.' || c == ' ' ) return 1; return 0; } static int IsWhiteSpace( char space ) { if( space == ' ' || space == '\t' || space == '\r' || space == '\n' ) return 1; return 0; } static int IsCommandChar( char c ) { if( c == 'v' || c == 'p' || c == 's' || c == 'e' || c == 't' ) return 1; return 0; } static int IsDelimitChar( char c ) { if( c == '(' || c == ')' ) return 1; return 0; } static char *ScanForwardUntil( char *string, char scan ) { while( string[0] ) { if ( string[0] == scan ) return string; string++; } return string; } // backwards scan psz for last '/' // return substring in szpath null terminated // if '/' not found, return 'vox/' static char *VOX_GetDirectory( char *szpath, char *psz ) { char c; int cb = 0; char *pszscan = psz + com.strlen( psz ) - 1; // scan backwards until first '/' or start of string c = *pszscan; while( pszscan > psz && c != '/' ) { c = *( --pszscan ); cb++; } if( c != '/' ) { // didn't find '/', return default directory com.strcpy( szpath, "vox/" ); return psz; } cb = com.strlen( psz ) - cb; Mem_Copy( szpath, psz, cb ); szpath[cb] = 0; return pszscan + 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 ) { int i; if( com.is_digit( pSentenceName ) && (i = com.atoi( pSentenceName )) < g_numSentences ) { if( psentencenum ) *psentencenum = i; return (g_Sentences[i].pName + com.strlen( g_Sentences[i].pName ) + 1 ); } for( i = 0; i < g_numSentences; i++ ) { if( !com.stricmp( pSentenceName, g_Sentences[i].pName )) { if( psentencenum ) *psentencenum = i; return (g_Sentences[i].pName + com.strlen( g_Sentences[i].pName ) + 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, *pszscan = psz; Mem_Set( rgpparseword, 0, sizeof( char* ) * CVOXWORDMAX ); if( !psz ) return NULL; i = 0; rgpparseword[i++] = psz; while( !fdone && i < CVOXWORDMAX ) { // scan up to next word c = *pszscan; while( c && !IsNextWord( c )) c = *(++pszscan); // if '(' then scan for matching ')' if( c == '(' ) { pszscan = ScanForwardUntil( pszscan, ')' ); c = *(++pszscan); if( !c ) fdone = 1; } if( fdone || !c ) { fdone = 1; } else { // if . or , insert pause into rgpparseword, // unless this is the last character if(( c == '.' || c == ',' ) && *(pszscan+1) != '\n' && *(pszscan+1) != '\r' && *(pszscan+1) != 0 ) { if( c == '.' ) rgpparseword[i++] = voxperiod; else rgpparseword[i++] = voxcomma; if( i >= CVOXWORDMAX ) break; } // null terminate substring *pszscan++ = 0; // skip whitespace c = *pszscan; while( c && IsSkipSpace( c )) c = *(++pszscan); if( !c ) fdone = 1; else rgpparseword[i++] = pszscan; } } return rgpparseword; } float VOX_GetVolumeScale( channel_t *pchan ) { if( pchan->currentWord.pData ) { if ( pchan->words[pchan->wordIndex].volume ) { float volume = pchan->words[pchan->wordIndex].volume * 0.01f; if( volume < 1.0f ) return volume; } } return 1.0f; } void VOX_SetChanVol( channel_t *ch ) { float scale; if( !ch->currentWord.pData ) return; scale = VOX_GetVolumeScale( ch ); if( scale == 1.0f ) return; ch->rightvol = (int)(ch->rightvol * scale); ch->leftvol = (int)(ch->leftvol * scale); } //=============================================================================== // 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 ) { char *pszsave = psz; char c, ct, sznum[8]; static voxword_t voxwordDefault; int i; // init to defaults if this is the first word in string. if( fFirst ) { 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 + com.strlen( psz ) - 1 ); // no formatting, return if( c != ')' ) return 1; // scan forward to first '(' c = *psz; while( !IsDelimitChar( c )) c = *(++psz); // bogus formatting if( c == ')' ) return 0; // null terminate *psz = 0; ct = *(++psz); while( 1 ) { // scan until we hit a character in the commandSet while( ct && !IsCommandChar( ct )) ct = *(++psz); if( ct == ')' ) break; Mem_Set( sznum, 0, sizeof( sznum )); i = 0; c = *(++psz); if( !isdigit( c )) break; // read number while( isdigit( c ) && i < sizeof( sznum ) - 1 ) { sznum[i++] = c; c = *(++psz); } // get value of number i = com.atoi( sznum ); switch( ct ) { case 'v': pvoxword->volume = 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; } ct = c; } // if the string has zero length, this was an isolated // parameter block. Set default voxword to these // values if( com.strlen( pszsave ) == 0 ) { voxwordDefault = *pvoxword; return 0; } return 1; } void VOX_LoadWord( channel_t *pchan ) { 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; // don't allow overlapped ranges if( end <= start ) end = 0; if( start || end ) { int sampleCount = pSource->samples; if( start ) { pchan->currentWord.sample = S_ZeroCrossingAfter( pSource, (int)(sampleCount * 0.01f * start)); } if( end ) { pchan->currentWord.forcedEndSample = S_ZeroCrossingBefore( pSource, (int)(sampleCount * 0.01f * end) ); // past current position? limit. if( pchan->currentWord.forcedEndSample < pchan->currentWord.sample ) pchan->currentWord.forcedEndSample = pchan->currentWord.sample; } } pchan->currentWord.pData = pSource; } } else pchan->currentWord.pData = NULL; // sentence is finished } 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 ) pchan->words[i] = pwords[i++]; pchan->words[i].sfx = NULL; pchan->wordIndex = 0; VOX_LoadWord( pchan ); } wavdata_t *VOX_LoadNextWord( channel_t *pchan ) { pchan->wordIndex++; VOX_LoadWord( pchan ); // set new word if( pchan->currentWord.pData ) { pchan->sfx = pchan->words[pchan->wordIndex].sfx; pchan->pos = pchan->currentWord.sample; pchan->end = paintedtime + pchan->currentWord.forcedEndSample; return pchan->currentWord.pData; } S_FreeChannel( pchan ); // channel stopped return NULL; } // link all sounds in sentence, start playing first word. void VOX_LoadSound( channel_t *pchan, const char *pszin ) { char buffer[512]; int i, cword; char pathbuffer[64]; char szpath[32]; voxword_t rgvoxword[CVOXWORDMAX]; char *psz; if( !pszin || !*pszin ) return; Mem_Set( rgvoxword, 0, sizeof( voxword_t ) * CVOXWORDMAX ); Mem_Set( buffer, 0, sizeof( buffer )); // lookup actual string in g_Sentences, // set pointer to string data psz = VOX_LookupString( pszin, NULL ); if( !psz ) { MsgDev( D_ERROR, "VOX_LoadSound: no sentence named %s\n", pszin ); return; } // get directory from string, advance psz psz = VOX_GetDirectory( szpath, psz ); if( com.strlen( psz ) > sizeof( buffer ) - 1 ) { MsgDev( D_ERROR, "VOX_LoadSound: sentence is too long %s\n", psz ); return; } // copy into buffer com.strcpy( buffer, psz ); psz = buffer; // parse sentence (also inserts null terminators between words) VOX_ParseString( psz ); // for each word in the sentence, construct the filename, // lookup the sfx and save each pointer in a temp array 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) com.strcpy( pathbuffer, szpath ); com.strncat( pathbuffer, rgpparseword[i], sizeof( pathbuffer )); com.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++; } i++; } VOX_LoadFirstWord( pchan, rgvoxword ); pchan->isSentence = true; pchan->sfx = rgvoxword[0].sfx; } //----------------------------------------------------------------------------- // 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( !pSentenceData ) return; pStart = pSentenceData; while( *pSentenceData ) { pNext = ScanForwardUntil( pSentenceData, '{' ); // find length of "good" portion of the string (not a {} command) length = pNext - pSentenceData; if( tempBufferPos + length > sizeof( tempBuffer )) { MsgDev( D_ERROR, "sentence too long!\n" ); return; } // Copy good string to temp buffer Mem_Copy( tempBuffer + tempBufferPos, pSentenceData, length ); // move the copy position tempBufferPos += length; pSentenceData = pNext; // skip ahead of the opening brace if( *pSentenceData ) pSentenceData++; // skip whitespace while( *pSentenceData && *pSentenceData <= 32 ) pSentenceData++; // simple comparison of string commands: switch( com.tolower( *pSentenceData )) { case 'l': // all commands starting with the letter 'l' here if( !com.strnicmp( pSentenceData, "len", 3 )) { g_Sentences[sentenceIndex].length = com.atof( pSentenceData + 3 ); } break; case 0: default: break; } pSentenceData = ScanForwardUntil( pSentenceData, '}' ); // skip the closing brace if( *pSentenceData ) pSentenceData++; // skip trailing whitespace while( *pSentenceData && *pSentenceData <= 32 ) pSentenceData++; } if( tempBufferPos < sizeof( tempBuffer )) { // terminate cleaned up copy tempBuffer[tempBufferPos] = 0; // copy it over the original data com.strcpy( pStart, tempBuffer ); } } // 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 ) { char c, *pch, *pFileData; char *pchlast, *pSentenceData; int fileSize; // load file pFileData = (char *)FS_LoadFile( psentenceFileName, &fileSize ); if( !pFileData ) { MsgDev( D_WARN, "couldn't load %s\n", psentenceFileName ); return; } pch = pFileData; pchlast = pch + fileSize; while( pch < pchlast ) { // only process this pass on sentences pSentenceData = NULL; // skip newline, cr, tab, space c = *pch; while( pch < pchlast && IsWhiteSpace( c )) c = *(++pch); // skip entire line if first char is / if( *pch != '/' ) { 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; } // 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; // the current sentence has an index of count-1 VOX_ParseLineCommands( pSentenceData, index ); } } } void VOX_Init( void ) { Mem_Set( g_Sentences, 0, sizeof( g_Sentences )); g_numSentences = 0; VOX_ReadSentenceFile( "sound/sentences.txt" ); } void VOX_Shutdown( void ) { g_numSentences = 0; }