xash3d-fwgs/engine/client/s_vox.c
Gleb Mazovetskiy 5e0a0765ce Trim all trailing whitespace
The `.editorconfig` file in this repo is configured to trim all trailing
whitespace regardless of whether the line is modified.

Trims all trailing whitespace in the repository to make the codebase easier
to work with in editors that respect `.editorconfig`.

`git blame` becomes less useful on these lines but it already isn't very useful.

Commands:

```
find . -type f -name '*.h' -exec sed --in-place 's/[[:space:]]\+$//' {} \+
find . -type f -name '*.c' -exec sed --in-place 's/[[:space:]]\+$//' {} \+
```
2021-01-04 20:55:10 +03:00

692 lines
16 KiB
C

/*
s_vox.c - npc sentences
Copyright (C) 2010 Uncle Mike
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, either version 3 of the License, or
(at your option) any later version.
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.
*/
#include "common.h"
#include "sound.h"
#include "const.h"
#include <ctype.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( const char c )
{
if( c == '.' || c == ',' || c == ' ' || c == '(' )
return 1;
return 0;
}
static int IsSkipSpace( const char c )
{
if( c == ',' || c == '.' || c == ' ' )
return 1;
return 0;
}
static int IsWhiteSpace( const char space )
{
if( space == ' ' || space == '\t' || space == '\r' || space == '\n' )
return 1;
return 0;
}
static int IsCommandChar( const char c )
{
if( c == 'v' || c == 'p' || c == 's' || c == 'e' || c == 't' )
return 1;
return 0;
}
static int IsDelimitChar( const char c )
{
if( c == '(' || c == ')' )
return 1;
return 0;
}
static char *ScanForwardUntil( char *string, const 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, len;
char *p;
len = Q_strlen( psz );
p = psz + len - 1;
// scan backwards until first '/' or start of string
c = *p;
while( p > psz && c != '/' )
{
c = *( --p );
cb++;
}
if( c != '/' )
{
// didn't find '/', return default directory
Q_strcpy( szpath, "vox/" );
return psz;
}
cb = len - cb;
memcpy( szpath, psz, cb );
szpath[cb] = 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 )
{
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 );
}
for( i = 0; i < g_numSentences; i++ )
{
if( !Q_stricmp( pSentenceName, g_Sentences[i].pName ))
{
if( psentencenum ) *psentencenum = i;
return (g_Sentences[i].pName + Q_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, *p = psz;
memset( rgpparseword, 0, sizeof( char* ) * CVOXWORDMAX );
if( !psz ) return NULL;
i = 0;
rgpparseword[i++] = psz;
while( !fdone && i < CVOXWORDMAX )
{
// scan up to next word
c = *p;
while( c && !IsNextWord( c ))
c = *(++p);
// if '(' then scan for matching ')'
if( c == '(' )
{
p = ScanForwardUntil( p, ')' );
c = *(++p);
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 == ',' ) && *(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;
// skip whitespace
c = *p;
while( c && IsSkipSpace( c ))
c = *(++p);
if( !c ) fdone = 1;
else rgpparseword[i++] = p;
}
}
return rgpparseword;
}
float VOX_GetVolumeScale( channel_t *pchan )
{
if( pchan->currentWord )
{
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 )
return;
scale = VOX_GetVolumeScale( ch );
if( scale == 1.0f ) return;
ch->rightvol = (int)(ch->rightvol * scale);
ch->leftvol = (int)(ch->leftvol * scale);
}
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;
}
}
return pitch;
}
//===============================================================================
// 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 + Q_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;
memset( 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 = Q_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( Q_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;
// apply mixer
pchan->currentWord = &pchan->pMixer;
pchan->currentWord->pData = pSource;
// don't allow overlapped ranges
if( end <= start ) end = 0;
if( start || end )
{
int sampleCount = pSource->samples;
if( start )
{
S_SetSampleStart( pchan, pSource, (int)(sampleCount * 0.01f * start));
}
if( end )
{
S_SetSampleEnd( pchan, pSource, (int)(sampleCount * 0.01f * end));
}
}
}
}
}
void VOX_FreeWord( channel_t *pchan )
{
pchan->currentWord = NULL; // sentence is finished
memset( &pchan->pMixer, 0, sizeof( pchan->pMixer ));
// release unused sounds
if( pchan->words[pchan->wordIndex].sfx )
{
// 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;
}
}
}
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];
i++;
}
pchan->words[i].sfx = NULL;
pchan->wordIndex = 0;
VOX_LoadWord( pchan );
}
// 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 )
{
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;
}
// 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;
memset( rgvoxword, 0, sizeof( voxword_t ) * CVOXWORDMAX );
memset( buffer, 0, sizeof( buffer ));
// lookup actual string in g_Sentences,
// set pointer to string data
psz = VOX_LookupString( pszin, NULL );
if( !psz )
{
Con_DPrintf( S_ERROR "VOX_LoadSound: no such sentence %s\n", pszin );
return;
}
// get directory from string, advance psz
psz = VOX_GetDirectory( szpath, psz );
if( Q_strlen( psz ) > sizeof( buffer ) - 1 )
{
Con_Printf( S_ERROR "VOX_LoadSound: sentence is too long %s\n", psz );
return;
}
// copy into buffer
Q_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)
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++;
}
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 ))
{
Con_Printf( S_ERROR "Sentence too long (max length %lu characters)\n", sizeof(tempBuffer) - 1 );
return;
}
// Copy good string to temp buffer
memcpy( 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( 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;
}
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
Q_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;
fs_offset_t fileSize;
// load file
pFileData = (char *)FS_LoadFile( psentenceFileName, &fileSize, false );
if( !pFileData ) return; // this game just doesn't used vox sound system
pch = pFileData;
pchlast = pch + fileSize;
while( pch < pchlast )
{
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
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 )
{
memset( g_Sentences, 0, sizeof( g_Sentences ));
g_numSentences = 0;
VOX_ReadSentenceFile( DEFAULT_SOUNDPATH "sentences.txt" );
}
void VOX_Shutdown( void )
{
g_numSentences = 0;
}