townengine/src/twn_audio.c

367 lines
11 KiB
C

#include "twn_audio_c.h"
#include "twn_engine_context_c.h"
#include "twn_util.h"
#include "twn_util_c.h"
#include <SDL2/SDL.h>
#include <stb_ds.h>
#include <physfs.h>
#define STB_VORBIS_NO_PUSHDATA_API
#define STB_VORBIS_HEADER_ONLY
#include <stb_vorbis.c>
#include <stdint.h>
#include <string.h>
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 AudioFileCache {
char *key;
struct AudioFileCacheValue {
unsigned char *data;
size_t len;
} value;
} *audio_file_cache;
static int64_t get_audio_data(const char *path, unsigned char **data) {
const struct AudioFileCache *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 AudioFileCacheValue 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;
}
static AudioFileType infer_audio_file_type(const char *path) {
size_t path_len = SDL_strlen(path);
for (int i = 0; i < AUDIO_FILE_TYPE_COUNT; ++i) {
size_t ext_length = SDL_strlen(audio_exts[i]);
if (path_len <= ext_length)
continue;
if (SDL_strcmp(&path[path_len - ext_length], audio_exts[i]) == 0)
return (AudioFileType)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 AudioContext init_audio_context(const char *path, AudioFileType 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 AudioContext) {
.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 AudioContext) {
.xm = { .handle = handle }
};
}
case AUDIO_FILE_TYPE_UNKNOWN:
case AUDIO_FILE_TYPE_COUNT:
default:
CRY("Audio error", "Unhandled audio format (in init)");
return (union AudioContext){0};
}
return (union AudioContext){0};
}
static void repeat_audio(AudioChannel *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;
}
case AUDIO_FILE_TYPE_UNKNOWN:
case AUDIO_FILE_TYPE_COUNT:
default:
CRY("Audio error", "Unhandled audio format (in repeat)");
break;
}
}
void audio_play(const char *path,
const char *channel,
bool repeat,
float volume,
float panning)
{
AudioChannelItem *pair = shgetp_null(ctx.audio_channels, channel);
/* create a channel if it doesn't exist */
if (!pair) {
AudioFileType file_type = infer_audio_file_type(path);
AudioChannel new_channel = {
.file_type = file_type,
.context = init_audio_context(path, file_type),
.path = path,
.name = channel,
.repeat = repeat,
.volume = volume,
.panning = panning,
};
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);
}
TWN_API void audio_set(const char *channel, AudioParam param, float value) {
AudioChannelItem *pair = shgetp_null(ctx.audio_channels, channel);
if (!pair) {
log_warn("No channel by the name of %s to set a parameter for", channel);
return;
}
switch (param) {
case AUDIO_PARAM_REPEAT:
pair->value.repeat = (bool)value;
break;
case AUDIO_PARAM_VOLUME:
if (value > 1.0f) {
log_warn("Out of range volume for channel %s set", channel);
value = 1.0f;
}
if (value < 0.0f) {
log_warn("Out of range volume for channel %s set", channel);
value = 0.0f;
}
pair->value.volume = value;
break;
case AUDIO_PARAM_PANNING:
if (value > 1.0f) {
log_warn("Out of range panning for channel %s set", channel);
value = 1.0f;
}
if (value < -1.0f) {
log_warn("Out of range panning for channel %s set", channel);
value = -1.0f;
}
pair->value.panning = value;
break;
default:
CRY("Audio channel parameter setting failed", "Invalid parameter enum given");
}
}
/* this assumes float based streams */
static void audio_mixin_streams(const AudioChannel *channel,
uint8_t *restrict a,
uint8_t *restrict b,
size_t frames)
{
float *const sa = (float *)(void *)a;
float *const sb = (float *)(void *)b;
const float left_panning = fminf(fabsf(channel->panning - 1.0f), 1.0f);
const float right_panning = fminf(fabsf(channel->panning + 1.0f), 1.0f);
for (size_t s = 0; s < frames; s += 2) {
/* left channel */
sa[s] += (float)(sb[s] * channel->volume * left_panning);
/* right channel */
sa[s + 1] += (float)(sb[s + 1] * channel->volume * right_panning);
}
}
/* remember: sample is data for all channels where frame is a part of it */
static void audio_sample_and_mixin_channel(const AudioChannel *channel,
uint8_t *stream,
int len)
{
static uint8_t buffer[16384];
const int float_buffer_frames = sizeof (buffer) / sizeof (float);
const int stream_frames = len / (int)(sizeof (float));
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) > float_buffer_frames ?
float_buffer_frames : stream_frames - i;
const int samples_per_channel = stb_vorbis_get_samples_float_interleaved(
channel->context.vorbis.handle,
channel->context.vorbis.channel_count,
(float *)buffer,
n_frames);
/* handle end of file */
if (samples_per_channel == 0) {
if (channel->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(float)], 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->repeat) {
/* seek to start and try sampling some more */
xm_restart(channel->context.xm.handle);
continue;
} else
/* leave silence */
break;
}
/* panning and mixing */
audio_mixin_streams(channel,
&stream[i * sizeof(float)],
buffer,
samples_per_channel * 2);
i += samples_per_channel * 2;
}
break;
}
case AUDIO_FILE_TYPE_UNKNOWN:
case AUDIO_FILE_TYPE_COUNT:
default:
CRY("Audio error", "Unhandled audio format (in sampling)");
break;
}
}
static void sanity_check_channel(const AudioChannel *channel) {
if (channel->volume < 0.0f || channel->volume > 1.0f)
log_warn("Volume argument is out of range for channel (%s)", channel->name);
if (channel->panning < -1.0f || channel->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);
}
}
TWN_API void audio_play_args(PlayAudioArgs args) {
const char *channel = m_or(args, channel, NULL);
const bool repeat = m_or(args, repeat, false);
const float volume = m_or(args, volume, 1.0f);
const float panning = m_or(args, panning, 0.0f);
audio_play(args.path, channel, repeat, volume, panning);
}