/* * Copyright (C) 1997-2005 Id Software, Inc. * (C) 2010 skuller.net * (C) 2005 Stuart Dalton (badcdev@gmail.com) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or (at * your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA * 02111-1307, USA. * * ======================================================================= * * This is the OpenAL backend for the Quake II Soundsystem. Most of these * functions were optained from the Q2Pro project, and some are from zeq2. * We adapted them to work with Yamagi Quake II. The OpenAL backend is * split into two layers. This is the upper layer, doing the actual work. * The lower layer implements the interface between Q2 and the OpenAL * implementation. This backend is overmuch complicated due to the * restrictions of the frontend. * * ======================================================================= */ #ifdef USE_OPENAL #include "../header/client.h" #include "../../backends/generic/header/qal.h" #include "header/local.h" #include "header/vorbis.h" /* translates from AL coordinate system to quake */ #define AL_UnpackVector(v) - v[1], v[2], -v[0] #define AL_CopyVector(a, b) ((b)[0] = -(a)[1], (b)[1] = (a)[2], (b)[2] = -(a)[0]) /* The OpenAL implementation should support at least this number of sources */ #define MIN_CHANNELS 16 /* Globals */ cvar_t *s_openal_maxgain; int active_buffers; qboolean streamPlaying; static ALuint s_srcnums[MAX_CHANNELS - 1]; static ALuint streamSource; static int s_framecount; static ALuint underwaterFilter; /* ----------------------------------------------------------------- */ /* * Silence / stop all OpenAL streams */ static void AL_StreamDie(void) { int numBuffers; streamPlaying = false; qalSourceStop(streamSource); /* Un-queue any buffers, and delete them */ qalGetSourcei(streamSource, AL_BUFFERS_QUEUED, &numBuffers); while (numBuffers--) { ALuint buffer; qalSourceUnqueueBuffers(streamSource, 1, &buffer); qalDeleteBuffers(1, &buffer); active_buffers--; } } /* * Updates stream sources by removing all played * buffers and restarting playback if necessary. */ static void AL_StreamUpdate(void) { int numBuffers; ALint state; /* Un-queue any buffers, and delete them */ qalGetSourcei(streamSource, AL_BUFFERS_PROCESSED, &numBuffers); while (numBuffers--) { ALuint buffer; qalSourceUnqueueBuffers(streamSource, 1, &buffer); qalDeleteBuffers(1, &buffer); active_buffers--; } /* Start the streamSource playing if necessary */ qalGetSourcei(streamSource, AL_BUFFERS_QUEUED, &numBuffers); qalGetSourcei(streamSource, AL_SOURCE_STATE, &state); if (state == AL_STOPPED) { streamPlaying = false; } if (!streamPlaying && numBuffers) { qalSourcePlay(streamSource); streamPlaying = true; } } /* ----------------------------------------------------------------- */ /* * Uploads a sample to OpenAL and places * a dummy entry in the frontends cache. * This is not nice but necessary for the * frontend to work. */ sfxcache_t * AL_UploadSfx(sfx_t *s, wavinfo_t *s_info, byte *data) { sfxcache_t *sc; ALsizei size; ALenum format; ALuint name; size = s_info->samples * s_info->width; format = s_info->width == 2 ? AL_FORMAT_MONO16 : AL_FORMAT_MONO8; if (!size) { return NULL; } qalGetError(); qalGenBuffers(1, &name); qalBufferData(name, format, data, size, s_info->rate); active_buffers++; if (qalGetError() != AL_NO_ERROR) { return NULL; } /* allocate placeholder sfxcache */ sc = s->cache = Z_TagMalloc(sizeof(*sc), 0); sc->length = s_info->samples * 1000 / s_info->rate; sc->loopstart = s_info->loopstart; sc->width = s_info->width; sc->size = size; sc->bufnum = name; return sc; } /* * Deletes a sample from OpenALs internal * cache. The dummy entry in the frontends * cache is deleted by the frontend. */ void AL_DeleteSfx(sfx_t *s) { sfxcache_t *sc; ALuint name; sc = s->cache; if (!sc) { return; } name = sc->bufnum; qalDeleteBuffers(1, &name); active_buffers--; } /* ----------------------------------------------------------------- */ /* * Performance stereo spatialization * of a channel in the frontends * sense. */ static void AL_Spatialize(channel_t *ch) { vec3_t origin; if ((ch->entnum == -1) || (ch->entnum == cl.playernum + 1) || !ch->dist_mult) { /* from view entity (player) => nothing to do, * position is still (0,0,0) and relative, * as set in AL_PlayChannel() */ return; } else if (ch->fixed_origin) { VectorCopy(ch->origin, origin); qalSource3f(ch->srcnum, AL_POSITION, AL_UnpackVector(origin)); return; } else { CL_GetEntitySoundOrigin(ch->entnum, origin); qalSource3f(ch->srcnum, AL_POSITION, AL_UnpackVector(origin)); return; } } /* * Plays a channel (in the frontends * sense) with OpenAL. */ void AL_PlayChannel(channel_t *ch) { sfxcache_t *sc; float vol; /* Debug */ if (s_show->value > 1) { Com_Printf("%s: %s\n", __func__, ch->sfx->name); } /* Clamp volume */ vol = ch->oal_vol; if (vol > 1.0f) { vol = 1.0f; } sc = ch->sfx->cache; ch->srcnum = s_srcnums[ch - channels]; qalGetError(); qalSourcef(ch->srcnum, AL_REFERENCE_DISTANCE, SOUND_FULLVOLUME); qalSourcef(ch->srcnum, AL_MAX_DISTANCE, 8192); qalSourcef(ch->srcnum, AL_ROLLOFF_FACTOR, ch->dist_mult * (8192 - SOUND_FULLVOLUME)); qalSourcef(ch->srcnum, AL_GAIN, vol); qalSourcei(ch->srcnum, AL_BUFFER, sc->bufnum); qalSourcei(ch->srcnum, AL_LOOPING, ch->autosound ? AL_TRUE : AL_FALSE); if ((ch->entnum == -1) || (ch->entnum == cl.playernum + 1) || !ch->dist_mult) { /* anything coming from the view entity will always * be full volume and at the listeners position */ qalSource3f(ch->srcnum, AL_POSITION, 0.0f, 0.0f, 0.0f); qalSourcei(ch->srcnum, AL_SOURCE_RELATIVE, AL_TRUE); } else { /* all other sources are *not* relative */ qalSourcei(ch->srcnum, AL_SOURCE_RELATIVE, AL_FALSE); } /* Spatialize it */ AL_Spatialize(ch); /* Play it */ qalSourcePlay(ch->srcnum); /* Do not play broken channels */ if (qalGetError() != AL_NO_ERROR) { AL_StopChannel(ch); } } /* * Stops playback of a "channel" * in the frontends sense. */ void AL_StopChannel(channel_t *ch) { /* Debug output */ if (s_show->value > 1) { Com_Printf("%s: %s\n", __func__, ch->sfx->name); } /* stop it */ qalSourceStop(ch->srcnum); qalSourcei(ch->srcnum, AL_BUFFER, AL_NONE); memset(ch, 0, sizeof(*ch)); } /* * Stops playback of all channels. */ void AL_StopAllChannels(void) { int i; channel_t *ch; ch = channels; /* It doesn't matter if a channel is active or not. */ for (i = 0; i < s_numchannels; i++, ch++) { if (!ch->sfx) { continue; } AL_StopChannel(ch); } s_rawend = 0; /* Remove all pending samples */ AL_StreamDie(); } /* ----------------------------------------------------------------- */ /* * Returns the channel which contains * the looping sound for the entity * "entnum". */ static channel_t * AL_FindLoopingSound(int entnum, sfx_t *sfx) { int i; channel_t *ch; ch = channels; for (i = 0; i < s_numchannels; i++, ch++) { if (!ch->sfx) { continue; } if (!ch->autosound) { continue; } if (ch->entnum != entnum) { continue; } if (ch->sfx != sfx) { continue; } return ch; } return NULL; } /* * Plays an looping sound with OpenAL */ static void AL_AddLoopSounds(void) { int i; int sounds[MAX_EDICTS]; channel_t *ch; sfx_t *sfx; sfxcache_t *sc; int num; entity_state_t *ent; if ((cls.state != ca_active) || cl_paused->value || !s_ambient->value) { return; } S_BuildSoundList(sounds); for (i = 0; i < cl.frame.num_entities; i++) { if (!sounds[i]) { continue; } sfx = cl.sound_precache[sounds[i]]; if (!sfx) { continue; /* bad sound effect */ } sc = sfx->cache; if (!sc) { continue; } num = (cl.frame.parse_entities + i) & (MAX_PARSE_ENTITIES - 1); ent = &cl_parse_entities[num]; ch = AL_FindLoopingSound(ent->number, sfx); if (ch) { ch->autoframe = s_framecount; ch->end = paintedtime + sc->length; continue; } /* allocate a channel */ ch = S_PickChannel(0, 0); if (!ch) { continue; } ch->autosound = true; /* remove next frame */ ch->autoframe = s_framecount; ch->sfx = sfx; ch->entnum = ent->number; ch->master_vol = 1; ch->dist_mult = SOUND_LOOPATTENUATE; ch->end = paintedtime + sc->length; /* it seems like looped sounds are always played at full volume * see SDL_AddLoopSounds() which calls SDL_SpatializeOrigin() with volume 255.0f * so set it to full volume (1.0 * s_volume). */ ch->oal_vol = s_volume->value; AL_PlayChannel(ch); } } /* * Starts all pending playsounds. */ static void AL_IssuePlaysounds(void) { playsound_t *ps; /* start any playsounds */ while (1) { ps = s_pendingplays.next; if (ps == &s_pendingplays) { break; /* no more pending sounds */ } if (ps->begin > paintedtime) { break; } S_IssuePlaysound(ps); } } /* * Queues raw samples for playback. Used * by the background music an cinematics. */ void AL_RawSamples(int samples, int rate, int width, int channels, byte *data, float volume) { ALuint buffer; ALuint format = 0; /* 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; } } /* Create a buffer, and stuff the data into it */ qalGenBuffers(1, &buffer); qalBufferData(buffer, format, (ALvoid *)data, (samples * width * channels), rate); active_buffers++; /* set volume */ if (volume > 1.0f) { volume = 1.0f; } qalSourcef(streamSource, AL_GAIN, volume); /* Shove the data onto the streamSource */ qalSourceQueueBuffers(streamSource, 1, &buffer); /* emulate behavior of S_RawSamples for s_rawend */ s_rawend += samples; } /* * Kills all raw samples still in flight. * This is used to stop music playback * when silence is triggered. */ void AL_UnqueueRawSamples() { AL_StreamDie(); } void oal_update_underwater() { int i; float gain_hf; qboolean update = false; ALuint filter; if (underwaterFilter == 0) return; if (s_underwater->modified) { update = true; s_underwater->modified = false; snd_is_underwater_enabled = ((int)s_underwater->value != 0); } if (s_underwater_gain_hf->modified) { update = true; s_underwater_gain_hf->modified = false; } if (!update) return; gain_hf = s_underwater_gain_hf->value; if (gain_hf < AL_LOWPASS_MIN_GAINHF) gain_hf = AL_LOWPASS_MIN_GAINHF; if (gain_hf > AL_LOWPASS_MAX_GAINHF) gain_hf = AL_LOWPASS_MAX_GAINHF; qalFilterf(underwaterFilter, AL_LOWPASS_GAINHF, gain_hf); if (snd_is_underwater_enabled && snd_is_underwater) filter = underwaterFilter; else filter = 0; for (i = 0; i < s_numchannels; ++i) qalSourcei(s_srcnums[i], AL_DIRECT_FILTER, filter); } /* * Main update function. Called every frame, * performes all necessary calculations. */ void AL_Update(void) { int i; channel_t *ch; vec_t orientation[6]; paintedtime = cls.realtime; /* set listener (player) parameters */ AL_CopyVector(listener_forward, orientation); AL_CopyVector(listener_up, orientation + 3); qalListenerf(AL_MAX_GAIN, s_openal_maxgain->value); qalDistanceModel(AL_LINEAR_DISTANCE_CLAMPED); qalListener3f(AL_POSITION, AL_UnpackVector(listener_origin)); qalListenerfv(AL_ORIENTATION, orientation); /* update spatialization for dynamic sounds */ ch = channels; for (i = 0; i < s_numchannels; i++, ch++) { if (!ch->sfx) { continue; } if (ch->autosound) { /* autosounds are regenerated fresh each frame */ if (ch->autoframe != s_framecount) { AL_StopChannel(ch); continue; } } else { ALenum state; qalGetError(); qalGetSourcei(ch->srcnum, AL_SOURCE_STATE, &state); if ((qalGetError() != AL_NO_ERROR) || (state == AL_STOPPED)) { AL_StopChannel(ch); continue; } } if (s_show->value) { Com_Printf("%3i %s\n", ch->master_vol, ch->sfx->name); } /* respatialize channel */ AL_Spatialize(ch); } s_framecount++; /* add loopsounds */ AL_AddLoopSounds(); /* add music */ #ifdef OGG OGG_Stream(); #endif AL_StreamUpdate(); AL_IssuePlaysounds(); oal_update_underwater(); } /* ----------------------------------------------------------------- */ /* * Enables underwater effect */ void AL_Underwater() { int i; if (sound_started != SS_OAL) { return; } if (underwaterFilter == 0) return; /* Apply to all sources */ for (i = 0; i < s_numchannels; i++) { qalSourcei(s_srcnums[i], AL_DIRECT_FILTER, underwaterFilter); } } /* * Disables the underwater effect */ void AL_Overwater() { int i; if (sound_started != SS_OAL) { return; } if (underwaterFilter == 0) return; /* Apply to all sources */ for (i = 0; i < s_numchannels; i++) { qalSourcei(s_srcnums[i], AL_DIRECT_FILTER, 0); } } /* ----------------------------------------------------------------- */ /* * Set up the stream sources */ static void AL_InitStreamSource() { 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_BUFFER, 0); qalSourcei(streamSource, AL_LOOPING, AL_FALSE); qalSourcei(streamSource, AL_SOURCE_RELATIVE, AL_TRUE); } /* * Set up the underwater filter */ static void AL_InitUnderwaterFilter() { underwaterFilter = 0; if (!(qalGenFilters && qalFilteri && qalFilterf && qalDeleteFilters)) return; /* Generate a filter */ qalGenFilters(1, &underwaterFilter); if (qalGetError() != AL_NO_ERROR) { Com_Printf("Couldn't generate an OpenAL filter!\n"); return; } /* Low pass filter for underwater effect */ qalFilteri(underwaterFilter, AL_FILTER_TYPE, AL_FILTER_LOWPASS); if (qalGetError() != AL_NO_ERROR) { Com_Printf("Low pass filter is not supported!\n"); return; } qalFilterf(underwaterFilter, AL_LOWPASS_GAIN, AL_LOWPASS_DEFAULT_GAIN); s_underwater->modified = true; s_underwater_gain_hf->modified = true; } /* * Initializes the OpenAL backend */ qboolean AL_Init(void) { int i; if (!QAL_Init()) { Com_Printf("ERROR: OpenAL failed to initialize.\n"); return false; } s_openal_maxgain = Cvar_Get("s_openal_maxgain", "1.0", CVAR_ARCHIVE); /* check for linear distance extension */ if (!qalIsExtensionPresent("AL_EXT_LINEAR_DISTANCE")) { Com_Printf("ERROR: Required AL_EXT_LINEAR_DISTANCE extension is missing.\n"); QAL_Shutdown(); return false; } /* generate source names */ qalGetError(); qalGenSources(1, &streamSource); if (qalGetError() != AL_NO_ERROR) { Com_Printf("ERROR: Couldn't get a single Source.\n"); QAL_Shutdown(); return false; } else { /* -1 because we already got one channel for streaming */ for (i = 0; i < MAX_CHANNELS - 1; i++) { qalGenSources(1, &s_srcnums[i]); if (qalGetError() != AL_NO_ERROR) { break; } } if (i < MIN_CHANNELS - 1) { Com_Printf("ERROR: Required at least %d sources, but got %d.\n", MIN_CHANNELS, i + 1); QAL_Shutdown(); return false; } } s_numchannels = i; AL_InitStreamSource(); AL_InitUnderwaterFilter(); Com_Printf("Number of OpenAL sources: %d\n\n", s_numchannels); return true; } /* * Shuts the OpenAL backend down */ void AL_Shutdown(void) { Com_Printf("Shutting down OpenAL.\n"); AL_StreamDie(); qalDeleteSources(1, &streamSource); qalDeleteFilters(1, &underwaterFilter); if (s_numchannels) { /* delete source names */ qalDeleteSources(s_numchannels, s_srcnums); memset(s_srcnums, 0, sizeof(s_srcnums)); s_numchannels = 0; } QAL_Shutdown(); } #endif /* USE_OPENAL */