1
0
forked from 0ad/0ad
0ad/source/lib/res/sound/snd_mgr.cpp
janwas 836cd08d5e # add CppDoc file header to all files, along with their descriptions.
also:
- renamed wdll to delay_load, and wdll_ver to dll_ver
- removed empty wcpu

This was SVN commit r3751.
2006-04-11 23:59:08 +00:00

2062 lines
56 KiB
C++

/**
* =========================================================================
* 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 <sstream> // to extract snd_open's definition file contents
#include <string>
#include <vector>
#include <algorithm>
#include <deque>
#include <math.h>
#include "maths/MathUtil.h"
#ifdef __APPLE__
# include <OpenAL/alut.h>
#else
# include <AL/al.h>
# include <AL/alc.h>
# include <AL/alut.h> // alutLoadWAVMemory
#endif
// for AL_FORMAT_VORBIS_EXT decl on Linux
#if OS_LINUX
# include <AL/alexttypes.h>
#endif
// for DLL-load hack in alc_init
#if OS_WIN
# include "sysdep/win/win_internal.h"
#endif
#include "../res.h"
#include "snd_mgr.h"
#include "lib/timer.h"
#include "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.
#define WIN_LOADLIBRARY_HACK 1
// 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.
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.
// 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.
//
// return 0 on success, or the status returned by OpenAL re-init.
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 ERR_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 ERR_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;
static void alc_shutdown()
{
if(alc_ctx)
{
alcMakeContextCurrent(0);
alcDestroyContext(alc_ctx);
}
if(alc_dev)
alcCloseDevice(alc_dev);
}
static LibError alc_init()
{
LibError ret = ERR_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
// 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.
// must be non-negative; 1 -> unattenuated, 0.5 -> -6 dB, 0 -> silence.
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 ERR_OK;
}
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();
}
// return euclidean distance squared between listener and point.
// used to determine sound priority.
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.
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;
}
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).
//
///////////////////////////////////////////////////////////////////////////////
static const uint AL_SRC_MAX = 64;
// 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 ALuint al_srcs[AL_SRC_MAX];
// stack of sources (first allocated is [0])
static uint al_src_allocated;
// number of valid sources in al_srcs[] (set by al_src_init)
static uint al_src_used = 0;
// number of sources currently in use
static uint al_src_cap = AL_SRC_MAX;
// user-set limit on how many sources may be used
// 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;
}
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;
}
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.
// return 0 on success, or 1 if limit was ignored
// (e.g. if higher than an implementation-defined limit anyway).
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 ERR_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 ERR_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.
//
///////////////////////////////////////////////////////////////////////////////
// called from each snd_open; no harm if called more than once.
static LibError al_init()
{
// only take action on first call, OR when re-initializing.
if(al_initialized)
return ERR_OK;
CHECK_ERR(alc_init());
al_initialized = true;
// these can't fail:
al_src_init();
al_listener_latch();
return ERR_OK;
}
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;
}
// called from snd_dev_set (no other settings require re-init ATM).
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 ERR_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.
//
///////////////////////////////////////////////////////////////////////////////
static const char* devs;
// 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).
// prepare to enumerate all device names (this resets the list returned by
// snd_dev_next). return 0 on success, otherwise -1 (only if the requisite
// OpenAL extension isn't available). on failure, 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.
// may be called each time the device list is needed.
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 ERR_OK;
}
// return the next device name, or 0 if all have been returned.
// do not call unless snd_dev_prepare_enum succeeded!
// not thread-safe! (static data from snd_dev_prepare_enum is used)
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;
static void* io_bufs;
// one large allocation for all buffers
static void* io_buf_freelist;
// list of free buffers. start of buffer holds pointer to next in list.
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;
}
// 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;
}
}
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;
}
// 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.
struct Stream
{
Handle hf;
Handle ios[MAX_IOS];
uint active_ios;
void* last_buf;
// set by stream_buf_get, used by stream_buf_discard to free buf.
};
// called from SndData_reload and snd_data_buf_get.
static LibError stream_issue(Stream* s)
{
if(s->active_ios >= MAX_IOS)
return ERR_OK;
void* buf = io_buf_alloc();
if(!buf)
WARN_RETURN(ERR_NO_MEM);
Handle h = vfs_io_issue(s->hf, STREAM_BUF_SIZE, buf);
CHECK_ERR(h);
s->ios[s->active_ios++] = h;
return ERR_OK;
}
// if the first pending IO hasn't completed, return ERR_AGAIN;
// otherwise, return a negative error code or 0 on success,
// if the pending IO's buffer is returned.
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.
RETURN_ERR(vfs_io_wait(hio, data, size));
// no delay, since vfs_io_has_completed == 1
s->last_buf = data;
// (next stream_buf_discard will free this buffer)
return ERR_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!
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.
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);
CHECK_ERR(s->hf);
for(int i = 0; i < MAX_IOS; i++)
CHECK_ERR(stream_issue(s));
return ERR_OK;
}
// close a stream, which may currently be active.
// returns the first error that occurred while waiting for IOs / closing file.
static LibError stream_close(Stream* s)
{
LibError ret = ERR_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;
}
static LibError stream_validate(const Stream* s)
{
if(s->active_ios > MAX_IOS)
return ERR_1;
// <last_buf> has no invariant we could check.
return ERR_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.
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 = strrchr(fn, '.');
// .. OGG (data will be passed directly to OpenAL)
if(ext && !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(ext && !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 ERR_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<u8> 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 ERR_OK;
}
static LibError SndData_validate(const SndData* sd)
{
if(sd->al_fmt == 0)
return ERR_11;
if((uint)sd->al_freq > 100000) // suspicious
return ERR_12;
if(sd->al_buf == 0)
return ERR_13;
if(sd->is_stream)
RETURN_ERR(stream_validate(&sd->s));
return ERR_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 ERR_OK;
}
// open and return a handle to a sound file's data.
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
}
// close the sound file data <hsd> and set hsd to 0.
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;
// called from SndData_reload; will later be removed via hsd_list_free_all.
static void hsd_list_add(Handle hsd)
{
hsd_list.push_back(hsd);
}
// 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);
}
///////////////////////////////////////////////////////////////////////////////
// returns:
// ERR_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.
// or negative error code.
static LibError snd_data_buf_get(Handle hsd, ALuint& al_buf)
{
LibError err = ERR_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
CHECK_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
CHECK_ERR(err);
// al_buf valid and next IO issued successfully.
return ERR_OK;
}
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 ERR_OK;
// stream: we had allocated an additional buffer, so free it now.
al_buf_free(al_buf);
return ERR_OK;
}
//-----------------------------------------------------------------------------
// fading
//-----------------------------------------------------------------------------
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;
}
// cosine curve
static float fade_factor_s_curve(float t)
{
float y = cos(t*M_PI + M_PI);
// map [-1,1] to [0,1]
return (y + 1.0f) / 2.0f;
}
enum FadeRet
{
FADE_NO_CHANGE,
FADE_CHANGED,
FADE_TO_0_FINISHED
};
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;
}
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
};
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
uint flags; // VSrcFlags
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.
CHECK_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 = strchr(fn, '.');
if(ext && !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 ERR_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)
return ERR_1;
// no limitations on <pos>
if(!(0.0f <= vs->gain && vs->gain <= 1.0f))
return ERR_2;
if(!(0.0f < vs->pitch && vs->pitch <= 1.0f))
return ERR_3;
if(*(u8*)&vs->loop > 1 || *(u8*)&vs->relative > 1)
return ERR_4;
// <static_pri> and <cur_pri> have no invariant we could check.
return ERR_OK;
}
static LibError VSrc_to_string(const VSrc* vs, char* buf)
{
snprintf(buf, H_STRING_LEN, "al_src = %d", vs->al_src);
return ERR_OK;
}
// open and return a handle to a sound instance.
//
// if <snd_fn> is 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, <snd_fn> is taken to be the sound file name and
// gain is set to the default of 1.0 (no attenuation).
//
// is_stream (default false) forces the sound to be opened as a stream:
// opening is faster, it won't be kept in memory, but only one instance
// can be open at a time.
//
// note: RES_UNIQUE forces each instance to get a new resource
// (which is of course what we want).
Handle snd_open(const char* snd_fn, bool is_stream)
{
uint flags = 0;
if(is_stream)
flags |= VS_IS_STREAM;
return h_alloc(H_VSrc, snd_fn, RES_UNIQUE, flags);
}
// close the sound <hvs> and set hvs to 0. if it was playing,
// it will be stopped. sounds are closed automatically when done
// playing; this is provided for completeness only.
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<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);
}
// skip past <skip> entries; if end_idx != 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;
}
static void list_sort()
{
std::sort(vsrcs.begin(), vsrcs.end(), is_greater);
}
// O(N)!
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;
}
static void list_prune_removed()
{
// prune removed entries (value 0), so code below can grant the first
// al_src_cap entries a soure. see rationale in list_remove.
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 ERR_OK;
}
///////////////////////////////////////////////////////////////////////////////
// send the properties to OpenAL (when we actually have a source).
// called by snd_set* and vsrc_grant.
static void vsrc_latch(VSrc* vs)
{
if(!vs->al_src)
return;
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;
}
// return 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;
static LibError vsrc_update(VSrc* vs)
{
if(!vs->al_src)
return ERR_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 ERR_OK; // don't continue - <vs> 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 ERR_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)
CHECK_ERR(ret);
alSourceQueueBuffers(vs->al_src, 1, &al_buf);
al_check("vsrc_update alSourceQueueBuffers");
}
while(to_fill-- && ret == ERR_OK);
// SndData has reported that no further buffers are available.
if(ret == ERR_EOF)
vs->flags |= VS_EOF;
}
return ERR_OK;
}
// attempt to (re)start playing. fails if a source cannot be allocated
// (see below). called by snd_play and voice management.
static LibError vsrc_grant(VSrc* vs)
{
// already playing - bail
if(vs->al_src)
return ERR_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("vsrc_grant alSourcePlay");
return ERR_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.
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 ERR_OK;
}
///////////////////////////////////////////////////////////////////////////////
// request the sound <hs> 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.
// priority (min 0 .. max 1, default 0) indicates which sounds are
// considered more important; this is attenuated by distance to the
// listener (see snd_update).
LibError snd_play(Handle hs, float static_pri)
{
H_DEREF(hs, 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 ERR_OK;
}
// change 3d position of the sound source.
// if relative (default false), (x,y,z) is treated as relative to the
// listener; otherwise, it is the position in world coordinates.
//
// may be called at any time; fails with invalid handle return if
// the sound has already been closed (e.g. it never played).
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 ERR_OK;
}
// change gain (amplitude modifier) of the sound source.
// must be non-negative; 1 -> unattenuated, 0.5 -> -6 dB, 0 -> silence.
//
// 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).
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 ERR_OK;
}
// change pitch shift of the sound source.
// 1.0 means no change; each reduction by 50% equals a pitch shift of
// -12 semitones (one octave). zero is invalid.
//
// may be called at any time; fails with invalid handle return if
// the sound has already been closed (e.g. it never played).
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 ERR_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).
LibError snd_set_loop(Handle hvs, bool loop)
{
H_DEREF(hvs, VSrc, vs);
vs->loop = loop;
vsrc_latch(vs);
return ERR_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. <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 current <final_gain> parameter.
// special cases:
// - if <initial_gain> < 0 (an otherwise illegal value), the sound's
// current gain is used as the start value (useful for fading out).
// - if <final_gain> is 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.
//
// 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.
// 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
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 ERR_OK;
}
///////////////////////////////////////////////////////////////////////////////
//
// voice management: grants the currently most 'important' sounds
// a hardware voice.
//
///////////////////////////////////////////////////////////////////////////////
static float magnitude_2(const float v[3])
{
return v[0]*v[0] + v[1]*v[1] + v[2]*v[2];
}
// list_foreach callback
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;
}
static void reclaim(VSrc* vs)
{
vsrc_reclaim(vs);
if(!vs->loop)
snd_free(vs->hvs);
}
static void grant(VSrc* vs)
{
vsrc_grant(vs);
}
// no-op if OpenAL not yet initialized.
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 ERR_OK;
}
///////////////////////////////////////////////////////////////////////////////
// perform housekeeping (e.g. streaming); call once a frame.
//
// additionally, if any parameter is non-NULL, we set the listener
// position, look direction, and up vector (in world coordinates).
// (allow any and all of them to be 0 in case world isn't initialized yet).
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 ERR_OK;
if(pos || dir || up)
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 ERR_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. returns ERR_AGAIN if sound disabled,
// otherwise the status returned by al_init.
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.
LibError snd_disable(bool disabled)
{
snd_disabled = disabled;
if(snd_disabled)
{
if(al_initialized)
debug_warn("already initialized => disable is pointless");
return ERR_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
}