0ad/source/lib/res/snd.cpp
janwas 1d69845cee snd: better snd_disable dox
ia32: fix comments and fix EBP warning
wdbg: fix bug if exception locus is unavailable
wdetect: store full DS driver path (-> version reported correctly)

This was SVN commit r2491.
2005-07-20 00:54:39 +00:00

1799 lines
46 KiB
C++
Executable File

// OpenAL sound engine
//
// Copyright (c) 2004-5 Jan Wassenberg
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// Contact info:
// Jan.Wassenberg@stud.uni-karlsruhe.de
// http://www.stud.uni-karlsruhe.de/~urkt/
#include "precompiled.h"
#include "res/res.h"
#include "res/snd.h"
#include <sstream> // to extract snd_open's definition file contents
#include <string>
#include <vector>
#include <algorithm>
#include <math.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
#ifdef OS_LINUX
# include <AL/alexttypes.h>
#endif
// for DLL-load hack in alc_init
#ifdef _WIN32
# include "sysdep/win/win_internal.h"
#endif
#define OGG_HACK
#include "ogghack.h"
#ifdef _MSC_VER
# pragma comment(lib, "openal32.lib")
# pragma comment(lib, "alut.lib") // alutLoadWAVMemory
#endif
// components:
// - alc*: OpenAL context
// readies OpenAL for use; allows specifying the device.
// - al_listener*: OpenAL listener
// owns position/orientation and master gain.
// - al_buf*: OpenAL buffer suballocator
// for convenience; also makes sure all have been freed at exit.
// - al_src*: OpenAL source suballocator
// avoids high source alloc cost. also enforces user-set source limit.
// - al_init*: OpenAL startup mechanism
// allows deferred init (speeding up start time) and runtime reset.
// - snd_dev*: device enumeration
// lists names of all available devices (for sound options screen).
// - io_buf*: buffer suballocator
// avoids frequent large alloc/frees and therefore heap fragmentation.
// - stream*: streaming
// passes chunks of data (read via async I/O) to snd_data on request.
// - snd_data*: sound data provider
// holds audio data (clip or stream) and returns OpenAL buffers on request.
// - hsd_list*: list of SndData instances
// ensures all are freed when desired (despite being cached).
// - list*: list of active sounds.
// sorts by priority for voice management, and has each VSrc update itself.
// - vsrc*: audio source
// owns source properties and queue, references SndData.
// - vm*: voice management
// grants the currently most 'important' sounds a hardware voice.
static bool al_initialized = false;
// indicates OpenAL is ready for use. checked by other components
// when deciding if they can pass settings changes to OpenAL directly,
// or whether they need to be saved until init.
// used by snd_dev_set to reset OpenAL after device has been changed.
static int al_reinit();
// used by VSrc_reload to init on demand.
static int snd_init();
// used by al_shutdown to free all VSrc and SndData objects, respectively,
// so that they release their OpenAL sources and buffers.
static int 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");
}
///////////////////////////////////////////////////////////////////////////////
//
// 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.
int 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 0;
// 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 0;
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 int alc_init()
{
int ret = 0;
// HACK: OpenAL loads and unloads these DLLs several times on Windows.
// we hold a reference to prevent the actual unload,
// thus speeding up startup by 100..400 ms. everything works ATM;
// hopefully, OpenAL doesn't rely on them actually being unloaded.
#ifdef _WIN32
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.
#ifdef _WIN32
__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 = -1;
}
// release DLL references, so BoundsChecker doesn't complain at exit.
#ifdef _WIN32
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];
static float al_listener_orientation[6];
// 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("al_listener_latch");
}
}
// set amplitude modifier, which is effectively applied to all sounds.
// must be non-negative; 1 -> unattenuated, 0.5 -> -6 dB, 0 -> silence.
int snd_set_master_gain(float gain)
{
if(gain < 0)
{
debug_warn("snd_set_master_gain: gain < 0");
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 0;
}
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_buf_alloc");
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_buf_free");
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("al_buf_shutdown: 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("al_src_shutdown");
}
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).
int 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 0;
}
// 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.
else
return 1;
}
///////////////////////////////////////////////////////////////////////////////
//
// 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 int al_init()
{
// only take action on first call, OR when re-initializing.
if(al_initialized)
return 0;
CHECK_ERR(alc_init());
al_initialized = true;
// these can't fail:
al_src_init();
al_listener_latch();
return 0;
}
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 int 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 0;
// 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.
int snd_dev_prepare_enum()
{
if(alcIsExtensionPresent(0, (ALubyte*)"ALC_ENUMERATION_EXT") != AL_TRUE)
return ERR_NO_SYS;
devs = (const char*)alcGetString(0, ALC_DEVICE_SPECIFIER);
return 0;
}
// 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)
{
if(!io_bufs)
debug_warn("io_buf_alloc: not enough memory to allocate buffer pool");
else
debug_warn("io_buf_alloc: max #streams exceeded");
// can't happen (tm) because stream_open enforces MAX_STREAMS.
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 int stream_issue(Stream* s)
{
if(s->active_ios >= MAX_IOS)
return 0;
void* buf = io_buf_alloc();
if(!buf)
return ERR_NO_MEM;
Handle h = vfs_start_io(s->hf, STREAM_BUF_SIZE, buf);
CHECK_ERR(h);
s->ios[s->active_ios++] = h;
return 0;
}
// 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 int stream_buf_get(Stream* s, void*& data, size_t& size)
{
if(s->active_ios == 0)
return ERR_EOF;
Handle hio = s->ios[0];
// has it finished? if not, bail.
int is_complete = vfs_io_complete(hio);
CHECK_ERR(is_complete);
if(is_complete == 0)
return ERR_AGAIN;
// get its buffer.
CHECK_ERR(vfs_wait_io(hio, data, size));
// no delay, since vfs_io_complete == 1
s->last_buf = data;
// (next stream_buf_discard will free this buffer)
return 0;
}
// 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 int stream_buf_discard(Stream* s)
{
Handle hio = s->ios[0];
int ret = vfs_discard_io(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 int stream_open(Stream* s, const char* fn)
{
if(active_streams >= MAX_STREAMS)
{
debug_warn("stream_open: MAX_STREAMS exceeded - why?");
return -1;
// fail, because we wouldn't have enough IO buffers for all
}
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 0;
}
// close a stream, which may currently be active.
// returns the first error that occurred while waiting for IOs / closing file.
static int stream_close(Stream* s)
{
int ret = 0;
int 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;
}
///////////////////////////////////////////////////////////////////////////////
//
// 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;
bool is_stream;
#ifdef OGG_HACK
// pointer to Ogg instance
void* o;
#endif
};
H_TYPE_DEFINE(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;
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);
}
///////////////////////////////////////////////////////////////////////////////
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)
{
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 int 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)
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
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)
return -1;
return stream_open(&sd->s, fn);
}
// else: clip
void* file;
size_t file_size;
RETURN_ERR(vfs_load(fn, file, file_size));
ALvoid* al_data = 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, 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);
mem_free(file);
return 0;
}
// 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 int snd_data_free(Handle& hsd)
{
return h_free(hsd, H_SndData);
}
// (need to convert ERR_EOF and ERR_AGAIN to legitimate return values -
// for the caller, those aren't errors.)
enum BufRet
{
// buffer has been returned; barring errors, more will be available.
BUF_OK = 0,
// this was the last buffer we will return (end of file reached).
BUF_EOF = 1,
// no buffer returned - still streaming in ATM. call again later.
BUF_AGAIN = 2,
// anything else: negative error code
};
static int snd_data_buf_get(Handle hsd, ALuint& al_buf)
{
int err = 0;
// 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 BUF_EOF;
}
// stream:
// .. check if IO finished.
void* data;
size_t size;
err = stream_buf_get(&sd->s, data, size);
if(err == ERR_AGAIN)
return BUF_AGAIN;
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 BUF_EOF;
CHECK_ERR(err);
// al_buf valid and next IO issued successfully.
return BUF_OK;
}
static int 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 0;
// stream: we had allocated an additional buffer, so free it now.
al_buf_free(al_buf);
return 0;
}
///////////////////////////////////////////////////////////////////////////////
//
// 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
};
struct VSrc
{
ALuint al_src;
// handle to this VSrc, so that it can close itself.
Handle hvs;
// associated sound data
Handle hsd;
// controls vsrc_update behavior
uint flags;
// AL source properties (set via snd_set*)
ALfloat pos[3];
ALfloat gain; // [0,1]
ALfloat pitch; // (0,1]
ALboolean loop;
ALboolean relative;
// priority for voice management
float static_pri; // as given by snd_play
float cur_pri; // holds newly calculated value
};
H_TYPE_DEFINE(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::vector<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("list_remove: 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 free_vs(VSrc* vs)
{
snd_free(vs->hvs);
}
static int list_free_all()
{
list_foreach(free_vs);
return 0;
}
///////////////////////////////////////////////////////////////////////////////
// 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("vsrc_latch");
}
// 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("vsrc_deque_finished_bufs");
return num_processed;
}
static int vsrc_update(VSrc* vs)
{
if(!vs->al_src)
return 0;
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 0;
}
}
// 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);
CHECK_ERR(ret);
alSourceQueueBuffers(vs->al_src, 1, &al_buf);
al_check("vsrc_update alSourceQueueBuffers");
}
while(to_fill-- && ret == BUF_OK);
// SndData has reported that no further buffers are available.
if(ret == BUF_EOF)
vs->flags |= VS_EOF;
}
return 0;
}
// attempt to (re)start playing. fails if a source cannot be allocated
// (see below). called by snd_play and voice management.
static int vsrc_grant(VSrc* vs)
{
// already playing - bail
if(vs->al_src)
return 0;
// 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 -1;
// 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("vsrc_grant");
// 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 0;
}
// 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 int vsrc_reclaim(VSrc* vs)
{
// don't own a source - bail.
if(!vs->al_src)
return -1;
alSourceStop(vs->al_src);
al_check("src_stop");
// (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 0;
}
///////////////////////////////////////////////////////////////////////////////
static void VSrc_init(VSrc* vs, va_list args)
{
vs->flags = va_arg(args, uint);
}
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;
}
vsrc_reclaim(vs);
snd_data_free(vs->hsd);
}
static int 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.
int 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 && !strcmp(ext, ".txt"))
{
void* def_file;
size_t def_size;
RETURN_ERR(vfs_load(fn, def_file, def_size));
std::istringstream def(std::string((char*)def_file, (int)def_size));
mem_free(def_file);
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);
CHECK_ERR(vs->hsd);
return 0;
}
// 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.
int snd_free(Handle& hvs)
{
return h_free(hvs, H_VSrc);
}
// 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).
int 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 0;
}
// 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).
int 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 0;
}
// change gain (amplitude modifier) of the sound source.
// must be non-negative; 1 -> unattenuated, 0.5 -> -6 dB, 0 -> silence.
// may be called at any time; fails with invalid handle return if
// the sound has already been closed (e.g. it never played).
int snd_set_gain(Handle hvs, float gain)
{
H_DEREF(hvs, VSrc, vs);
if(!(0.0f <= gain && gain <= 1.0f))
return -1;
vs->gain = gain;
vsrc_latch(vs);
return 0;
}
// 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).
int snd_set_pitch(Handle hvs, float pitch)
{
H_DEREF(hvs, VSrc, vs);
if(!(0.0f < pitch && pitch <= 1.0f))
return -1;
vs->pitch = pitch;
vsrc_latch(vs);
return 0;
}
// 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).
int snd_set_loop(Handle hvs, bool loop)
{
H_DEREF(hvs, VSrc, vs);
vs->loop = loop;
vsrc_latch(vs);
return 0;
}
///////////////////////////////////////////////////////////////////////////////
//
// 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 int 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);
// add / remove buffers for each source.
list_foreach((void(*)(VSrc*))vsrc_update);
return 0;
}
///////////////////////////////////////////////////////////////////////////////
// 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).
int 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 0;
if(pos || dir || up)
al_listener_set_pos(pos, dir, up);
vm_update();
return 0;
}
// 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 int snd_init()
{
// (note: each VSrc_reload and therefore snd_open will fail)
if(snd_disabled)
return ERR_AGAIN;
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.
int snd_disable(bool disabled)
{
snd_disabled = disabled;
if(snd_disabled)
{
if(al_initialized)
debug_warn("snd_disable: already initialized => disable is pointless");
return 0;
}
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
}