575 lines
18 KiB
C
575 lines
18 KiB
C
#include "twn_audio_c.h"
|
|
#include "twn_engine_context_c.h"
|
|
#include "twn_util.h"
|
|
#include "twn_util_c.h"
|
|
#include "twn_audio.h"
|
|
|
|
#include <SDL2/SDL.h>
|
|
#include <stb_ds.h>
|
|
#include <physfs.h>
|
|
#include <physfsrwops.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 */
|
|
".wav", /* AUDIO_FILE_TYPE_WAV */
|
|
".xm", /* AUDIO_FILE_TYPE_XM */
|
|
};
|
|
|
|
static const uint8_t audio_exts_len[AUDIO_FILE_TYPE_COUNT] = {
|
|
sizeof ".ogg" - 1,
|
|
sizeof ".wav" - 1,
|
|
sizeof ".xm" - 1,
|
|
};
|
|
|
|
/* TODO: allow for vectorization and packed vectors (alignment care and alike) */
|
|
/* TODO: free channel name duplicates */
|
|
|
|
/* 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 const path_len = SDL_strlen(path);
|
|
|
|
for (int i = 0; i < AUDIO_FILE_TYPE_COUNT; ++i) {
|
|
if (SDL_strncmp(&path[path_len - audio_exts_len[i]], audio_exts[i], audio_exts_len[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? */
|
|
/* TODO: reuse often used decoded/decompressed data */
|
|
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,
|
|
}
|
|
};
|
|
}
|
|
|
|
/* TODO: transform to destination format immediately? */
|
|
case AUDIO_FILE_TYPE_WAV: {
|
|
SDL_AudioSpec spec;
|
|
uint8_t *data;
|
|
uint32_t len;
|
|
if (!SDL_LoadWAV_RW(PHYSFSRWOPS_openRead(path), 1, &spec, &data, &len)) {
|
|
CRY_SDL("Cannot load .wav file:");
|
|
break;
|
|
}
|
|
|
|
SDL_AudioCVT cvt;
|
|
int conv = SDL_BuildAudioCVT(&cvt,
|
|
spec.format,
|
|
spec.channels,
|
|
spec.freq,
|
|
AUDIO_F32,
|
|
2,
|
|
AUDIO_FREQUENCY);
|
|
if (conv < 0) {
|
|
CRY_SDL("Cannot resample .wav:");
|
|
break;
|
|
}
|
|
|
|
if (conv != 0) {
|
|
data = SDL_realloc(data, len * cvt.len_mult);
|
|
cvt.buf = data;
|
|
cvt.len = len;
|
|
if (SDL_ConvertAudio(&cvt) < 0) {
|
|
CRY_SDL("Error resampling .wav:");
|
|
break;
|
|
}
|
|
spec.channels = 2;
|
|
spec.freq = AUDIO_FREQUENCY;
|
|
/* TODO: test this */
|
|
spec.samples = (uint16_t)((size_t)(SDL_floor((double)len * cvt.len_ratio)) / sizeof (float) / 2);
|
|
} else {
|
|
spec.samples = (uint16_t)((size_t)(SDL_floor((double)len * cvt.len_ratio)) / sizeof (float) / 2);
|
|
}
|
|
|
|
return (union AudioContext) {
|
|
.wav = {
|
|
.position = 0,
|
|
.samples = data,
|
|
.spec = spec,
|
|
}
|
|
};
|
|
}
|
|
|
|
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 free_audio_channel(AudioChannel channel) {
|
|
switch (channel.file_type) {
|
|
case AUDIO_FILE_TYPE_OGG: {
|
|
SDL_free(channel.context.vorbis.data);
|
|
break;
|
|
}
|
|
case AUDIO_FILE_TYPE_WAV: {
|
|
SDL_free(channel.context.wav.samples);
|
|
break;
|
|
}
|
|
case AUDIO_FILE_TYPE_XM: {
|
|
xm_free_context(channel.context.xm.handle);
|
|
break;
|
|
}
|
|
case AUDIO_FILE_TYPE_COUNT:
|
|
case AUDIO_FILE_TYPE_UNKNOWN:
|
|
default:
|
|
SDL_assert_always(false);
|
|
break;
|
|
}
|
|
SDL_free(channel.path);
|
|
}
|
|
|
|
|
|
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_WAV: {
|
|
channel->context.wav.position = 0;
|
|
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)
|
|
{
|
|
if (!ctx.audio_initialized) {
|
|
SDL_InitSubSystem(SDL_INIT_AUDIO);
|
|
|
|
profile_start("audio initialization");
|
|
|
|
SDL_AudioSpec request, got;
|
|
SDL_zero(request);
|
|
|
|
request.freq = AUDIO_FREQUENCY;
|
|
request.format = AUDIO_F32;
|
|
request.channels = 2;
|
|
request.samples = 4096;
|
|
if (!ctx.push_audio_model)
|
|
request.callback = audio_callback;
|
|
/* TODO: check for errors */
|
|
ctx.audio_device = SDL_OpenAudioDevice(NULL, 0, &request, &got, 0);
|
|
ctx.audio_stream_format = got.format;
|
|
ctx.audio_stream_frequency = got.freq;
|
|
ctx.audio_stream_channel_count = got.channels;
|
|
/* TODO: relax this */
|
|
SDL_assert_always(got.freq == AUDIO_FREQUENCY);
|
|
SDL_assert_always(got.format == AUDIO_F32);
|
|
SDL_assert_always(got.channels == 2);
|
|
|
|
SDL_PauseAudioDevice(ctx.audio_device, 0);
|
|
|
|
profile_end("audio initialization");
|
|
|
|
ctx.audio_initialized = true;
|
|
}
|
|
|
|
SDL_LockAudioDevice(ctx.audio_device);
|
|
|
|
if (channel) {
|
|
AudioChannelItem *pair = shgetp_null(ctx.audio_channels, channel);
|
|
|
|
/* create a channel if it doesn't exist */
|
|
if (!pair) {
|
|
AudioFileType const file_type = infer_audio_file_type(path);
|
|
AudioChannel const new_channel = {
|
|
.file_type = file_type,
|
|
.context = init_audio_context(path, file_type),
|
|
.path = SDL_strdup(path),
|
|
.repeat = repeat,
|
|
.volume = volume,
|
|
.panning = panning,
|
|
};
|
|
shput(ctx.audio_channels, SDL_strdup(channel), new_channel);
|
|
pair = shgetp_null(ctx.audio_channels, channel);
|
|
}
|
|
|
|
/* repeat if so */
|
|
else if (strcmp(pair->value.path, path) == 0)
|
|
repeat_audio(&pair->value);
|
|
else {
|
|
/* start anew, reuse channel name */
|
|
free_audio_channel(pair->value);
|
|
AudioFileType const file_type = infer_audio_file_type(path);
|
|
AudioChannel const new_channel = {
|
|
.file_type = file_type,
|
|
.context = init_audio_context(path, file_type),
|
|
.path = SDL_strdup(path),
|
|
.repeat = repeat,
|
|
.volume = volume,
|
|
.panning = panning,
|
|
};
|
|
pair->value = new_channel;
|
|
}
|
|
} else {
|
|
/* audio without channel plays without repeat and ability to change parameters over its course, nor stop it */
|
|
AudioFileType const file_type = infer_audio_file_type(path);
|
|
AudioChannel new_channel = {
|
|
.file_type = file_type,
|
|
.context = init_audio_context(path, file_type),
|
|
.path = SDL_strdup(path),
|
|
.repeat = false,
|
|
.volume = volume,
|
|
.panning = panning,
|
|
};
|
|
|
|
if (repeat)
|
|
log_warn("Cannot repeat audio played on unnamed scratch channel (for %s)", path);
|
|
|
|
arrpush(ctx.unnamed_audio_channels, new_channel);
|
|
}
|
|
|
|
SDL_UnlockAudioDevice(ctx.audio_device);
|
|
}
|
|
|
|
|
|
TWN_API void audio_parameter(const char *channel, const char *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;
|
|
}
|
|
|
|
if (SDL_strncmp(param, "repeat", sizeof "repeat" - 1) == 0) {
|
|
pair->value.repeat = (bool)value;
|
|
|
|
} else if (SDL_strncmp(param, "volume", sizeof "volume" - 1) == 0) {
|
|
if (value > 1.0f || value < 0.0f) {
|
|
log_warn("Out of range volume for channel %s set", channel);
|
|
value = clampf(value, 0.0f, 1.0f);
|
|
}
|
|
pair->value.volume = value;
|
|
|
|
} else if (SDL_strncmp(param, "panning", sizeof "panning" - 1) == 0) {
|
|
if (value > 1.0f || value < -1.0f) {
|
|
log_warn("Out of range panning for channel %s set", channel);
|
|
value = clampf(value, -1.0f, +1.0f);
|
|
}
|
|
pair->value.panning = value;
|
|
|
|
} else
|
|
CRY("Audio channel parameter setting failed", "Invalid parameter enum given");
|
|
}
|
|
|
|
|
|
/* TODO: handle it more properly in regards to clipping and alike */
|
|
/* this assumes float based streams */
|
|
static void audio_mixin_streams(const AudioChannel *channel,
|
|
uint8_t *restrict a,
|
|
uint8_t *restrict b,
|
|
size_t frames,
|
|
bool last)
|
|
{
|
|
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);
|
|
|
|
if (last) {
|
|
for (size_t s = 0; s < frames; ++s) {
|
|
/* left channel */
|
|
sa[s * 2 + 0] += (float)(sb[s * 2 + 0] * channel->volume * left_panning);
|
|
/* right channel */
|
|
sa[s * 2 + 1] += (float)(sb[s * 2 + 1] * channel->volume * right_panning);
|
|
}
|
|
} else {
|
|
for (size_t s = 0; s < frames; ++s) {
|
|
/* left channel */
|
|
sa[s * 2 + 0] += (float)(sb[s * 2 + 0] * channel->volume * left_panning);
|
|
sa[s * 2 + 0] *= 1 / (float)M_2_SQRTPI;
|
|
/* right channel */
|
|
sa[s * 2 + 1] += (float)(sb[s * 2 + 1] * channel->volume * right_panning);
|
|
sa[s * 2 + 1] *= 1 / (float)M_2_SQRTPI;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* remember: frame consists of sample * channel_count */
|
|
static void audio_sample_and_mixin_channel(AudioChannel *channel,
|
|
uint8_t *stream,
|
|
int len, bool last)
|
|
{
|
|
static uint8_t buffer[16384]; /* TODO: better make it a growable scratch instead, which will simplify things */
|
|
const size_t float_buffer_frames = sizeof (buffer) / sizeof (float) / 2;
|
|
const size_t stream_frames = len / sizeof (float) / 2;
|
|
|
|
switch (channel->file_type) {
|
|
case AUDIO_FILE_TYPE_OGG: {
|
|
/* feed stream for needed conversions */
|
|
for (size_t i = 0; i < stream_frames; ) {
|
|
const size_t n_frames = (stream_frames - i) > float_buffer_frames ?
|
|
float_buffer_frames : stream_frames - i;
|
|
|
|
const size_t samples_per_channel = stb_vorbis_get_samples_float_interleaved(
|
|
channel->context.vorbis.handle,
|
|
2,
|
|
(float *)buffer,
|
|
(int)n_frames * 2);
|
|
|
|
/* 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 */
|
|
channel->finished = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* panning and mixing */
|
|
audio_mixin_streams(channel,
|
|
&stream[i * sizeof(float) * 2], buffer,
|
|
samples_per_channel,
|
|
last);
|
|
|
|
i += samples_per_channel;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case AUDIO_FILE_TYPE_WAV: {
|
|
/* feed stream for needed conversions */
|
|
for (size_t i = 0; i < stream_frames; ) {
|
|
const size_t limit = MIN(stream_frames - i, channel->context.wav.spec.samples - channel->context.wav.position);
|
|
|
|
/* same format, just feed it directly */
|
|
audio_mixin_streams(channel,
|
|
&stream[i * sizeof(float) * 2],
|
|
&((uint8_t *)channel->context.wav.samples)[channel->context.wav.position * sizeof (float) * 2],
|
|
limit,
|
|
last);
|
|
|
|
channel->context.wav.position += limit;
|
|
|
|
if (channel->context.wav.position >= channel->context.wav.spec.samples) {
|
|
if (channel->repeat)
|
|
channel->context.wav.position = 0;
|
|
else {
|
|
/* leave silence */
|
|
channel->finished = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
i += limit;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case AUDIO_FILE_TYPE_XM: {
|
|
for (size_t i = 0; i < stream_frames; ) {
|
|
const size_t n_frames = (stream_frames - i) > float_buffer_frames ?
|
|
float_buffer_frames : stream_frames - i;
|
|
|
|
const size_t samples_per_channel = xm_generate_samples(channel->context.xm.handle,
|
|
(float *)buffer,
|
|
n_frames);
|
|
|
|
/* 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 {
|
|
channel->finished = true;
|
|
/* leave silence */
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* panning and mixing */
|
|
audio_mixin_streams(channel,
|
|
&stream[i * sizeof(float) * 2],
|
|
buffer,
|
|
samples_per_channel,
|
|
last);
|
|
|
|
i += samples_per_channel;
|
|
}
|
|
|
|
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 AudioChannelItem channel) {
|
|
if (channel.value.volume < 0.0f || channel.value.volume > 1.0f)
|
|
log_warn("Volume argument is out of range for channel (%s)", channel.key ? channel.key : "unnamed");
|
|
|
|
if (channel.value.panning < -1.0f || channel.value.panning > 1.0f)
|
|
log_warn("Panning argument is out of range for channel (%s)", channel.key ? channel.key : "unnamed");
|
|
}
|
|
|
|
|
|
void audio_callback(void *userdata, uint8_t *stream, int len) {
|
|
(void)userdata;
|
|
|
|
/* prepare for mixing */
|
|
SDL_memset(stream, 0, len);
|
|
|
|
size_t const audio_channels_len = shlen(ctx.audio_channels);
|
|
for (size_t i = 0; i < audio_channels_len; ++i) {
|
|
sanity_check_channel(ctx.audio_channels[i]);
|
|
audio_sample_and_mixin_channel(&ctx.audio_channels[i].value,
|
|
stream, len,
|
|
i == audio_channels_len - 1);
|
|
}
|
|
|
|
size_t const unnamed_audio_channels_len = shlen(ctx.unnamed_audio_channels);
|
|
for (size_t i = 0; i < unnamed_audio_channels_len; ++i) {
|
|
sanity_check_channel((AudioChannelItem){NULL, ctx.unnamed_audio_channels[i]});
|
|
audio_sample_and_mixin_channel(&ctx.unnamed_audio_channels[i],
|
|
stream, len,
|
|
i == unnamed_audio_channels_len - 1);
|
|
}
|
|
|
|
/* ditch finished unnamed */
|
|
size_t i = 0;
|
|
while (i < unnamed_audio_channels_len) {
|
|
if (ctx.unnamed_audio_channels[i].finished) {
|
|
free_audio_channel(ctx.unnamed_audio_channels[i]);
|
|
arrdelswap(ctx.unnamed_audio_channels, i);
|
|
} else i++;
|
|
}
|
|
}
|
|
|
|
|
|
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);
|
|
}
|