628 lines
15 KiB
C
628 lines
15 KiB
C
//=======================================================================
|
|
// 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;
|
|
} |