twn_audio.c: a lot of fixes, optional TWN_FEATURE_PUSH_AUDIO for converging game ticks and audio, proper .wav handling with resample
This commit is contained in:
parent
eefd53a630
commit
6298394957
@ -27,6 +27,7 @@ set(TWN_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR} CACHE INTERNAL "")
|
|||||||
|
|
||||||
# feature configuration, set them with -DFEATURE=ON/OFF in cli
|
# feature configuration, set them with -DFEATURE=ON/OFF in cli
|
||||||
option(TWN_FEATURE_DYNLIB_GAME "Enable dynamic library loading support" ON)
|
option(TWN_FEATURE_DYNLIB_GAME "Enable dynamic library loading support" ON)
|
||||||
|
option(TWN_FEATURE_PUSH_AUDIO "Enable frame based audio push for easy realtime audio" ON)
|
||||||
option(TWN_USE_AMALGAM "Enable use of twn_amalgam.c as a single compilation unit" ON)
|
option(TWN_USE_AMALGAM "Enable use of twn_amalgam.c as a single compilation unit" ON)
|
||||||
|
|
||||||
# todo: figure out how to compile for dynamic linking instead
|
# todo: figure out how to compile for dynamic linking instead
|
||||||
@ -142,6 +143,8 @@ set_target_properties(${TWN_TARGET} PROPERTIES
|
|||||||
C_STANDARD_REQUIRED ON
|
C_STANDARD_REQUIRED ON
|
||||||
C_EXTENSIONS ON) # extensions are required by stb_ds.h
|
C_EXTENSIONS ON) # extensions are required by stb_ds.h
|
||||||
|
|
||||||
|
add_compile_definitions(${TWN_TARGET} $<$<BOOL:${TWN_FEATURE_PUSH_AUDIO}>:TWN_FEATURE_PUSH_AUDIO>)
|
||||||
|
|
||||||
# precompile commonly used not-so-small headers
|
# precompile commonly used not-so-small headers
|
||||||
target_precompile_headers(${TWN_TARGET} PRIVATE
|
target_precompile_headers(${TWN_TARGET} PRIVATE
|
||||||
$<$<NOT:$<BOOL:${EMSCRIPTEN}>>:third-party/glad/include/glad/glad.h>
|
$<$<NOT:$<BOOL:${EMSCRIPTEN}>>:third-party/glad/include/glad/glad.h>
|
||||||
|
139
src/twn_audio.c
139
src/twn_audio.c
@ -22,6 +22,7 @@ static const char *audio_exts[AUDIO_FILE_TYPE_COUNT] = {
|
|||||||
".xm", /* AUDIO_FILE_TYPE_XM */
|
".xm", /* AUDIO_FILE_TYPE_XM */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* TODO: allow for vectorization and packed vectors (alignment care and alike) */
|
||||||
|
|
||||||
/* TODO: count frames without use, free the memory when threshold is met */
|
/* TODO: count frames without use, free the memory when threshold is met */
|
||||||
/* TODO: count repeated usages for sound effect cases with rendering to ram? */
|
/* TODO: count repeated usages for sound effect cases with rendering to ram? */
|
||||||
@ -115,11 +116,40 @@ static union AudioContext init_audio_context(const char *path, AudioFileType typ
|
|||||||
break;
|
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) {
|
return (union AudioContext) {
|
||||||
.wav = {
|
.wav = {
|
||||||
.position = 0,
|
.position = 0,
|
||||||
.samples = data,
|
.samples = data,
|
||||||
.spec = spec
|
.spec = spec,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -162,11 +192,22 @@ static union AudioContext init_audio_context(const char *path, AudioFileType typ
|
|||||||
|
|
||||||
static void free_audio_channel(AudioChannel channel) {
|
static void free_audio_channel(AudioChannel channel) {
|
||||||
switch (channel.file_type) {
|
switch (channel.file_type) {
|
||||||
|
case AUDIO_FILE_TYPE_OGG: {
|
||||||
|
SDL_free(channel.context.vorbis.data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case AUDIO_FILE_TYPE_WAV: {
|
case AUDIO_FILE_TYPE_WAV: {
|
||||||
SDL_free(channel.context.wav.samples);
|
SDL_free(channel.context.wav.samples);
|
||||||
break;
|
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:
|
default:
|
||||||
|
SDL_assert_always(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -235,7 +276,7 @@ void audio_play(const char *path,
|
|||||||
.file_type = file_type,
|
.file_type = file_type,
|
||||||
.context = init_audio_context(path, file_type),
|
.context = init_audio_context(path, file_type),
|
||||||
.path = path,
|
.path = path,
|
||||||
.name = channel,
|
.name = NULL,
|
||||||
.repeat = false,
|
.repeat = false,
|
||||||
.volume = volume,
|
.volume = volume,
|
||||||
.panning = panning,
|
.panning = panning,
|
||||||
@ -278,6 +319,7 @@ TWN_API void audio_parameter(const char *channel, const char *param, float value
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* TODO: handle it more properly in regards to clipping and alike */
|
||||||
/* this assumes float based streams */
|
/* this assumes float based streams */
|
||||||
static void audio_mixin_streams(const AudioChannel *channel,
|
static void audio_mixin_streams(const AudioChannel *channel,
|
||||||
uint8_t *restrict a,
|
uint8_t *restrict a,
|
||||||
@ -290,37 +332,37 @@ static void audio_mixin_streams(const AudioChannel *channel,
|
|||||||
const float left_panning = fminf(fabsf(channel->panning - 1.0f), 1.0f);
|
const float left_panning = fminf(fabsf(channel->panning - 1.0f), 1.0f);
|
||||||
const float right_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) {
|
for (size_t s = 0; s < frames; ++s) {
|
||||||
/* left channel */
|
/* left channel */
|
||||||
sa[s] += (float)(sb[s] * channel->volume * left_panning);
|
sa[s * 2 + 0] += (float)(sb[s * 2 + 0] * channel->volume * left_panning);
|
||||||
|
|
||||||
/* right channel */
|
/* right channel */
|
||||||
sa[s + 1] += (float)(sb[s + 1] * channel->volume * right_panning);
|
sa[s * 2 + 1] += (float)(sb[s * 2 + 1] * channel->volume * right_panning);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* remember: sample is data for all channels where frame is a part of it */
|
/* remember: frame consists of sample * channel_count */
|
||||||
static void audio_sample_and_mixin_channel(AudioChannel *channel,
|
static void audio_sample_and_mixin_channel(AudioChannel *channel,
|
||||||
uint8_t *stream,
|
uint8_t *stream,
|
||||||
int len)
|
int len)
|
||||||
{
|
{
|
||||||
static uint8_t buffer[16384];
|
static uint8_t buffer[16384]; /* TODO: better make it a growable scratch instead, which will simplify things */
|
||||||
const int float_buffer_frames = sizeof (buffer) / sizeof (float) / 2;
|
const size_t float_buffer_frames = sizeof (buffer) / sizeof (float) / 2;
|
||||||
const int stream_frames = len / (int)(sizeof (float));
|
const size_t stream_frames = len / sizeof (float) / 2;
|
||||||
|
|
||||||
switch (channel->file_type) {
|
switch (channel->file_type) {
|
||||||
case AUDIO_FILE_TYPE_OGG: {
|
case AUDIO_FILE_TYPE_OGG: {
|
||||||
/* feed stream for needed conversions */
|
/* feed stream for needed conversions */
|
||||||
for (int i = 0; i < stream_frames; ) {
|
for (size_t i = 0; i < stream_frames; ) {
|
||||||
const int n_frames = (stream_frames - i) > float_buffer_frames ?
|
const size_t n_frames = (stream_frames - i) > float_buffer_frames ?
|
||||||
float_buffer_frames : stream_frames - i;
|
float_buffer_frames : stream_frames - i;
|
||||||
|
|
||||||
const int samples_per_channel = stb_vorbis_get_samples_float_interleaved(
|
const size_t samples_per_channel = stb_vorbis_get_samples_float_interleaved(
|
||||||
channel->context.vorbis.handle,
|
channel->context.vorbis.handle,
|
||||||
channel->context.vorbis.channel_count,
|
2,
|
||||||
(float *)buffer,
|
(float *)buffer,
|
||||||
n_frames);
|
(int)n_frames * 2);
|
||||||
|
|
||||||
/* handle end of file */
|
/* handle end of file */
|
||||||
if (samples_per_channel == 0) {
|
if (samples_per_channel == 0) {
|
||||||
@ -337,10 +379,10 @@ static void audio_sample_and_mixin_channel(AudioChannel *channel,
|
|||||||
|
|
||||||
/* panning and mixing */
|
/* panning and mixing */
|
||||||
audio_mixin_streams(channel,
|
audio_mixin_streams(channel,
|
||||||
&stream[i * sizeof(float)], buffer,
|
&stream[i * sizeof(float) * 2], buffer,
|
||||||
samples_per_channel * 2);
|
samples_per_channel);
|
||||||
|
|
||||||
i += samples_per_channel * 2;
|
i += samples_per_channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@ -348,49 +390,18 @@ static void audio_sample_and_mixin_channel(AudioChannel *channel,
|
|||||||
|
|
||||||
case AUDIO_FILE_TYPE_WAV: {
|
case AUDIO_FILE_TYPE_WAV: {
|
||||||
/* feed stream for needed conversions */
|
/* feed stream for needed conversions */
|
||||||
for (int i = 0; i < stream_frames; ) {
|
for (size_t i = 0; i < stream_frames; ) {
|
||||||
const int n_frames = (stream_frames - i) > float_buffer_frames ?
|
const size_t limit = MIN(stream_frames - i, channel->context.wav.spec.samples - channel->context.wav.position);
|
||||||
float_buffer_frames : stream_frames - i;
|
|
||||||
|
|
||||||
int const limit = MIN(n_frames, channel->context.wav.spec.samples);
|
/* same format, just feed it directly */
|
||||||
|
audio_mixin_streams(channel,
|
||||||
switch (channel->context.wav.spec.format) {
|
&stream[i * sizeof(float) * 2],
|
||||||
case AUDIO_U16: {
|
&((uint8_t *)channel->context.wav.samples)[channel->context.wav.position * sizeof (float) * 2],
|
||||||
if (channel->context.wav.spec.channels == 1) {
|
limit);
|
||||||
for (int x = 0; x < limit; ++x) {
|
|
||||||
((float *)buffer)[x * 2 + 0] = (float)((uint16_t *)channel->context.wav.samples)[x] / (float)UINT16_MAX;
|
|
||||||
((float *)buffer)[x * 2 + 1] = (float)((uint16_t *)channel->context.wav.samples)[x] / (float)UINT16_MAX;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case AUDIO_S16: {
|
|
||||||
if (channel->context.wav.spec.channels == 1) {
|
|
||||||
for (int x = 0; x < limit; ++x) {
|
|
||||||
if ((float)((int16_t *)channel->context.wav.samples)[x] < 0) {
|
|
||||||
((float *)buffer)[x * 2 + 0] = (float)((int16_t *)channel->context.wav.samples)[x] / (float)INT16_MIN;
|
|
||||||
((float *)buffer)[x * 2 + 1] = (float)((int16_t *)channel->context.wav.samples)[x] / (float)INT16_MIN;
|
|
||||||
} else {
|
|
||||||
((float *)buffer)[x * 2 + 0] = (float)((int16_t *)channel->context.wav.samples)[x] / (float)INT16_MAX;
|
|
||||||
((float *)buffer)[x * 2 + 1] = (float)((int16_t *)channel->context.wav.samples)[x] / (float)INT16_MAX;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
log_warn("Unsupported .wav PCM format (%x), producing silence", channel->context.wav.spec.format);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* panning and mixing */
|
|
||||||
audio_mixin_streams(channel, &stream[i * sizeof(float)], buffer, limit * 2);
|
|
||||||
|
|
||||||
channel->context.wav.position += limit;
|
channel->context.wav.position += limit;
|
||||||
|
|
||||||
if (channel->context.wav.position == channel->context.wav.spec.samples) {
|
if (channel->context.wav.position >= channel->context.wav.spec.samples) {
|
||||||
if (channel->repeat)
|
if (channel->repeat)
|
||||||
channel->context.wav.position = 0;
|
channel->context.wav.position = 0;
|
||||||
else {
|
else {
|
||||||
@ -400,20 +411,20 @@ static void audio_sample_and_mixin_channel(AudioChannel *channel,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i += limit * 2;
|
i += limit;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case AUDIO_FILE_TYPE_XM: {
|
case AUDIO_FILE_TYPE_XM: {
|
||||||
for (int i = 0; i < stream_frames; ) {
|
for (size_t i = 0; i < stream_frames; ) {
|
||||||
const int n_frames = (stream_frames - i) > float_buffer_frames ?
|
const size_t n_frames = (stream_frames - i) > float_buffer_frames ?
|
||||||
float_buffer_frames : stream_frames - i;
|
float_buffer_frames : stream_frames - i;
|
||||||
|
|
||||||
const int samples_per_channel = xm_generate_samples(channel->context.xm.handle,
|
const size_t samples_per_channel = xm_generate_samples(channel->context.xm.handle,
|
||||||
(float *)buffer,
|
(float *)buffer,
|
||||||
n_frames / 2);
|
n_frames);
|
||||||
|
|
||||||
/* handle end of file */
|
/* handle end of file */
|
||||||
if (samples_per_channel == 0) {
|
if (samples_per_channel == 0) {
|
||||||
@ -430,11 +441,11 @@ static void audio_sample_and_mixin_channel(AudioChannel *channel,
|
|||||||
|
|
||||||
/* panning and mixing */
|
/* panning and mixing */
|
||||||
audio_mixin_streams(channel,
|
audio_mixin_streams(channel,
|
||||||
&stream[i * sizeof(float)],
|
&stream[i * sizeof(float) * 2],
|
||||||
buffer,
|
buffer,
|
||||||
samples_per_channel * 2);
|
samples_per_channel);
|
||||||
|
|
||||||
i += samples_per_channel * 2;
|
i += samples_per_channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
@ -232,6 +232,12 @@ static void main_loop(void) {
|
|||||||
if (ctx.window_size_has_changed)
|
if (ctx.window_size_has_changed)
|
||||||
update_viewport();
|
update_viewport();
|
||||||
game_object_tick();
|
game_object_tick();
|
||||||
|
#ifdef TWN_FEATURE_PUSH_AUDIO
|
||||||
|
static uint8_t audio_buffer[(AUDIO_FREQUENCY / 60) * sizeof (float) * 2];
|
||||||
|
audio_callback(NULL, audio_buffer, sizeof audio_buffer);
|
||||||
|
if (SDL_QueueAudio(ctx.audio_device, audio_buffer, sizeof audio_buffer))
|
||||||
|
CRY_SDL("Error queueing audio: ");
|
||||||
|
#endif
|
||||||
input_state_update(&ctx.input);
|
input_state_update(&ctx.input);
|
||||||
preserve_persistent_ctx_fields();
|
preserve_persistent_ctx_fields();
|
||||||
|
|
||||||
@ -560,12 +566,16 @@ static bool initialize(void) {
|
|||||||
request.freq = AUDIO_FREQUENCY;
|
request.freq = AUDIO_FREQUENCY;
|
||||||
request.format = AUDIO_F32;
|
request.format = AUDIO_F32;
|
||||||
request.channels = 2;
|
request.channels = 2;
|
||||||
|
#ifndef TWN_FEATURE_PUSH_AUDIO
|
||||||
request.callback = audio_callback;
|
request.callback = audio_callback;
|
||||||
|
#endif
|
||||||
/* TODO: check for errors */
|
/* TODO: check for errors */
|
||||||
ctx.audio_device = SDL_OpenAudioDevice(NULL, 0, &request, &got, 0);
|
ctx.audio_device = SDL_OpenAudioDevice(NULL, 0, &request, &got, 0);
|
||||||
ctx.audio_stream_format = got.format;
|
ctx.audio_stream_format = got.format;
|
||||||
ctx.audio_stream_frequency = got.freq;
|
ctx.audio_stream_frequency = got.freq;
|
||||||
ctx.audio_stream_channel_count = got.channels;
|
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.format == AUDIO_F32);
|
||||||
SDL_assert_always(got.channels == 2);
|
SDL_assert_always(got.channels == 2);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user