townengine/src/audio.c

236 lines
6.8 KiB
C
Raw Normal View History

2024-07-08 06:46:12 +00:00
#include "private/audio.h"
#include "audio.h"
#include "context.h"
#include "util.h"
#include <SDL2/SDL.h>
#include <stb_ds.h>
#include <physfs.h>
#define STB_VORBIS_HEADER_ONLY
#include <stb_vorbis.c>
#include <stdint.h>
#include <string.h>
/* 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);
2024-07-08 06:57:04 +00:00
/* 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;
}
2024-07-08 06:46:12 +00:00
/* panning and mixing */
2024-07-08 06:57:29 +00:00
const float left_panning = fminf(fabsf(channel->args.panning - 1.0f), 1.0f);
const float right_panning = fminf(fabsf(channel->args.panning + 1.0f), 1.0f);
2024-07-08 06:46:12 +00:00
#if AUDIO_N_CHANNELS == 2
for (int s = 0; s < samples_per_channel * ctx.audio_stream_channel_count; s += 2) {
/* left channel */
2024-07-08 06:57:29 +00:00
sstream[i + s] += (int16_t)(((int16_t *)buffer)[s] *
channel->args.volume * left_panning);
2024-07-08 06:46:12 +00:00
/* right channel */
2024-07-08 06:57:29 +00:00
sstream[i + s + 1] += (int16_t)(((int16_t *)buffer)[s + 1] *
channel->args.volume * right_panning);
2024-07-08 06:46:12 +00:00
}
#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);
}
}