diff --git a/assets/sounds/README.md b/assets/sounds/README.md new file mode 100644 index 0000000..6d5fa97 --- /dev/null +++ b/assets/sounds/README.md @@ -0,0 +1,16 @@ +# Sounds + +## Attribution + +### `bubble-pop.wav` + +Trimmed from a Pixabay sound effect. + +- Title: Bubble Pop 06 +- Creator: Universfield +- Source: [pixabay.com/sound-effects/bubble-pop-06-351337/](https://pixabay.com/sound-effects/bubble-pop-06-351337/) +- License: Pixabay Content License +- License summary: [pixabay.com/service/license-summary/](https://pixabay.com/service/license-summary/) +- License terms: [pixabay.com/service/terms/](https://pixabay.com/service/terms/) + +Optional credit line (Pixabay suggested): `by Universfield via Pixabay` diff --git a/assets/sounds/bubble-pop.wav b/assets/sounds/bubble-pop.wav new file mode 100644 index 0000000..cc88497 Binary files /dev/null and b/assets/sounds/bubble-pop.wav differ diff --git a/src/helpers/audio.c b/src/helpers/audio.c new file mode 100644 index 0000000..3addd05 --- /dev/null +++ b/src/helpers/audio.c @@ -0,0 +1,142 @@ +#include "audio.h" + +#include +#include +#include + +bool audio_manager_create(audio_manager_t* manager) { + SDL_assert(manager != NULL); + + memset(manager, 0, sizeof(*manager)); + + if (SDL_InitSubSystem(SDL_INIT_AUDIO) == false) { + SDL_Log("Failed to initialize SDL audio subsystem: %s", SDL_GetError()); + return false; + } + + const SDL_AudioSpec spec = {.format = SDL_AUDIO_F32, .channels = 2, .freq = 44100}; + + manager->stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, NULL, NULL); + if (manager->stream == NULL) { + SDL_Log("Failed to open audio device stream: %s", SDL_GetError()); + SDL_QuitSubSystem(SDL_INIT_AUDIO); + return false; + } + + if (SDL_ResumeAudioStreamDevice(manager->stream) == false) { + SDL_Log("Failed to resume audio stream device: %s", SDL_GetError()); + SDL_DestroyAudioStream(manager->stream); + manager->stream = NULL; + SDL_QuitSubSystem(SDL_INIT_AUDIO); + return false; + } + + manager->is_initialized = true; + return true; +} + +void audio_manager_destroy(audio_manager_t* manager) { + SDL_assert(manager != NULL); + + if (manager->is_initialized == false) { + return; + } + + for (size_t i = 0; i < SOUND_COUNT; ++i) { + if (manager->sounds[i].buffer != NULL) { + SDL_free(manager->sounds[i].buffer); + manager->sounds[i].buffer = NULL; + } + manager->sounds[i].length = 0; + } + + if (manager->stream != NULL) { + SDL_DestroyAudioStream(manager->stream); + manager->stream = NULL; + } + + SDL_QuitSubSystem(SDL_INIT_AUDIO); + + manager->is_initialized = false; +} + +bool audio_manager_load_sound(audio_manager_t* manager, sound_id_t id, const char* filepath) { + SDL_assert(manager != NULL); + SDL_assert(filepath != NULL); + + if (manager->is_initialized == false) { + SDL_Log("Audio manager not initialized, cannot load sound: %s", filepath); + return false; + } + + if (id >= SOUND_COUNT) { + SDL_Log("Invalid sound ID: %d", id); + return false; + } + + if (manager->sounds[id].buffer != NULL) { + SDL_Log("Sound already loaded for ID %d, skipping: %s", id, filepath); + return true; + } + + SDL_AudioSpec spec; + uint8_t* buffer = NULL; + Uint32 length = 0; + + if (SDL_LoadWAV(filepath, &spec, &buffer, &length) == false) { + SDL_Log("Failed to load audio file '%s': %s", filepath, SDL_GetError()); + return false; + } + + SDL_AudioSpec target_spec; + if (SDL_GetAudioStreamFormat(manager->stream, &target_spec, NULL) == false) { + SDL_Log("Failed to get audio stream format: %s", SDL_GetError()); + SDL_free(buffer); + return false; + } + + uint8_t* converted_buffer = NULL; + int converted_length = 0; + + if (SDL_ConvertAudioSamples(&spec, buffer, (int)length, &target_spec, &converted_buffer, &converted_length) == + false) { + SDL_Log("Failed to convert audio samples for '%s': %s", filepath, SDL_GetError()); + SDL_free(buffer); + return false; + } + + SDL_free(buffer); + + manager->sounds[id].buffer = converted_buffer; + manager->sounds[id].length = (size_t)converted_length; + manager->sounds[id].spec = target_spec; + + SDL_Log("Successfully loaded sound: %s (ID: %d, %d bytes)", filepath, id, converted_length); + return true; +} + +bool audio_manager_play_sound(audio_manager_t* manager, sound_id_t id) { + SDL_assert(manager != NULL); + + if (manager->is_initialized == false) { + return false; + } + + if (id >= SOUND_COUNT) { + SDL_Log("Invalid sound ID: %d", id); + return false; + } + + if (manager->sounds[id].buffer == NULL) { + SDL_Log("Sound not loaded for ID %d, cannot play", id); + return false; + } + + if (SDL_PutAudioStreamData(manager->stream, manager->sounds[id].buffer, (int)manager->sounds[id].length) == + false) { + SDL_Log("Failed to queue audio data for playback: %s", SDL_GetError()); + return false; + } + + return true; +} diff --git a/src/helpers/audio.h b/src/helpers/audio.h new file mode 100644 index 0000000..0c44556 --- /dev/null +++ b/src/helpers/audio.h @@ -0,0 +1,31 @@ +#ifndef AUDIO_H +#define AUDIO_H + +#include +#include +#include + +typedef enum { + SOUND_EAT_FOOD, + SOUND_COUNT // Must be last +} sound_id_t; + +typedef struct { + uint8_t* buffer; + size_t length; + SDL_AudioSpec spec; +} sound_data_t; + +typedef struct { + SDL_AudioStream* stream; + sound_data_t sounds[SOUND_COUNT]; + bool is_initialized; +} audio_manager_t; + +bool audio_manager_create(audio_manager_t* manager); +void audio_manager_destroy(audio_manager_t* manager); + +bool audio_manager_load_sound(audio_manager_t* manager, sound_id_t id, const char* filepath); +bool audio_manager_play_sound(audio_manager_t* manager, sound_id_t id); + +#endif // AUDIO_H diff --git a/src/snake.c b/src/snake.c index 91f957f..808b49e 100644 --- a/src/snake.c +++ b/src/snake.c @@ -129,6 +129,14 @@ bool snake_create(snake_t* snake, const char* title) { return false; } + if (audio_manager_create(&snake->audio) == false) { + SDL_Log("Warning: Failed to initialize audio, continuing without sound"); + } else { + if (audio_manager_load_sound(&snake->audio, SOUND_EAT_FOOD, "assets/sounds/bubble-pop.wav") == false) { + SDL_Log("Warning: Failed to load eating sound effect"); + } + } + Uint64 seed = SDL_GetTicksNS(); seed ^= SDL_GetPerformanceCounter(); seed ^= (Uint64)(uintptr_t)snake; @@ -175,6 +183,7 @@ void snake_destroy(snake_t* snake) { snake->text_score = NULL; } + audio_manager_destroy(&snake->audio); window_destroy(&snake->window); vector2i_set(&snake->position_head, 0, 0); @@ -353,6 +362,8 @@ void snake_update_fixed(snake_t* snake) { // Grow the snake if it hits array_food. if (test_food_collision(snake) == true) { + audio_manager_play_sound(&snake->audio, SOUND_EAT_FOOD); + vector2i_t new_segment_position; if (dynamic_array_is_empty(&snake->array_body) == true) { new_segment_position = snake->previous_position_head; diff --git a/src/snake.h b/src/snake.h index b816124..94ee747 100644 --- a/src/snake.h +++ b/src/snake.h @@ -2,6 +2,7 @@ #define SNAKE_H #include "helpers/window.h" +#include "helpers/audio.h" #include "utils/vector.h" #include "utils/dynamic_array.h" @@ -32,6 +33,7 @@ typedef struct { typedef struct { window_t window; + audio_manager_t audio; bool is_paused;