mirror of
https://github.com/FWGS/xash3d-fwgs
synced 2024-11-26 11:49:25 +01:00
Alibek Omarov
e23580c1de
This file initially came from HLND, a Chinese GoldSrc recreation. It turned out to be suspiciously close to the original version, down to the comments and code style. We don't work with leaked sources here, so remove it. A proper parser should be reimplemented from ground-up, when we will start working on CZDS support.
604 lines
12 KiB
C
604 lines
12 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>
|
|
|
|
static int cszrawsentences = 0;
|
|
static char *rgpszrawsentence[CVOXFILESENTENCEMAX];
|
|
static const char *voxperiod = "_period", *voxcomma = "_comma";
|
|
|
|
// 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;
|
|
}
|
|
|
|
void VOX_LoadWord( channel_t *ch )
|
|
{
|
|
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 );
|
|
}
|
|
|
|
void VOX_FreeWord( channel_t *ch )
|
|
{
|
|
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;
|
|
}
|
|
|
|
void VOX_SetChanVol( channel_t *ch )
|
|
{
|
|
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;
|
|
}
|
|
|
|
float VOX_ModifyPitch( channel_t *ch, float pitch )
|
|
{
|
|
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;
|
|
}
|
|
|
|
static const char *VOX_GetDirectory( char *szpath, const char *psz, int nsize )
|
|
{
|
|
const char *p;
|
|
int len;
|
|
|
|
// search / backwards
|
|
p = Q_strrchr( psz, '/' );
|
|
|
|
if( !p )
|
|
{
|
|
Q_strncpy( szpath, "vox/", nsize );
|
|
return psz;
|
|
}
|
|
|
|
len = p - psz + 1;
|
|
|
|
if( len > nsize )
|
|
{
|
|
Con_Printf( "VOX_GetDirectory: invalid directory in: %s\n", psz );
|
|
return NULL;
|
|
}
|
|
|
|
memcpy( szpath, psz, len );
|
|
szpath[len] = 0;
|
|
|
|
return p + 1;
|
|
}
|
|
|
|
static const char *VOX_LookupString( const char *pszin )
|
|
{
|
|
int i = -1, len;
|
|
const char *c;
|
|
|
|
// check if we are an immediate sentence
|
|
if( *pszin == '#' )
|
|
{
|
|
// immediate sentence, probably coming from "speak" command
|
|
return pszin + 1;
|
|
}
|
|
|
|
// check if we received an index
|
|
if( Q_isdigit( pszin ))
|
|
{
|
|
i = Q_atoi( pszin );
|
|
|
|
if( i >= cszrawsentences )
|
|
i = -1;
|
|
}
|
|
|
|
// last hope: find it in sentences array
|
|
if( i == -1 )
|
|
{
|
|
for( i = 0; i < cszrawsentences; i++ )
|
|
{
|
|
if( !Q_stricmp( pszin, rgpszrawsentence[i] ))
|
|
break;
|
|
}
|
|
}
|
|
|
|
// not found, exit
|
|
if( i == cszrawsentences )
|
|
return NULL;
|
|
|
|
len = Q_strlen( rgpszrawsentence[i] );
|
|
|
|
c = &rgpszrawsentence[i][len + 1];
|
|
for( ; *c == ' ' || *c == '\t'; c++ );
|
|
|
|
return c;
|
|
}
|
|
|
|
static int VOX_ParseString( char *psz, char *rgpparseword[CVOXWORDMAX] )
|
|
{
|
|
int i = 0;
|
|
|
|
if( !psz )
|
|
return i;
|
|
|
|
rgpparseword[i++] = psz;
|
|
|
|
while( i < CVOXWORDMAX )
|
|
{
|
|
// skip to next word
|
|
for( ; *psz &&
|
|
*psz != ' ' &&
|
|
*psz != '.' &&
|
|
*psz != ',' &&
|
|
*psz != '('; psz++ );
|
|
|
|
// skip anything in between ( and )
|
|
if( *psz == '(' )
|
|
{
|
|
for( ; *psz && *psz != ')'; psz++ );
|
|
psz++;
|
|
}
|
|
|
|
if( !*psz )
|
|
return i;
|
|
|
|
// . 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++] = (char *)voxperiod;
|
|
else rgpparseword[i++] = (char *)voxcomma;
|
|
|
|
if( i >= CVOXWORDMAX )
|
|
return i;
|
|
}
|
|
|
|
*psz++ = 0;
|
|
|
|
for( ; *psz && ( *psz == '.' || *psz == ' ' || *psz == ',' );
|
|
psz++ );
|
|
|
|
if( !*psz )
|
|
return i;
|
|
|
|
rgpparseword[i++] = psz;
|
|
}
|
|
|
|
return i;
|
|
}
|
|
|
|
static qboolean VOX_ParseWordParams( char *psz, voxword_t *pvoxword, qboolean fFirst )
|
|
{
|
|
int len, i;
|
|
char sznum[8], *pszsave = psz;
|
|
static voxword_t voxwordDefault;
|
|
|
|
if( fFirst )
|
|
{
|
|
voxwordDefault.fKeepCached = 0;
|
|
voxwordDefault.pitch = -1;
|
|
voxwordDefault.volume = 100;
|
|
voxwordDefault.start = 0;
|
|
voxwordDefault.end = 100;
|
|
voxwordDefault.timecompress = 0;
|
|
}
|
|
|
|
*pvoxword = voxwordDefault;
|
|
|
|
len = Q_strlen( psz );
|
|
|
|
if( len == 0 )
|
|
return false;
|
|
|
|
// no special params
|
|
if( psz[len-1] != ')' )
|
|
return true;
|
|
|
|
for( ; *psz != '(' && *psz != ')'; psz++ );
|
|
|
|
// invalid syntax
|
|
if( *psz == ')' )
|
|
return false;
|
|
|
|
// split filename and params
|
|
*psz++ = '\0';
|
|
|
|
for( ;; )
|
|
{
|
|
char command;
|
|
|
|
// find command
|
|
for( ; *psz &&
|
|
*psz != 'v' &&
|
|
*psz != 'p' &&
|
|
*psz != 's' &&
|
|
*psz != 'e' &&
|
|
*psz != 't'; psz++ )
|
|
{
|
|
if( *psz == ')' )
|
|
break;
|
|
}
|
|
|
|
command = *psz++;
|
|
|
|
if( !isdigit( *psz ))
|
|
break;
|
|
|
|
memset( sznum, 0, sizeof( sznum ));
|
|
for( i = 0; i < sizeof( sznum ) - 1 && isdigit( *psz ); i++, psz++ )
|
|
sznum[i] = *psz;
|
|
|
|
i = Q_atoi( sznum );
|
|
switch( command )
|
|
{
|
|
case 'e': pvoxword->end = i; break;
|
|
case 'p': pvoxword->pitch = i; break;
|
|
case 's': pvoxword->start = i; break;
|
|
case 't': pvoxword->timecompress = i; break;
|
|
case 'v': pvoxword->volume = i; break;
|
|
}
|
|
}
|
|
|
|
// no actual word but new defaults
|
|
if( Q_strlen( pszsave ) == 0 )
|
|
{
|
|
voxwordDefault = *pvoxword;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void VOX_LoadSound( channel_t *ch, const char *pszin )
|
|
{
|
|
char buffer[512], szpath[32], pathbuffer[64];
|
|
char *rgpparseword[CVOXWORDMAX];
|
|
const char *psz;
|
|
int i, j;
|
|
|
|
if( !pszin )
|
|
return;
|
|
|
|
memset( buffer, 0, sizeof( buffer ));
|
|
memset( rgpparseword, 0, sizeof( rgpparseword ));
|
|
|
|
psz = VOX_LookupString( pszin );
|
|
|
|
if( !psz )
|
|
{
|
|
Con_Printf( "VOX_LoadSound: no sentence named %s\n", pszin );
|
|
return;
|
|
}
|
|
|
|
psz = VOX_GetDirectory( szpath, psz, sizeof( szpath ));
|
|
|
|
if( !psz )
|
|
{
|
|
Con_Printf( "VOX_LoadSound: failed getting directory for %s\n", pszin );
|
|
return;
|
|
}
|
|
|
|
if( Q_strlen( psz ) >= sizeof( buffer ) )
|
|
{
|
|
Con_Printf( "VOX_LoadSound: sentence is too long %s", psz );
|
|
return;
|
|
}
|
|
|
|
Q_strncpy( buffer, psz, sizeof( buffer ));
|
|
VOX_ParseString( buffer, rgpparseword );
|
|
|
|
j = 0;
|
|
for( i = 0; rgpparseword[i]; i++ )
|
|
{
|
|
if( !VOX_ParseWordParams( rgpparseword[i], &ch->words[j], i == 0 ))
|
|
continue;
|
|
|
|
Q_snprintf( pathbuffer, sizeof( pathbuffer ), "%s%s.wav", szpath, rgpparseword[i] );
|
|
|
|
ch->words[j].sfx = S_FindName( pathbuffer, &ch->words[j].fKeepCached );
|
|
|
|
j++;
|
|
}
|
|
|
|
ch->words[j].sfx = NULL;
|
|
ch->sfx = ch->words[0].sfx;
|
|
ch->wordIndex = 0;
|
|
ch->isSentence = true;
|
|
|
|
VOX_LoadWord( ch );
|
|
}
|
|
|
|
static void VOX_ReadSentenceFile_( byte *buf, fs_offset_t size )
|
|
{
|
|
char *p, *last;
|
|
|
|
p = (char *)buf;
|
|
last = p + size;
|
|
|
|
while( p < last )
|
|
{
|
|
char *name = NULL, *value = NULL;
|
|
|
|
if( cszrawsentences >= CVOXFILESENTENCEMAX )
|
|
break;
|
|
|
|
for( ; p < last && ( *p == '\n' || *p == '\r' || *p == '\t' || *p == ' ' );
|
|
p++ );
|
|
|
|
if( *p != '/' )
|
|
{
|
|
name = p;
|
|
|
|
for( ; p < last && *p != ' ' && *p != '\t' ; p++ );
|
|
|
|
if( p < last )
|
|
*p++ = 0;
|
|
|
|
value = p;
|
|
}
|
|
|
|
for( ; p < last && *p != '\n' && *p != '\r'; p++ );
|
|
|
|
if( p < last )
|
|
*p++ = 0;
|
|
|
|
if( name )
|
|
{
|
|
int index = cszrawsentences;
|
|
int size = strlen( name ) + strlen( value ) + 2;
|
|
|
|
rgpszrawsentence[index] = Mem_Malloc( host.mempool, size );
|
|
memcpy( rgpszrawsentence[index], name, size );
|
|
rgpszrawsentence[index][size - 1] = 0;
|
|
cszrawsentences++;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void VOX_ReadSentenceFile( const char *path )
|
|
{
|
|
byte *buf;
|
|
fs_offset_t size;
|
|
|
|
VOX_Shutdown();
|
|
|
|
buf = FS_LoadFile( path, &size, false );
|
|
if( !buf ) return;
|
|
|
|
VOX_ReadSentenceFile_( buf, size );
|
|
|
|
Mem_Free( buf );
|
|
}
|
|
|
|
void VOX_Init( void )
|
|
{
|
|
VOX_ReadSentenceFile( DEFAULT_SOUNDPATH "sentences.txt" );
|
|
}
|
|
|
|
void VOX_Shutdown( void )
|
|
{
|
|
int i;
|
|
|
|
for( i = 0; i < cszrawsentences; i++ )
|
|
Mem_Free( rgpszrawsentence[i] );
|
|
|
|
cszrawsentences = 0;
|
|
}
|
|
|
|
#if XASH_ENGINE_TESTS
|
|
#include "tests.h"
|
|
|
|
static void Test_VOX_GetDirectory( void )
|
|
{
|
|
const char *data[] =
|
|
{
|
|
"", "", "vox/",
|
|
"bark bark", "bark bark", "vox/",
|
|
"barney/meow", "meow", "barney/"
|
|
|
|
};
|
|
int i;
|
|
|
|
for( i = 0; i < sizeof( data ) / sizeof( data[0] ); i += 3 )
|
|
{
|
|
string szpath;
|
|
const char *p = VOX_GetDirectory( szpath, data[i+0], sizeof( szpath ));
|
|
|
|
TASSERT_STR( p, data[i+1] );
|
|
TASSERT_STR( szpath, data[i+2] );
|
|
}
|
|
}
|
|
|
|
static void Test_VOX_LookupString( void )
|
|
{
|
|
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++] = (char*)"exactmatch\000123";
|
|
rgpszrawsentence[cszrawsentences++] = (char*)"CaseInsensitive\000456";
|
|
rgpszrawsentence[cszrawsentences++] = (char*)"SentenceWithTabs\0\t\t\t789";
|
|
rgpszrawsentence[cszrawsentences++] = (char*)"SentenceWithSpaces\0 SPAAACE";
|
|
rgpszrawsentence[cszrawsentences++] = (char*)"SentenceWithTabsAndSpaces\0\t \t\t MEOW";
|
|
|
|
for( i = 0; i < sizeof( data ) / sizeof( data[0] ); i += 2 )
|
|
{
|
|
p = VOX_LookupString( data[i] );
|
|
|
|
TASSERT_STR( p, data[i+1] );
|
|
}
|
|
|
|
cszrawsentences = 0;
|
|
}
|
|
|
|
static void Test_VOX_ParseString( void )
|
|
{
|
|
char *rgpparseword[CVOXWORDMAX];
|
|
const char *data[] =
|
|
{
|
|
"(p100) my ass is, heavy!(p80 t20) clik.",
|
|
"(p100)", "my", "ass", "is", "_comma", "heavy!(p80 t20)", "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 );
|
|
|
|
i++;
|
|
|
|
while( data[i] )
|
|
{
|
|
TASSERT_STR( data[i], rgpparseword[j] );
|
|
i++;
|
|
j++;
|
|
}
|
|
|
|
TASSERT( j == wordcount );
|
|
|
|
i++;
|
|
}
|
|
}
|
|
|
|
static void Test_VOX_ParseWordParams( void )
|
|
{
|
|
string buffer;
|
|
qboolean ret;
|
|
voxword_t word;
|
|
|
|
Q_strncpy( buffer, "heavy!(p80)", sizeof( buffer ));
|
|
ret = VOX_ParseWordParams( buffer, &word, true );
|
|
TASSERT_STR( buffer, "heavy!" );
|
|
TASSERT( word.pitch == 80 );
|
|
TASSERT( ret );
|
|
|
|
Q_strncpy( buffer, "(p105)", sizeof( buffer ));
|
|
ret = VOX_ParseWordParams( buffer, &word, false );
|
|
TASSERT_STR( buffer, "" );
|
|
TASSERT( word.pitch == 105 );
|
|
TASSERT( !ret );
|
|
|
|
Q_strncpy( buffer, "quiet(v50)", sizeof( buffer ));
|
|
ret = VOX_ParseWordParams( buffer, &word, false );
|
|
TASSERT_STR( buffer, "quiet" );
|
|
TASSERT( word.pitch == 105 ); // defaulted
|
|
TASSERT( word.volume == 50 );
|
|
TASSERT( ret );
|
|
}
|
|
|
|
void Test_RunVOX( void )
|
|
{
|
|
TRUN( Test_VOX_GetDirectory() );
|
|
TRUN( Test_VOX_LookupString() );
|
|
TRUN( Test_VOX_ParseString() );
|
|
TRUN( Test_VOX_ParseWordParams() );
|
|
}
|
|
|
|
#endif /* XASH_ENGINE_TESTS */
|