#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); /* handle end of file */ if (samples_per_channel == 0) { if (channel->args.repeat) /* seek to start and try sampling some more */ stb_vorbis_seek_start(channel->context.vorbis.handle); else /* leave silence */ break; } /* 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); } }