346 lines
10 KiB
C
346 lines
10 KiB
C
#include "twn_audio_c.h"
|
|
#include "twn_config.h"
|
|
#include "twn_engine_context_c.h"
|
|
#include "twn_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 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;
|
|
}
|
|
|
|
|
|
void audio_play(const char *path, const char *channel) {
|
|
const AudioChannelItem *pair = shgetp_null(ctx.audio_channels, channel);
|
|
if (!pair)
|
|
audio_play_ex(path, channel, audio_get_default_args());
|
|
else
|
|
audio_play_ex(path, channel, pair->value.args);
|
|
}
|
|
|
|
|
|
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 }
|
|
};
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
default:
|
|
CRY("Audio error", "Unhandled audio format (in repeat)");
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
void audio_play_ex(const char *path, const char *channel, PlayAudioArgs args) {
|
|
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 = {
|
|
.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);
|
|
}
|
|
|
|
|
|
PlayAudioArgs *audio_get_args(const char *channel) {
|
|
AudioChannelItem *pair = shgetp_null(ctx.audio_channels, channel);
|
|
if (!pair)
|
|
return NULL;
|
|
|
|
return &pair->value.args;
|
|
}
|
|
|
|
|
|
PlayAudioArgs audio_get_default_args(void) {
|
|
return (PlayAudioArgs){
|
|
.repeat = false,
|
|
.crossfade = false,
|
|
.volume = 1.0f,
|
|
.panning = 0.0f,
|
|
};
|
|
}
|
|
|
|
|
|
/* this assumes int16_t based streams */
|
|
static void audio_mixin_streams(const AudioChannel *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 AudioChannel *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 AudioChannel *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);
|
|
}
|
|
}
|