1
0
forked from 0ad/0ad
0ad/source/lib/res/sound/snd_mgr.cpp
janwas 8667ea74c8 fixes and improvements
- directoryPosix: replace most methods with boost filesystem (but not
all: the latter cannot efficiently enumerate files AND query their
size/mtime)
- AllocatorChecker: better name for member functions
- file: move the File class here
- trace: bugfix
- io: move UnalignedWriter to write_buffer.cpp (basically the same
thing)
- vfs: remove unnecessary "vfs" warts from variable names
- vfs_tree: VfsFile now stores single Name/Size/MTime fields instead of
the FileInfo record (less clunky)
- vfs_path: use boost filesystem's version of the basename/extension
functions
- lf_alloc: remove (no longer necessary, won't be finished - not worth
the trouble)
- path_util: remove path_foreach_component (replaced by better path
traversal logic) and PathPackage (obsoleted by fs::path)

! resource loading code now receives VfsPath as its filename. there is
also OsPath (native absolute path) and Path (relative to binaries/data)

- tex is now independent of file loading code; it just en/decodes
in-memory buffers
- wdll_ver: clean up, use smart pointer to simplify bailout code
- wsdl: remove nonexistent failure path from calc_gamma (cruised by here
because SDL_SetGamme is failing once after a cold boot at work)
- wsnd: simplify OpenAL DLL search, use boost::filesystem
- wutil: Wow64 redirection is now packaged in a (RAII) class

This was SVN commit r5525.
2007-12-22 18:15:52 +00:00

2066 lines
54 KiB
C++

/**
* =========================================================================
* File : snd_mgr.cpp
* Project : 0 A.D.
* Description : OpenAL sound engine. handles sound I/O, buffer
* : suballocation and voice management/prioritization.
* =========================================================================
*/
// license: GPL; see lib/license.txt
#include "precompiled.h"
#include "snd_mgr.h"
#include <sstream> // to extract snd_open's definition file contents
#include <string>
#include <vector>
#include <algorithm>
#include <deque>
#include <math.h>
#include "lib/path_util.h"
#include "../h_mgr.h"
#include "lib/file/vfs/vfs.h"
extern PIVFS g_VFS;
// for DLL-load hack in alc_init
#if OS_WIN
# include "lib/sysdep/win/win.h"
#endif
#include "lib/timer.h"
#include "lib/app_hooks.h"
#include "lib/external_libraries/openal.h"
#define OGG_HACK
#include "ogghack.h"
// 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 0
#else
# define WIN_LOADLIBRARY_HACK 0
#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.
// 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.
static bool al_initialized = false;
// 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 one
* error at a time, so this is called before and 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;
// count # times we've been called for the same function (this shows
// which of multiple AL_CHECK inside a function is the culprit)
static const char* last_caller;
static int num_times_within_same_function = 1;
if(caller == last_caller)
num_times_within_same_function++;
else
{
num_times_within_same_function = 1;
last_caller = caller;
}
const char* str = (const char*)alGetString(err);
debug_printf("OpenAL error: %s; called from %s (#%d)\n", str, caller, num_times_within_same_function);
debug_assert(0);
}
// 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.
//-----------------------------------------------------------------------------
// default (0): use OpenAL default device.
// note: that's why this needs to be a pointer instead of an array.
static const char* alc_dev_name = 0;
/**
* 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;
}
// no-op if not initialized yet, otherwise re-init.
return al_reinit();
}
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((ALCchar*)alc_dev_name);
}
// if invalid handle, handle it; otherwise, continue handler search.
__except(GetExceptionCode() == EXCEPTION_INVALID_HANDLE)
{
// ignore
}
#else
alc_dev = alcOpenDevice(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);
// FIXME Hack to get around exclusive access to the sound device
#if OS_UNIX
ret = INFO::OK;
#else
ret = ERR::FAIL;
#endif
}
// 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 };
// float view_direction[3], up_vector[3]; passed directly to OpenAL
static float al_listener_orientation[6] = {0, 0, -1, 0, 1, 0};
/**
* send the current listener properties to OpenAL.
*
* also called from al_init.
*/
static void al_listener_latch()
{
if(al_initialized)
{
AL_CHECK;
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;
// position will get sent too.
// this isn't called often, so we don't care.
al_listener_latch();
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)
{
AL_CHECK;
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));
AL_CHECK;
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()
{
debug_assert(al_bufs_outstanding == 0);
}
//-----------------------------------------------------------------------------
// 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);
AL_CHECK;
alDeleteSources(al_src_allocated, al_srcs);
AL_CHECK;
al_src_allocated = 0;
}
/**
* 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;
// don't compare against cap - it might have been
// decreased to less than were in use.
debug_assert(al_src_used < al_src_allocated);
}
/**
* 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, (ALCchar*)"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;
}
//-----------------------------------------------------------------------------
// 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
{
ALenum al_fmt;
ALsizei al_freq;
ALuint al_buf;
uint is_valid : 1;
#ifdef OGG_HACK
// pointer to Ogg instance
void* o;
#endif
};
H_TYPE_DEFINE(SndData);
static void SndData_init(SndData * UNUSED(sd), va_list UNUSED(args))
{
}
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;
al_buf_free(sd->al_buf);
#ifdef OGG_HACK
if(sd->o) ogg_release(sd->o);
#endif
}
// note: try not call this until SndData_reload is known to have succeeded.
// came up in topic#10719, "Problem freeing sounds loaded by JavaScript".
// irrespective of the h_force_free problem documented in hsd_free_all, we
// do not want to pollute hsd_list with handles that end up being freed
// (e.g. the handle was established in preparation for loading from file,
// but that load failed).
static void hsd_list_add(Handle hsd);
static LibError SndData_reload(SndData* sd, const VfsPath& pathname, Handle hsd)
{
// currently only supports OGG; WAV is no longer supported. writing our own loader is infeasible
// due to a seriously watered down spec with many incompatible variants.
// pulling in an external library (e.g. freealut) is deemed not worth the
// effort - OGG should be better in all cases.
#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
shared_ptr<u8> file; size_t file_size;
RETURN_ERR(g_VFS->LoadFile(pathname, file, file_size));
ALvoid* al_data = (ALvoid*)file.get();
ALsizei al_size = (ALsizei)file_size;
#ifdef OGG_HACK
std::vector<u8> data;
data.reserve(500000);
sd->o = ogg_create();
ogg_give_raw(sd->o, file.get(), 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;
#endif
sd->al_buf = al_buf_alloc(al_data, al_size, sd->al_fmt, sd->al_freq);
sd->is_valid = 1;
hsd_list_add(hsd);
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);
return INFO::OK;
}
static LibError SndData_to_string(const SndData* sd, char* buf)
{
const char* type = "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 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 VfsPath& pathname, bool is_stream)
{
debug_assert(!is_stream); // no longer supported
return h_alloc(H_SndData, pathname);
}
/**
* 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<Handle> 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).
//
// NB: re-initializing the sound library (e.g. after changing
// HW settings) requires all handles to be freed, even if cached.
// hence, we use h_force_free. unfortunately this causes the
// handle's tag to be ignored. it is conceivable that the wrong
// handle could be freed here.
//
// we rule this out with the following argument. either we're
// called when re-initializing sound or at exit. in the former
// case, h_force_free does check the handle type: only sounds are
// ever freed. we don't care if the wrong one is closed since all
// must be stomped upon. in the latter case, it definitely doesn't
// matter what we free. hence, no problem.
}
// 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::CB_CONTINUE = buffer has been returned; more are expected to be available.
* INFO::OK = buffer has been returned but is the last one (EOF).
* ERR::AGAIN = IO pending; call again later.
*/
static LibError snd_data_buf_get(Handle hsd, ALuint& al_buf)
{
H_DEREF(hsd, SndData, sd);
// clip: just return buffer (which was created in snd_data_load)
al_buf = sd->al_buf;
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 UNUSED(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).
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
const double pi = 3.14159265358979323846;
float y = cos(t*pi + 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;
debug_assert(0.0f <= fi.initial_val && fi.initial_val <= 1.0f);
debug_assert(0.0f <= fi.final_val && fi.final_val <= 1.0f);
// 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;
debug_assert(0.0f <= t && t <= 1.0f);
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,inf)
ALfloat pitch; /// (0,1]
ALboolean loop;
ALboolean relative;
// AL cone properties
ALfloat cone_inner;
ALfloat cone_outer;
ALfloat cone_gain;
/// 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 VfsPath& pathname, 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);
VfsPath dataPathname;
// pathname is a definition file containing the data file name and
// its gain.
if(fs::extension((const fs::path&)pathname) == ".txt")
{
shared_ptr<u8> buf; size_t size;
RETURN_ERR(g_VFS->LoadFile(pathname, buf, size));
std::istringstream def(std::string((char*)buf.get(), (int)size));
def >> dataPathname;
def >> vs->gain;
vs->gain /= 100.0f; // is stored as percent
}
// read the sound file directly and assume default gain (1.0).
else
{
dataPathname = pathname;
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(dataPathname, 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 <pos>
if(!(0.0f <= vs->gain && vs->gain <= 1.0f))
WARN_RETURN(ERR::_2);
if(!(0.0f < vs->pitch && vs->pitch <= 2.0f))
WARN_RETURN(ERR::_3);
if(*(u8*)&vs->loop > 1 || *(u8*)&vs->relative > 1)
WARN_RETURN(ERR::_4);
// <static_pri> and <cur_pri> 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 pathname. 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 VfsPath& pathname, 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, pathname, 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)
{
if(!hvs)
return INFO::OK;
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<VSrc*> 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_assert(0); // 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;
}
//-----------------------------------------------------------------------------
/**
* one-time init of parameters we will not later change. this separate from
* vsrc_latch as a tiny optimization.
*
* @param VSrc*
**/
static void vsrc_set_initial(VSrc* vs)
{
if(!vs->al_src)
return;
AL_CHECK;
// AL_VELOCITY and AL_DIRECTION correctly default to (0,0,0)
if(1) // TODO: music tracks and voiceovers, not sound effects
{
alSourcef (vs->al_src, AL_ROLLOFF_FACTOR, 0.0f);
alSourcei (vs->al_src, AL_SOURCE_RELATIVE, AL_TRUE);
}
AL_CHECK;
}
/**
* 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;
AL_CHECK;
#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) && vs->gain >= 0.0f);
debug_assert(!isnan(vs->pitch) && vs->pitch > 0.0f);
debug_assert(vs->loop == AL_TRUE || vs->loop == AL_FALSE);
debug_assert(!isnan(vs->cone_inner) && 0.0f <= vs->cone_inner && vs->cone_inner <= 360.0f);
debug_assert(!isnan(vs->cone_outer) && 0.0f <= vs->cone_outer && vs->cone_outer <= 360.0f);
debug_assert(!isnan(vs->cone_gain) && 0.0f <= vs->cone_gain && vs->cone_gain <= 1.0f);
#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);
alSourcef (vs->al_src, AL_CONE_INNER_ANGLE, vs->cone_inner);
alSourcef (vs->al_src, AL_CONE_OUTER_ANGLE, vs->cone_outer);
alSourcef (vs->al_src, AL_CONE_OUTER_GAIN, vs->cone_gain);
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 fade_ret = fade(vs->fade, snd_update_time, vs->gain);
// auto-free after fadeout.
if(fade_ret == FADE_TO_0_FINISHED)
{
vsrc_free(vs);
return INFO::OK; // don't continue - <vs> has been freed.
}
// fade in progress; latch current gain value.
else if(fade_ret == FADE_CHANGED)
vsrc_latch(vs);
int num_queued;
alGetSourcei(vs->al_src, AL_BUFFERS_QUEUED, &num_queued);
al_check("vsrc_update alGetSourcei");
const int num_processed = vsrc_deque_finished_bufs(vs);
UNUSED2(num_processed);
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
{
// request and queue a buffer.
ALuint al_buf;
LibError ret = snd_data_buf_get(vs->hsd, al_buf);
if(ret < 0 && ret != ERR::AGAIN)
return ret;
alSourceQueueBuffers(vs->al_src, 1, &al_buf);
al_check("vsrc_update alSourceQueueBuffers");
// SndData has reported that no further buffers are available.
if(ret == INFO::OK)
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
// (once only) set some properties with unspecified initial values
vsrc_set_initial(vs);
// set remaining (user-specifiable) properties.
vsrc_latch(vs);
// queue up some buffers (enough to start playing, at least).
vsrc_update(vs);
AL_CHECK;
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
AL_CHECK;
alSourceStop(vs->al_src);
AL_CHECK;
// deque and free remaining buffers (if sound was closed abruptly).
// (note: all queued buffers are now considered 'processed')
vsrc_deque_finished_bufs(vs);
al_src_free(vs->al_src);
return INFO::OK;
}
//-----------------------------------------------------------------------------
// snd_mgr API
//-----------------------------------------------------------------------------
/**
* 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 doubling/halving 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(pitch <= 0.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 <initial_gain> immediately and is moved toward
* <final_gain> over <length> 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;
}
/**
* set sound cone information
*
* cone_inner and cone_outer should be in the range 0.0 - 360.0,
* cone_gain (which is the outer cone gain) should range from 0.0 - 1.0
* @param cone_inner inner angle of sound cone
* @param cone_outer outer angle of sound cone
* @param cone_gain outer cone gain
* @return LibError
**/
LibError snd_set_cone(Handle hvs, const float cone_inner, const float cone_outer, const float cone_gain)
{
H_DEREF(hvs, VSrc, vs);
if(!(0.0f <= cone_inner && cone_inner <= 360.0f))
WARN_RETURN(ERR::INVALID_PARAM);
if(!(0.0f <= cone_outer && cone_outer <= 360.0f))
WARN_RETURN(ERR::INVALID_PARAM);
if(!(0.0f <= cone_gain && cone_gain <= 1.0f))
WARN_RETURN(ERR::INVALID_PARAM);
// set parameters in vsrc
vs->cone_inner = cone_inner;
vs->cone_outer = cone_outer;
vs->cone_gain = cone_gain;
vsrc_latch(vs);
return INFO::OK;
}
/** --TODO: Test to ensure this works (not currently necessary for intensity)
* find out if a sound is still playing
*
* @param hvs - handle to the snd to check
* @return bool true if playing
**/
bool is_playing(Handle hvs)
{
// (can't use H_DEREF due to bool return value)
VSrc* vs = H_USER_DATA(hvs, VSrc);
// sound has played and was already freed or is otherwise not loaded.
if(!vs)
return false;
// 'just' finished playing
if(!vs->al_src)
return false;
return true;
}
//-----------------------------------------------------------------------------
// 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 = std::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 LibError snd_init()
{
// (note: each VSrc_reload and therefore snd_open will fail)
if(snd_disabled)
return ERR::AGAIN; // NOWARN
return al_init();
}
LibError snd_disable(bool disabled)
{
snd_disabled = disabled;
if(snd_disabled)
{
debug_assert(!al_initialized); // 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()
{
al_shutdown(); // calls list_free_all
}