You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
715 lines
14 KiB
715 lines
14 KiB
/* |
|
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> |
|
|
|
#define TRIM_SCAN_MAX 255 |
|
#define TRIM_SAMPLES_BELOW_8 2 |
|
#define TRIM_SAMPLES_BELOW_16 512 // 65k * 2 / 256 |
|
|
|
#define CVOXFILESENTENCEMAX 4096 |
|
|
|
static int cszrawsentences = 0; |
|
static char *rgpszrawsentence[CVOXFILESENTENCEMAX]; |
|
static const char *voxperiod = "_period", *voxcomma = "_comma"; |
|
|
|
static qboolean S_ShouldTrimSample8( const int8_t *buf, int channels ) |
|
{ |
|
if( abs( buf[0] ) > TRIM_SAMPLES_BELOW_8 ) |
|
return false; |
|
|
|
if( channels >= 2 && abs( buf[1] ) > TRIM_SAMPLES_BELOW_8 ) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
static qboolean S_ShouldTrimSample16( const int16_t *buf, int channels ) |
|
{ |
|
if( abs( buf[0] ) > TRIM_SAMPLES_BELOW_16 ) |
|
return false; |
|
|
|
if( channels >= 2 && abs( buf[1] ) > TRIM_SAMPLES_BELOW_16 ) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
static int S_TrimStart( const wavdata_t *wav, int start ) |
|
{ |
|
size_t channels = wav->channels, width = wav->width, i; |
|
|
|
if( wav->type != WF_PCMDATA ) |
|
return start; |
|
|
|
if( width == 1 ) |
|
{ |
|
const int8_t *data = (const int8_t *)&wav->buffer[channels * width * start]; |
|
|
|
for( i = 0; i < TRIM_SCAN_MAX && start < wav->samples; i++ ) |
|
{ |
|
if( !S_ShouldTrimSample8( data, wav->channels )) |
|
break; |
|
|
|
start += channels; |
|
data += channels; |
|
} |
|
} |
|
else if( width == 2 ) |
|
{ |
|
const int16_t *data = (const int16_t *)&wav->buffer[channels * width * start]; |
|
|
|
for( i = 0; i < TRIM_SCAN_MAX && start < wav->samples; i++ ) |
|
{ |
|
if( !S_ShouldTrimSample16( data, wav->channels )) |
|
break; |
|
|
|
start += channels; |
|
data += channels; |
|
} |
|
} |
|
|
|
return start; |
|
} |
|
|
|
static int S_TrimEnd( const wavdata_t *wav, int end ) |
|
{ |
|
size_t channels = wav->channels, width = wav->width, i; |
|
|
|
if( wav->type != WF_PCMDATA ) |
|
return end; |
|
|
|
if( width == 1 ) |
|
{ |
|
const int8_t *data = (const int8_t *)&wav->buffer[channels * width * end]; |
|
|
|
for( i = 0; i < TRIM_SCAN_MAX && end > 0; i++ ) |
|
{ |
|
if( !S_ShouldTrimSample8( data, wav->channels )) |
|
break; |
|
|
|
end -= channels; |
|
data -= channels; |
|
} |
|
} |
|
else if( width == 2 ) |
|
{ |
|
const int16_t *data = (const int16_t *)&wav->buffer[channels * width * end]; |
|
|
|
for( i = 0; i < TRIM_SCAN_MAX && end > 0; i++ ) |
|
{ |
|
if( !S_ShouldTrimSample16( data, wav->channels )) |
|
break; |
|
|
|
end -= channels; |
|
data -= channels; |
|
} |
|
} |
|
|
|
return end; |
|
} |
|
|
|
static void S_TrimStartEndTimes( channel_t *ch, wavdata_t *wav, int start, int end ) |
|
{ |
|
ch->pMixer.sample = start = S_TrimStart( wav, start ); |
|
|
|
// don't overrun the buffer while trimming end |
|
if( end == 0 ) |
|
end = wav->samples - wav->channels; |
|
|
|
if( end < start ) |
|
end = start; |
|
|
|
ch->pMixer.forcedEndSample = S_TrimEnd( wav, end ); |
|
} |
|
|
|
// 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; |
|
|
|
S_TrimStartEndTimes( ch, data, start * 0.01f * samples, 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 */
|
|
|