/** * @file sound.c * * @brief Handle all the sound details. */ #include #include #include #include #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; }