diff --git a/CMakeLists.txt b/CMakeLists.txt index f6748e3..222dd2c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,7 @@ set(TWN_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR} CACHE INTERNAL "") # feature configuration, set them with -DFEATURE=ON/OFF in cli 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) # 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_EXTENSIONS ON) # extensions are required by stb_ds.h +add_compile_definitions(${TWN_TARGET} $<$:TWN_FEATURE_PUSH_AUDIO>) + # precompile commonly used not-so-small headers target_precompile_headers(${TWN_TARGET} PRIVATE $<$>:third-party/glad/include/glad/glad.h> diff --git a/src/twn_audio.c b/src/twn_audio.c index a799072..1dc93a1 100644 --- a/src/twn_audio.c +++ b/src/twn_audio.c @@ -22,6 +22,7 @@ static const char *audio_exts[AUDIO_FILE_TYPE_COUNT] = { ".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 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; } + 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 + .spec = spec, } }; } @@ -162,11 +192,22 @@ static union AudioContext init_audio_context(const char *path, AudioFileType typ 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; } } @@ -235,7 +276,7 @@ void audio_play(const char *path, .file_type = file_type, .context = init_audio_context(path, file_type), .path = path, - .name = channel, + .name = NULL, .repeat = false, .volume = volume, .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 */ static void audio_mixin_streams(const AudioChannel *channel, 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 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 */ - sa[s] += (float)(sb[s] * channel->volume * left_panning); + sa[s * 2 + 0] += (float)(sb[s * 2 + 0] * channel->volume * left_panning); /* 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, uint8_t *stream, int len) { - static uint8_t buffer[16384]; - const int float_buffer_frames = sizeof (buffer) / sizeof (float) / 2; - const int stream_frames = len / (int)(sizeof (float)); + 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 (int i = 0; i < stream_frames; ) { - const int n_frames = (stream_frames - i) > float_buffer_frames ? + 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 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.channel_count, + 2, (float *)buffer, - n_frames); + (int)n_frames * 2); /* handle end of file */ if (samples_per_channel == 0) { @@ -337,10 +379,10 @@ static void audio_sample_and_mixin_channel(AudioChannel *channel, /* panning and mixing */ audio_mixin_streams(channel, - &stream[i * sizeof(float)], buffer, - samples_per_channel * 2); + &stream[i * sizeof(float) * 2], buffer, + samples_per_channel); - i += samples_per_channel * 2; + i += samples_per_channel; } break; @@ -348,49 +390,18 @@ static void audio_sample_and_mixin_channel(AudioChannel *channel, case AUDIO_FILE_TYPE_WAV: { /* 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; + 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); - int const limit = MIN(n_frames, channel->context.wav.spec.samples); - - switch (channel->context.wav.spec.format) { - case AUDIO_U16: { - if (channel->context.wav.spec.channels == 1) { - 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); + /* 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); 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) channel->context.wav.position = 0; else { @@ -400,20 +411,20 @@ static void audio_sample_and_mixin_channel(AudioChannel *channel, } } - i += limit * 2; + i += limit; } break; } case AUDIO_FILE_TYPE_XM: { - for (int i = 0; i < stream_frames; ) { - const int n_frames = (stream_frames - i) > float_buffer_frames ? + 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 int samples_per_channel = xm_generate_samples(channel->context.xm.handle, - (float *)buffer, - n_frames / 2); + 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) { @@ -430,11 +441,11 @@ static void audio_sample_and_mixin_channel(AudioChannel *channel, /* panning and mixing */ audio_mixin_streams(channel, - &stream[i * sizeof(float)], + &stream[i * sizeof(float) * 2], buffer, - samples_per_channel * 2); + samples_per_channel); - i += samples_per_channel * 2; + i += samples_per_channel; } break; diff --git a/src/twn_loop.c b/src/twn_loop.c index 1f1fdcc..7f93b07 100644 --- a/src/twn_loop.c +++ b/src/twn_loop.c @@ -232,6 +232,12 @@ static void main_loop(void) { if (ctx.window_size_has_changed) update_viewport(); 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); preserve_persistent_ctx_fields(); @@ -560,12 +566,16 @@ static bool initialize(void) { request.freq = AUDIO_FREQUENCY; request.format = AUDIO_F32; request.channels = 2; + #ifndef TWN_FEATURE_PUSH_AUDIO request.callback = audio_callback; + #endif /* 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);