/** * ========================================================================= * File : snd_mgr.cpp * Project : 0 A.D. * Description : OpenAL sound engine. handles sound I/O, buffer * : suballocation and voice management/prioritization. * * @author Jan.Wassenberg@stud.uni-karlsruhe.de * ========================================================================= */ /* * Copyright (c) 2004-2005 Jan Wassenberg * * Redistribution and/or modification are also permitted under the * terms of the GNU General Public License as published by the * Free Software Foundation (version 2 or later, at your option). * * 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. */ #include "precompiled.h" #include // to extract snd_open's definition file contents #include #include #include #include #include #include "maths/MathUtil.h" #ifdef __APPLE__ # include #else # include # include # include // alutLoadWAVMemory #endif // for AL_FORMAT_VORBIS_EXT decl on Linux #if OS_LINUX # include #endif // for DLL-load hack in alc_init #if OS_WIN # include "lib/sysdep/win/win_internal.h" #endif #include "lib/res/res.h" #include "snd_mgr.h" #include "lib/timer.h" #include "lib/app_hooks.h" #define OGG_HACK #include "ogghack.h" #if MSC_VERSION # pragma comment(lib, "openal32.lib") # pragma comment(lib, "alut.lib") // alutLoadWAVMemory #endif // HACK: OpenAL loads and unloads certain DLLs several times on Windows. // that looks unnecessary and wastes 100..400 ms on startup. // we hold a reference to prevent the actual unload. everything works ATM; // hopefully, OpenAL doesn't rely on them actually being unloaded. #if OS_WIN # define WIN_LOADLIBRARY_HACK 1 #endif // components: // - alc*: OpenAL context // readies OpenAL for use; allows specifying the device. // - al_listener*: OpenAL listener // owns position/orientation and master gain. // - al_buf*: OpenAL buffer suballocator // for convenience; also makes sure all have been freed at exit. // - al_src*: OpenAL source suballocator // avoids high source alloc cost. also enforces user-set source limit. // - al_init*: OpenAL startup mechanism // allows deferred init (speeding up start time) and runtime reset. // - snd_dev*: device enumeration // lists names of all available devices (for sound options screen). // - io_buf*: buffer suballocator // avoids frequent large alloc/frees and therefore heap fragmentation. // - stream*: streaming // passes chunks of data (read via async I/O) to snd_data on request. // - snd_data*: sound data provider // holds audio data (clip or stream) and returns OpenAL buffers on request. // - hsd_list*: list of SndData instances // ensures all are freed when desired (despite being cached). // - list*: list of active sounds. // sorts by priority for voice management, and has each VSrc update itself. // - vsrc*: audio source // owns source properties and queue, references SndData. // - vm*: voice management // grants the currently most 'important' sounds a hardware voice. static bool al_initialized = false; // indicates OpenAL is ready for use. checked by other components // when deciding if they can pass settings changes to OpenAL directly, // or whether they need to be saved until init. // used by snd_dev_set to reset OpenAL after device has been changed. static LibError al_reinit(); // used by VSrc_reload to init on demand. static LibError snd_init(); // used by al_shutdown to free all VSrc and SndData objects, respectively, // so that they release their OpenAL sources and buffers. static LibError list_free_all(); static void hsd_list_free_all(); /** * check if OpenAL indicates an error has occurred. it can only report * 1 error at a time, so this is called after every OpenAL request. * * @param caller Name of calling function (typically passed via __func__) */ static void al_check(const char* caller = "(unknown)") { debug_assert(al_initialized); ALenum err = alGetError(); if(err == AL_NO_ERROR) return; const char* str = (const char*)alGetString(err); debug_printf("OpenAL error: %s; called from %s\n", str, caller); debug_warn("OpenAL error"); } // convenience version that automatically passes in function name. #define AL_CHECK al_check(__func__) /////////////////////////////////////////////////////////////////////////////// // // OpenAL context: readies OpenAL for use; allows specifying the device, // in case there are problems with OpenAL's default choice. // /////////////////////////////////////////////////////////////////////////////// static const char* alc_dev_name = 0; // default (0): use OpenAL default device. // note: that's why this needs to be a pointer instead of an array. /** * tell OpenAL to use the specified device in future. * * @param alc_new_dev_name Device name. * @return LibError * * name = 0 reverts to OpenAL's default choice, which will also * be used if this routine is never called. * * the device name is typically taken from a config file at init-time; * the snd_dev* enumeration routines below are used to present a list * of choices to the user in the options screen. * * if OpenAL hasn't yet been initialized (i.e. no sounds have been opened), * this just stores the device name for use when init does occur. * note: we can't check now if it's invalid (if so, init will fail). * otherwise, we shut OpenAL down (thereby stopping all sounds) and * re-initialize with the new device. that's fairly time-consuming, * so preferably call this routine before sounds are loaded. */ LibError snd_dev_set(const char* alc_new_dev_name) { // requesting a specific device if(alc_new_dev_name) { // already using that device - done. (don't re-init) if(alc_dev_name && !strcmp(alc_dev_name, alc_new_dev_name)) return INFO_OK; // store name (need to copy it, since alc_init is called later, // and it must then still be valid) static char buf[32]; strcpy_s(buf, sizeof(buf), alc_new_dev_name); alc_dev_name = buf; } // requesting default device else { // already using default device - done. (don't re-init) if(alc_dev_name == 0) return INFO_OK; alc_dev_name = 0; } return al_reinit(); // no-op if not initialized yet, otherwise re-init. } static ALCcontext* alc_ctx = 0; static ALCdevice* alc_dev = 0; /** * free the OpenAL context and device. */ static void alc_shutdown() { if(alc_ctx) { alcMakeContextCurrent(0); alcDestroyContext(alc_ctx); } if(alc_dev) alcCloseDevice(alc_dev); } /** * Ready OpenAL for use by setting up a device and context. * * @return LibError */ static LibError alc_init() { LibError ret = INFO_OK; #if WIN_LOADLIBRARY_HACK HMODULE dlls[3]; dlls[0] = LoadLibrary("wrap_oal.dll"); dlls[1] = LoadLibrary("setupapi.dll"); dlls[2] = LoadLibrary("wdmaud.drv"); #endif // for reasons unknown, the NV native OpenAL implementation // causes an "invalid handle" exception internally when loaded // (it's not caused by the DLL load hack above). everything works and // we can continue normally; we just need to catch it to prevent the // unhandled exception filter from reporting it. #if OS_WIN __try { alc_dev = alcOpenDevice((ALubyte*)alc_dev_name); } // if invalid handle, handle it; otherwise, continue handler search. __except(GetExceptionCode() == EXCEPTION_INVALID_HANDLE) { // ignore } #else alc_dev = alcOpenDevice((ALubyte*)alc_dev_name); #endif if(alc_dev) { alc_ctx = alcCreateContext(alc_dev, 0); // no attrlist needed if(alc_ctx) alcMakeContextCurrent(alc_ctx); } // check if init succeeded. // some OpenAL implementations don't indicate failure here correctly; // we need to check if the device and context pointers are actually valid. ALCenum err = alcGetError(alc_dev); if(err != ALC_NO_ERROR || !alc_dev || !alc_ctx) { debug_printf("alc_init failed. alc_dev=%p alc_ctx=%p alc_dev_name=%s err=%d\n", alc_dev, alc_ctx, alc_dev_name, err); ret = ERR_FAIL; } // make note of which sound device is actually being used // (e.g. DS3D, native, MMSYSTEM) - needed when reporting OpenAL bugs. const char* dev_name = (const char*)alcGetString(alc_dev, ALC_DEVICE_SPECIFIER); wchar_t buf[200]; swprintf(buf, ARRAY_SIZE(buf), L"SND| alc_init: success, using %hs\n", dev_name); ah_log(buf); #if WIN_LOADLIBRARY_HACK // release DLL references, so BoundsChecker doesn't complain at exit. for(int i = 0; i < ARRAY_SIZE(dlls); i++) if(dlls[i] != INVALID_HANDLE_VALUE) FreeLibrary(dlls[i]); #endif return ret; } /////////////////////////////////////////////////////////////////////////////// // // listener: owns position/orientation and master gain. // if they're set before al_initialized, we pass the saved values to // OpenAL immediately after init (instead of waiting until next update). // /////////////////////////////////////////////////////////////////////////////// static float al_listener_gain = 1.0; static float al_listener_pos[3] = { 0, 0, 0 }; static float al_listener_orientation[6] = {0, 0, -1, 0, 1, 0}; // float view_direction[3], up_vector[3]; passed directly to OpenAL /** * send the current listener properties to OpenAL. * * also called from al_init. */ static void al_listener_latch() { if(al_initialized) { alListenerf(AL_GAIN, al_listener_gain); alListenerfv(AL_POSITION, al_listener_pos); alListenerfv(AL_ORIENTATION, al_listener_orientation); AL_CHECK; } } /** * set amplitude modifier, which is effectively applied to all sounds. * in layman's terms, this is the global "volume". * * @param gain Modifier: must be non-negative; * 1 -> unattenuated, 0.5 -> -6 dB, 0 -> silence. * @return LibError */ LibError snd_set_master_gain(float gain) { if(gain < 0) WARN_RETURN(ERR_INVALID_PARAM); al_listener_gain = gain; al_listener_latch(); // position will get sent too. // this isn't called often, so we don't care. return INFO_OK; } /** * set position of the listener (corresponds to camera in graphics). * coordinates are in world space; the system doesn't matter. * * @param pos position support vector * @param dir view direction * @param up up vector */ static void al_listener_set_pos(const float pos[3], const float dir[3], const float up[3]) { int i; for(i = 0; i < 3; i++) al_listener_pos[i] = pos[i]; for(i = 0; i < 3; i++) al_listener_orientation[i] = dir[i]; for(i = 0; i < 3; i++) al_listener_orientation[3+i] = up[i]; al_listener_latch(); } /** * get distance between listener and point. * this is used to determine sound priority. * * @param point position support vector * @return float euclidean distance squared */ static float al_listener_dist_2(const float point[3]) { const float dx = al_listener_pos[0] - point[0]; const float dy = al_listener_pos[1] - point[1]; const float dz = al_listener_pos[2] - point[2]; return dx*dx + dy*dy + dz*dz; } /////////////////////////////////////////////////////////////////////////////// // // AL buffer suballocator: allocates buffers as needed (alGenBuffers is fast). // this interface is a bit more convenient than the OpenAL routines, and we // verify that all buffers have been freed at exit. // /////////////////////////////////////////////////////////////////////////////// static int al_bufs_outstanding; /** * allocate a new buffer, and fill it with the specified data. * * @param data raw sound data buffer * @param size size of buffer in bytes * @param al_fmt AL_FORMAT_* describing the sound data * @param al_freq sampling frequency (typically 22050 Hz) * @return ALuint buffer name */ static ALuint al_buf_alloc(ALvoid* data, ALsizei size, ALenum al_fmt, ALsizei al_freq) { ALuint al_buf; alGenBuffers(1, &al_buf); alBufferData(al_buf, al_fmt, data, size, al_freq); AL_CHECK; al_bufs_outstanding++; return al_buf; } /** * free the buffer and its contained sound data. * * @param al_buf buffer name */ static void al_buf_free(ALuint al_buf) { // no-op if 0 (needed in case SndData_reload fails - // sd->al_buf will not have been set) if(!al_buf) return; debug_assert(alIsBuffer(al_buf)); alDeleteBuffers(1, &al_buf); AL_CHECK; al_bufs_outstanding--; } /** * make sure all buffers have been returned to us via al_buf_free. * called from al_shutdown. */ static void al_buf_shutdown() { if(al_bufs_outstanding != 0) debug_warn("not all buffers freed!"); } /////////////////////////////////////////////////////////////////////////////// // // AL source suballocator: allocate all available sources up-front and // pass them out as needed (alGenSources is quite slow, taking 3..5 ms per // source returned). also responsible for enforcing user-specified limit // on total number of sources (to reduce mixing cost on low-end systems). // /////////////////////////////////////////////////////////////////////////////// // regardless of sound card caps, we won't use more than this ("enough"). // necessary in case OpenAL doesn't limit #sources (e.g. if SW mixing). static const uint AL_SRC_MAX = 64; // stack of sources (first allocated is [0]) static ALuint al_srcs[AL_SRC_MAX]; // number of valid sources in al_srcs[] (set by al_src_init) static uint al_src_allocated; // number of sources currently in use static uint al_src_used = 0; // user-set limit on how many sources may be used static uint al_src_cap = AL_SRC_MAX; /** * grab as many sources as possible up to the limit. * called from al_init. */ static void al_src_init() { // grab as many sources as possible and count how many we get. for(uint i = 0; i < AL_SRC_MAX; i++) { alGenSources(1, &al_srcs[i]); // we've reached the limit, no more are available. if(alGetError() != AL_NO_ERROR) break; debug_assert(alIsSource(al_srcs[i])); al_src_allocated++; } // limit user's cap to what we actually got. // (in case snd_set_max_src was called before this) if(al_src_cap > al_src_allocated) al_src_cap = al_src_allocated; // make sure we got the minimum guaranteed by OpenAL. debug_assert(al_src_allocated >= 16); } /** * release all sources on freelist (currently stack). * all sources must have been returned to us via al_src_free. * called from al_shutdown. */ static void al_src_shutdown() { debug_assert(al_src_used == 0); alDeleteSources(al_src_allocated, al_srcs); al_src_allocated = 0; AL_CHECK; } /** * try to allocate a source. * * @return ALuint source name, or 0 if none available */ static ALuint al_src_alloc() { // no more to give if(al_src_used >= al_src_cap) return 0; ALuint al_src = al_srcs[al_src_used++]; return al_src; } /** * mark a source as free and available for reuse. * * @param al_src source name */ static void al_src_free(ALuint al_src) { debug_assert(alIsSource(al_src)); al_srcs[--al_src_used] = al_src; debug_assert(al_src_used < al_src_allocated); // don't compare against cap - it might have been // decreased to less than were in use. } /** * set maximum number of voices to play simultaneously, * to reduce mixing cost on low-end systems. * this limit may be ignored if e.g. there's a stricter * implementation- defined ceiling anyway. * * @param limit max. number of sources * @return LibError */ LibError snd_set_max_voices(uint limit) { // valid if cap is legit (less than what we allocated in al_src_init), // or if al_src_init hasn't been called yet. note: we accept anything // in the second case, as al_src_init will sanity-check al_src_cap. if(!al_src_allocated || limit < al_src_allocated) { al_src_cap = limit; return INFO_OK; } // user is requesting a cap higher than what we actually allocated. // that's fine (not an error), but we won't set the cap, since it // determines how many sources may be returned. // there's no return value to indicate this because the cap is // precisely that - an upper limit only, we don't care if it can't be met. else return INFO_OK; } /////////////////////////////////////////////////////////////////////////////// // // OpenAL startup mechanism: allows deferring init until sounds are actually // played, therefore speeding up perceived game start time. // also resets OpenAL when settings (e.g. device) are changed at runtime. // /////////////////////////////////////////////////////////////////////////////// /** * master OpenAL init; makes sure all subsystems are ready for use. * called from each snd_open; no harm if called more than once. * * @return LibError */ static LibError al_init() { // only take action on first call, OR when re-initializing. if(al_initialized) return INFO_OK; RETURN_ERR(alc_init()); al_initialized = true; // these can't fail: al_src_init(); al_listener_latch(); return INFO_OK; } /** * shut down all module subsystems. */ static void al_shutdown() { // was never initialized - nothing to do. if(!al_initialized) return; // somewhat tricky: go through gyrations to free OpenAL resources. // .. free all active sounds so that they release their source. // the SndData reference is also removed, // but these remain open, since they are cached. list_free_all(); // .. actually free all (still cached) SndData instances. hsd_list_free_all(); // .. all sources and buffers have been returned to their suballocators. // now free them all. al_src_shutdown(); al_buf_shutdown(); alc_shutdown(); al_initialized = false; } /** * re-initialize OpenAL. currently only required for changing devices. * * @return LibError */ static LibError al_reinit() { // not yet initialized. settings have been saved, and will be // applied by the component init routines called from al_init. if(!al_initialized) return INFO_OK; // re-init (stops all currently playing sounds) al_shutdown(); return al_init(); } /////////////////////////////////////////////////////////////////////////////// // // device enumeration: list all devices and allow the user to choose one, // in case the default device has problems. // /////////////////////////////////////////////////////////////////////////////// // set by snd_dev_prepare_enum; used by snd_dev_next. // consists of back-to-back C strings, terminated by an extra '\0'. // (this is taken straight from OpenAL; dox say this format may change). static const char* devs; /** * Prepare to enumerate all device names (this resets the list returned by * snd_dev_next). * * may be called each time the device list is needed. * * @return LibError; always successful unless the requisite device * enumeration extension isn't available. in the latter case, * a "cannot enum device" message should be presented to the user, * and snd_dev_set need not be called; OpenAL will use its default device. */ LibError snd_dev_prepare_enum() { if(alcIsExtensionPresent(0, (ALubyte*)"ALC_ENUMERATION_EXT") != AL_TRUE) WARN_RETURN(ERR_NO_SYS); devs = (const char*)alcGetString(0, ALC_DEVICE_SPECIFIER); return INFO_OK; } /** * Get next device name. * * do not call unless snd_dev_prepare_enum succeeded! * not thread-safe! (static data from snd_dev_prepare_enum is used) * * @return const char* device name, or 0 if all have been returned */ const char* snd_dev_next() { if(!*devs) return 0; const char* dev = devs; devs += strlen(dev)+1; return dev; } /////////////////////////////////////////////////////////////////////////////// // // stream: passes chunks of data (read via async I/O) to snd_data on request. // /////////////////////////////////////////////////////////////////////////////// // one stream apiece for music and voiceover (narration during tutorial). // allowing more is possible, but would be inefficent due to seek overhead. // set this limit to catch questionable usage (e.g. streaming normal sounds). static const int MAX_STREAMS = 2; // maximum IOs queued per stream. static const int MAX_IOS = 4; static const size_t STREAM_BUF_SIZE = 32*KiB; // // I/O buffer suballocator: avoids frequent large alloc/frees // when streaming, and therefore heap fragmentation. // /////////////////////////////////////////////////////////////////////////////// static const int TOTAL_IOS = MAX_STREAMS * MAX_IOS; static const size_t TOTAL_BUF_SIZE = TOTAL_IOS * STREAM_BUF_SIZE; // one large allocation for all buffers static void* io_bufs; // list of free buffers. start of buffer holds pointer to next in list. static void* io_buf_freelist; /** * Free an IO buffer. * * @param void* IO buffer */ static void io_buf_free(void* p) { debug_assert(io_bufs <= p && p <= (char*)io_bufs+TOTAL_BUF_SIZE); *(void**)p = io_buf_freelist; io_buf_freelist = p; } /** * Allocate a memory pool for all IO buffers. * Called from first io_buf_alloc. */ static void io_buf_init() { // allocate 1 big aligned block for all buffers. io_bufs = mem_alloc(TOTAL_BUF_SIZE, 4*KiB); // .. failed; io_buf_alloc calls will return 0 if(!io_bufs) return; // build freelist. char* p = (char*)io_bufs; for(int i = 0; i < TOTAL_IOS; i++) { io_buf_free(p); p += STREAM_BUF_SIZE; } } /** * Allocate a fixed-size IO buffer. * * @return void* buffer, or 0 (and warning) if not enough memory. */ static void* io_buf_alloc() { ONCE(io_buf_init()); void* buf = io_buf_freelist; // note: we have to bail now; can't update io_buf_freelist. if(!buf) { // no buffer allocated if(!io_bufs) WARN_ERR(ERR_NO_MEM); // too many streams - can't happen (tm) because // stream_open enforces MAX_STREAMS. else WARN_ERR(ERR_LIMIT); return 0; } io_buf_freelist = *(void**)io_buf_freelist; return buf; } /** * Free memory pool holding all IO buffers. * no-op if io_buf_alloc was never called. * called by snd_shutdown. */ static void io_buf_shutdown() { mem_free(io_bufs); } // // stream: owns queue and buffers; uses VFS async I/O // /////////////////////////////////////////////////////////////////////////////// // rationale: no need for a centralized queue - we have a suballocator, // so reallocs aren't a problem; central scheduling isn't necessary, // because we'll only have <= 2 streams active at a time. /** * Information for streaming sounds from file * (i.e. loading pieces of it in the background) */ struct Stream { Handle hf; Handle ios[MAX_IOS]; uint active_ios; /// set by stream_buf_get, used by stream_buf_discard to free buf. void* last_buf; }; /** * Begin reading the next segment (asynchronously). * Called from SndData_reload and snd_data_buf_get. * * @param Stream* * @return LibError */ static LibError stream_issue(Stream* s) { if(s->active_ios >= MAX_IOS) return INFO_OK; void* buf = io_buf_alloc(); if(!buf) WARN_RETURN(ERR_NO_MEM); Handle h = vfs_io_issue(s->hf, STREAM_BUF_SIZE, buf); RETURN_ERR(h); s->ios[s->active_ios++] = h; return INFO_OK; } /** * Access the data most recently streamed in. * * @param Stream* * @param data pointer to buffer * @param size [bytes] * @return LibError; if the first pending IO hasn't completed, * ERR_AGAIN (not an error). */ static LibError stream_buf_get(Stream* s, void*& data, size_t& size) { if(s->active_ios == 0) WARN_RETURN(ERR_EOF); Handle hio = s->ios[0]; // has it finished? if not, bail. int is_complete = vfs_io_has_completed(hio); RETURN_ERR(is_complete); if(is_complete == 0) return ERR_AGAIN; // NOWARN // get its buffer. // no delay, since vfs_io_has_completed == 1 RETURN_ERR(vfs_io_wait(hio, data, size)); // (next stream_buf_discard will free this buffer) s->last_buf = data; return INFO_OK; } /** * Free the buffer that was last returned by stream_buf_get, * and remove its IO slot from our queue. * * Must be called exactly once after every successful stream_buf_get; * call before calling any other stream_* functions! * * @param Stream* * @return LibError */ static LibError stream_buf_discard(Stream* s) { Handle hio = s->ios[0]; LibError ret = vfs_io_discard(hio); // we implement the required 'circular queue' as a stack; // have to shift all items after this one down. s->active_ios--; for(uint i = 0; i < s->active_ios; i++) s->ios[i] = s->ios[i+1]; io_buf_free(s->last_buf); // can't fail s->last_buf = 0; return ret; } static uint active_streams; /** * open a stream and begin reading from disk. * * @param Stream* * @param fn VFS filename. * @return LibError */ static LibError stream_open(Stream* s, const char* fn) { // bail because we wouldn't have enough IO buffers for all if(active_streams >= MAX_STREAMS) WARN_RETURN(ERR_LIMIT); active_streams++; s->hf = vfs_open(fn); RETURN_ERR(s->hf); for(int i = 0; i < MAX_IOS; i++) RETURN_ERR(stream_issue(s)); return INFO_OK; } /** * close a stream, which may currently be active. * * @param Stream* * @return LibError - the first error that occurred while waiting for * IOs / closing file. */ static LibError stream_close(Stream* s) { LibError ret = INFO_OK; LibError err; // for each pending IO: for(uint i = 0; i < s->active_ios; i++) { // .. wait until complete, void* data; size_t size; // unused do err = stream_buf_get(s, data, size); while(err == ERR_AGAIN); if(err < 0 && ret == 0) ret = err; // .. and discard. err = stream_buf_discard(s); if(err < 0 && ret == 0) ret = err; } err = vfs_close(s->hf); if(err < 0 && ret == 0) ret = err; active_streams--; return ret; } /** * Make sure the given Stream is valid/self-consistent. * * @param const Stream* * @return LibError */ static LibError stream_validate(const Stream* s) { if(s->active_ios > MAX_IOS) WARN_RETURN(ERR_1); // has no invariant we could check. return INFO_OK; } /////////////////////////////////////////////////////////////////////////////// // // sound data provider: holds audio data (clip or stream) and returns // OpenAL buffers on request. // /////////////////////////////////////////////////////////////////////////////// // rationale for separate VSrc (instance) and SndData resources: // - need to be able to fade out and cancel loops. // => VSrc isn't fire and forget; need to access sounds at runtime. // - allowing access via direct pointer is unsafe // => need Handle-based access // - don't want to reload sound data on every play() // => need either a separate caching mechanism or one central data resource. // - want to support reloading (for consistency if not necessity) // => can't hack via h_find / setting fn_key to 0; need a separate instance. // rationale for integrating IO logic into SndData: // if the IO layer were completely separate, we'd need a callback to pass // the buffer to OpenAL, or mark processed buffers as "discardable" for // next time. both are ugly; we instead integrate IO into the SndData code. // IOs are passed to OpenAL and then discarded immediately. /** * Holder for sound data - either a clip, or stream. */ struct SndData { // stream Stream s; ALenum al_fmt; ALsizei al_freq; // clip ALuint al_buf; uint is_stream : 1; uint is_valid : 1; #ifdef OGG_HACK // pointer to Ogg instance void* o; #endif }; H_TYPE_DEFINE(SndData); static void SndData_init(SndData* sd, va_list args) { // olsner: pass as int instead of bool for GCC compat. sd->is_stream = va_arg(args, int) != 0; } static void SndData_dtor(SndData* sd) { // in case reload failed and the following haven't been initialized yet // (freeing them would fail in that case, so bail) if(!sd->is_valid) return; if(sd->is_stream) stream_close(&sd->s); else al_buf_free(sd->al_buf); #ifdef OGG_HACK if(sd->o) ogg_release(sd->o); #endif } static void hsd_list_add(Handle hsd); static LibError SndData_reload(SndData* sd, const char* fn, Handle hsd) { hsd_list_add(hsd); // // detect sound format by checking file extension // enum FileType { FT_WAV, FT_OGG } file_type; const char* ext = path_extension(fn); // .. OGG (data will be passed directly to OpenAL) if(!stricmp(ext, "ogg")) { #ifdef OGG_HACK #else // first use of OGG: check if OpenAL extension is available. // note: this is required! OpenAL does its init here. static int ogg_supported = -1; if(ogg_supported == -1) ogg_supported = alIsExtensionPresent((ALubyte*)"AL_EXT_vorbis")? 1 : 0; if(!ogg_supported) WARN_RETURN(ERR_NO_SYS); sd->al_fmt = AL_FORMAT_VORBIS_EXT; sd->al_freq = 0; #endif file_type = FT_OGG; } // .. WAV else if(!stricmp(ext, "wav")) file_type = FT_WAV; // .. unknown extension else WARN_RETURN(ERR_UNKNOWN_FORMAT); if(sd->is_stream) { // refuse to stream anything that cannot be passed directly to OpenAL - // we'd have to extract the audio data ourselves (not worth it). if(file_type != FT_OGG) WARN_RETURN(ERR_NOT_SUPPORTED); RETURN_ERR(stream_open(&sd->s, fn)); sd->is_valid = 1; return INFO_OK; } // else: clip FileIOBuf file; size_t file_size; RETURN_ERR(vfs_load(fn, file, file_size)); ALvoid* al_data = (ALvoid*)file; ALsizei al_size = (ALsizei)file_size; if(file_type == FT_WAV) { ALbyte* memory = (ALbyte*)file; ALboolean al_loop; // unused alutLoadWAVMemory(memory, &sd->al_fmt, &al_data, &al_size, &sd->al_freq, &al_loop); } #ifdef OGG_HACK std::vector data; data.reserve(500000); if(file_type == FT_OGG) { sd->o = ogg_create(); ogg_give_raw(sd->o, (void*)file, file_size); ogg_open(sd->o, sd->al_fmt, sd->al_freq); size_t datasize=0; size_t bytes_read; do { const size_t bufsize = 32*KiB; char buf[bufsize]; bytes_read = ogg_read(sd->o, buf, bufsize); data.insert(data.end(), &buf[0], &buf[bytes_read]); datasize += bytes_read; } while(bytes_read > 0); al_data = &data[0]; al_size = (ALsizei)datasize; } else { sd->o = NULL; } #endif sd->al_buf = al_buf_alloc(al_data, al_size, sd->al_fmt, sd->al_freq); sd->is_valid = 1; (void)file_buf_free(file); return INFO_OK; } static LibError SndData_validate(const SndData* sd) { if(sd->al_fmt == 0) WARN_RETURN(ERR_11); if((uint)sd->al_freq > 100000) // suspicious WARN_RETURN(ERR_12); if(sd->al_buf == 0) WARN_RETURN(ERR_13); if(sd->is_stream) RETURN_ERR(stream_validate(&sd->s)); return INFO_OK; } static LibError SndData_to_string(const SndData* sd, char* buf) { const char* type = sd->is_stream? "stream" : "clip"; snprintf(buf, H_STRING_LEN, "%s; al_buf=%d", type, sd->al_buf); return INFO_OK; } /** * open and return a handle to a sound file's data. * * @param fn VFS filename * @param is_stream (default false) indicates whether this file should be * streamed in (opening is faster, it won't be kept in memory, but * only one instance can be open at a time; makes sense for large music files) * or loaded immediately. * @return Handle or LibError on failure */ static Handle snd_data_load(const char* fn, bool is_stream) { // don't allow reloading stream objects // (both references would read from the same file handle). const uint flags = is_stream? RES_UNIQUE : 0; return h_alloc(H_SndData, fn, flags, (int)is_stream); // (int) rationale: see SndData_init } /** * Free the sound. * * @param hsd Handle to SndData; set to 0 afterwards. * @return LibError */ static LibError snd_data_free(Handle& hsd) { return h_free(hsd, H_SndData); } // // SndData instance list: ensures all allocated since last al_shutdown // are freed when desired (they are cached => extra work needed). // /////////////////////////////////////////////////////////////////////////////// // rationale: all SndData objects (actually, their OpenAL buffers) must be // freed during al_shutdown, to prevent leaks. we can't rely on list* // to free all VSrc, and thereby their associated SndData objects - // completed sounds are no longer in the list. // // nor can we use the h_mgr_shutdown automatic leaked resource cleanup: // we need to be able to al_shutdown at runtime // (when resetting OpenAL, after e.g. device change). // // h_mgr support is required to forcibly close SndData objects, // since they are cached (kept open). it requires that this come after // H_TYPE_DEFINE(SndData), since h_force_free needs the handle type. // // we never need to delete single entries: hsd_list_free_all // (called by al_shutdown) frees each entry and clears the entire list. typedef std::vector Handles; static Handles hsd_list; /** * Add hsd to the list. * called from SndData_reload; will later be removed via hsd_list_free_all. * @param hsd Handle to SndData */ static void hsd_list_add(Handle hsd) { hsd_list.push_back(hsd); } /** * Free all sounds on list. * called by al_shutdown (at exit, or when reinitializing OpenAL). */ static void hsd_list_free_all() { for(Handles::iterator it = hsd_list.begin(); it != hsd_list.end(); ++it) { Handle& hsd = *it; (void)h_force_free(hsd, H_SndData); // ignore errors - if hsd was a stream, and its associated source // was active when al_shutdown was called, it will already have been // freed (list_free_all would free the source; it then releases // its SndData reference, which closes the instance because it's // RES_UNIQUE). } // leave its memory intact, so we don't have to reallocate it later // if we are now reinitializing OpenAL (not exiting). hsd_list.resize(0); } /////////////////////////////////////////////////////////////////////////////// /** * Get the sound's AL buffer (typically to play it) * * @param hsd Handle to SndData. * @param al_buf buffer name. * @return LibError, most commonly: * INFO_OK = buffer has been returned; more are expected to be available. * ERR_EOF = buffer has been returned but is the last one * (end of file reached) * ERR_AGAIN = no buffer returned yet; still streaming in ATM. * call back later. */ static LibError snd_data_buf_get(Handle hsd, ALuint& al_buf) { LibError err = INFO_OK; // in case H_DEREF fails al_buf = 0; H_DEREF(hsd, SndData, sd); // clip: just return buffer (which was created in snd_data_load) if(!sd->is_stream) { al_buf = sd->al_buf; return ERR_EOF; // NOWARN } // stream: // .. check if IO finished. void* data; size_t size; err = stream_buf_get(&sd->s, data, size); if(err == ERR_AGAIN) return ERR_AGAIN; // NOWARN RETURN_ERR(err); // .. yes: pass to OpenAL and discard IO buffer. al_buf = al_buf_alloc(data, (ALsizei)size, sd->al_fmt, sd->al_freq); stream_buf_discard(&sd->s); // .. try to issue the next IO. // if EOF reached, indicate al_buf is the last that will be returned. err = stream_issue(&sd->s); if(err == ERR_EOF) return ERR_EOF; // NOWARN RETURN_ERR(err); // al_buf valid and next IO issued successfully. return INFO_OK; } /** * Indicate the sound's buffer is no longer needed. * * @param hsd Handle to SndData. * @param al_buf buffer name * @return LibError */ static LibError snd_data_buf_free(Handle hsd, ALuint al_buf) { H_DEREF(hsd, SndData, sd); // clip: no-op (caller will later release hsd reference; // when hsd actually unloads, sd->al_buf will be freed). if(!sd->is_stream) return INFO_OK; // stream: we had allocated an additional buffer, so free it now. al_buf_free(al_buf); return INFO_OK; } //----------------------------------------------------------------------------- // fading //----------------------------------------------------------------------------- /** * control block for a fade operation. */ struct FadeInfo { double start_time; FadeType type; float length; float initial_val; float final_val; }; static float fade_factor_linear(float t) { return t; } static float fade_factor_exponential(float t) { // t**3 return t*t*t; } static float fade_factor_s_curve(float t) { // cosine curve float y = cos(t*M_PI + M_PI); // map [-1,1] to [0,1] return (y + 1.0f) / 2.0f; } /** * fade() return value; indicates if the fade operation is complete. */ enum FadeRet { FADE_NO_CHANGE, FADE_CHANGED, FADE_TO_0_FINISHED }; /** * Carry out the requested fade operation. * * This is called for each active VSrc; if they have no fade operation * active, nothing happens. Note: as an optimization, we could make a * list of VSrc with fade active and only call this for those; * not yet necessary, though. * * @param fi Describes the fade operation * @param cur_time typically returned via get_time() * @param out_val Output gain value, i.e. the current result of the fade. * @return FadeRet */ static FadeRet fade(FadeInfo& fi, double cur_time, float& out_val) { // no fade in progress - abort immediately. this check is necessary to // avoid division-by-zero below. if(fi.type == FT_NONE) return FADE_NO_CHANGE; // end reached - if fi.length is 0, but the fade is "in progress", do the // processing here, and skip the dangerous division if(fi.type == FT_ABORT || (cur_time >= fi.start_time + fi.length)) { // make sure exact value is hit out_val = fi.final_val; // special case: we were fading out; caller will free the sound. if(fi.final_val == 0.0f) return FADE_TO_0_FINISHED; // wipe out all values amd mark as no longer actively fading memset(&fi, 0, sizeof(fi)); fi.type = FT_NONE; return FADE_CHANGED; } // how far into the fade are we? [0,1] const float t = (cur_time - fi.start_time) / fi.length; float factor; switch(fi.type) { case FT_LINEAR: factor = fade_factor_linear(t); break; case FT_EXPONENTIAL: factor = fade_factor_exponential(t); break; case FT_S_CURVE: factor = fade_factor_s_curve(t); break; // initialize with anything at all, just so that the calculation // below runs through; we reset out_val after that. case FT_ABORT: factor = 0.0f; break; NODEFAULT; } out_val = fi.initial_val + factor*(fi.final_val - fi.initial_val); return FADE_CHANGED; } /** * Is the fade operation currently active? * * @param FadeInfo * @return bool */ static bool fade_is_active(FadeInfo& fi) { return (fi.type != FT_NONE); } /////////////////////////////////////////////////////////////////////////////// // // virtual sound source: a sound the user wants played. // owns source properties, buffer queue, and references SndData. // /////////////////////////////////////////////////////////////////////////////// // rationale: combine Src and VSrc - best interface, due to needing hsd, // buffer queue (# processed) in update enum VSrcFlags { // SndData has reported EOF. will close down after last buffer completes. VS_EOF = 1, // tell SndData to open the sound as a stream. // require caller to pass this explicitly, so they're aware // of the limitation that there can only be 1 instance of those. VS_IS_STREAM = 2, // this VSrc was added via list_add and needs to be removed with // list_remove in VSrc_dtor. // not set if load fails somehow (avoids list_remove "not found" error). VS_IN_LIST = 4, VS_ALL_FLAGS = VS_EOF|VS_IS_STREAM|VS_IN_LIST }; /** * control block for a virtual source, which represents a sound that the * application wants played. it may or may not be played, depending on * priority and whether an actual OpenAL source is available. */ struct VSrc { /// handle to this VSrc, so that it can close itself. Handle hvs; /// associated sound data Handle hsd; // AL source properties (set via snd_set*) ALfloat pos[3]; ALfloat gain; /// [0,1] ALfloat pitch; /// (0,1] ALboolean loop; ALboolean relative; /// controls vsrc_update behavior (VSrcFlags) uint flags; ALuint al_src; // priority for voice management float static_pri; /// as given by snd_play float cur_pri; /// holds newly calculated value FadeInfo fade; }; H_TYPE_DEFINE(VSrc); static void VSrc_init(VSrc* vs, va_list args) { vs->flags = va_arg(args, uint); vs->fade.type = FT_NONE; } static void list_remove(VSrc* vs); static LibError vsrc_reclaim(VSrc* vs); static void VSrc_dtor(VSrc* vs) { // only remove if added (not the case if load failed) if(vs->flags & VS_IN_LIST) { list_remove(vs); vs->flags &= ~VS_IN_LIST; } // these are safe, even if reload (partially) failed: vsrc_reclaim(vs); (void)snd_data_free(vs->hsd); } static LibError VSrc_reload(VSrc* vs, const char* fn, Handle hvs) { // cannot wait till play(), need to init here: // must load OpenAL so that snd_data_load can check for OGG extension. LibError err = snd_init(); // .. don't complain if sound is disabled; fail silently. if(err == ERR_AGAIN) return err; // .. catch genuine errors during init. RETURN_ERR(err); // // if extension is "txt", fn is a definition file containing the // sound file name and its gain; otherwise, read directly from fn // and assume default gain (1.0). // const char* snd_fn; // actual sound file name std::string snd_fn_s; // extracted from stringstream; // declare here so that it doesn't go out of scope below. const char* ext = path_extension(fn); if(!stricmp(ext, "txt")) { FileIOBuf buf; size_t size; RETURN_ERR(vfs_load(fn, buf, size)); std::istringstream def(std::string((char*)buf, (int)size)); (void)file_buf_free(buf); float gain_percent; def >> snd_fn_s; def >> gain_percent; snd_fn = snd_fn_s.c_str(); vs->gain = gain_percent / 100.0f; } else { snd_fn = fn; vs->gain = 1.0f; } // note: vs->gain can legitimately be > 1.0 - don't clamp. vs->pitch = 1.0f; vs->hvs = hvs; // needed so we can snd_free ourselves when done playing. bool is_stream = (vs->flags & VS_IS_STREAM) != 0; vs->hsd = snd_data_load(snd_fn, is_stream); RETURN_ERR(vs->hsd); return INFO_OK; } static LibError VSrc_validate(const VSrc* vs) { // al_src can legitimately be 0 (if vs is low-pri) if(vs->flags & ~VS_ALL_FLAGS) WARN_RETURN(ERR_1); // no limitations on if(!(0.0f <= vs->gain && vs->gain <= 1.0f)) WARN_RETURN(ERR_2); if(!(0.0f < vs->pitch && vs->pitch <= 1.0f)) WARN_RETURN(ERR_3); if(*(u8*)&vs->loop > 1 || *(u8*)&vs->relative > 1) WARN_RETURN(ERR_4); // and have no invariant we could check. return INFO_OK; } static LibError VSrc_to_string(const VSrc* vs, char* buf) { snprintf(buf, H_STRING_LEN, "al_src = %d", vs->al_src); return INFO_OK; } /** * open and return a handle to a sound instance. * * @param snd_fn VFS filename. if a text file (extension ".txt"), * it is assumed to be a definition file containing the * sound file name and its gain (0.0 .. 1.0). * otherwise, it is taken to be the sound file name and * gain is set to the default of 1.0 (no attenuation). * @param is_stream (default false) indicates whether this file should be * streamed in (opening is faster, it won't be kept in memory, but * only one instance can be open at a time; makes sense for large music files) * or loaded immediately. * @return Handle or LibError on failure */ Handle snd_open(const char* snd_fn, bool is_stream) { uint flags = 0; if(is_stream) flags |= VS_IS_STREAM; // note: RES_UNIQUE forces each instance to get a new resource // (which is of course what we want). return h_alloc(H_VSrc, snd_fn, RES_UNIQUE, flags); } /** * Free the sound; if it was playing, it will be stopped. * Note: sounds are closed automatically when done playing; * this is provided for completeness only. * * @param hvs Handle to VSrc. will be set to 0 afterwards. * @return LibError */ LibError snd_free(Handle& hvs) { return h_free(hvs, H_VSrc); } // // list of active sounds. used by voice management component, // and to have each VSrc update itself (queue new buffers). // /////////////////////////////////////////////////////////////////////////////// // VSrc fields are used -> must come after struct VSrc // sorted in descending order of current priority // (we sometimes remove low pri items, which requires moving down // everything that comes after them, so we want those to come last). // // don't use list, to avoid lots of allocs (expect thousands of VSrcs). typedef std::deque VSrcs; typedef VSrcs::iterator It; static VSrcs vsrcs; // don't need to sort now - caller will list_sort() during update. static void list_add(VSrc* vs) { vsrcs.push_back(vs); } /** * call back for each VSrc entry in the list. * * @param cb Callback function * @param skip number of entries to skip (default 0) * @param end_idx if not the default value of 0, stop before that entry. */ static void list_foreach(void (*cb)(VSrc*), uint skip = 0, uint end_idx = 0) { It begin = vsrcs.begin() + skip; It end = vsrcs.end(); if(end_idx) end = vsrcs.begin()+end_idx; // can't use std::for_each: some entries may have been deleted // (i.e. set to 0) since last update. for(It it = begin; it != end; ++it) { VSrc* vs = *it; if(vs) cb(vs); } } static bool is_greater(const VSrc* vs1, const VSrc* vs2) { return vs1->cur_pri > vs2->cur_pri; } /// sort list by decreasing 'priority' (most important first) static void list_sort() { std::sort(vsrcs.begin(), vsrcs.end(), is_greater); } /** * scan list and remove the given VSrc (by setting it to 0; list will be * pruned later (see rationale below). * O(N)! * * @param vs VSrc to remove. */ static void list_remove(VSrc* vs) { for(size_t i = 0; i < vsrcs.size(); i++) { if(vsrcs[i] == vs) { // found it; several ways we could remove: // - shift everything else down (slow) -> no // - fill the hole with e.g. the last element // (vsrcs would no longer be sorted by priority) -> no // - replace with 0 (will require prune_removed and // more work in foreach) -> best alternative vsrcs[i] = 0; return; } } debug_warn("VSrc not found"); } static bool is_null(VSrc* vs) { return (vs == 0); } /** * remove entries that were set to 0 by list_remove, so that * code below can grant the first al_src_cap entries a soure. */ static void list_prune_removed() { It new_end = remove_if(vsrcs.begin(), vsrcs.end(), is_null); vsrcs.erase(new_end, vsrcs.end()); } static void vsrc_free(VSrc* vs) { snd_free(vs->hvs); } static LibError list_free_all() { list_foreach(vsrc_free); return INFO_OK; } /////////////////////////////////////////////////////////////////////////////// /** * Send the VSrc properties to OpenAL (when we actually have a source). * called by snd_set* and vsrc_grant. * * @param VSrc* */ static void vsrc_latch(VSrc* vs) { if(!vs->al_src) return; #ifndef NDEBUG // paranoid value checking; helps determine which parameter is // the problem when the below AL_CHECK fails. debug_assert(!isnan(vs->pos[0]) && !isnan(vs->pos[1]) && !isnan(vs->pos[2])); debug_assert(vs->relative == AL_TRUE || vs->relative == AL_FALSE); debug_assert(!isnan(vs->gain)); debug_assert(!isnan(vs->pitch) && vs->pitch > 0.0f); debug_assert(vs->loop == AL_TRUE || vs->loop == AL_FALSE); #endif alSourcefv(vs->al_src, AL_POSITION, vs->pos); alSourcei (vs->al_src, AL_SOURCE_RELATIVE, vs->relative); alSourcef (vs->al_src, AL_GAIN, vs->gain); alSourcef (vs->al_src, AL_PITCH, vs->pitch); alSourcei (vs->al_src, AL_LOOPING, vs->loop); AL_CHECK; } /** * Dequeue any of the VSrc's sound buffers that are finished playing. * * @param VSrc* * @return int number of entries that were removed. */ static int vsrc_deque_finished_bufs(VSrc* vs) { int num_processed; alGetSourcei(vs->al_src, AL_BUFFERS_PROCESSED, &num_processed); for(int i = 0; i < num_processed; i++) { ALuint al_buf; alSourceUnqueueBuffers(vs->al_src, 1, &al_buf); snd_data_buf_free(vs->hsd, al_buf); } AL_CHECK; return num_processed; } // HACK: fade() requires the current time. we don't want to query that // anew in every vsrc_update (slow and may mess up crossfades), and passing // as a parameter isn't possible due to the list_foreach interface. // therefore, static variable (set from snd_update). static double snd_update_time; /** * Update the VSrc - perform fade (if active), queue/unqueue buffers. * Called once a frame. * * @param VSrc* * @return LibError */ static LibError vsrc_update(VSrc* vs) { if(!vs->al_src) return INFO_OK; FadeRet ret = fade(vs->fade, snd_update_time, vs->gain); // auto-free after fadeout. if(ret == FADE_TO_0_FINISHED) { vsrc_free(vs); return INFO_OK; // don't continue - has been freed. } // fade in progress; latch current gain value. else if(ret == FADE_CHANGED) vsrc_latch(vs); int num_queued; alGetSourcei(vs->al_src, AL_BUFFERS_QUEUED, &num_queued); al_check("vsrc_update alGetSourcei"); int num_processed = vsrc_deque_finished_bufs(vs); if(vs->flags & VS_EOF) { // no more buffers left, and EOF reached - done playing. if(num_queued == 0) { snd_free(vs->hvs); return INFO_OK; } } // can still read from SndData else { // decide how many buffers we are going to request // (start off with MAX_IOS; after that, replace finished buffers). int to_fill = MAX_IOS; if(num_queued > 0) to_fill = num_processed; // request and queue each buffer. int ret; do { ALuint al_buf; ret = snd_data_buf_get(vs->hsd, al_buf); // these 2 are legit (see above); otherwise, bail. if(ret != ERR_AGAIN && ret != ERR_EOF) RETURN_ERR(ret); alSourceQueueBuffers(vs->al_src, 1, &al_buf); al_check("vsrc_update alSourceQueueBuffers"); } while(to_fill-- && ret == INFO_OK); // SndData has reported that no further buffers are available. if(ret == ERR_EOF) vs->flags |= VS_EOF; } return INFO_OK; } /** * Try to give the VSrc an AL source so that it can (re)start playing. * called by snd_play and voice management. * * @param VSrc* * @return LibError (ERR_FAIL if no AL source is available) */ static LibError vsrc_grant(VSrc* vs) { // already playing - bail if(vs->al_src) return INFO_OK; // try to alloc source. if none are available, bail - // we get called in that hope that one is available by snd_play. vs->al_src = al_src_alloc(); if(!vs->al_src) return ERR_FAIL; // NOWARN // OpenAL docs don't specify default values, so initialize everything // ourselves to be sure. note: alSourcefv param is not const. float zero3[3] = { 0.0f, 0.0f, 0.0f }; alSourcefv(vs->al_src, AL_VELOCITY, zero3); alSourcefv(vs->al_src, AL_DIRECTION, zero3); alSourcef (vs->al_src, AL_ROLLOFF_FACTOR, 0.0f); alSourcei (vs->al_src, AL_SOURCE_RELATIVE, AL_TRUE); AL_CHECK; // set remaining (user-specifiable) properties. vsrc_latch(vs); // queue up some buffers (enough to start playing, at least). vsrc_update(vs); alSourcePlay(vs->al_src); AL_CHECK; return INFO_OK; } /** * stop playback, and reclaim the OpenAL source. * called when closing the VSrc, or when voice management decides * this VSrc must yield to others of higher priority. * * @param VSrc* * @return LibError */ static LibError vsrc_reclaim(VSrc* vs) { // don't own a source - bail. if(!vs->al_src) return ERR_FAIL; // NOWARN alSourceStop(vs->al_src); AL_CHECK; // (note: all queued buffers are now considered 'processed') // deque and free remaining buffers (if sound was closed abruptly). vsrc_deque_finished_bufs(vs); al_src_free(vs->al_src); return INFO_OK; } /////////////////////////////////////////////////////////////////////////////// /** * Request the sound be played. * * Once done playing, the sound is automatically closed (allows * fire-and-forget play code). * if no hardware voice is available, this sound may not be played at all, * or in the case of looped sounds, start later. * * @param hvs Handle to VSrc * @param static_pri (min 0 .. max 1, default 0) indicates which sounds are * considered more important; this is attenuated by distance to the * listener (see snd_update). * @return LibError */ LibError snd_play(Handle hvs, float static_pri) { H_DEREF(hvs, VSrc, vs); // note: vs->hsd is valid, otherwise snd_open would have failed // and returned an invalid handle (caught above). vs->static_pri = static_pri; list_add(vs); vs->flags |= VS_IN_LIST; // optimization (don't want to do full update here - too slow) // either we get a source and playing begins immediately, // or it'll be taken care of on next update. vsrc_grant(vs); return INFO_OK; } /** * Change 3d position of the sound source. * * May be called at any time; fails with invalid handle return if * the sound has already been closed (e.g. it never played). * * @param hvs Handle to VSrc * @param x,y,z coordinates (interpretation: see below) * @param relative if true, (x,y,z) is treated as relative to the listener; * otherwise, it is the position in world coordinates (default). * @return LibError */ LibError snd_set_pos(Handle hvs, float x, float y, float z, bool relative) { H_DEREF(hvs, VSrc, vs); vs->pos[0] = x; vs->pos[1] = y; vs->pos[2] = z; vs->relative = relative; vsrc_latch(vs); return INFO_OK; } /** * Change gain (amplitude modifier) of the sound source. * * should not be called during a fade (see note in implementation); * fails with invalid handle return if the sound has already been * closed (e.g. it never played). * * @param hvs Handle to VSrc * @param gain modifier; must be non-negative; * 1 -> unattenuated, 0.5 -> -6 dB, 0 -> silence. * @return LibError */ LibError snd_set_gain(Handle hvs, float gain) { H_DEREF(hvs, VSrc, vs); if(!(0.0f <= gain && gain <= 1.0f)) WARN_RETURN(ERR_INVALID_PARAM); // if fading, gain changes would be overridden during the next // snd_update. attempting this indicates a logic error. we abort to // avoid undesired jumps in gain that might surprise (and deafen) user. if(fade_is_active(vs->fade)) WARN_RETURN(ERR_LOGIC); vs->gain = gain; vsrc_latch(vs); return INFO_OK; } /** * Change pitch shift of the sound source. * * may be called at any time; fails with invalid handle return if * the sound has already been closed (e.g. it never played). * * @param hvs Handle to VSrc * @param pitch shift: 1.0 means no change; each reduction by 50% equals a * pitch shift of -12 semitones (one octave). zero is invalid. * @return LibError */ LibError snd_set_pitch(Handle hvs, float pitch) { H_DEREF(hvs, VSrc, vs); if(!(0.0f < pitch && pitch <= 1.0f)) WARN_RETURN(ERR_INVALID_PARAM); vs->pitch = pitch; vsrc_latch(vs); return INFO_OK; } /** * Enable/disable looping on the sound source. * used to implement variable-length sounds (e.g. while building). * * may be called at any time; fails with invalid handle return if * the sound has already been closed (e.g. it never played). * * notes: * - looping sounds are not discarded if they cannot be played for lack of * a hardware voice at the moment play was requested. * - once looping is again disabled and the sound has reached its end, * the sound instance is freed automatically (as if never looped). * * @param hvs Handle to VSrc * @param bool loop * @return LibError */ LibError snd_set_loop(Handle hvs, bool loop) { H_DEREF(hvs, VSrc, vs); vs->loop = loop; vsrc_latch(vs); return INFO_OK; } /** * Fade the sound source in or out over time. * Its gain starts at immediately and is moved toward * over seconds. * * may be called at any time; fails with invalid handle return if * the sound has already been closed (e.g. it never played). * * note that this function doesn't busy-wait until the fade is complete; * any number of fades may be active at a time (allows cross-fading). * each snd_update calculates a new gain value for all pending fades. * it is safe to start another fade on the same sound source while * one is currently in progress; the old one is dropped. * * @param hvs Handle to VSrc * @param initial_gain gain. if < 0 (an otherwise illegal value), the sound's * current gain is used as the start value (useful for fading out). * @param final_gain gain. if 0, the sound is freed when the fade completes or * is aborted, thus allowing fire-and-forget fadeouts. no cases are * foreseen where this is undesirable, and it is easier to implement * than an extra set-free-after-fade-flag function. * @param length duration of fade [s] * @param type determines the fade curve: linear, exponential or S-curve. * for guidance on which to use, see * http://www.transom.org/tools/editing_mixing/200309.stupidfadetricks.html * you can also pass FT_ABORT to stop fading (if in progress) and * set gain to the final_gain parameter passed here. * @return LibError */ LibError snd_fade(Handle hvs, float initial_gain, float final_gain, float length, FadeType type) { H_DEREF(hvs, VSrc, vs); if(type != FT_LINEAR && type != FT_EXPONENTIAL && type != FT_S_CURVE && type != FT_ABORT) WARN_RETURN(ERR_INVALID_PARAM); // special case - set initial value to current gain (see above). if(initial_gain < 0.0f) initial_gain = vs->gain; const double cur_time = get_time(); FadeInfo& fi = vs->fade; fi.type = type; fi.start_time = cur_time; fi.initial_val = initial_gain; fi.final_val = final_gain; fi.length = length; (void)fade(fi, cur_time, vs->gain); vsrc_latch(vs); return INFO_OK; } /////////////////////////////////////////////////////////////////////////////// // // voice management: grants the currently most 'important' sounds // a hardware voice. // /////////////////////////////////////////////////////////////////////////////// /// length of vector squared (avoids costly sqrt) static float magnitude_2(const float v[3]) { return v[0]*v[0] + v[1]*v[1] + v[2]*v[2]; } /** * determine new priority of the VSrc based on distance to listener and * static priority. * called via list_foreach. * * @param VSrc* */ static void calc_cur_pri(VSrc* vs) { const float MAX_DIST_2 = 1000.0f; const float falloff = 10.0f; float d_2; // euclidean distance to listener (squared) if(vs->relative) d_2 = magnitude_2(vs->pos); else d_2 = al_listener_dist_2(vs->pos); // scale priority down exponentially float e = d_2 / MAX_DIST_2; // 0.0f (close) .. 1.0f (far) // assume farther away than OpenAL cutoff - no sound contribution float cur_pri = -1.0f; if(e < 1.0f) cur_pri = vs->static_pri / pow(falloff, e); vs->cur_pri = cur_pri; } /** * convenience function that strips all unimportant VSrc of their AL source. * called via list_foreach; also immediately frees discarded clips. */ static void reclaim(VSrc* vs) { vsrc_reclaim(vs); if(!vs->loop) snd_free(vs->hvs); } /// thunk that allows calling via list_foreach (return types differ) static void grant(VSrc* vs) { vsrc_grant(vs); } /** * update voice management, i.e. recalculate priority and assign AL sources. * no-op if OpenAL not yet initialized. * * @return LibError */ static LibError vm_update() { list_prune_removed(); // update current priorities (a function of static priority and distance). list_foreach(calc_cur_pri); // sort by descending current priority. list_sort(); // partition list; the first al_src_cap will be granted a source // (if they don't have one already), after reclaiming all sources from // the remainder of the VSrc list entries. uint first_unimportant = MIN((uint)vsrcs.size(), al_src_cap); list_foreach(reclaim, first_unimportant, 0); list_foreach(grant, 0, first_unimportant); return INFO_OK; } /////////////////////////////////////////////////////////////////////////////// /** * perform housekeeping (e.g. streaming); call once a frame. * * @param pos position support vector. if NULL, all parameters are * ignored and listener position unchanged; this is useful in case the * world isn't initialized yet. * @param dir view direction * @param up up vector * @return LibError */ LibError snd_update(const float* pos, const float* dir, const float* up) { // there's no sense in updating anything if we weren't initialized // yet (most notably, if sound is disabled). we check for this to // avoid confusing the code below. the caller should complain if // this fails, so report success here (everything will work once // sound is re-enabled). if(!al_initialized) return INFO_OK; if(pos) al_listener_set_pos(pos, dir, up); vm_update(); // for each source: add / remove buffers; carry out fading. snd_update_time = get_time(); // see decl list_foreach((void (*)(VSrc*))vsrc_update); return INFO_OK; } // prevent OpenAL from being initialized when snd_init is called. static bool snd_disabled = false; /** * extra layer on top of al_init that allows 'disabling' sound. * called from each snd_open. * * @return LibError from al_init, or ERR_AGAIN if sound disabled */ static inline LibError snd_init() { // (note: each VSrc_reload and therefore snd_open will fail) if(snd_disabled) return ERR_AGAIN; // NOWARN return al_init(); } /** * (temporarily) disable all sound output. because it causes future snd_open * calls to immediately abort before they demand-initialize OpenAL, * startup is sped up considerably (500..1000ms). therefore, this must be * called before the first snd_open to have any effect; otherwise, the * cat will already be out of the bag and we debug_warn of it. * * rationale: this is a quick'n dirty way of speeding up startup during * development without having to change the game's sound code. * * can later be called to reactivate sound; all settings ever changed * will be applied and subsequent sound load / play requests will work. * * @param bool disabled * @return LibError */ LibError snd_disable(bool disabled) { snd_disabled = disabled; if(snd_disabled) { if(al_initialized) debug_warn("already initialized => disable is pointless"); return INFO_OK; } else return snd_init(); // note: won't return ERR_AGAIN, since snd_disabled == false } /** * free all resources and shut down the sound system. * call before h_mgr_shutdown. */ void snd_shutdown() { io_buf_shutdown(); al_shutdown(); // calls list_free_all }