mirror of
https://bitbucket.org/CPMADevs/cnq3
synced 2024-11-10 06:31:48 +00:00
1197 lines
30 KiB
C++
1197 lines
30 KiB
C++
/*
|
|
===========================================================================
|
|
Copyright (C) 1999-2005 Id Software, Inc.
|
|
|
|
This file is part of Quake III Arena source code.
|
|
|
|
Quake III Arena source code 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 2 of the License,
|
|
or (at your option) any later version.
|
|
|
|
Quake III Arena source code 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.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with Quake III Arena source code; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
===========================================================================
|
|
*/
|
|
//
|
|
// main control for any streaming sound output device
|
|
|
|
#include "snd_local.h"
|
|
#include "snd_codec.h"
|
|
#include "client.h"
|
|
|
|
|
|
static void S_Update_DMA();
|
|
static void S_UpdateBackgroundTrack();
|
|
static void S_Base_StopBackgroundTrack();
|
|
|
|
static snd_stream_t* s_backgroundStream = NULL;
|
|
static char s_backgroundLoop[MAX_QPATH];
|
|
|
|
|
|
static const float SOUND_MAX_DIST = 1250;
|
|
|
|
static const int MASTER_VOL = 127;
|
|
|
|
channel_t s_channels[MAX_CHANNELS];
|
|
channel_t loop_channels[MAX_CHANNELS];
|
|
int numLoopChannels;
|
|
|
|
static qbool s_soundStarted;
|
|
static qbool s_soundMuted;
|
|
|
|
static int listener_number;
|
|
static vec3_t listener_origin;
|
|
static vec3_t listener_axis[3];
|
|
|
|
dma_t dma;
|
|
int s_soundtime;
|
|
int s_paintedtime;
|
|
|
|
#define MAX_SFX 4096
|
|
static sfx_t s_knownSfx[MAX_SFX];
|
|
static int s_numSfx;
|
|
|
|
#define SFX_HASH_SIZE 128
|
|
static sfx_t* sfxHash[SFX_HASH_SIZE];
|
|
|
|
static cvar_t* s_show;
|
|
static cvar_t* s_mixahead;
|
|
static cvar_t* s_mixPreStep;
|
|
cvar_t* s_testsound;
|
|
|
|
static loopSound_t loopSounds[MAX_GENTITIES];
|
|
static channel_t *freelist = NULL;
|
|
|
|
int s_rawend;
|
|
portable_samplepair_t s_rawsamples[MAX_RAW_SAMPLES];
|
|
|
|
|
|
static void S_Base_SoundInfo()
|
|
{
|
|
if (!s_soundStarted) {
|
|
Com_Printf( "sound system not started\n" );
|
|
return;
|
|
}
|
|
|
|
Com_Printf( "DMA: %d channels, %dHz, %d bits\n", dma.channels, dma.speed, dma.samplebits );
|
|
//Com_Printf( "%5d samples\n", dma.samples );
|
|
//Com_Printf( "%5d submission_chunk\n", dma.submission_chunk );
|
|
//Com_Printf( "0x%x dma buffer\n", dma.buffer );
|
|
if ( s_backgroundStream ) {
|
|
Com_Printf( "Music: %s\n", s_backgroundLoop );
|
|
}
|
|
}
|
|
|
|
|
|
static void S_Base_SoundList()
|
|
{
|
|
const sfx_t* sfx = &s_knownSfx[1];
|
|
for ( int i = 1; i < s_numSfx; ++i, ++sfx ) {
|
|
Com_Printf( "%6i [%s] : %s\n", sfx->soundLength, sfx->inMemory ? "MEM" : "PGD", sfx->soundName );
|
|
}
|
|
Com_Printf( "%d sounds loaded\n", s_numSfx - 1 );
|
|
|
|
S_DisplayFreeMemory();
|
|
}
|
|
|
|
|
|
static void S_ChannelFree( channel_t* v )
|
|
{
|
|
v->thesfx = NULL;
|
|
*(channel_t **)v = freelist;
|
|
freelist = (channel_t*)v;
|
|
}
|
|
|
|
static channel_t* S_ChannelMalloc()
|
|
{
|
|
channel_t *v;
|
|
if (freelist == NULL) {
|
|
return NULL;
|
|
}
|
|
v = freelist;
|
|
freelist = *(channel_t **)freelist;
|
|
v->allocTime = Com_Milliseconds();
|
|
return v;
|
|
}
|
|
|
|
static void S_ChannelSetup()
|
|
{
|
|
channel_t *p, *q;
|
|
|
|
Com_Memset( s_channels, 0, sizeof( s_channels ) );
|
|
|
|
p = s_channels;
|
|
q = p + MAX_CHANNELS;
|
|
while (--q > p) {
|
|
*(channel_t **)q = q-1;
|
|
}
|
|
|
|
*(channel_t **)q = NULL;
|
|
freelist = p + MAX_CHANNELS - 1;
|
|
Com_DPrintf("Channel memory manager started\n");
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////////////////////////
|
|
|
|
|
|
// will allocate a new sfx if it isn't found
|
|
|
|
static sfx_t* S_FindName( const char *name )
|
|
{
|
|
int i;
|
|
|
|
if (!name) {
|
|
Com_Error( ERR_FATAL, "S_FindName: NULL" );
|
|
}
|
|
|
|
if (!name[0]) {
|
|
Com_Error( ERR_FATAL, "S_FindName: empty name" );
|
|
}
|
|
|
|
if (strlen(name) >= MAX_QPATH) {
|
|
Com_Error( ERR_FATAL, "S_FindName: name too long: %s", name );
|
|
}
|
|
|
|
int hash = Q_FileHash( name, SFX_HASH_SIZE );
|
|
sfx_t* sfx = sfxHash[hash];
|
|
|
|
// see if already loaded
|
|
while (sfx) {
|
|
if (!Q_stricmp(sfx->soundName, name) ) {
|
|
return sfx;
|
|
}
|
|
sfx = sfx->next;
|
|
}
|
|
|
|
// find a free sfx
|
|
for (i = 1; i < s_numSfx; ++i) {
|
|
if (!s_knownSfx[i].soundName[0]) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (i == s_numSfx) {
|
|
if (s_numSfx == MAX_SFX) {
|
|
Com_Error( ERR_FATAL, "S_FindName: out of sfx_t" );
|
|
}
|
|
s_numSfx++;
|
|
}
|
|
|
|
sfx = &s_knownSfx[i];
|
|
Com_Memset( sfx, 0, sizeof(*sfx) );
|
|
strcpy( sfx->soundName, name );
|
|
|
|
sfx->next = sfxHash[hash];
|
|
sfxHash[hash] = sfx;
|
|
|
|
return sfx;
|
|
}
|
|
|
|
|
|
static void S_memoryLoad( sfx_t* sfx )
|
|
{
|
|
// Unfortunately, sound and image loads during gameplay are perfectly valid.
|
|
// Mod code really should pre-load as much as it can during CGAME_INIT.
|
|
// Some stuff can't really be pre-loaded, such as when loading a new player model.
|
|
if ( cls.state == CA_ACTIVE && com_developer && com_developer->integer ) {
|
|
enum { SoundBufferSize = 16 };
|
|
static sfx_t* sounds[SoundBufferSize];
|
|
static int soundIndex = 0;
|
|
static int64_t lastLoadTime = -9000;
|
|
static int64_t lastMsgTime = -9000;
|
|
static int loadCount = 1;
|
|
|
|
sounds[soundIndex++] = sfx;
|
|
|
|
const int64_t currTime = Sys_Milliseconds();
|
|
if ( currTime < lastLoadTime + 1000 )
|
|
++loadCount;
|
|
else
|
|
loadCount = 1;
|
|
lastLoadTime = currTime;
|
|
|
|
if ( loadCount > 2 && currTime >= lastMsgTime + 1000 ) {
|
|
Com_Printf( "^3WARNING: excessive sound loads\n" );
|
|
for ( int i = 0; i < SoundBufferSize; ++i ) {
|
|
sfx_t* const sound = sounds[ ( soundIndex + i ) % SoundBufferSize ];
|
|
if ( sound != NULL )
|
|
Com_Printf( "^3- %s\n", sound->soundName );
|
|
}
|
|
lastMsgTime = currTime;
|
|
loadCount = 1;
|
|
}
|
|
}
|
|
|
|
if ( !S_LoadSound( sfx ) )
|
|
sfx->defaultSound = qtrue;
|
|
sfx->inMemory = qtrue;
|
|
}
|
|
|
|
|
|
static sfxHandle_t S_Base_RegisterSound( const char* name )
|
|
{
|
|
if (!s_soundStarted) {
|
|
return 0;
|
|
}
|
|
|
|
if ( name[0] == '\0' ) {
|
|
if ( !cls.cgameNewDemoPlayer ) {
|
|
Com_Printf( "Sound name is empty\n" );
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
if ( strlen( name ) >= MAX_QPATH ) {
|
|
Com_Printf( "Sound name exceeds MAX_QPATH\n" );
|
|
return 0;
|
|
}
|
|
|
|
sfx_t* sfx = S_FindName( name );
|
|
if ( sfx->soundData ) {
|
|
if ( sfx->defaultSound ) {
|
|
Com_Printf( S_COLOR_YELLOW "WARNING: could not find %s - using default\n", sfx->soundName );
|
|
return 0;
|
|
}
|
|
return sfx - s_knownSfx;
|
|
}
|
|
|
|
sfx->inMemory = qfalse;
|
|
S_memoryLoad( sfx );
|
|
|
|
if ( sfx->defaultSound ) {
|
|
Com_Printf( S_COLOR_YELLOW "WARNING: could not find %s - using default\n", sfx->soundName );
|
|
return 0;
|
|
}
|
|
|
|
return sfx - s_knownSfx;
|
|
}
|
|
|
|
|
|
static void S_Base_BeginRegistration()
|
|
{
|
|
s_soundMuted = qfalse; // we can play again
|
|
|
|
SND_setup();
|
|
|
|
Com_Memset( s_knownSfx, 0, sizeof( s_knownSfx ) );
|
|
Com_Memset( sfxHash, 0, sizeof(sfx_t*) * SFX_HASH_SIZE );
|
|
|
|
// eat sound 0's slot or the first real sound registered will be lost forever
|
|
s_numSfx = 1;
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////////////////////////
|
|
|
|
|
|
// used for spatializing s_channels (duh, really?)
|
|
|
|
static void S_SpatializeOrigin( const vec3_t origin, int master_vol, int* left_vol, int* right_vol )
|
|
{
|
|
vec_t dot;
|
|
vec_t dist;
|
|
vec_t lscale, rscale;
|
|
vec3_t source_vec;
|
|
vec3_t vec;
|
|
|
|
// calculate stereo seperation and distance attenuation
|
|
VectorSubtract( origin, listener_origin, source_vec );
|
|
dist = VectorNormalize( source_vec );
|
|
|
|
if (dist >= SOUND_MAX_DIST) {
|
|
*left_vol = *right_vol = 0;
|
|
return;
|
|
}
|
|
|
|
dist *= (1.0f / SOUND_MAX_DIST);
|
|
vec_t scale = master_vol * (1.0f - dist);
|
|
|
|
// attenuate correctly even if we can't spatialise
|
|
if (dma.channels == 1) {
|
|
*left_vol = *right_vol = scale;
|
|
return;
|
|
}
|
|
|
|
VectorRotate( source_vec, listener_axis, vec );
|
|
dot = -vec[1];
|
|
|
|
rscale = 0.5 * (1.0 + dot);
|
|
lscale = 0.5 * (1.0 - dot);
|
|
if ( rscale < 0 ) {
|
|
rscale = 0;
|
|
}
|
|
if ( lscale < 0 ) {
|
|
lscale = 0;
|
|
}
|
|
|
|
*right_vol = scale * rscale;
|
|
if (*right_vol < 0)
|
|
*right_vol = 0;
|
|
|
|
*left_vol = scale * lscale;
|
|
if (*left_vol < 0)
|
|
*left_vol = 0;
|
|
}
|
|
|
|
|
|
/*
|
|
validate the parms and queue the sound up
|
|
if origin is NULL, the sound will be dynamically sourced from the entity
|
|
|
|
in theory, entchannel 0 (CHAN_AUTO) will not override a playing sound
|
|
unless it has to, and other channels will automatically override
|
|
|
|
in reality, the code doesn't actually bother
|
|
and EVERYTHING except CHAN_ANNOUNCER is treated as CHAN_AUTO
|
|
|
|
for a "correct" implementation that did actually honor channel numbers:
|
|
|
|
of the 7 per-entity channels:
|
|
LOCAL is in fact NEVER used
|
|
LOCAL_SOUND and ANNOUNCER are implicitly "this client only"
|
|
the remaining 4 are common to all player entities:
|
|
VOICE is used for pain, jump, death, and very little else
|
|
BODY is used exclusively for footsteps and (incorrectly) for holdable item use
|
|
ITEM is used exclusively for PU sounds (quad, suit, and regen)
|
|
WEAPON is used exclusively for firing sounds
|
|
|
|
everything else is just thrown at CHAN_AUTO, whether it's "right" or not
|
|
that includes things like water events, weapon changes, item pickups, and so on
|
|
and auto IS right for most of them, because e.g.
|
|
you can pick up two items in (much) less time than the wavs take to play
|
|
and it's critical that they DO both play (for us, at least - baseq3 is broken as fuck anyway)
|
|
or you could "hide" an armor pickup with an ammobox pickup, etc (doom bfg style :P)
|
|
|
|
so in fact, a "correct" implementation is actually VERY UNdesirable
|
|
not just because of the amount of correction the cgame would need, but conceptually too
|
|
*/
|
|
|
|
static void S_Base_StartSound( const vec3_t origin, int entityNum, int entchannel, sfxHandle_t sfxHandle )
|
|
{
|
|
int i;
|
|
channel_t* ch;
|
|
|
|
if ( !s_soundStarted || s_soundMuted || !sfxHandle ) {
|
|
return;
|
|
}
|
|
|
|
if ( !origin && ( entityNum < 0 || entityNum > MAX_GENTITIES ) ) {
|
|
Com_Error( ERR_DROP, "S_StartSound: bad entitynum %i", entityNum );
|
|
}
|
|
|
|
if ( sfxHandle < 0 || sfxHandle >= s_numSfx ) {
|
|
Com_Printf( S_COLOR_YELLOW "S_StartSound: handle %i out of range\n", sfxHandle );
|
|
return;
|
|
}
|
|
|
|
sfx_t* sfx = &s_knownSfx[ sfxHandle ];
|
|
|
|
if (sfx->inMemory == qfalse) {
|
|
S_memoryLoad(sfx);
|
|
}
|
|
|
|
if ( s_show->integer == 1 ) {
|
|
Com_Printf( "%i : %s\n", s_paintedtime, sfx->soundName );
|
|
}
|
|
|
|
int time = Com_Milliseconds();
|
|
sfx->lastTimeUsed = time;
|
|
|
|
// Com_Printf("playing %s\n", sfx->soundName);
|
|
|
|
// a UNIQUE entity starting the same sound twice in a frame is either a bug,
|
|
// a timedemo, or a shitmap (eg q3ctf4) giving multiple items on spawn.
|
|
// even if you can create a case where it IS "valid", it's still pointless
|
|
// because you implicitly can't DISTINGUISH between the sounds:
|
|
// all that happens is the sound plays at double volume, which is just annoying
|
|
|
|
if ( entityNum != ENTITYNUM_WORLD ) {
|
|
ch = s_channels;
|
|
for ( i = 0; i < MAX_CHANNELS; ++i, ++ch ) {
|
|
if ( (ch->entnum == entityNum) && (ch->allocTime == time) && (ch->thesfx == sfx) ) {
|
|
//Com_Printf("double sound start: %d %s\n", entityNum, sfx->soundName);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
ch = S_ChannelMalloc();
|
|
|
|
if (!ch) {
|
|
// realistically, this will only happen in timedemos,
|
|
// and MAYBE on ubershitty maps (3W, CA, space) with massive PG spam etc
|
|
// so it's really not a priority or even an interesting case
|
|
channel_t* chOldest = NULL;
|
|
|
|
ch = s_channels;
|
|
for ( i = 0; i < MAX_CHANNELS; ++i, ++ch ) {
|
|
if ( (ch->entnum == entityNum) && (ch->entchannel != CHAN_ANNOUNCER) ) {
|
|
if ( !chOldest || (ch->allocTime < chOldest->allocTime) ) {
|
|
chOldest = ch;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!chOldest) {
|
|
// the entity itself has no reusable channels, so use the oldest "world" one
|
|
// which will typically be ambient noise or something equally unimportant
|
|
// like the tail end of an mg impact or whatever from several frames ago
|
|
ch = s_channels;
|
|
for ( i = 0; i < MAX_CHANNELS; ++i, ++ch ) {
|
|
if ( ch->entnum == ENTITYNUM_WORLD ) {
|
|
if ( !chOldest || (ch->allocTime < chOldest->allocTime) ) {
|
|
chOldest = ch;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!chOldest) {
|
|
// no channels of ANY kind free or even reusable?! something is VERY wrong
|
|
//Com_Printf( "dropping sound %s for entity %d\n", sfx->soundName, entityNum );
|
|
return;
|
|
}
|
|
|
|
ch = chOldest;
|
|
ch->allocTime = time;
|
|
}
|
|
|
|
if (origin) {
|
|
VectorCopy( origin, ch->origin );
|
|
ch->fixed_origin = qtrue;
|
|
} else {
|
|
ch->fixed_origin = qfalse;
|
|
}
|
|
|
|
ch->master_vol = MASTER_VOL;
|
|
ch->entnum = entityNum;
|
|
ch->thesfx = sfx;
|
|
ch->startSample = START_SAMPLE_IMMEDIATE;
|
|
ch->entchannel = entchannel;
|
|
ch->leftvol = ch->master_vol; // these will get calced at next spatialize
|
|
ch->rightvol = ch->master_vol; // unless the game isn't running
|
|
}
|
|
|
|
|
|
static void S_Base_StartLocalSound( sfxHandle_t sfxHandle, int channelNum )
|
|
{
|
|
if ( !s_soundStarted || s_soundMuted || !sfxHandle ) {
|
|
return;
|
|
}
|
|
|
|
if ( sfxHandle < 0 || sfxHandle >= s_numSfx ) {
|
|
Com_Printf( S_COLOR_YELLOW "S_StartLocalSound: handle %i out of range\n", sfxHandle );
|
|
return;
|
|
}
|
|
|
|
S_Base_StartSound( NULL, listener_number, channelNum, sfxHandle );
|
|
}
|
|
|
|
|
|
// if we are about to perform file access, clear the buffer
|
|
// so sound doesn't stutter
|
|
|
|
static void S_Base_ClearSoundBuffer()
|
|
{
|
|
if (!s_soundStarted)
|
|
return;
|
|
|
|
// stop looping sounds
|
|
Com_Memset(loopSounds, 0, MAX_GENTITIES*sizeof(loopSound_t));
|
|
Com_Memset(loop_channels, 0, MAX_CHANNELS*sizeof(channel_t));
|
|
numLoopChannels = 0;
|
|
|
|
S_ChannelSetup();
|
|
|
|
s_rawend = 0;
|
|
|
|
Sys_S_BeginPainting();
|
|
if (dma.buffer) {
|
|
int clear = (dma.samplebits == 8) ? 0x80 : 0x00;
|
|
Com_Memset( dma.buffer, clear, dma.samples * dma.samplebits/8 );
|
|
}
|
|
Sys_S_Submit();
|
|
}
|
|
|
|
|
|
static void S_Base_StopAllSounds()
|
|
{
|
|
if ( !s_soundStarted ) {
|
|
return;
|
|
}
|
|
|
|
// stop the background music
|
|
S_Base_StopBackgroundTrack();
|
|
|
|
S_Base_ClearSoundBuffer();
|
|
}
|
|
|
|
|
|
// disables sounds until the next S_BeginRegistration
|
|
// this is called when the hunk is cleared and the sounds are no longer valid
|
|
|
|
static void S_Base_DisableSounds()
|
|
{
|
|
S_Base_StopAllSounds();
|
|
s_soundMuted = qtrue;
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////////////////////////
|
|
|
|
// continuous looping sounds are added each frame by cgame
|
|
|
|
|
|
static void S_Base_ClearLoopingSounds()
|
|
{
|
|
for (int i = 0; i < MAX_GENTITIES; ++i)
|
|
loopSounds[i].active = qfalse;
|
|
numLoopChannels = 0;
|
|
}
|
|
|
|
|
|
// called during entity generation for a frame
|
|
|
|
static void S_Base_AddLoopingSound( int entityNum, const vec3_t origin, sfxHandle_t sfxHandle )
|
|
{
|
|
if ( !s_soundStarted || s_soundMuted || !sfxHandle ) {
|
|
return;
|
|
}
|
|
|
|
if ( sfxHandle < 0 || sfxHandle >= s_numSfx ) {
|
|
Com_Printf( S_COLOR_YELLOW "S_AddLoopingSound: handle %i out of range\n", sfxHandle );
|
|
return;
|
|
}
|
|
|
|
sfx_t* sfx = &s_knownSfx[ sfxHandle ];
|
|
|
|
if (sfx->inMemory == qfalse) {
|
|
S_memoryLoad( sfx );
|
|
}
|
|
|
|
if ( !sfx->soundLength ) {
|
|
Com_Error( ERR_DROP, "%s has length 0", sfx->soundName );
|
|
}
|
|
|
|
VectorCopy( origin, loopSounds[entityNum].origin );
|
|
loopSounds[entityNum].sfx = sfx;
|
|
loopSounds[entityNum].active = qtrue;
|
|
}
|
|
|
|
|
|
/*
|
|
Spatialize all of the looping (ie ambient) sounds.
|
|
All sounds are on the same cycle, so any duplicates can just
|
|
sum up the channel multipliers.
|
|
*/
|
|
static void S_AddLoopSounds()
|
|
{
|
|
int i, j, time;
|
|
int left_total, right_total, left, right;
|
|
channel_t *ch;
|
|
loopSound_t *loop, *loop2;
|
|
static int loopFrame;
|
|
|
|
numLoopChannels = 0;
|
|
|
|
time = Com_Milliseconds();
|
|
|
|
loopFrame++;
|
|
for ( i = 0; (i < MAX_GENTITIES) && (numLoopChannels < MAX_CHANNELS); ++i) {
|
|
loop = &loopSounds[i];
|
|
if ( !loop->active || loop->mergeFrame == loopFrame ) {
|
|
continue; // already merged into an earlier sound
|
|
}
|
|
|
|
S_SpatializeOrigin( loop->origin, MASTER_VOL, &left_total, &right_total );
|
|
|
|
loop->sfx->lastTimeUsed = time;
|
|
|
|
for (j=(i+1); j< MAX_GENTITIES ; j++) {
|
|
loop2 = &loopSounds[j];
|
|
if ( !loop2->active || (loop2->sfx != loop->sfx) ) {
|
|
continue;
|
|
}
|
|
loop2->mergeFrame = loopFrame;
|
|
|
|
S_SpatializeOrigin( loop2->origin, MASTER_VOL, &left, &right );
|
|
|
|
loop2->sfx->lastTimeUsed = time;
|
|
left_total += left;
|
|
right_total += right;
|
|
}
|
|
if (left_total == 0 && right_total == 0) {
|
|
continue; // not audible
|
|
}
|
|
|
|
// allocate a channel
|
|
ch = &loop_channels[numLoopChannels];
|
|
|
|
if (left_total > 255) {
|
|
left_total = 255;
|
|
}
|
|
if (right_total > 255) {
|
|
right_total = 255;
|
|
}
|
|
|
|
ch->master_vol = MASTER_VOL;
|
|
ch->leftvol = left_total;
|
|
ch->rightvol = right_total;
|
|
ch->thesfx = loop->sfx;
|
|
numLoopChannels++;
|
|
}
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////////////////////////
|
|
|
|
|
|
// music streaming
|
|
|
|
static void S_Base_RawSamples( int samples, int rate, int width, int channels, const byte *data, float volume )
|
|
{
|
|
int i;
|
|
int src, dst;
|
|
float scale;
|
|
int intVolume;
|
|
|
|
if ( !s_soundStarted || s_soundMuted ) {
|
|
return;
|
|
}
|
|
|
|
intVolume = 256 * volume * s_volume->value;
|
|
|
|
if ( s_rawend < s_soundtime ) {
|
|
Com_DPrintf( "S_RawSamples: resetting minimum: %i < %i\n", s_rawend, s_soundtime );
|
|
s_rawend = s_soundtime;
|
|
}
|
|
|
|
scale = (float)rate / dma.speed;
|
|
|
|
//Com_Printf ("%i < %i < %i\n", s_soundtime, s_paintedtime, s_rawend);
|
|
if (channels == 2 && width == 2)
|
|
{
|
|
if (scale == 1.0)
|
|
{ // optimized case
|
|
for (i=0 ; i<samples ; i++)
|
|
{
|
|
dst = s_rawend&(MAX_RAW_SAMPLES-1);
|
|
s_rawend++;
|
|
s_rawsamples[dst].left = ((short *)data)[i*2] * intVolume;
|
|
s_rawsamples[dst].right = ((short *)data)[i*2+1] * intVolume;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (i=0 ; ; i++)
|
|
{
|
|
src = i*scale;
|
|
if (src >= samples)
|
|
break;
|
|
dst = s_rawend&(MAX_RAW_SAMPLES-1);
|
|
s_rawend++;
|
|
s_rawsamples[dst].left = ((short *)data)[src*2] * intVolume;
|
|
s_rawsamples[dst].right = ((short *)data)[src*2+1] * intVolume;
|
|
}
|
|
}
|
|
}
|
|
else if (channels == 1 && width == 2)
|
|
{
|
|
for (i=0 ; ; i++)
|
|
{
|
|
src = i*scale;
|
|
if (src >= samples)
|
|
break;
|
|
dst = s_rawend&(MAX_RAW_SAMPLES-1);
|
|
s_rawend++;
|
|
s_rawsamples[dst].left = ((short *)data)[src] * intVolume;
|
|
s_rawsamples[dst].right = ((short *)data)[src] * intVolume;
|
|
}
|
|
}
|
|
else if (channels == 2 && width == 1)
|
|
{
|
|
intVolume *= 256;
|
|
|
|
for (i=0 ; ; i++)
|
|
{
|
|
src = i*scale;
|
|
if (src >= samples)
|
|
break;
|
|
dst = s_rawend&(MAX_RAW_SAMPLES-1);
|
|
s_rawend++;
|
|
s_rawsamples[dst].left = ((char *)data)[src*2] * intVolume;
|
|
s_rawsamples[dst].right = ((char *)data)[src*2+1] * intVolume;
|
|
}
|
|
}
|
|
else if (channels == 1 && width == 1)
|
|
{
|
|
intVolume *= 256;
|
|
|
|
for (i=0 ; ; i++)
|
|
{
|
|
src = i*scale;
|
|
if (src >= samples)
|
|
break;
|
|
dst = s_rawend&(MAX_RAW_SAMPLES-1);
|
|
s_rawend++;
|
|
s_rawsamples[dst].left = (((byte *)data)[src]-128) * intVolume;
|
|
s_rawsamples[dst].right = (((byte *)data)[src]-128) * intVolume;
|
|
}
|
|
}
|
|
|
|
if ( s_rawend > s_soundtime + MAX_RAW_SAMPLES ) {
|
|
Com_DPrintf( "S_RawSamples: overflowed %i > %i\n", s_rawend, s_soundtime );
|
|
}
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////////////////////////
|
|
|
|
|
|
// let the sound system know where an entity currently is
|
|
|
|
static void S_Base_UpdateEntityPosition( int entityNum, const vec3_t origin )
|
|
{
|
|
if ( entityNum < 0 || entityNum > MAX_GENTITIES ) {
|
|
Com_Error( ERR_DROP, "S_UpdateEntityPosition: bad entitynum %i", entityNum );
|
|
}
|
|
VectorCopy( origin, loopSounds[entityNum].origin );
|
|
}
|
|
|
|
|
|
// change the volumes of all the playing sounds for changes in their positions
|
|
|
|
static void S_Base_Respatialize( int entityNum, const vec3_t head, const vec3_t axis[3], int inwater )
|
|
{
|
|
vec3_t origin;
|
|
|
|
if ( !s_soundStarted || s_soundMuted ) {
|
|
return;
|
|
}
|
|
|
|
listener_number = entityNum;
|
|
VectorCopy( head, listener_origin );
|
|
VectorCopy( axis[0], listener_axis[0] );
|
|
VectorCopy( axis[1], listener_axis[1] );
|
|
VectorCopy( axis[2], listener_axis[2] );
|
|
|
|
// update spatialization for dynamic sounds
|
|
channel_t* ch = s_channels;
|
|
for (int i = 0; i < MAX_CHANNELS; ++i, ++ch) {
|
|
if ( !ch->thesfx ) {
|
|
continue;
|
|
}
|
|
// anything coming from the view entity will always be full volume
|
|
if (ch->entnum == listener_number) {
|
|
ch->leftvol = ch->master_vol;
|
|
ch->rightvol = ch->master_vol;
|
|
} else {
|
|
if (ch->fixed_origin) {
|
|
VectorCopy( ch->origin, origin );
|
|
} else {
|
|
VectorCopy( loopSounds[ ch->entnum ].origin, origin );
|
|
}
|
|
S_SpatializeOrigin( origin, ch->master_vol, &ch->leftvol, &ch->rightvol );
|
|
}
|
|
}
|
|
|
|
S_AddLoopSounds();
|
|
}
|
|
|
|
|
|
// returns true if any new sounds were started since the last mix
|
|
// and clears out any expired sounds
|
|
|
|
static qbool S_ScanChannelStarts()
|
|
{
|
|
qbool newSamples = qfalse;
|
|
|
|
channel_t* ch = s_channels;
|
|
for (int i = 0; i < MAX_CHANNELS; ++i, ++ch) {
|
|
if ( !ch->thesfx ) {
|
|
continue;
|
|
}
|
|
|
|
// if this channel was just started this frame,
|
|
// set the sample count so it begins mixing
|
|
// into the very first sample
|
|
if ( ch->startSample == START_SAMPLE_IMMEDIATE ) {
|
|
ch->startSample = s_paintedtime;
|
|
newSamples = qtrue;
|
|
continue;
|
|
}
|
|
|
|
// if it is completely finished by now, clear it
|
|
if ( ch->startSample + ch->thesfx->soundLength <= s_paintedtime ) {
|
|
S_ChannelFree(ch);
|
|
}
|
|
}
|
|
|
|
return newSamples;
|
|
}
|
|
|
|
|
|
// called once each time through the main loop
|
|
|
|
static void S_Base_Update()
|
|
{
|
|
if ( !s_soundStarted || s_soundMuted ) {
|
|
return;
|
|
}
|
|
|
|
if ( s_show->integer == 2 ) {
|
|
int total = 0;
|
|
const channel_t* ch = s_channels;
|
|
for (int i = 0; i < MAX_CHANNELS; ++i, ++ch) {
|
|
if (ch->thesfx && (ch->leftvol || ch->rightvol) ) {
|
|
Com_Printf( "%3i %3i %s\n", ch->leftvol, ch->rightvol, ch->thesfx->soundName );
|
|
total++;
|
|
}
|
|
}
|
|
//Com_Printf( "----(%i)---- painted: %i\n", total, s_paintedtime );
|
|
}
|
|
|
|
// add raw data from streamed samples
|
|
S_UpdateBackgroundTrack();
|
|
|
|
// mix some sound
|
|
S_Update_DMA();
|
|
}
|
|
|
|
|
|
static void S_GetSoundtime()
|
|
{
|
|
static int buffers;
|
|
static int oldsamplepos;
|
|
|
|
int fullsamples = dma.samples / dma.channels;
|
|
|
|
if ( CL_VideoRecording() ) {
|
|
s_soundtime += (int)ceil( dma.speed / cl_aviFrameRate->value );
|
|
return;
|
|
}
|
|
|
|
// it is possible to miscount buffers if it has wrapped twice between
|
|
// calls to S_Update. Oh well.
|
|
int samplepos = Sys_S_GetDMAPos();
|
|
if (samplepos < oldsamplepos) {
|
|
buffers++; // buffer wrapped
|
|
if (s_paintedtime > 0x40000000) {
|
|
// time to chop things off to avoid 32 bit limits
|
|
buffers = 0;
|
|
s_paintedtime = fullsamples;
|
|
S_Base_StopAllSounds();
|
|
}
|
|
}
|
|
oldsamplepos = samplepos;
|
|
|
|
s_soundtime = buffers*fullsamples + samplepos/dma.channels;
|
|
|
|
#if 0
|
|
// check to make sure that we haven't overshot
|
|
if (s_paintedtime < s_soundtime) {
|
|
Com_DPrintf( "S_GetSoundtime : overflow\n" );
|
|
s_paintedtime = s_soundtime;
|
|
}
|
|
#endif
|
|
|
|
if ( dma.submission_chunk < 256 ) {
|
|
/* NOTE: this is SO wrong - it SHOULD just be s_soundtime
|
|
because this is essentially lying and saying it's filled a piece of the DMA
|
|
that it absolutely hasn't, so that piece is only valid because of mixahead
|
|
but for some insane reason, NOT doing this causes garbled sound on SOME machines
|
|
(even though the buffer data is still valid because of the previous frame's overfill)
|
|
so i'm at a loss to explain how this could possibly be required, other than
|
|
"something ELSE in the sound code is fucked up in a complementary way", or it's
|
|
a driver problem. either way, i can't repro it so we're just stuck with the hack
|
|
*/
|
|
s_paintedtime = s_soundtime + s_mixPreStep->value * dma.speed;
|
|
} else {
|
|
s_paintedtime = s_soundtime + dma.submission_chunk;
|
|
}
|
|
}
|
|
|
|
|
|
static void S_Update_DMA()
|
|
{
|
|
static int prevSoundtime = -1;
|
|
|
|
if ( !s_soundStarted || s_soundMuted ) {
|
|
return;
|
|
}
|
|
|
|
S_GetSoundtime();
|
|
|
|
if (s_soundtime == prevSoundtime) {
|
|
return;
|
|
}
|
|
prevSoundtime = s_soundtime;
|
|
|
|
// clear any sound effects that end before the current time,
|
|
// and start any new sounds
|
|
S_ScanChannelStarts();
|
|
|
|
// mix ahead of current position
|
|
unsigned endtime = s_soundtime + s_mixahead->value * dma.speed;
|
|
|
|
// mix to an even submission block size
|
|
endtime = (endtime + dma.submission_chunk-1) & ~(dma.submission_chunk-1);
|
|
|
|
// never mix more than the complete buffer
|
|
int samps = dma.samples >> (dma.channels-1);
|
|
if (endtime - s_soundtime > samps)
|
|
endtime = s_soundtime + samps;
|
|
|
|
Sys_S_BeginPainting();
|
|
|
|
S_PaintChannels( endtime );
|
|
|
|
Sys_S_Submit();
|
|
}
|
|
|
|
|
|
/*
|
|
===============================================================================
|
|
|
|
background music functions
|
|
|
|
===============================================================================
|
|
*/
|
|
|
|
|
|
static void S_Base_StopBackgroundTrack()
|
|
{
|
|
if(!s_backgroundStream)
|
|
return;
|
|
S_CodecCloseStream(s_backgroundStream);
|
|
s_backgroundStream = NULL;
|
|
s_rawend = 0;
|
|
}
|
|
|
|
|
|
static void S_Base_StartBackgroundTrack( const char *intro, const char *loop )
|
|
{
|
|
if ( !intro ) {
|
|
intro = "";
|
|
}
|
|
if ( !loop || !loop[0] ) {
|
|
loop = intro;
|
|
}
|
|
Com_DPrintf( "S_StartBackgroundTrack( %s, %s )\n", intro, loop );
|
|
|
|
if ( !intro[0] ) {
|
|
return;
|
|
}
|
|
|
|
if( !loop ) {
|
|
s_backgroundLoop[0] = 0;
|
|
} else {
|
|
Q_strncpyz( s_backgroundLoop, loop, sizeof( s_backgroundLoop ) );
|
|
}
|
|
|
|
// close the background track, but DON'T reset s_rawend
|
|
// if restarting the same background track
|
|
if (s_backgroundStream) {
|
|
S_CodecCloseStream(s_backgroundStream);
|
|
s_backgroundStream = NULL;
|
|
}
|
|
|
|
// Open stream
|
|
s_backgroundStream = S_CodecOpenStream(intro);
|
|
if (!s_backgroundStream) {
|
|
Com_Printf( S_COLOR_YELLOW "WARNING: couldn't open music file %s\n", intro );
|
|
return;
|
|
}
|
|
|
|
if (s_backgroundStream->info.channels != 2 || s_backgroundStream->info.rate != 22050) {
|
|
Com_Printf(S_COLOR_YELLOW "WARNING: music file %s is not 22k stereo\n", intro );
|
|
}
|
|
}
|
|
|
|
|
|
static void S_UpdateBackgroundTrack()
|
|
{
|
|
static float musicVolume = 0;
|
|
|
|
int bufferSamples;
|
|
int fileSamples;
|
|
byte raw[30000]; // just enough to fit in a mac stack frame
|
|
int fileBytes;
|
|
int r;
|
|
|
|
if (!s_backgroundStream) {
|
|
return;
|
|
}
|
|
|
|
// fade music in or out if the volume changes (mainly for to/from 0)
|
|
musicVolume = (musicVolume + (s_musicVolume->value * 2)) / 4.0f;
|
|
|
|
// don't bother playing anything if musicvolume is 0
|
|
if ( musicVolume <= 0 ) {
|
|
return;
|
|
}
|
|
|
|
// see how many samples should be copied into the raw buffer
|
|
if ( s_rawend < s_soundtime ) {
|
|
s_rawend = s_soundtime;
|
|
}
|
|
|
|
while ( s_rawend < s_soundtime + MAX_RAW_SAMPLES ) {
|
|
bufferSamples = MAX_RAW_SAMPLES - (s_rawend - s_soundtime);
|
|
|
|
// decide how much data needs to be read from the file
|
|
fileSamples = bufferSamples * s_backgroundStream->info.rate / dma.speed;
|
|
|
|
// fileSamples can become 0 when dma.speed > s_backgroundStream->info.rate
|
|
if ( fileSamples <= 0 ) {
|
|
break;
|
|
}
|
|
|
|
// our max buffer size
|
|
fileBytes = fileSamples * (s_backgroundStream->info.width * s_backgroundStream->info.channels);
|
|
if ( fileBytes > sizeof(raw) ) {
|
|
fileBytes = sizeof(raw);
|
|
fileSamples = fileBytes / (s_backgroundStream->info.width * s_backgroundStream->info.channels);
|
|
}
|
|
|
|
r = S_CodecReadStream( s_backgroundStream, fileBytes, raw );
|
|
if (r < fileBytes) {
|
|
fileBytes = r;
|
|
fileSamples = r / (s_backgroundStream->info.width * s_backgroundStream->info.channels);
|
|
}
|
|
|
|
if(r > 0)
|
|
{
|
|
// add to raw buffer
|
|
S_Base_RawSamples( fileSamples, s_backgroundStream->info.rate,
|
|
s_backgroundStream->info.width, s_backgroundStream->info.channels, raw, musicVolume );
|
|
}
|
|
else
|
|
{
|
|
// loop
|
|
if(s_backgroundLoop[0])
|
|
{
|
|
S_CodecCloseStream(s_backgroundStream);
|
|
s_backgroundStream = NULL;
|
|
S_Base_StartBackgroundTrack( s_backgroundLoop, s_backgroundLoop );
|
|
if(!s_backgroundStream)
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
S_Base_StopBackgroundTrack();
|
|
return;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
|
|
qbool S_FreeOldestSound()
|
|
{
|
|
sfx_t* sfx;
|
|
int oldest = Com_Milliseconds();
|
|
int slot = 0;
|
|
|
|
for (int i = 1; i < s_numSfx; ++i) {
|
|
sfx = &s_knownSfx[i];
|
|
if (sfx->inMemory && sfx->lastTimeUsed < oldest) {
|
|
slot = i;
|
|
oldest = sfx->lastTimeUsed;
|
|
}
|
|
}
|
|
|
|
if (slot == 0)
|
|
return qfalse;
|
|
|
|
sfx = &s_knownSfx[slot];
|
|
|
|
Com_DPrintf( "S_FreeOldestSound: freeing sound %s\n", sfx->soundName );
|
|
|
|
sndBuffer* buffer = sfx->soundData;
|
|
while (buffer) {
|
|
sndBuffer* next = buffer->next;
|
|
SND_free(buffer);
|
|
buffer = next;
|
|
}
|
|
|
|
sfx->inMemory = qfalse;
|
|
sfx->soundData = NULL;
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
|
|
static void S_Base_Shutdown()
|
|
{
|
|
if ( !s_soundStarted ) {
|
|
return;
|
|
}
|
|
|
|
Sys_S_Shutdown();
|
|
|
|
s_soundStarted = qfalse;
|
|
|
|
Cmd_RemoveCommand( "s_info" );
|
|
}
|
|
|
|
|
|
static const cvarTableItem_t cl_cvars[] =
|
|
{
|
|
{ &s_mixahead, "s_mixahead", "0.2", CVAR_ARCHIVE, CVART_FLOAT },
|
|
{ &s_mixPreStep, "s_mixPreStep", "0.05", CVAR_ARCHIVE, CVART_FLOAT },
|
|
{ &s_show, "s_show", "0", CVAR_CHEAT, CVART_INTEGER, "0", "2" },
|
|
{ &s_testsound, "s_testsound", "0", CVAR_CHEAT, CVART_BOOL }
|
|
};
|
|
|
|
|
|
qbool S_Base_Init( soundInterface_t *si )
|
|
{
|
|
Com_Memset( si, 0, sizeof(*si) );
|
|
|
|
Cvar_RegisterArray( cl_cvars, MODULE_SOUND );
|
|
|
|
if (!Sys_S_Init())
|
|
return qfalse;
|
|
|
|
s_soundStarted = qtrue;
|
|
s_soundMuted = qtrue;
|
|
s_numSfx = 0;
|
|
|
|
Com_Memset( sfxHash, 0, sizeof(sfx_t *)*SFX_HASH_SIZE );
|
|
|
|
s_soundtime = 0;
|
|
s_paintedtime = 0;
|
|
|
|
si->Shutdown = S_Base_Shutdown;
|
|
si->StartSound = S_Base_StartSound;
|
|
si->StartLocalSound = S_Base_StartLocalSound;
|
|
si->StartBackgroundTrack = S_Base_StartBackgroundTrack;
|
|
si->StopBackgroundTrack = S_Base_StopBackgroundTrack;
|
|
si->RawSamples = S_Base_RawSamples;
|
|
si->StopAllSounds = S_Base_StopAllSounds;
|
|
si->ClearLoopingSounds = S_Base_ClearLoopingSounds;
|
|
si->AddLoopingSound = S_Base_AddLoopingSound;
|
|
si->Respatialize = S_Base_Respatialize;
|
|
si->UpdateEntityPosition = S_Base_UpdateEntityPosition;
|
|
si->Update = S_Base_Update;
|
|
si->DisableSounds = S_Base_DisableSounds;
|
|
si->BeginRegistration = S_Base_BeginRegistration;
|
|
si->RegisterSound = S_Base_RegisterSound;
|
|
si->ClearSoundBuffer = S_Base_ClearSoundBuffer;
|
|
si->SoundInfo = S_Base_SoundInfo;
|
|
si->SoundList = S_Base_SoundList;
|
|
|
|
S_Base_StopAllSounds();
|
|
|
|
return qtrue;
|
|
}
|