713 lines
16 KiB
C
713 lines
16 KiB
C
/**
|
|
* @file sound.c
|
|
*
|
|
* @brief Handle all the sound details.
|
|
*/
|
|
|
|
#include <sys/stat.h>
|
|
|
|
#include <SDL.h>
|
|
#include <SDL_thread.h>
|
|
#include <SDL/SDL_mixer.h>
|
|
|
|
#include "lephisto.h"
|
|
#include "log.h"
|
|
#include "pack.h"
|
|
#include "music.h"
|
|
#include "physics.h"
|
|
#include "sound.h"
|
|
|
|
#define SOUND_CHANNEL_MAX 256 /**< Number of sound channels to allocate. Overkill. */
|
|
|
|
#define SOUND_PREFIX "../snd/sounds/" /**< Prefix of where to find sounds. */
|
|
#define SOUND_SUFFIX ".wav" /**< Suffix of sounds. */
|
|
|
|
/* Global sound properties. */
|
|
int sound_disabled = 0; /**< Whether sound is disabled. */
|
|
static int sound_reserved = 0; /**< Amount of reserved channels. */
|
|
static double sound_pos[3]; /**< Position of listener. */
|
|
|
|
/**
|
|
* @struct alSound
|
|
*
|
|
* @brief Contains a sound buffer.
|
|
*/
|
|
typedef struct alSound_ {
|
|
char* name; /**< Buffers name. */
|
|
Mix_Chunk* buffer; /**< Buffer data. */
|
|
} alSound;
|
|
|
|
/**
|
|
* @typedef voice_state_t
|
|
*
|
|
* @brief The state of a voice.
|
|
*
|
|
* @sa alVoice
|
|
*/
|
|
typedef enum voice_state_ {
|
|
VOICE_STOPPED, /**< Voice is stopped. */
|
|
VOICE_PLAYING, /**< Voice is playing. */
|
|
VOICE_DESTROY /**< Voice should get destroyed asap. */
|
|
} voice_state_t;
|
|
|
|
/**
|
|
* @struct alVoice
|
|
*
|
|
* @brief Represents a voice in the game.
|
|
*
|
|
* A voice would be any object that is creating sound.
|
|
*/
|
|
typedef struct alVoice_ {
|
|
struct alVoice_* prev; /**< Linked list previous member. */
|
|
struct alVoice_* next; /**< Linked list next member. */
|
|
|
|
int id; /**< Identifier of the voice. */
|
|
double pos[2]; /**< Position of the voice. */
|
|
int channel; /**< Channel currently in use. */
|
|
unsigned int state; /**< Current state of the sound. */
|
|
} alVoice;
|
|
|
|
/* List of sounds available (All preloaded into a buffer). */
|
|
static int voice_genid = 0; /**< Voice identifier generator. */
|
|
static alSound* sound_list = NULL; /**< List of available sounds. */
|
|
static int sound_nlist = 0; /**< Number of available sounds. */
|
|
|
|
/* Voice linked list. */
|
|
static SDL_mutex* voice_lock = NULL;
|
|
static alVoice* voice_active = NULL; /**< Active voices. */
|
|
static alVoice* voice_pool = NULL; /**< Pool of free voices. */
|
|
|
|
/* General prototypes. */
|
|
static void print_MixerVersion(void);
|
|
static int sound_makeList(void);
|
|
static Mix_Chunk* sound_load(char* filename);
|
|
static void sound_free(alSound* snd);
|
|
/* Voices. */
|
|
static void voice_markStopped(int channel);
|
|
static alVoice* voice_new(void);
|
|
static int voice_add(alVoice* v);
|
|
static alVoice* voice_get(int id);
|
|
|
|
/**
|
|
* @fn int sound_init(void)
|
|
*
|
|
* @brief Initializes the sound subsystem.
|
|
* @return 0 on success.
|
|
*/
|
|
int sound_init(void) {
|
|
if(sound_disabled) return 0;
|
|
|
|
SDL_InitSubSystem(SDL_INIT_AUDIO);
|
|
if(Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 1024) < 0) {
|
|
WARN("Opening Audio: %s", Mix_GetError());
|
|
DEBUG();
|
|
sound_disabled = 1; /* Just disable sound then. */
|
|
music_disabled = 1;
|
|
return -1;
|
|
}
|
|
|
|
Mix_AllocateChannels(SOUND_CHANNEL_MAX);
|
|
|
|
/* Debug magic. */
|
|
print_MixerVersion();
|
|
|
|
/* Load up all the sounds. */
|
|
sound_makeList();
|
|
sound_volume(0.4);
|
|
|
|
/* Finish function. */
|
|
Mix_ChannelFinished(voice_markStopped);
|
|
|
|
/* Init the music. */
|
|
music_init();
|
|
|
|
/* Create the voice lock. */
|
|
voice_lock = SDL_CreateMutex();
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @fn static void print_MixerVersion(void)
|
|
*
|
|
* @brief Print the current and compiled SDL_Mixer versions.
|
|
*/
|
|
static void print_MixerVersion(void) {
|
|
int frequency;
|
|
Uint16 format;
|
|
int channels;
|
|
SDL_version compiled;
|
|
const SDL_version* linked;
|
|
char device[PATH_MAX];
|
|
|
|
/* Query stuff. */
|
|
Mix_QuerySpec(&frequency, &format, &channels);
|
|
MIX_VERSION(&compiled);
|
|
linked = Mix_Linked_Version();
|
|
SDL_AudioDriverName(device, PATH_MAX);
|
|
|
|
/* Version itself. */
|
|
DEBUG("SDL_Mixer: %d.%d.%d [compiled: %d.%d.%d]",
|
|
compiled.major, compiled.minor, compiled.patch,
|
|
linked->major, linked->minor, linked->patch);
|
|
|
|
/* Check if major/minor version differ. */
|
|
if((linked->major*100 + linked->minor) > compiled.major*100 + compiled.minor)
|
|
WARN("SDL_Mixer is newer than compiled version.");
|
|
if((linked->major*100 + linked->minor) < compiled.major*100 + compiled.minor)
|
|
WARN("SDL_Mixer is older than compiled version.");
|
|
/* Print other debug info. */
|
|
DEBUG("Driver: %s", device);
|
|
DEBUG("Format: %d Hz %s", frequency, (channels == 2) ? "Sterio" : "Mono");
|
|
DEBUG();
|
|
}
|
|
|
|
/**
|
|
* @fn void sound_exit(void)
|
|
*
|
|
* @brief Clean up after the sound subsystem.
|
|
*/
|
|
void sound_exit(void) {
|
|
int i;
|
|
alVoice* v;
|
|
|
|
/* Close the audio. */
|
|
Mix_CloseAudio();
|
|
SDL_DestroyMutex(voice_lock);
|
|
|
|
/* Free the voices. */
|
|
while(voice_active != NULL) {
|
|
v = voice_active;
|
|
voice_active = v->next;
|
|
free(v);
|
|
}
|
|
while(voice_pool != NULL) {
|
|
v = voice_pool;
|
|
voice_pool = v->next;
|
|
free(v);
|
|
}
|
|
|
|
/* Free the sounds. */
|
|
for(i = 0; i < sound_nlist; i++)
|
|
sound_free(&sound_list[i]);
|
|
free(sound_list);
|
|
sound_list = NULL;
|
|
sound_nlist = 0;
|
|
|
|
music_exit();
|
|
}
|
|
|
|
/**
|
|
* @fn void sound_get(char* name)
|
|
*
|
|
* @brief Get the buffer to sound of name.
|
|
* @param name Name of the sound to get id of.
|
|
* @return ID of the sound matching name.
|
|
*/
|
|
int sound_get(char* name) {
|
|
int i;
|
|
|
|
if(sound_disabled) return 0;
|
|
|
|
for(i = 0; i < sound_nlist; i++)
|
|
if(strcmp(name, sound_list[i].name)==0) {
|
|
return i;
|
|
}
|
|
|
|
WARN("Sound '%s' not found in sound list", name);
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* @fn int sound_play(int sound)
|
|
*
|
|
* @brief Play the sound in the first available channel.
|
|
* @param sound Sound to play.
|
|
* @return Voice identifier on success..
|
|
*/
|
|
int sound_play(int sound) {
|
|
alVoice* v;
|
|
|
|
if(sound_disabled) return 0;
|
|
|
|
if((sound < 0) || (sound > sound_nlist))
|
|
return -1;
|
|
|
|
v->channel = Mix_PlayChannel(-1, sound_list[sound].buffer, 0);
|
|
|
|
if(v->channel < 0)
|
|
WARN("Unable to play sound: %s", Mix_GetError());
|
|
|
|
v->state = VOICE_PLAYING;
|
|
v->id = ++voice_genid;
|
|
voice_add(v);
|
|
return v->id;
|
|
}
|
|
|
|
/**
|
|
* @fn int sound_playPos(int sound, double x, double y)
|
|
*
|
|
* @brief Play a sound based on position.
|
|
* @param sound Sound to play.
|
|
* @param x X position of sound.
|
|
* @param y Y position of sound.
|
|
* @return 0 on success.
|
|
*/
|
|
int sound_playPos(int sound, double x, double y) {
|
|
alVoice* v;
|
|
double angle, dist;
|
|
double px, py;
|
|
int idist;
|
|
|
|
if(sound_disabled) return 0;
|
|
|
|
if((sound < 0) || (sound > sound_nlist))
|
|
return -1;
|
|
|
|
/* Get a new voice. */
|
|
v = voice_new();
|
|
|
|
v->pos[0] = x;
|
|
v->pos[1] = y;
|
|
|
|
px = v->pos[0] - sound_pos[0];
|
|
py = v->pos[1] - sound_pos[1];
|
|
|
|
angle = sound_pos[2] - ANGLE(px, py)/M_PI*180.;
|
|
dist = MOD(px, py);
|
|
|
|
v->channel = Mix_PlayChannel(-1, sound_list[sound].buffer, 0);
|
|
|
|
if(v->channel < 0) {
|
|
WARN("Unable to play sound: %s", Mix_GetError());
|
|
return -1;
|
|
}
|
|
|
|
/* Need to make sure distance doesn't overflow. */
|
|
idist = (int)dist / 13.;
|
|
if(idist > 255) idist = 255;
|
|
if(Mix_SetPosition(v->channel, (Sint16)angle, (Uint8)idist) < 0) {
|
|
WARN("Unable to set sound position: %s", Mix_GetError());
|
|
return -1;
|
|
}
|
|
|
|
/* Actually add the voice to the list. */
|
|
v->state = VOICE_PLAYING;
|
|
v->id = ++voice_genid;
|
|
voice_add(v);
|
|
|
|
return v->id;
|
|
}
|
|
|
|
/**
|
|
* @fn int sound_updatePos(int voice, double x, double y)
|
|
*
|
|
* @brief Update the position of a voice.
|
|
* @param voice Identifier of the voice to update.
|
|
* @param x New x position to update to.
|
|
* @param y New y position to update to.
|
|
*/
|
|
int sound_updatePos(int voice, double x, double y) {
|
|
alVoice* v;
|
|
double angle, dist;
|
|
double px, py;
|
|
|
|
if(sound_disabled) return 0;
|
|
|
|
|
|
v = voice_get(voice);
|
|
|
|
if(v != NULL) {
|
|
v->pos[0] = x;
|
|
v->pos[1] = y;
|
|
|
|
px = x - sound_pos[0];
|
|
py = y - sound_pos[1];
|
|
|
|
angle = sound_pos[2] - ANGLE(px, py)/M_PI*180.;
|
|
dist = MOD(px, py);
|
|
|
|
if(Mix_SetPosition(v->channel, (int)angle, (int)dist/10) < 0) {
|
|
WARN("Unable to set sound position: %s", Mix_GetError());
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @fn int sound_update(void)
|
|
*
|
|
* @brief Update the sounds removing obsolete ones and such.
|
|
* @return 0 on success.
|
|
*/
|
|
int sound_update(void) {
|
|
alVoice* v, *tv;
|
|
|
|
if(sound_disabled) return 0;
|
|
|
|
if(voice_active == NULL) return 0;
|
|
|
|
/* The actualy control loop. */
|
|
for(v = voice_active; v != NULL; v = v->next) {
|
|
/* Destroy and toss into pool. */
|
|
if((v->state == VOICE_STOPPED) || (v->state == VOICE_DESTROY)) {
|
|
/* Remove from active list. */
|
|
tv = v->prev;
|
|
if(tv == NULL) {
|
|
voice_active = v->next;
|
|
if(voice_active != NULL)
|
|
voice_active->prev = NULL;
|
|
} else {
|
|
tv->next = v->next;
|
|
if(tv->next != NULL)
|
|
tv->next->prev = tv;
|
|
}
|
|
|
|
/* Add to free pool. */
|
|
v->next = voice_pool;
|
|
v->prev = NULL;
|
|
voice_pool = v;
|
|
v->channel = 0;
|
|
if(v->next != NULL)
|
|
v->next->prev = v;
|
|
|
|
/* Avoid loop blockage. */
|
|
v = (tv != NULL) ? tv->next : voice_active;
|
|
if(v == NULL) break;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @fn void sound_stop(int voice)
|
|
*
|
|
* @brief Stop a voice from playing.
|
|
* @param voice Identifier of the voice to stop.
|
|
*/
|
|
void sound_stop(int voice) {
|
|
alVoice* v;
|
|
|
|
if(sound_disabled) return;
|
|
|
|
v = voice_get(voice);
|
|
if(v != NULL) {
|
|
Mix_HaltChannel(v->channel);
|
|
v->state = VOICE_STOPPED;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @fn int sound_updateListener(double dir, double x, double y)
|
|
*
|
|
* @brief Update the sound listener.
|
|
* @param dir Direction listener if facing.
|
|
* @param x X position of the listener.
|
|
* @param y Y position of the listener.
|
|
*
|
|
* @sa sound_playPos
|
|
*/
|
|
int sound_updateListener(double dir, double x, double y) {
|
|
sound_pos[0] = x;
|
|
sound_pos[1] = y;
|
|
sound_pos[2] = dir/M_PI*180.;
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @fn static int sound_makeList(void)
|
|
*
|
|
* @brief Make the list of available sounds.
|
|
*/
|
|
static int sound_makeList(void) {
|
|
char** files;
|
|
uint32_t nfiles, i;
|
|
char tmp[64];
|
|
int len;
|
|
int mem;
|
|
|
|
if(sound_disabled) return 0;
|
|
|
|
/* Get the file list. */
|
|
files = pack_listfiles(data, &nfiles);
|
|
|
|
/* Load the profiles. */
|
|
mem = 0;
|
|
for(i = 0; i < nfiles; i++)
|
|
if((strncmp(files[i], SOUND_PREFIX, strlen(SOUND_PREFIX))==0) &&
|
|
(strncmp(files[i] + strlen(files[i]) - strlen(SOUND_SUFFIX),
|
|
SOUND_SUFFIX, strlen(SOUND_SUFFIX))==0)) {
|
|
|
|
/* Expand the selection size. */
|
|
sound_nlist++;
|
|
if(sound_nlist > mem) { /* We must grow. */
|
|
mem += 32; /* We'll overallocate most likely. */
|
|
sound_list = realloc(sound_list, mem*sizeof(alSound));
|
|
}
|
|
|
|
/* Remove the prefix and suffix. */
|
|
len = strlen(files[i]) - strlen(SOUND_SUFFIX SOUND_PREFIX);
|
|
strncpy(tmp, files[i] + strlen(SOUND_PREFIX), len);
|
|
tmp[len] = '\0';
|
|
|
|
/* give it the new name. */
|
|
sound_list[sound_nlist-1].name = strdup(tmp);
|
|
sound_list[sound_nlist-1].buffer = sound_load(files[i]);
|
|
}
|
|
|
|
/* Shrink to minimum ram usage. */
|
|
sound_list = realloc(sound_list, sound_nlist*sizeof(alSound));
|
|
|
|
/* Free the char* allocated by pack. */
|
|
for(i = 0; i < nfiles; i++)
|
|
free(files[i]);
|
|
free(files);
|
|
|
|
DEBUG("Loaded %d sound%s", sound_nlist, (sound_nlist==1)?"":"s");
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @fn int sound_volume(const double vol)
|
|
*
|
|
* @brief Set the volume.
|
|
* @param vol Volume to set to.
|
|
* @return 0 on success.
|
|
*/
|
|
int sound_volume(const double vol) {
|
|
if(sound_disabled) return 0;
|
|
return Mix_Volume(-1, MIX_MAX_VOLUME*vol);
|
|
}
|
|
|
|
/**
|
|
* @fn stastic Mix_Chunk* sound_load(char* filename)
|
|
*
|
|
* @brief Load a sound into the sound_list.
|
|
* @paramfilename Name for the file to load.
|
|
* @return The SDL_Mixer of the loaded chunk.
|
|
*
|
|
* @sa sound_makeList
|
|
*/
|
|
static Mix_Chunk* sound_load(char* filename) {
|
|
void* wavdata;
|
|
unsigned int size;
|
|
SDL_RWops* rw;
|
|
Mix_Chunk* buffer;
|
|
|
|
if(sound_disabled) return NULL;
|
|
|
|
/* Get the file data buffer from the packfile. */
|
|
wavdata = pack_readfile(DATA, filename, &size);
|
|
|
|
rw = SDL_RWFromMem(wavdata, size);
|
|
|
|
/* Bind to OpenAL buffer. */
|
|
buffer = Mix_LoadWAV_RW(rw, 1);
|
|
|
|
if(buffer == NULL)
|
|
DEBUG("Unable to load sound '%s' : %s", filename, Mix_GetError());
|
|
|
|
/* Finish up. */
|
|
free(wavdata);
|
|
return buffer;
|
|
}
|
|
|
|
/**
|
|
* @fn static void sound_free(alSound* snd)
|
|
*
|
|
* @brief Frees the sound.
|
|
* @param snd Sound to free.
|
|
*/
|
|
static void sound_free(alSound* snd) {
|
|
/* Free the stuff. */
|
|
if(snd->name) {
|
|
free(snd->name);
|
|
snd->name = NULL;
|
|
}
|
|
|
|
Mix_FreeChunk(snd->buffer);
|
|
snd->buffer = NULL;
|
|
}
|
|
|
|
/**
|
|
* @fn int sound_reverse(int num)
|
|
*
|
|
* @brief Reserve num channels.
|
|
* @param num Number of channels to reserve.
|
|
* @return 0 on success.
|
|
*/
|
|
int sound_reserve(int num) {
|
|
int ret;
|
|
|
|
if(sound_disabled) return 0;
|
|
|
|
sound_reserved += num;
|
|
ret = Mix_ReserveChannels(num);
|
|
|
|
if(ret != sound_reserved) {
|
|
WARN("Unable to reserve %d channels: %s", sound_reserved, Mix_GetError());
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @fn int sound_createGroup(int tag, int start, int size)
|
|
*
|
|
* @brief Create a sound group.
|
|
* @param tag Identifier of the group to create.
|
|
* @param start Where to start creating the group.
|
|
* @param size Size of the group.
|
|
* @return 0 on success.
|
|
*/
|
|
int sound_createGroup(int tag, int start, int size) {
|
|
int ret;
|
|
|
|
if(sound_disabled) return 0;
|
|
|
|
ret = Mix_GroupChannels(start, start+size-1, tag);
|
|
|
|
if(ret != size) {
|
|
WARN("Unable to create sound group: %s", Mix_GetError());
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @fn int sound_playGroup(int group, int sound, int once)
|
|
*
|
|
* @brief Play a sound in a group.
|
|
* @param group Group to play sound in.
|
|
* @param sound Sound to play.
|
|
* @param once Whether to play only once.
|
|
* @param 0 on success.
|
|
*/
|
|
int sound_playGroup(int group, int sound, int once) {
|
|
int ret, channel;
|
|
|
|
if(sound_disabled) return 0;
|
|
|
|
channel = Mix_GroupAvailable(group);
|
|
if(channel == -1) {
|
|
WARN("Group '%d' has no free channels!", group);
|
|
return -1;
|
|
}
|
|
|
|
ret = Mix_PlayChannel(channel, sound_list[sound].buffer,
|
|
(once == 0) ? -1 : 0);
|
|
|
|
if(ret < 0) {
|
|
WARN("Unable to play sound %d for group %d: %s",
|
|
sound, group, Mix_GetError());
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @fn void sound_stopGroup(int group)
|
|
*
|
|
* @brief Stop all the sounds in a group.
|
|
* @param group Group to stop all it's sounds.
|
|
*/
|
|
void sound_stopGroup(int group) {
|
|
if(sound_disabled) return;
|
|
|
|
Mix_HaltGroup(group);
|
|
}
|
|
|
|
/**
|
|
* @fn static void voice_markStopped(int channel)
|
|
*
|
|
* @brief Mark the voice to which channel belongs to as stopped.
|
|
*/
|
|
static void voice_markStopped(int channel) {
|
|
alVoice* v;
|
|
|
|
for(v = voice_active; v != NULL; v = v->next)
|
|
if(v->channel == channel) {
|
|
v->state = VOICE_STOPPED;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @fn static alVoice* voice_new(void)
|
|
*
|
|
* @brief Get a new voice ready to be used.
|
|
* @return New voice ready to use.
|
|
*/
|
|
static alVoice* voice_new(void) {
|
|
alVoice* v;
|
|
|
|
/* No free voices, allocate a new one. */
|
|
if(voice_pool == NULL) {
|
|
v = malloc(sizeof(alVoice));
|
|
memset(v, 0, sizeof(alVoice));
|
|
voice_pool = v;
|
|
return v;
|
|
}
|
|
|
|
/* First free voice. */
|
|
v = voice_pool; /* We do not touch the next nor prev, it's still in the pool. */
|
|
return v;
|
|
}
|
|
|
|
/**
|
|
* @fn static int voice_add(alVoice* v)
|
|
*
|
|
* @brief Add a voice to the active voice stack.
|
|
* @param v Voice to add to the active voice stack.
|
|
* @return 0 on success.
|
|
*/
|
|
static int voice_add(alVoice* v) {
|
|
alVoice* tv;
|
|
|
|
/* Remove from pool. */
|
|
if(v->prev != NULL) {
|
|
tv = v->prev;
|
|
tv->next = v->next;
|
|
if(tv->next != NULL)
|
|
voice_pool->prev = NULL;
|
|
} else { /* Set pool to be the next. */
|
|
voice_pool = v->next;
|
|
}
|
|
|
|
/* Insert to the front of active voices. */
|
|
tv = voice_active;
|
|
v->next = tv;
|
|
v->prev = NULL;
|
|
voice_active = v;
|
|
if(tv != NULL)
|
|
tv->prev = v;
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @fn static alVoice* voice_get(int id)
|
|
*
|
|
* @brief Get a voice by identifier.
|
|
* @param id Identifier to look for.
|
|
* @return Voice matching identifier or NULL if not found.
|
|
*/
|
|
static alVoice* voice_get(int id) {
|
|
alVoice* v;
|
|
|
|
if(voice_active == NULL) return NULL;
|
|
|
|
for(v = voice_active; v != NULL; v = v->next)
|
|
if(v->id == id) {
|
|
return v;
|
|
}
|
|
return NULL;
|
|
}
|
|
|