From 8a8f62dc256bc754c8cb69cc9f94f682e9e16668 Mon Sep 17 00:00:00 2001 From: veclavtalica Date: Mon, 8 Jul 2024 09:46:12 +0300 Subject: [PATCH] .ogg playback --- CMakeLists.txt | 7 +- data/music/test.ogg | 3 + src/audio.c | 228 +++++++++++++++++++++++++++++++++++++++ src/audio.h | 36 +++++++ src/config.h | 3 + src/context.c | 2 +- src/context.h | 6 +- src/game/scenes/ingame.c | 7 ++ src/main.c | 23 +++- src/private/audio.h | 49 +++++++++ src/util.c | 4 +- src/util.h | 4 +- 12 files changed, 363 insertions(+), 9 deletions(-) create mode 100644 data/music/test.ogg create mode 100644 src/audio.c create mode 100644 src/audio.h create mode 100644 src/private/audio.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a9216f..269e810 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,10 +19,13 @@ add_subdirectory(third-party/physfs) set(SOURCE_FILES third-party/physfs/extras/physfsrwops.c + third-party/stb/stb_vorbis.c + src/config.h src/context.h src/context.c src/main.c + src/audio.c src/util.c src/util.h src/rendering.c src/rendering.h src/textures.c src/textures.h @@ -75,8 +78,7 @@ else() -Wshadow -Wdouble-promotion -Wconversion -Wno-sign-conversion - -Werror=vla - -Werror=alloca) + -Werror=vla) set(BUILD_FLAGS # these SHOULDN'T break anything... -fno-math-errno @@ -88,6 +90,7 @@ else() -flto) set(BUILD_FLAGS_DEBUG -g3 + -gdwarf -fsanitize-trap=undefined) target_compile_options(${PROJECT_NAME} PRIVATE ${WARNING_FLAGS} diff --git a/data/music/test.ogg b/data/music/test.ogg new file mode 100644 index 0000000..10bbd82 --- /dev/null +++ b/data/music/test.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a964aeb4120f1542d7ff3371eb640a67354a082f44e71a58020c9002163e361 +size 2127638 diff --git a/src/audio.c b/src/audio.c new file mode 100644 index 0000000..85848fb --- /dev/null +++ b/src/audio.c @@ -0,0 +1,228 @@ +#include "private/audio.h" +#include "audio.h" +#include "context.h" +#include "util.h" + +#include +#include +#include + +#define STB_VORBIS_HEADER_ONLY +#include + +#include +#include + +/* TODO: default to float sampling format? */ + +static const char *audio_exts[audio_file_type_count] = { + ".ogg", /* audio_file_type_ogg */ + ".xm", /* audio_file_type_xm */ +}; + + +/* TODO: count frames without use, free the memory when threshold is met */ +/* stores path to data hash, useful for sound effects */ +static struct audio_file_cache { + char *key; + struct audio_file_cache_value { + unsigned char *data; + size_t len; + } value; +} *audio_file_cache; + + +static int64_t get_audio_data(const char *path, unsigned char **data) { + const struct audio_file_cache *cache = shgetp_null(audio_file_cache, path); + if (!cache) { + unsigned char *file; + int64_t len = file_to_bytes(path, &file); + if (len == -1) { + CRY("Audio error", "Error reading file"); + return -1; + } + + const struct audio_file_cache_value value = { file, (size_t)len }; + shput(audio_file_cache, path, value); + + *data = file; + return len; + } + + *data = cache->value.data; + return (int64_t)cache->value.len; +} + + +void play_audio(const char *path, const char *channel) { + const struct audio_channel_pair *pair = shgetp_null(ctx.audio_channels, channel); + if (!pair) + play_audio_ex(path, channel, get_default_audio_args()); + else + play_audio_ex(path, channel, pair->value.args); +} + + +static t_audio_file_type infer_audio_file_type(const char *path) { + size_t path_len = strlen(path); + + for (int i = 0; i < audio_file_type_count; ++i) { + size_t ext_length = strlen(audio_exts[i]); + if (path_len <= ext_length) + continue; + + if (strcmp(&path[path_len - ext_length], audio_exts[i]) == 0) + return (t_audio_file_type)i; + } + + return audio_file_type_unknown; +} + + +/* TODO: error propagation and clearing of resources on partial success? */ +/* or should we expect things to simply fail? */ +static union audio_context init_audio_context(const char *path, t_audio_file_type type) { + switch (type) { + case audio_file_type_ogg: { + unsigned char *data; + int64_t len = get_audio_data(path, &data); + if (len == -1) { + CRY("Audio error", "Error reading file"); + break; + } + + int error = 0; + stb_vorbis* handle = stb_vorbis_open_memory(data, (int)len, &error, NULL); + if (error != 0) { + CRY("Audio error", "Error reading .ogg file"); + break; + } + + stb_vorbis_info info = stb_vorbis_get_info(handle); + + return (union audio_context){ + .vorbis = { + .data = data, + .handle = handle, + .frequency = info.sample_rate, + .channel_count = (uint8_t)info.channels, + } + }; + } + + default: + CRY("Audio error", "Unhandled audio format (in init)"); + return (union audio_context){0}; + } + + return (union audio_context){0}; +} + + +static void repeat_audio(struct audio_channel *channel) { + (void)channel; + + /* TODO */ +} + + +void play_audio_ex(const char *path, const char *channel, t_play_audio_args args) { + struct audio_channel_pair *pair = shgetp_null(ctx.audio_channels, channel); + + /* create a channel if it doesn't exist */ + if (!pair) { + t_audio_file_type file_type = infer_audio_file_type(path); + struct audio_channel new_channel = { + .args = args, + .file_type = file_type, + .context = init_audio_context(path, file_type), + .path = path, + }; + shput(ctx.audio_channels, channel, new_channel); + pair = shgetp_null(ctx.audio_channels, channel); + } + + /* TODO: destroy and create new context when channel is reused for different file */ + + /* works for both restarts and new audio */ + if (strcmp(pair->value.path, path) == 0) + repeat_audio(&pair->value); +} + + +t_play_audio_args get_default_audio_args(void) { + return (t_play_audio_args){ + .repeat = false, + .crossfade = false, + .volume = 1.0f, + .panning = 0.0f, + }; +} + + +static void audio_sample_and_mixin_channel(struct audio_channel *channel, + uint8_t *stream, + int len) { + int16_t *const sstream = (int16_t *)stream; + static uint8_t buffer[16384]; + const int buffer_frames = sizeof (buffer) / sizeof (int16_t); + + switch (channel->file_type) { + case audio_file_type_ogg: { + /* feed stream for needed conversions */ + for (int i = 0; i < (len / 2);) { + const int n_frames = ((len / 2) - i) > buffer_frames ? buffer_frames : (len / 2) - i; + + /* TODO: handle end of file */ + const int samples_per_channel = stb_vorbis_get_samples_short_interleaved( + channel->context.vorbis.handle, + channel->context.vorbis.channel_count, + (int16_t *)buffer, + n_frames); + + /* panning and mixing */ + +#if AUDIO_N_CHANNELS == 2 + + for (int s = 0; s < samples_per_channel * ctx.audio_stream_channel_count; s += 2) { + /* left channel */ + { + const float panning = fminf(fabsf(channel->args.panning - 1.0f), 1.0f); + const float volume = channel->args.volume * panning; + sstream[i + s] += (int16_t)(((int16_t *)buffer)[s] * volume); + } + + /* right channel */ + { + const float panning = fminf(fabsf(channel->args.panning + 1.0f), 1.0f); + const float volume = channel->args.volume * panning; + sstream[i + s + 1] += (int16_t)(((int16_t *)buffer)[s + 1] * volume); + } + } +#else +#error "Unimplemented channel count" +#endif + + i += samples_per_channel * ctx.audio_stream_channel_count; + } + + break; + } + + default: + CRY("Audio error", "Unhandled audio format (in sampling)"); + break; + } +} + + +void audio_callback(void *userdata, uint8_t *stream, int len) { + (void)userdata; + + /* prepare for mixing */ + SDL_memset(stream, sizeof (uint16_t), len * ctx.audio_stream_channel_count); + + for (int i = 0; i < shlen(ctx.audio_channels); ++i) { + audio_sample_and_mixin_channel(&ctx.audio_channels[i].value, stream, len); + } +} diff --git a/src/audio.h b/src/audio.h new file mode 100644 index 0000000..5812b2e --- /dev/null +++ b/src/audio.h @@ -0,0 +1,36 @@ +#ifndef AUDIO_H +#define AUDIO_H + +#include + + +typedef struct play_audio_args { + /* default: false */ + bool repeat; + /* crossfade between already playing audio on a given channel, if any */ + /* default: false */ + bool crossfade; + /* range: 0.0f to 1.0f */ + /* default: 100.0f */ + float volume; + /* range: -1.0 to 1.0f */ + /* default: 0.0f */ + float panning; +} t_play_audio_args; + + +/* plays audio file at specified channel or anywhere if NULL is passed */ +/* path must contain valid file extension to infer which file format it is */ +/* supported formats: .ogg, .xm */ +/* preserves args that are already specified on the channel */ +void play_audio(const char *path, const char *channel); + +void play_audio_ex(const char *path, const char *channel, t_play_audio_args args); + +void set_audio_args(const char *channel, t_play_audio_args args); + +t_play_audio_args get_audio_args(const char *channel); + +t_play_audio_args get_default_audio_args(void); + +#endif diff --git a/src/config.h b/src/config.h index c1d6e95..805214f 100644 --- a/src/config.h +++ b/src/config.h @@ -24,6 +24,9 @@ #define NUM_KEYBIND_SLOTS 8 +#define AUDIO_FREQUENCY 48000 +#define AUDIO_N_CHANNELS 2 + /* 1024 * 1024 */ /* #define UMKA_STACK_SIZE 1048576 */ diff --git a/src/context.c b/src/context.c index 45cd133..2c8a7bc 100644 --- a/src/context.c +++ b/src/context.c @@ -1,3 +1,3 @@ #include "context.h" -t_ctx ctx; +t_ctx ctx = {0}; diff --git a/src/context.h b/src/context.h index 55d4d8f..5a49ca1 100644 --- a/src/context.h +++ b/src/context.h @@ -26,7 +26,11 @@ typedef struct context { struct rect_primitive *render_queue_rectangles; struct circle_primitive *render_queue_circles; - struct audio_channel *audio_channels; + struct audio_channel_pair *audio_channels; + SDL_AudioDeviceID audio_device; + int audio_stream_frequency; + SDL_AudioFormat audio_stream_format; + uint8_t audio_stream_channel_count; struct circle_radius_cache_item *circle_radius_hash; diff --git a/src/game/scenes/ingame.c b/src/game/scenes/ingame.c index 24b9496..749f3a3 100644 --- a/src/game/scenes/ingame.c +++ b/src/game/scenes/ingame.c @@ -2,6 +2,8 @@ #include "title.h" #include "scene.h" +#include "../../audio.h" + static void ingame_tick(struct state *state) { struct scene_ingame *scn = (struct scene_ingame *)state->scene; @@ -28,5 +30,10 @@ struct scene *ingame_scene(struct state *state) { new_scene->world = world_create(); new_scene->player = player_create(new_scene->world); + play_audio_ex("music/test.ogg", "soundtrack", (t_play_audio_args){ + .volume = 0.8f, + .panning = -0.5f + }); + return (struct scene *)new_scene; } diff --git a/src/main.c b/src/main.c index 8804245..ad1b544 100644 --- a/src/main.c +++ b/src/main.c @@ -4,10 +4,11 @@ #include "util.h" #include "textures.h" #include "game/game.h" +#include "private/audio.h" #include -#include #include +#include #include #include @@ -179,6 +180,26 @@ static bool initialize(void) { SDL_RenderSetLogicalSize(ctx.renderer, RENDER_BASE_WIDTH, RENDER_BASE_HEIGHT); SDL_GetRendererOutputSize(ctx.renderer, &ctx.window_w, &ctx.window_h); + /* audio initialization */ + { + + SDL_AudioSpec request, got; + SDL_zero(request); + + request.freq = AUDIO_FREQUENCY; + request.format = AUDIO_S16; + request.channels = AUDIO_N_CHANNELS; + request.callback = audio_callback; + /* TODO: check for errors */ + ctx.audio_device = SDL_OpenAudioDevice(NULL, 0, &request, &got, 0); + ctx.audio_stream_format = got.format; + ctx.audio_stream_frequency = got.freq; + ctx.audio_stream_channel_count = got.channels; + + SDL_PauseAudioDevice(ctx.audio_device, 0); + + } + /* images */ if (IMG_Init(IMG_INIT_PNG) == 0) { CRY_SDL("SDL_image initialization failed."); diff --git a/src/private/audio.h b/src/private/audio.h new file mode 100644 index 0000000..16a4344 --- /dev/null +++ b/src/private/audio.h @@ -0,0 +1,49 @@ +#ifndef PRIVATE_AUDIO_H +#define PRIVATE_AUDIO_H + +#include "../audio.h" + +#include + +#define STB_VORBIS_HEADER_ONLY +#include + +#include +#include + + +typedef enum audio_file_type { + audio_file_type_ogg, + audio_file_type_xm, + audio_file_type_count, + audio_file_type_unknown, +} t_audio_file_type; + + +union audio_context { + struct { + stb_vorbis *handle; + unsigned char *data; + int frequency; + uint8_t channel_count; + } vorbis; +}; + + +struct audio_channel { + t_play_audio_args args; + enum audio_file_type file_type; + union audio_context context; /* interpreted by `file_type` value */ + const char *path; +}; + + +struct audio_channel_pair { + char *key; + struct audio_channel value; +}; + + +void audio_callback(void *userdata, uint8_t *stream, int len); + +#endif diff --git a/src/util.c b/src/util.c index 6509132..5551eee 100644 --- a/src/util.c +++ b/src/util.c @@ -101,7 +101,7 @@ void *ccalloc(size_t num, size_t size) { } -int64_t file_to_bytes(char *path, unsigned char **buf_out) { +int64_t file_to_bytes(const char *path, unsigned char **buf_out) { SDL_RWops *handle = PHYSFSRWOPS_openRead(path); if (handle == NULL) { @@ -119,7 +119,7 @@ int64_t file_to_bytes(char *path, unsigned char **buf_out) { } -char *file_to_str(char *path) { +char *file_to_str(const char *path) { SDL_RWops *handle = PHYSFSRWOPS_openRead(path); if (handle == NULL) { diff --git a/src/util.h b/src/util.h index c6bd2e1..bbdb150 100644 --- a/src/util.h +++ b/src/util.h @@ -51,10 +51,10 @@ void *ccalloc(size_t num, size_t size); /* sets buf_out to a pointer to a byte buffer which must be freed. */ /* returns the size of this buffer. */ -int64_t file_to_bytes(char *path, unsigned char **buf_out); +int64_t file_to_bytes(const char *path, unsigned char **buf_out); /* returns a pointer to a string which must be freed */ -char *file_to_str(char *path); +char *file_to_str(const char *path); /* returns true if str ends with suffix */