/* =========================================================================== Copyright (C) 1999-2005 Id Software, Inc. Copyright (C) 2000-2006 Tim Angus Copyright (C) 2005 Stuart Dalton (badcdev@gmail.com) This file is part of Tremulous. Tremulous 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. Tremulous 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 Tremulous; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA =========================================================================== */ #include "snd_local.h" #include "snd_codec.h" #include "client.h" #if USE_OPENAL #include "qal.h" // Console variables specific to OpenAL cvar_t *s_alPrecache; cvar_t *s_alGain; cvar_t *s_alSources; cvar_t *s_alDopplerFactor; cvar_t *s_alDopplerSpeed; cvar_t *s_alMinDistance; cvar_t *s_alRolloff; cvar_t *s_alDriver; cvar_t *s_alMaxSpeakerDistance; cvar_t *s_alSpatEntOrigin; /* ================= S_AL_Format ================= */ static ALuint S_AL_Format(int width, int channels) { ALuint format = AL_FORMAT_MONO16; // Work out format if(width == 1) { if(channels == 1) format = AL_FORMAT_MONO8; else if(channels == 2) format = AL_FORMAT_STEREO8; } else if(width == 2) { if(channels == 1) format = AL_FORMAT_MONO16; else if(channels == 2) format = AL_FORMAT_STEREO16; } return format; } /* ================= S_AL_ErrorMsg ================= */ static const char *S_AL_ErrorMsg(ALenum error) { switch(error) { case AL_NO_ERROR: return "No error"; case AL_INVALID_NAME: return "Invalid name"; case AL_INVALID_ENUM: return "Invalid enumerator"; case AL_INVALID_VALUE: return "Invalid value"; case AL_INVALID_OPERATION: return "Invalid operation"; case AL_OUT_OF_MEMORY: return "Out of memory"; default: return "Unknown error"; } } //=========================================================================== typedef struct alSfx_s { char filename[MAX_QPATH]; ALuint buffer; // OpenAL buffer qboolean isDefault; // Couldn't be loaded - use default FX qboolean inMemory; // Sound is stored in memory qboolean isLocked; // Sound is locked (can not be unloaded) int lastUsedTime; // Time last used } alSfx_t; static qboolean alBuffersInitialised = qfalse; // Sound effect storage, data structures #define MAX_SFX 4096 static alSfx_t knownSfx[MAX_SFX]; static int numSfx = 0; static sfxHandle_t default_sfx; /* ================= S_AL_BufferFindFree Find a free handle ================= */ static sfxHandle_t S_AL_BufferFindFree( void ) { int i; for(i = 0; i < MAX_SFX; i++) { // Got one if(knownSfx[i].filename[0] == '\0') return i; } // Shit... Com_Error(ERR_FATAL, "S_AL_BufferFindFree: No free sound handles"); return -1; } /* ================= S_AL_BufferFind Find a sound effect if loaded, set up a handle otherwise ================= */ static sfxHandle_t S_AL_BufferFind(const char *filename) { // Look it up in the table sfxHandle_t sfx = -1; int i; for(i = 0; i < MAX_SFX; i++) { if(!Q_stricmp(knownSfx[i].filename, filename)) { sfx = i; break; } } // Not found in table? if(sfx == -1) { alSfx_t *ptr; sfx = S_AL_BufferFindFree(); // Clear and copy the filename over ptr = &knownSfx[sfx]; memset(ptr, 0, sizeof(*ptr)); strcpy(ptr->filename, filename); } // Return the handle return sfx; } /* ================= S_AL_BufferUseDefault ================= */ static void S_AL_BufferUseDefault(sfxHandle_t sfx) { if(sfx == default_sfx) Com_Error(ERR_FATAL, "Can't load default sound effect %s\n", knownSfx[sfx].filename); Com_Printf( S_COLOR_YELLOW "WARNING: Using default sound for %s\n", knownSfx[sfx].filename); knownSfx[sfx].isDefault = qtrue; knownSfx[sfx].buffer = knownSfx[default_sfx].buffer; } /* ================= S_AL_BufferUnload ================= */ static void S_AL_BufferUnload(sfxHandle_t sfx) { ALenum error; if(knownSfx[sfx].filename[0] == '\0') return; if(!knownSfx[sfx].inMemory) return; // Delete it qalDeleteBuffers(1, &knownSfx[sfx].buffer); if((error = qalGetError()) != AL_NO_ERROR) Com_Printf( S_COLOR_RED "ERROR: Can't delete sound buffer for %s\n", knownSfx[sfx].filename); knownSfx[sfx].inMemory = qfalse; } /* ================= S_AL_BufferEvict ================= */ static qboolean S_AL_BufferEvict( void ) { int i, oldestBuffer = -1; int oldestTime = Sys_Milliseconds( ); for( i = 0; i < MAX_SFX; i++ ) { if( !knownSfx[ i ].filename[ 0 ] ) continue; if( !knownSfx[ i ].inMemory ) continue; if( knownSfx[ i ].lastUsedTime < oldestTime ) { oldestTime = knownSfx[ i ].lastUsedTime; oldestBuffer = i; } } if( oldestBuffer >= 0 ) { S_AL_BufferUnload( oldestBuffer ); return qtrue; } else return qfalse; } /* ================= S_AL_BufferLoad ================= */ static void S_AL_BufferLoad(sfxHandle_t sfx) { ALenum error; void *data; snd_info_t info; ALuint format; // Nothing? if(knownSfx[sfx].filename[0] == '\0') return; // Player SFX if(knownSfx[sfx].filename[0] == '*') return; // Already done? if((knownSfx[sfx].inMemory) || (knownSfx[sfx].isDefault)) return; // Try to load data = S_CodecLoad(knownSfx[sfx].filename, &info); if(!data) { S_AL_BufferUseDefault(sfx); return; } format = S_AL_Format(info.width, info.channels); // Create a buffer qalGenBuffers(1, &knownSfx[sfx].buffer); if((error = qalGetError()) != AL_NO_ERROR) { S_AL_BufferUseDefault(sfx); Z_Free(data); Com_Printf( S_COLOR_RED "ERROR: Can't create a sound buffer for %s - %s\n", knownSfx[sfx].filename, S_AL_ErrorMsg(error)); return; } // Fill the buffer if( info.size == 0 ) { // We have no data to buffer, so buffer silence byte dummyData[ 2 ] = { 0 }; qalBufferData(knownSfx[sfx].buffer, AL_FORMAT_MONO16, (void *)dummyData, 2, 22050); } else qalBufferData(knownSfx[sfx].buffer, format, data, info.size, info.rate); error = qalGetError(); // If we ran out of memory, start evicting the least recently used sounds while(error == AL_OUT_OF_MEMORY) { if( !S_AL_BufferEvict( ) ) { S_AL_BufferUseDefault(sfx); Z_Free(data); Com_Printf( S_COLOR_RED "ERROR: Out of memory loading %s\n", knownSfx[sfx].filename); return; } // Try load it again qalBufferData(knownSfx[sfx].buffer, format, data, info.size, info.rate); error = qalGetError(); } // Some other error condition if(error != AL_NO_ERROR) { S_AL_BufferUseDefault(sfx); Z_Free(data); Com_Printf( S_COLOR_RED "ERROR: Can't fill sound buffer for %s - %s\n", knownSfx[sfx].filename, S_AL_ErrorMsg(error)); return; } // Free the memory Z_Free(data); // Woo! knownSfx[sfx].inMemory = qtrue; } /* ================= S_AL_BufferUse ================= */ static void S_AL_BufferUse(sfxHandle_t sfx) { if(knownSfx[sfx].filename[0] == '\0') return; if((!knownSfx[sfx].inMemory) && (!knownSfx[sfx].isDefault)) S_AL_BufferLoad(sfx); knownSfx[sfx].lastUsedTime = Sys_Milliseconds(); } /* ================= S_AL_BufferInit ================= */ static qboolean S_AL_BufferInit( void ) { if(alBuffersInitialised) return qtrue; // Clear the hash table, and SFX table memset(knownSfx, 0, sizeof(knownSfx)); numSfx = 0; // Load the default sound, and lock it default_sfx = S_AL_BufferFind("sound/feedback/hit.wav"); S_AL_BufferUse(default_sfx); knownSfx[default_sfx].isLocked = qtrue; // All done alBuffersInitialised = qtrue; return qtrue; } /* ================= S_AL_BufferShutdown ================= */ static void S_AL_BufferShutdown( void ) { int i; if(!alBuffersInitialised) return; // Unlock the default sound effect knownSfx[default_sfx].isLocked = qfalse; // Free all used effects for(i = 0; i < MAX_SFX; i++) S_AL_BufferUnload(i); // Clear the tables memset(knownSfx, 0, sizeof(knownSfx)); // All undone alBuffersInitialised = qfalse; } /* ================= S_AL_RegisterSound ================= */ static sfxHandle_t S_AL_RegisterSound( const char *sample, qboolean compressed ) { sfxHandle_t sfx = S_AL_BufferFind(sample); if( s_alPrecache->integer && (!knownSfx[sfx].inMemory) && (!knownSfx[sfx].isDefault)) S_AL_BufferLoad(sfx); knownSfx[sfx].lastUsedTime = Com_Milliseconds(); return sfx; } /* ================= S_AL_BufferGet Return's an sfx's buffer ================= */ static ALuint S_AL_BufferGet(sfxHandle_t sfx) { return knownSfx[sfx].buffer; } //=========================================================================== typedef struct src_s { ALuint alSource; // OpenAL source object sfxHandle_t sfx; // Sound effect in use int lastUsedTime; // Last time used alSrcPriority_t priority; // Priority int entity; // Owning entity (-1 if none) int channel; // Associated channel (-1 if none) int isActive; // Is this source currently in use? int isLocked; // This is locked (un-allocatable) int isLooping; // Is this a looping effect (attached to an entity) int isTracking; // Is this object tracking it's owner qboolean local; // Is this local (relative to the cam) } src_t; #define MAX_SRC 128 static src_t srcList[MAX_SRC]; static int srcCount = 0; static qboolean alSourcesInitialised = qfalse; static vec3_t lastListenerOrigin = { 0.0f, 0.0f, 0.0f }; typedef struct sentity_s { vec3_t origin; int srcAllocated; // If a src_t has been allocated to this entity int srcIndex; qboolean loopAddedThisFrame; alSrcPriority_t loopPriority; sfxHandle_t loopSfx; qboolean startLoopingSound; } sentity_t; static sentity_t entityList[MAX_GENTITIES]; /* ================= S_AL_SanitiseVector ================= */ #define S_AL_SanitiseVector(v) _S_AL_SanitiseVector(v,__LINE__) static void _S_AL_SanitiseVector( vec3_t v, int line ) { if( Q_isnan( v[ 0 ] ) || Q_isnan( v[ 1 ] ) || Q_isnan( v[ 2 ] ) ) { Com_DPrintf( S_COLOR_YELLOW "WARNING: vector with one or more NaN components " "being passed to OpenAL at %s:%d -- zeroing\n", __FILE__, line ); VectorClear( v ); } } /* ================= S_AL_SrcInit ================= */ static qboolean S_AL_SrcInit( void ) { int i; int limit; ALenum error; // Clear the sources data structure memset(srcList, 0, sizeof(srcList)); srcCount = 0; // Cap s_alSources to MAX_SRC limit = s_alSources->integer; if(limit > MAX_SRC) limit = MAX_SRC; else if(limit < 16) limit = 16; // Allocate as many sources as possible for(i = 0; i < limit; i++) { qalGenSources(1, &srcList[i].alSource); if((error = qalGetError()) != AL_NO_ERROR) break; srcCount++; } // All done. Print this for informational purposes Com_Printf( "Allocated %d sources.\n", srcCount); alSourcesInitialised = qtrue; return qtrue; } /* ================= S_AL_SrcShutdown ================= */ static void S_AL_SrcShutdown( void ) { int i; if(!alSourcesInitialised) return; // Destroy all the sources for(i = 0; i < srcCount; i++) { if(srcList[i].isLocked) Com_DPrintf( S_COLOR_YELLOW "WARNING: Source %d is locked\n", i); qalSourceStop(srcList[i].alSource); qalDeleteSources(1, &srcList[i].alSource); } memset(srcList, 0, sizeof(srcList)); alSourcesInitialised = qfalse; } /* ================= S_AL_SrcSetup ================= */ static void S_AL_SrcSetup(srcHandle_t src, sfxHandle_t sfx, alSrcPriority_t priority, int entity, int channel, qboolean local) { ALuint buffer; float null_vector[] = {0, 0, 0}; // Mark the SFX as used, and grab the raw AL buffer S_AL_BufferUse(sfx); buffer = S_AL_BufferGet(sfx); // Set up src struct srcList[src].lastUsedTime = Sys_Milliseconds(); srcList[src].sfx = sfx; srcList[src].priority = priority; srcList[src].entity = entity; srcList[src].channel = channel; srcList[src].isActive = qtrue; srcList[src].isLocked = qfalse; srcList[src].isLooping = qfalse; srcList[src].isTracking = qfalse; srcList[src].local = local; // Set up OpenAL source qalSourcei(srcList[src].alSource, AL_BUFFER, buffer); qalSourcef(srcList[src].alSource, AL_PITCH, 1.0f); qalSourcef(srcList[src].alSource, AL_GAIN, s_alGain->value * s_volume->value); qalSourcefv(srcList[src].alSource, AL_POSITION, null_vector); qalSourcefv(srcList[src].alSource, AL_VELOCITY, null_vector); qalSourcei(srcList[src].alSource, AL_LOOPING, AL_FALSE); qalSourcef(srcList[src].alSource, AL_REFERENCE_DISTANCE, s_alMinDistance->value); if(local) { qalSourcei(srcList[src].alSource, AL_SOURCE_RELATIVE, AL_TRUE); qalSourcef(srcList[src].alSource, AL_ROLLOFF_FACTOR, 0); } else { qalSourcei(srcList[src].alSource, AL_SOURCE_RELATIVE, AL_FALSE); qalSourcef(srcList[src].alSource, AL_ROLLOFF_FACTOR, s_alRolloff->value); } } /* ================= S_AL_SrcKill ================= */ static void S_AL_SrcKill(srcHandle_t src) { // I'm not touching it. Unlock it first. if(srcList[src].isLocked) return; // Stop it if it's playing if(srcList[src].isActive) qalSourceStop(srcList[src].alSource); // Remove the entity association if((srcList[src].isLooping) && (srcList[src].entity != -1)) { int ent = srcList[src].entity; entityList[ent].srcAllocated = qfalse; entityList[ent].srcIndex = -1; entityList[ent].loopAddedThisFrame = qfalse; entityList[ent].startLoopingSound = qfalse; } // Remove the buffer qalSourcei(srcList[src].alSource, AL_BUFFER, 0); srcList[src].sfx = 0; srcList[src].lastUsedTime = 0; srcList[src].priority = 0; srcList[src].entity = -1; srcList[src].channel = -1; srcList[src].isActive = qfalse; srcList[src].isLocked = qfalse; srcList[src].isLooping = qfalse; srcList[src].isTracking = qfalse; } /* ================= S_AL_SrcAlloc ================= */ static srcHandle_t S_AL_SrcAlloc( alSrcPriority_t priority, int entnum, int channel ) { int i; int empty = -1; int weakest = -1; int weakest_time = Sys_Milliseconds(); int weakest_pri = 999; for(i = 0; i < srcCount; i++) { // If it's locked, we aren't even going to look at it if(srcList[i].isLocked) continue; // Is it empty or not? if((!srcList[i].isActive) && (empty == -1)) empty = i; else if(srcList[i].priority < priority) { // If it's older or has lower priority, flag it as weak if((srcList[i].priority < weakest_pri) || (srcList[i].lastUsedTime < weakest_time)) { weakest_pri = srcList[i].priority; weakest_time = srcList[i].lastUsedTime; weakest = i; } } // The channel system is not actually adhered to by baseq3, and not // implemented in snd_dma.c, so while the following is strictly correct, it // causes incorrect behaviour versus defacto baseq3 #if 0 // Is it an exact match, and not on channel 0? if((srcList[i].entity == entnum) && (srcList[i].channel == channel) && (channel != 0)) { S_AL_SrcKill(i); return i; } #endif } // Do we have an empty one? if(empty != -1) { S_AL_SrcKill( empty ); return empty; } // No. How about an overridable one? if(weakest != -1) { S_AL_SrcKill(weakest); return weakest; } // Nothing. Return failure (cries...) return -1; } /* ================= S_AL_SrcFind Finds an active source with matching entity and channel numbers Returns -1 if there isn't one ================= */ #if 0 static srcHandle_t S_AL_SrcFind(int entnum, int channel) { int i; for(i = 0; i < srcCount; i++) { if(!srcList[i].isActive) continue; if((srcList[i].entity == entnum) && (srcList[i].channel == channel)) return i; } return -1; } #endif /* ================= S_AL_SrcLock Locked sources will not be automatically reallocated or managed ================= */ static void S_AL_SrcLock(srcHandle_t src) { srcList[src].isLocked = qtrue; } /* ================= S_AL_SrcUnlock Once unlocked, the source may be reallocated again ================= */ static void S_AL_SrcUnlock(srcHandle_t src) { srcList[src].isLocked = qfalse; } /* ================= S_AL_UpdateEntityPosition ================= */ static void S_AL_UpdateEntityPosition( int entityNum, const vec3_t origin ) { S_AL_SanitiseVector( (vec_t *)origin ); if ( entityNum < 0 || entityNum > MAX_GENTITIES ) Com_Error( ERR_DROP, "S_UpdateEntityPosition: bad entitynum %i", entityNum ); VectorCopy( origin, entityList[entityNum].origin ); } /* ================= S_AL_StartLocalSound Play a local (non-spatialized) sound effect ================= */ static void S_AL_StartLocalSound(sfxHandle_t sfx, int channel) { // Try to grab a source srcHandle_t src = S_AL_SrcAlloc(SRCPRI_LOCAL, -1, channel); if(src == -1) return; // Set up the effect S_AL_SrcSetup(src, sfx, SRCPRI_LOCAL, -1, channel, qtrue); // Start it playing qalSourcePlay(srcList[src].alSource); } /* ================= S_AL_StartSound Play a one-shot sound effect ================= */ static void S_AL_StartSound( vec3_t origin, int entnum, int entchannel, sfxHandle_t sfx ) { vec3_t sorigin; // Try to grab a source srcHandle_t src = S_AL_SrcAlloc(SRCPRI_ONESHOT, entnum, entchannel); if(src == -1) return; // Set up the effect S_AL_SrcSetup(src, sfx, SRCPRI_ONESHOT, entnum, entchannel, qfalse); if(origin == NULL) { srcList[src].isTracking = qtrue; VectorCopy( entityList[entnum].origin, sorigin ); } else VectorCopy( origin, sorigin ); S_AL_SanitiseVector( sorigin ); qalSourcefv(srcList[src].alSource, AL_POSITION, sorigin); // Start it playing qalSourcePlay(srcList[src].alSource); } /* ================= S_AL_ClearLoopingSounds ================= */ static void S_AL_ClearLoopingSounds( qboolean killall ) { int i; for(i = 0; i < srcCount; i++) { if((srcList[i].isLooping) && (srcList[i].entity != -1)) entityList[srcList[i].entity].loopAddedThisFrame = qfalse; } } /* ================= S_AL_SrcLoop ================= */ static void S_AL_SrcLoop( alSrcPriority_t priority, sfxHandle_t sfx, const vec3_t origin, const vec3_t velocity, int entityNum ) { int src; sentity_t *sent = &entityList[ entityNum ]; // Do we need to allocate a new source for this entity if( !sent->srcAllocated ) { // Try to get a channel src = S_AL_SrcAlloc( priority, entityNum, -1 ); if( src == -1 ) { Com_DPrintf( S_COLOR_YELLOW "WARNING: Failed to allocate source " "for loop sfx %d on entity %d\n", sfx, entityNum ); return; } sent->startLoopingSound = qtrue; } else src = sent->srcIndex; sent->srcAllocated = qtrue; sent->srcIndex = src; sent->loopPriority = priority; sent->loopSfx = sfx; // If this is not set then the looping sound is removed sent->loopAddedThisFrame = qtrue; // UGH // These lines should be called via S_AL_SrcSetup, but we // can't call that yet as it buffers sfxes that may change // with subsequent calls to S_AL_SrcLoop srcList[ src ].entity = entityNum; srcList[ src ].isLooping = qtrue; srcList[ src ].isActive = qtrue; // Set up the position and velocity qalSourcefv( srcList[ src ].alSource, AL_POSITION, (ALfloat *)sent->origin ); qalSourcefv( srcList[ src ].alSource, AL_VELOCITY, (ALfloat *)velocity ); } /* ================= S_AL_AddLoopingSound ================= */ static void S_AL_AddLoopingSound( int entityNum, const vec3_t origin, const vec3_t velocity, sfxHandle_t sfx ) { S_AL_SanitiseVector( (vec_t *)origin ); S_AL_SanitiseVector( (vec_t *)velocity ); S_AL_SrcLoop(SRCPRI_ENTITY, sfx, origin, velocity, entityNum); } /* ================= S_AL_AddRealLoopingSound ================= */ static void S_AL_AddRealLoopingSound( int entityNum, const vec3_t origin, const vec3_t velocity, sfxHandle_t sfx ) { S_AL_SanitiseVector( (vec_t *)origin ); S_AL_SanitiseVector( (vec_t *)velocity ); // There are certain maps (*cough* Q3:TA mpterra*) that have large quantities // of ET_SPEAKERS in the PVS at any given time. OpenAL can't cope with mixing // large numbers of sounds, so this culls them by distance if( DistanceSquared( origin, lastListenerOrigin ) > s_alMaxSpeakerDistance->value * s_alMaxSpeakerDistance->value ) return; S_AL_SrcLoop(SRCPRI_AMBIENT, sfx, origin, velocity, entityNum); } /* ================= S_AL_StopLoopingSound ================= */ static void S_AL_StopLoopingSound(int entityNum ) { if(entityList[entityNum].srcAllocated) S_AL_SrcKill(entityList[entityNum].srcIndex); } /* ================= S_AL_SrcUpdate Update state (move things around, manage sources, and so on) ================= */ static void S_AL_SrcUpdate( void ) { int i; int entityNum; ALint state; for(i = 0; i < srcCount; i++) { entityNum = srcList[i].entity; if(srcList[i].isLocked) continue; if(!srcList[i].isActive) continue; // Update source parameters if((s_alGain->modified)||(s_volume->modified)) qalSourcef(srcList[i].alSource, AL_GAIN, s_alGain->value * s_volume->value); if((s_alRolloff->modified)&&(!srcList[i].local)) qalSourcef(srcList[i].alSource, AL_ROLLOFF_FACTOR, s_alRolloff->value); if(s_alMinDistance->modified) qalSourcef(srcList[i].alSource, AL_REFERENCE_DISTANCE, s_alMinDistance->value); if( srcList[ i ].isLooping ) { sentity_t *sent = &entityList[ entityNum ]; // If a looping effect hasn't been touched this frame, kill it if( sent->loopAddedThisFrame ) { // The sound has changed without an intervening removal if( srcList[ i ].isActive && !sent->startLoopingSound && srcList[ i ].sfx != sent->loopSfx ) { qalSourceStop( srcList[ i ].alSource ); qalSourcei( srcList[ i ].alSource, AL_BUFFER, 0 ); sent->startLoopingSound = qtrue; } // Ths sound hasn't been started yet if( sent->startLoopingSound ) { S_AL_SrcSetup( i, sent->loopSfx, sent->loopPriority, entityNum, -1, qfalse ); srcList[ i ].isLooping = qtrue; qalSourcei( srcList[ i ].alSource, AL_LOOPING, AL_TRUE ); qalSourcePlay( srcList[ i ].alSource ); sent->startLoopingSound = qfalse; } } else S_AL_SrcKill( i ); continue; } // See if it needs to be moved if(srcList[i].isTracking) qalSourcefv(srcList[i].alSource, AL_POSITION, entityList[entityNum].origin); // Check if it's done, and flag it qalGetSourcei(srcList[i].alSource, AL_SOURCE_STATE, &state); if(state == AL_STOPPED) { S_AL_SrcKill(i); continue; } } } /* ================= S_AL_SrcShutup ================= */ static void S_AL_SrcShutup( void ) { int i; for(i = 0; i < srcCount; i++) S_AL_SrcKill(i); } /* ================= S_AL_SrcGet ================= */ static ALuint S_AL_SrcGet(srcHandle_t src) { return srcList[src].alSource; } //=========================================================================== static srcHandle_t streamSourceHandle = -1; static qboolean streamPlaying = qfalse; static ALuint streamSource; /* ================= S_AL_AllocateStreamChannel ================= */ static void S_AL_AllocateStreamChannel( void ) { // Allocate a streamSource at high priority streamSourceHandle = S_AL_SrcAlloc(SRCPRI_STREAM, -2, 0); if(streamSourceHandle == -1) return; // Lock the streamSource so nobody else can use it, and get the raw streamSource S_AL_SrcLock(streamSourceHandle); streamSource = S_AL_SrcGet(streamSourceHandle); // Set some streamSource parameters qalSourcei (streamSource, AL_BUFFER, 0 ); qalSourcei (streamSource, AL_LOOPING, AL_FALSE ); qalSource3f(streamSource, AL_POSITION, 0.0, 0.0, 0.0); qalSource3f(streamSource, AL_VELOCITY, 0.0, 0.0, 0.0); qalSource3f(streamSource, AL_DIRECTION, 0.0, 0.0, 0.0); qalSourcef (streamSource, AL_ROLLOFF_FACTOR, 0.0 ); qalSourcei (streamSource, AL_SOURCE_RELATIVE, AL_TRUE ); } /* ================= S_AL_FreeStreamChannel ================= */ static void S_AL_FreeStreamChannel( void ) { // Release the output streamSource S_AL_SrcUnlock(streamSourceHandle); streamSource = 0; streamSourceHandle = -1; } /* ================= S_AL_RawSamples ================= */ static void S_AL_RawSamples(int samples, int rate, int width, int channels, const byte *data, float volume) { ALuint buffer; ALuint format; format = S_AL_Format( width, channels ); // Create the streamSource if necessary if(streamSourceHandle == -1) { S_AL_AllocateStreamChannel(); // Failed? if(streamSourceHandle == -1) { Com_Printf( S_COLOR_RED "ERROR: Can't allocate streaming streamSource\n"); return; } } // Create a buffer, and stuff the data into it qalGenBuffers(1, &buffer); qalBufferData(buffer, format, (ALvoid *)data, (samples * width * channels), rate); // Shove the data onto the streamSource qalSourceQueueBuffers(streamSource, 1, &buffer); // Volume qalSourcef (streamSource, AL_GAIN, volume * s_volume->value * s_alGain->value); } /* ================= S_AL_StreamUpdate ================= */ static void S_AL_StreamUpdate( void ) { int numBuffers; ALint state; if(streamSourceHandle == -1) return; // Un-queue any buffers, and delete them qalGetSourcei( streamSource, AL_BUFFERS_PROCESSED, &numBuffers ); while( numBuffers-- ) { ALuint buffer; qalSourceUnqueueBuffers(streamSource, 1, &buffer); qalDeleteBuffers(1, &buffer); } // Start the streamSource playing if necessary qalGetSourcei( streamSource, AL_BUFFERS_QUEUED, &numBuffers ); qalGetSourcei(streamSource, AL_SOURCE_STATE, &state); if(state == AL_STOPPED) { streamPlaying = qfalse; // If there are no buffers queued up, release the streamSource if( !numBuffers ) S_AL_FreeStreamChannel( ); } if( !streamPlaying && numBuffers ) { qalSourcePlay( streamSource ); streamPlaying = qtrue; } } /* ================= S_AL_StreamDie ================= */ static void S_AL_StreamDie( void ) { if(streamSourceHandle == -1) return; streamPlaying = qfalse; qalSourceStop(streamSource); S_AL_FreeStreamChannel(); } //=========================================================================== #define NUM_MUSIC_BUFFERS 4 #define MUSIC_BUFFER_SIZE 4096 static qboolean musicPlaying = qfalse; static srcHandle_t musicSourceHandle = -1; static ALuint musicSource; static ALuint musicBuffers[NUM_MUSIC_BUFFERS]; static snd_stream_t *mus_stream; static char s_backgroundLoop[MAX_QPATH]; static byte decode_buffer[MUSIC_BUFFER_SIZE]; /* ================= S_AL_MusicSourceGet ================= */ static void S_AL_MusicSourceGet( void ) { // Allocate a musicSource at high priority musicSourceHandle = S_AL_SrcAlloc(SRCPRI_STREAM, -2, 0); if(musicSourceHandle == -1) return; // Lock the musicSource so nobody else can use it, and get the raw musicSource S_AL_SrcLock(musicSourceHandle); musicSource = S_AL_SrcGet(musicSourceHandle); // Set some musicSource parameters qalSource3f(musicSource, AL_POSITION, 0.0, 0.0, 0.0); qalSource3f(musicSource, AL_VELOCITY, 0.0, 0.0, 0.0); qalSource3f(musicSource, AL_DIRECTION, 0.0, 0.0, 0.0); qalSourcef (musicSource, AL_ROLLOFF_FACTOR, 0.0 ); qalSourcei (musicSource, AL_SOURCE_RELATIVE, AL_TRUE ); } /* ================= S_AL_MusicSourceFree ================= */ static void S_AL_MusicSourceFree( void ) { // Release the output musicSource S_AL_SrcUnlock(musicSourceHandle); musicSource = 0; musicSourceHandle = -1; } /* ================= S_AL_StopBackgroundTrack ================= */ static void S_AL_StopBackgroundTrack( void ) { if(!musicPlaying) return; // Stop playing qalSourceStop(musicSource); // De-queue the musicBuffers qalSourceUnqueueBuffers(musicSource, NUM_MUSIC_BUFFERS, musicBuffers); // Destroy the musicBuffers qalDeleteBuffers(NUM_MUSIC_BUFFERS, musicBuffers); // Free the musicSource S_AL_MusicSourceFree(); // Unload the stream if(mus_stream) S_CodecCloseStream(mus_stream); mus_stream = NULL; musicPlaying = qfalse; } /* ================= S_AL_MusicProcess ================= */ static void S_AL_MusicProcess(ALuint b) { ALenum error; int l; ALuint format; l = S_CodecReadStream(mus_stream, MUSIC_BUFFER_SIZE, decode_buffer); // Run out data to read, start at the beginning again if(l == 0) { S_CodecCloseStream(mus_stream); mus_stream = S_CodecOpenStream(s_backgroundLoop); if(!mus_stream) { S_AL_StopBackgroundTrack(); return; } l = S_CodecReadStream(mus_stream, MUSIC_BUFFER_SIZE, decode_buffer); } format = S_AL_Format(mus_stream->info.width, mus_stream->info.channels); if( l == 0 ) { // We have no data to buffer, so buffer silence byte dummyData[ 2 ] = { 0 }; qalBufferData( b, AL_FORMAT_MONO16, (void *)dummyData, 2, 22050 ); } else qalBufferData(b, format, decode_buffer, l, mus_stream->info.rate); if( ( error = qalGetError( ) ) != AL_NO_ERROR ) { S_AL_StopBackgroundTrack( ); Com_Printf( S_COLOR_RED "ERROR: while buffering data for music stream - %s\n", S_AL_ErrorMsg( error ) ); return; } } /* ================= S_AL_StartBackgroundTrack ================= */ static void S_AL_StartBackgroundTrack( const char *intro, const char *loop ) { int i; // Stop any existing music that might be playing S_AL_StopBackgroundTrack(); if ( !intro || !intro[0] ) { intro = loop; } if ( !loop || !loop[0] ) { loop = intro; } if((!intro || !intro[0]) && (!intro || !intro[0])) return; // Copy the loop over strncpy( s_backgroundLoop, loop, sizeof( s_backgroundLoop ) ); // Open the intro mus_stream = S_CodecOpenStream(intro); if(!mus_stream) return; // Allocate a musicSource S_AL_MusicSourceGet(); if(musicSourceHandle == -1) return; // Generate the musicBuffers qalGenBuffers(NUM_MUSIC_BUFFERS, musicBuffers); // Queue the musicBuffers up for(i = 0; i < NUM_MUSIC_BUFFERS; i++) { S_AL_MusicProcess(musicBuffers[i]); // check whether our stream still exists. if(!mus_stream) { // there was an error in reading which resulted in a // closed stream. We must bail out or we'll crash. // deallocate everything we allocated so far: qalDeleteBuffers(NUM_MUSIC_BUFFERS, musicBuffers); S_AL_MusicSourceFree(); return; } } qalSourceQueueBuffers(musicSource, NUM_MUSIC_BUFFERS, musicBuffers); // Set the initial gain property qalSourcef(musicSource, AL_GAIN, s_alGain->value * s_musicVolume->value); // Start playing qalSourcePlay(musicSource); musicPlaying = qtrue; } /* ================= S_AL_MusicUpdate ================= */ static void S_AL_MusicUpdate( void ) { int numBuffers; ALint state; if(!musicPlaying) return; qalGetSourcei( musicSource, AL_BUFFERS_PROCESSED, &numBuffers ); while( numBuffers-- ) { ALuint b; qalSourceUnqueueBuffers(musicSource, 1, &b); S_AL_MusicProcess(b); qalSourceQueueBuffers(musicSource, 1, &b); } // Hitches can cause OpenAL to be starved of buffers when streaming. // If this happens, it will stop playback. This restarts the source if // it is no longer playing, and if there are buffers available qalGetSourcei( musicSource, AL_SOURCE_STATE, &state ); qalGetSourcei( musicSource, AL_BUFFERS_QUEUED, &numBuffers ); if( state == AL_STOPPED && numBuffers ) { Com_DPrintf( S_COLOR_YELLOW "Restarted OpenAL music\n" ); qalSourcePlay(musicSource); } // Set the gain property qalSourcef(musicSource, AL_GAIN, s_alGain->value * s_musicVolume->value); } //=========================================================================== // Local state variables static ALCdevice *alDevice; static ALCcontext *alContext; #ifdef _WIN32 #define ALDRIVER_DEFAULT "OpenAL32.dll" #else #define ALDRIVER_DEFAULT "libopenal.so.0" #endif /* ================= S_AL_StopAllSounds ================= */ static void S_AL_StopAllSounds( void ) { S_AL_SrcShutup(); S_AL_StopBackgroundTrack(); } /* ================= S_AL_Respatialize ================= */ static void S_AL_Respatialize( int entityNum, const vec3_t origin, vec3_t axis[3], int inwater ) { float velocity[3] = {0.0f, 0.0f, 0.0f}; float orientation[6]; vec3_t sorigin; if( s_alSpatEntOrigin->integer && entityNum >= 0 ) VectorCopy( entityList[ entityNum ].origin, sorigin ); else { VectorCopy( origin, sorigin ); S_AL_SanitiseVector( sorigin ); } S_AL_SanitiseVector( axis[ 0 ] ); S_AL_SanitiseVector( axis[ 1 ] ); S_AL_SanitiseVector( axis[ 2 ] ); orientation[0] = axis[0][0]; orientation[1] = axis[0][1]; orientation[2] = axis[0][2]; orientation[3] = axis[2][0]; orientation[4] = axis[2][1]; orientation[5] = axis[2][2]; VectorCopy( sorigin, lastListenerOrigin ); // Set OpenAL listener paramaters qalListenerfv(AL_POSITION, (ALfloat *)sorigin); qalListenerfv(AL_VELOCITY, velocity); qalListenerfv(AL_ORIENTATION, orientation); } /* ================= S_AL_Update ================= */ static void S_AL_Update( void ) { // Update SFX channels S_AL_SrcUpdate(); // Update streams S_AL_StreamUpdate(); S_AL_MusicUpdate(); // Doppler if(s_doppler->modified) { s_alDopplerFactor->modified = qtrue; s_doppler->modified = qfalse; } // Doppler parameters if(s_alDopplerFactor->modified) { if(s_doppler->integer) qalDopplerFactor(s_alDopplerFactor->value); else qalDopplerFactor(0.0f); s_alDopplerFactor->modified = qfalse; } if(s_alDopplerSpeed->modified) { qalDopplerVelocity(s_alDopplerSpeed->value); s_alDopplerSpeed->modified = qfalse; } // Clear the modified flags on the other cvars s_alGain->modified = qfalse; s_volume->modified = qfalse; s_musicVolume->modified = qfalse; s_alMinDistance->modified = qfalse; s_alRolloff->modified = qfalse; } /* ================= S_AL_DisableSounds ================= */ static void S_AL_DisableSounds( void ) { S_AL_StopAllSounds(); } /* ================= S_AL_BeginRegistration ================= */ static void S_AL_BeginRegistration( void ) { } /* ================= S_AL_ClearSoundBuffer ================= */ static void S_AL_ClearSoundBuffer( void ) { } /* ================= S_AL_SoundList ================= */ static void S_AL_SoundList( void ) { } /* ================= S_AL_SoundInfo ================= */ static void S_AL_SoundInfo( void ) { Com_Printf( "OpenAL info:\n" ); Com_Printf( " Vendor: %s\n", qalGetString( AL_VENDOR ) ); Com_Printf( " Version: %s\n", qalGetString( AL_VERSION ) ); Com_Printf( " Renderer: %s\n", qalGetString( AL_RENDERER ) ); Com_Printf( " Extensions: %s\n", qalGetString( AL_EXTENSIONS ) ); } /* ================= S_AL_Shutdown ================= */ static void S_AL_Shutdown( void ) { // Shut down everything S_AL_StreamDie( ); S_AL_StopBackgroundTrack( ); S_AL_SrcShutdown( ); S_AL_BufferShutdown( ); // Check for Linux shutdown race condition // FIXME: this will probably not be necessary once OpenAL CVS // from 11/11/05 is released and prevelant if( Q_stricmp((const char*)qalGetString( AL_VENDOR ), "J. Valenzuela" ) ) { qalcMakeContextCurrent( NULL ); } qalcDestroyContext(alContext); qalcCloseDevice(alDevice); QAL_Shutdown(); } #endif /* ================= S_AL_Init ================= */ qboolean S_AL_Init( soundInterface_t *si ) { #if USE_OPENAL if( !si ) { return qfalse; } // New console variables s_alPrecache = Cvar_Get( "s_alPrecache", "1", CVAR_ARCHIVE ); s_alGain = Cvar_Get( "s_alGain", "0.4", CVAR_ARCHIVE ); s_alSources = Cvar_Get( "s_alSources", "96", CVAR_ARCHIVE ); s_alDopplerFactor = Cvar_Get( "s_alDopplerFactor", "1.0", CVAR_ARCHIVE ); s_alDopplerSpeed = Cvar_Get( "s_alDopplerSpeed", "2200", CVAR_ARCHIVE ); s_alMinDistance = Cvar_Get( "s_alMinDistance", "120", CVAR_CHEAT ); s_alRolloff = Cvar_Get( "s_alRolloff", "0.8", CVAR_CHEAT ); s_alMaxSpeakerDistance = Cvar_Get( "s_alMaxSpeakerDistance", "1024", CVAR_ARCHIVE ); s_alSpatEntOrigin = Cvar_Get( "s_alSpatEntOrigin", "0", CVAR_ARCHIVE ); s_alDriver = Cvar_Get( "s_alDriver", ALDRIVER_DEFAULT, CVAR_ARCHIVE ); // Load QAL if( !QAL_Init( s_alDriver->string ) ) { Com_Printf( "Failed to load library: \"%s\".\n", s_alDriver->string ); return qfalse; } // Open default device alDevice = qalcOpenDevice( NULL ); if( !alDevice ) { QAL_Shutdown( ); Com_Printf( "Failed to open OpenAL device.\n" ); return qfalse; } // Create OpenAL context alContext = qalcCreateContext( alDevice, NULL ); if( !alContext ) { QAL_Shutdown( ); qalcCloseDevice( alDevice ); Com_Printf( "Failed to create OpenAL context.\n" ); return qfalse; } qalcMakeContextCurrent( alContext ); // Initialize sources, buffers, music S_AL_BufferInit( ); S_AL_SrcInit( ); // Set up OpenAL parameters (doppler, etc) qalDopplerFactor( s_alDopplerFactor->value ); qalDopplerVelocity( s_alDopplerSpeed->value ); si->Shutdown = S_AL_Shutdown; si->StartSound = S_AL_StartSound; si->StartLocalSound = S_AL_StartLocalSound; si->StartBackgroundTrack = S_AL_StartBackgroundTrack; si->StopBackgroundTrack = S_AL_StopBackgroundTrack; si->RawSamples = S_AL_RawSamples; si->StopAllSounds = S_AL_StopAllSounds; si->ClearLoopingSounds = S_AL_ClearLoopingSounds; si->AddLoopingSound = S_AL_AddLoopingSound; si->AddRealLoopingSound = S_AL_AddRealLoopingSound; si->StopLoopingSound = S_AL_StopLoopingSound; si->Respatialize = S_AL_Respatialize; si->UpdateEntityPosition = S_AL_UpdateEntityPosition; si->Update = S_AL_Update; si->DisableSounds = S_AL_DisableSounds; si->BeginRegistration = S_AL_BeginRegistration; si->RegisterSound = S_AL_RegisterSound; si->ClearSoundBuffer = S_AL_ClearSoundBuffer; si->SoundInfo = S_AL_SoundInfo; si->SoundList = S_AL_SoundList; return qtrue; #else return qfalse; #endif }