/* =========================================================================== 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; cvar_t *s_khz; static loopSound_t loopSounds[MAX_GENTITIES]; static channel_t *freelist = NULL; int s_rawend; portable_samplepair_t s_rawsamples[MAX_RAW_SAMPLES]; #if defined( _WIN32 ) #define S_KHZ_DEFAULT "22" #else #define S_KHZ_DEFAULT "44" #endif 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) 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 }, { &s_khz, "s_khz", S_KHZ_DEFAULT, CVAR_ARCHIVE | CVAR_LATCH, CVART_INTEGER, "22", "44" } }; 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; }