diff --git a/Makefile b/Makefile index 9b2114a..b3d8df7 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,12 @@ NATIVE_CC := cc SDL_AUDIO_DRIVERS ?= sdl endif +ifneq ($(ENABLE_OPENAL),) +CFLAGS += -DENABLE_OPENAL +LDFLAGS += -lopenal +SDL_AUDIO_DRIVERS += openal +endif + PB12_COMPRESS := build/pb12$(EXESUFFIX) ifeq ($(PLATFORM),Darwin) diff --git a/SDL/audio.c b/SDL/audio.c index f9a49a7..c667f6a 100644 --- a/SDL/audio.c +++ b/SDL/audio.c @@ -1,6 +1,7 @@ #include #include #include +#include #include "audio/audio.h" #include "configuration.h" @@ -16,6 +17,9 @@ bool GB_audio_init(void) GB_AUDIO_DRIVER_REF(XAudio2_7), #endif GB_AUDIO_DRIVER_REF(SDL), +#ifdef ENABLE_OPENAL + GB_AUDIO_DRIVER_REF(OpenAL), +#endif }; // First try the preferred driver @@ -25,6 +29,9 @@ bool GB_audio_init(void) continue; } if (driver->audio_init()) { + if (driver->audio_deinit) { + atexit(driver->audio_deinit); + } return true; } } @@ -33,6 +40,9 @@ bool GB_audio_init(void) for (unsigned i = 0; i < sizeof(drivers) / sizeof(drivers[0]); i++) { driver = drivers[i]; if (driver->audio_init()) { + if (driver->audio_deinit) { + atexit(driver->audio_deinit); + } return true; } } @@ -91,6 +101,9 @@ const char *GB_audio_driver_name_at_index(unsigned index) GB_AUDIO_DRIVER_REF(XAudio2_7), #endif GB_AUDIO_DRIVER_REF(SDL), +#ifdef ENABLE_OPENAL + GB_AUDIO_DRIVER_REF(OpenAL), +#endif }; if (index >= sizeof(drivers) / sizeof(drivers[0])) { return ""; diff --git a/SDL/audio/audio.h b/SDL/audio/audio.h index 1b2fa71..0de5746 100644 --- a/SDL/audio/audio.h +++ b/SDL/audio/audio.h @@ -12,6 +12,7 @@ unsigned GB_audio_get_frequency(void); size_t GB_audio_get_queue_length(void); void GB_audio_queue_sample(GB_sample_t *sample); bool GB_audio_init(void); +void GB_audio_deinit(void); const char *GB_audio_driver_name(void); const char *GB_audio_driver_name_at_index(unsigned index); @@ -23,6 +24,7 @@ typedef struct { typeof(GB_audio_get_queue_length) *audio_get_queue_length; typeof(GB_audio_queue_sample) *audio_queue_sample; typeof(GB_audio_init) *audio_init; + typeof(GB_audio_deinit) *audio_deinit; const char *name; } GB_audio_driver_t; @@ -34,6 +36,7 @@ typedef struct { .audio_get_queue_length = _audio_get_queue_length, \ .audio_queue_sample = _audio_queue_sample, \ .audio_init = _audio_init, \ + .audio_deinit = _audio_deinit, \ .name = #_name, \ } diff --git a/SDL/audio/openal.c b/SDL/audio/openal.c new file mode 100644 index 0000000..8a2072b --- /dev/null +++ b/SDL/audio/openal.c @@ -0,0 +1,287 @@ +#include "audio.h" +#include +#include +#include +#include +#include + +#define BUFFER_LEN_MS 32 + +static ALCdevice *al_device = NULL; +static ALCcontext *al_context = NULL; +static GB_sample_t *audio_buffer = NULL; +static ALuint al_source = 0; +static ALCint sample_rate = 0; +static unsigned buffer_size = 0; +static unsigned buffer_pos = 0; + +#define AL_ERR_STRINGIFY(x) #x +#define AL_ERR_TOSTRING(x) AL_ERR_STRINGIFY(x) +#define AL_ERROR(msg) check_al_error(msg, __FILE__ ":" AL_ERR_TOSTRING(__LINE__)) + +// Check if the previous OpenAL call returned an error. +// If an error occurred a message will be logged to stderr. +bool check_al_error(const char *user_msg, const char *file) { + ALCenum error = alGetError(); + char *description; + + switch (error) { + case AL_NO_ERROR: + return false; + case AL_INVALID_NAME: + description = "A bad name (ID) was passed to an OpenAL function"; + break; + case AL_INVALID_ENUM: + description = "An invalid enum value was passed to an OpenAL function"; + break; + case AL_INVALID_VALUE: + description = "An invalid value was passed to an OpenAL function"; + break; + case AL_INVALID_OPERATION: + description = "The requested operation is not valid"; + break; + case AL_OUT_OF_MEMORY: + description = "The requested operation resulted in OpenAL running out of memory"; + break; + } + + if (user_msg != NULL) { + fprintf(stderr, "[%s] %s: %s\n", file, user_msg, description); + } + else { + fprintf(stderr, "[%s] %s\n", file, description); + } + + return true; +} + +static void _audio_deinit(void) { + // Stop the source (this should mark all queued buffers as processed) + alSourceStop(al_source); + + // Free the processed buffers while ignoring potential errors + ALint processed; + alGetSourcei(al_source, AL_BUFFERS_PROCESSED, &processed); + while (processed--) { + ALuint buffer; + alSourceUnqueueBuffers(al_source, 1, &buffer); + alDeleteBuffers(1, &buffer); + } + + alDeleteSources(1, &al_source); + alcDestroyContext(al_context); + alcCloseDevice(al_device); + + if (audio_buffer != NULL) { + free(audio_buffer); + audio_buffer = NULL; + } +} + +static void free_processed_buffers(void) +{ + ALint processed; + alGetSourcei(al_source, AL_BUFFERS_PROCESSED, &processed); + if (AL_ERROR("Failed to query number of processed buffers")) { + _audio_deinit(); + return; + } + + while (processed--) { + ALuint buffer; + + alSourceUnqueueBuffers(al_source, 1, &buffer); + if (AL_ERROR("Failed to unqueue buffer")) { + _audio_deinit(); + return; + } + + alDeleteBuffers(1, &buffer); + if (AL_ERROR("Failed to delete buffer")) { + _audio_deinit(); + return; + } + } +} + +static bool _audio_is_playing(void) +{ + ALenum state; + alGetSourcei(al_source, AL_SOURCE_STATE, &state); + if (AL_ERROR("Failed to query source state")) { + _audio_deinit(); + return false; + } + + return state == AL_PLAYING; +} + +static void _audio_clear_queue(void) +{ + alSourceStop(al_source); + if (AL_ERROR(NULL)) { + _audio_deinit(); + return; + } + + free_processed_buffers(); +} + +static void _audio_set_paused(bool paused) +{ + if (paused) { + alSourcePause(al_source); + } + else { + alSourcePlay(al_source); + } + + if (AL_ERROR(NULL)) { + _audio_deinit(); + } +} + +static unsigned _audio_get_frequency(void) +{ + return sample_rate; +} + +static size_t _audio_get_queue_length(void) +{ + // Get the number of all attached buffers + ALint buffers; + alGetSourcei(al_source, AL_BUFFERS_QUEUED, &buffers); + if (AL_ERROR("Failed to query number of queued buffers")) { + buffers = 0; + } + + // Get the number of all processed buffers (ready to be detached) + ALint processed; + alGetSourcei(al_source, AL_BUFFERS_PROCESSED, &processed); + if (AL_ERROR("Failed to query number of processed buffers")) { + processed = 0; + } + + return (buffers - processed) * buffer_size * sizeof(GB_sample_t); +} + +static void _audio_queue_sample(GB_sample_t *sample) +{ + audio_buffer[buffer_pos++] = *sample; + + if (buffer_pos == buffer_size) { + buffer_pos = 0; + + ALuint al_buffer; + alGenBuffers(1, &al_buffer); + if (AL_ERROR("Failed to create audio buffer")) { + return _audio_deinit(); + } + + alBufferData(al_buffer, AL_FORMAT_STEREO16, audio_buffer, buffer_size * sizeof(GB_sample_t), sample_rate); + if (AL_ERROR("Failed to buffer data")) { + return _audio_deinit(); + } + + alSourceQueueBuffers(al_source, 1, &al_buffer); + if (AL_ERROR("Failed to queue buffer")) { + return _audio_deinit(); + } + + // In case of an audio underrun, the source might + // have finished playing all attached buffers + // which means its status will be "AL_STOPPED". + if (!_audio_is_playing()) { + alSourcePlay(al_source); + } + + free_processed_buffers(); + } +} + +static bool _audio_init(void) +{ + // Open the default device + al_device = alcOpenDevice(NULL); + if (!al_device) { + AL_ERROR("Failed to open device"); + return false; + } + + // Create a new audio context without special attributes + al_context = alcCreateContext(al_device, NULL); + if (al_context == NULL) { + AL_ERROR("Failed to create context"); + _audio_deinit(); + return false; + } + + // Enable our audio context + if (!alcMakeContextCurrent(al_context)) { + AL_ERROR("Failed to set context"); + _audio_deinit(); + return false; + } + + // Query the sample rate of the playback device + alcGetIntegerv(al_device, ALC_FREQUENCY, 1, &sample_rate); + if (AL_ERROR("Failed to query sample rate")) { + _audio_deinit(); + return false; + } + + // Allocate our working buffer + buffer_size = (sample_rate * BUFFER_LEN_MS) / 1000; + audio_buffer = malloc(buffer_size * sizeof(GB_sample_t)); + if (audio_buffer == NULL) { + fprintf(stderr, "Failed to allocate audio buffer\n"); + _audio_deinit(); + return false; + } + + // Create our playback source + alGenSources(1, &al_source); + if (AL_ERROR(NULL)) { + _audio_deinit(); + return false; + } + + // Keep the pitch as is + alSourcef(al_source, AL_PITCH, 1); + if (AL_ERROR(NULL)) { + _audio_deinit(); + return false; + } + + // Keep the volume as is + alSourcef(al_source, AL_GAIN, 1); + if (AL_ERROR(NULL)) { + _audio_deinit(); + return false; + } + + // Position our source at the center of the 3D space + alSource3f(al_source, AL_POSITION, 0, 0, 0); + if (AL_ERROR(NULL)) { + _audio_deinit(); + return false; + } + + // Our source is fixed in space + alSource3f(al_source, AL_VELOCITY, 0, 0, 0); + if (AL_ERROR(NULL)) { + _audio_deinit(); + return false; + } + + // Our source does not loop + alSourcei(al_source, AL_LOOPING, AL_FALSE); + if (AL_ERROR(NULL)) { + _audio_deinit(); + return false; + } + + return true; +} + +GB_AUDIO_DRIVER(OpenAL); diff --git a/SDL/audio/sdl.c b/SDL/audio/sdl.c index 6b987f1..80ac302 100644 --- a/SDL/audio/sdl.c +++ b/SDL/audio/sdl.c @@ -97,4 +97,10 @@ static bool _audio_init(void) return true; } +void _audio_deinit(void) +{ + _audio_set_paused(true); + SDL_CloseAudioDevice(device_id); +} + GB_AUDIO_DRIVER(SDL); diff --git a/SDL/audio/xaudio2.c b/SDL/audio/xaudio2.c index b1ffe26..78fe877 100644 --- a/SDL/audio/xaudio2.c +++ b/SDL/audio/xaudio2.c @@ -151,4 +151,9 @@ static bool _audio_init(void) return true; } +void _audio_deinit(void) +{ + _audio_set_paused(true); +} + GB_AUDIO_DRIVER(XAudio2); diff --git a/SDL/audio/xaudio2_7.c b/SDL/audio/xaudio2_7.c index 788ba1c..adff450 100644 --- a/SDL/audio/xaudio2_7.c +++ b/SDL/audio/xaudio2_7.c @@ -171,4 +171,9 @@ static bool _audio_init(void) return true; } +void _audio_deinit(void) +{ + _audio_set_paused(true); +} + GB_AUDIO_DRIVER(XAudio2_7);