partially done work on total source tree rework, separation of engine context and game context, generalization of renderer for different backends as well as web platform target
This commit is contained in:
345
src/twn_audio.c
Normal file
345
src/twn_audio.c
Normal file
@ -0,0 +1,345 @@
|
||||
#include "townengine/audio/internal_api.h"
|
||||
#include "townengine/config.h"
|
||||
#include "townengine/context.h"
|
||||
#include "townengine/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 */
|
||||
/* TODO: count repeated usages for sound effect cases with rendering to ram? */
|
||||
/* 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_item *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,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
case audio_file_type_xm: {
|
||||
unsigned char *data;
|
||||
int64_t len = get_audio_data(path, &data);
|
||||
if (len == -1) {
|
||||
CRY("Audio error", "Error reading file");
|
||||
break;
|
||||
}
|
||||
|
||||
xm_context_t *handle;
|
||||
int response = xm_create_context_safe(&handle,
|
||||
(const char *)data,
|
||||
(size_t)len,
|
||||
AUDIO_FREQUENCY);
|
||||
if (response != 0) {
|
||||
CRY("Audio error", "Error loading xm module");
|
||||
break;
|
||||
}
|
||||
|
||||
xm_set_max_loop_count(handle, 1);
|
||||
|
||||
return (union audio_context) {
|
||||
.xm = { .handle = handle }
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
switch (channel->file_type) {
|
||||
case audio_file_type_ogg: {
|
||||
stb_vorbis_seek_start(channel->context.vorbis.handle);
|
||||
break;
|
||||
}
|
||||
|
||||
case audio_file_type_xm: {
|
||||
xm_restart(channel->context.xm.handle);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
CRY("Audio error", "Unhandled audio format (in repeat)");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void play_audio_ex(const char *path, const char *channel, t_play_audio_args args) {
|
||||
struct audio_channel_item *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,
|
||||
.name = channel,
|
||||
};
|
||||
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_audio_args(const char *channel) {
|
||||
struct audio_channel_item *pair = shgetp_null(ctx.audio_channels, channel);
|
||||
if (!pair)
|
||||
return NULL;
|
||||
|
||||
return &pair->value.args;
|
||||
}
|
||||
|
||||
|
||||
t_play_audio_args get_default_audio_args(void) {
|
||||
return (t_play_audio_args){
|
||||
.repeat = false,
|
||||
.crossfade = false,
|
||||
.volume = 1.0f,
|
||||
.panning = 0.0f,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/* this assumes int16_t based streams */
|
||||
static void audio_mixin_streams(const struct audio_channel *channel,
|
||||
uint8_t *restrict a,
|
||||
uint8_t *restrict b,
|
||||
size_t frames)
|
||||
{
|
||||
int16_t *const sa = (int16_t *)a;
|
||||
int16_t *const sb = (int16_t *)b;
|
||||
|
||||
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);
|
||||
|
||||
#if AUDIO_N_CHANNELS == 2
|
||||
|
||||
for (size_t s = 0; s < frames; s += 2) {
|
||||
/* left channel */
|
||||
sa[s] += (int16_t)(sb[s] * channel->args.volume * left_panning);
|
||||
|
||||
/* right channel */
|
||||
sa[s + 1] += (int16_t)(sb[s + 1] * channel->args.volume * right_panning);
|
||||
}
|
||||
#else
|
||||
#error "Unimplemented channel count"
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
/* remember: sample is data for all channels where frame is a part of it */
|
||||
static void audio_sample_and_mixin_channel(const struct audio_channel *channel,
|
||||
uint8_t *stream,
|
||||
int len)
|
||||
{
|
||||
static uint8_t buffer[16384];
|
||||
const int int16_buffer_frames = sizeof (buffer) / sizeof (int16_t);
|
||||
const int float_buffer_frames = sizeof (buffer) / sizeof (float);
|
||||
const int stream_frames = len / (int)(sizeof (int16_t));
|
||||
|
||||
switch (channel->file_type) {
|
||||
case audio_file_type_ogg: {
|
||||
/* feed stream for needed conversions */
|
||||
for (int i = 0; i < stream_frames; ) {
|
||||
const int n_frames = (stream_frames - i) > int16_buffer_frames ?
|
||||
int16_buffer_frames : stream_frames - i;
|
||||
|
||||
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);
|
||||
continue;
|
||||
} else
|
||||
/* leave silence */
|
||||
break;
|
||||
}
|
||||
|
||||
/* panning and mixing */
|
||||
audio_mixin_streams(channel,
|
||||
&stream[i * sizeof(int16_t)], buffer,
|
||||
samples_per_channel * 2);
|
||||
|
||||
i += samples_per_channel * 2;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case audio_file_type_xm: {
|
||||
for (int i = 0; i < stream_frames; ) {
|
||||
const int n_frames = (stream_frames - i) > float_buffer_frames ?
|
||||
float_buffer_frames : stream_frames - i;
|
||||
|
||||
const int samples_per_channel = xm_generate_samples(channel->context.xm.handle,
|
||||
(float *)buffer,
|
||||
n_frames / 2);
|
||||
|
||||
/* handle end of file */
|
||||
if (samples_per_channel == 0) {
|
||||
if (channel->args.repeat) {
|
||||
/* seek to start and try sampling some more */
|
||||
xm_restart(channel->context.xm.handle);
|
||||
continue;
|
||||
} else
|
||||
/* leave silence */
|
||||
break;
|
||||
}
|
||||
|
||||
/* convert floats to int16_t */
|
||||
for (int p = 0; p < samples_per_channel * 2; ++p)
|
||||
((int16_t *)buffer)[p] = (int16_t)(((float *)buffer)[p] * (float)INT16_MAX);
|
||||
|
||||
/* panning and mixing */
|
||||
audio_mixin_streams(channel,
|
||||
&stream[i * sizeof(int16_t)],
|
||||
buffer,
|
||||
samples_per_channel * 2);
|
||||
|
||||
i += samples_per_channel * 2;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
CRY("Audio error", "Unhandled audio format (in sampling)");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void sanity_check_channel(const struct audio_channel *channel) {
|
||||
if (channel->args.volume < 0.0f || channel->args.volume > 1.0f)
|
||||
log_warn("Volume argument is out of range for channel (%s)", channel->name);
|
||||
|
||||
if (channel->args.panning < -1.0f || channel->args.panning > 1.0f)
|
||||
log_warn("panning argument is out of range for channel (%s)", channel->name);
|
||||
}
|
||||
|
||||
|
||||
void audio_callback(void *userdata, uint8_t *stream, int len) {
|
||||
(void)userdata;
|
||||
|
||||
/* prepare for mixing */
|
||||
SDL_memset(stream, 0, len);
|
||||
|
||||
for (int i = 0; i < shlen(ctx.audio_channels); ++i) {
|
||||
sanity_check_channel(&ctx.audio_channels[i].value);
|
||||
audio_sample_and_mixin_channel(&ctx.audio_channels[i].value, stream, len);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user