From eefd53a63057db00103737b92a53b3e683c42a42 Mon Sep 17 00:00:00 2001 From: veclavtalica Date: Mon, 13 Jan 2025 19:56:20 +0300 Subject: [PATCH] twn_audio.c: .wav support and scratch channels --- src/twn_audio.c | 171 +++++++++++++++++++++++++++++++++---- src/twn_audio_c.h | 12 ++- src/twn_engine_context_c.h | 1 + 3 files changed, 166 insertions(+), 18 deletions(-) diff --git a/src/twn_audio.c b/src/twn_audio.c index 04b1046..a799072 100644 --- a/src/twn_audio.c +++ b/src/twn_audio.c @@ -7,6 +7,7 @@ #include #include #include +#include #define STB_VORBIS_NO_PUSHDATA_API #define STB_VORBIS_HEADER_ONLY @@ -17,6 +18,7 @@ static const char *audio_exts[AUDIO_FILE_TYPE_COUNT] = { ".ogg", /* AUDIO_FILE_TYPE_OGG */ + ".wav", /* AUDIO_FILE_TYPE_WAV */ ".xm", /* AUDIO_FILE_TYPE_XM */ }; @@ -73,6 +75,7 @@ static AudioFileType infer_audio_file_type(const char *path) { /* 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: { @@ -102,6 +105,25 @@ static union AudioContext init_audio_context(const char *path, AudioFileType typ }; } + /* 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; + } + + 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); @@ -138,6 +160,18 @@ 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_WAV: { + SDL_free(channel.context.wav.samples); + break; + } + default: + break; + } +} + + static void repeat_audio(AudioChannel *channel) { switch (channel->file_type) { case AUDIO_FILE_TYPE_OGG: { @@ -145,6 +179,11 @@ static void repeat_audio(AudioChannel *channel) { break; } + case AUDIO_FILE_TYPE_WAV: { + channel->context.wav.position = 0; + break; + } + case AUDIO_FILE_TYPE_XM: { xm_restart(channel->context.xm.handle); break; @@ -165,29 +204,48 @@ void audio_play(const char *path, float volume, float panning) { - AudioChannelItem *pair = shgetp_null(ctx.audio_channels, channel); + if (channel) { + 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); + /* create a channel if it doesn't exist */ + if (!pair) { + AudioFileType const 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); + } 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 = path, .name = channel, - .repeat = repeat, + .repeat = false, .volume = volume, .panning = panning, }; - shput(ctx.audio_channels, channel, new_channel); - pair = shgetp_null(ctx.audio_channels, channel); + + if (repeat) + log_warn("Cannot repeat audio played on unnamed scratch channel (for %s)", path); + + arrpush(ctx.unnamed_audio_channels, new_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); } @@ -243,12 +301,12 @@ static void audio_mixin_streams(const AudioChannel *channel, /* remember: sample is data for all channels where frame is a part of it */ -static void audio_sample_and_mixin_channel(const AudioChannel *channel, +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); + const int float_buffer_frames = sizeof (buffer) / sizeof (float) / 2; const int stream_frames = len / (int)(sizeof (float)); switch (channel->file_type) { @@ -270,9 +328,11 @@ static void audio_sample_and_mixin_channel(const AudioChannel *channel, /* seek to start and try sampling some more */ stb_vorbis_seek_start(channel->context.vorbis.handle); continue; - } else + } else { /* leave silence */ + channel->finished = true; break; + } } /* panning and mixing */ @@ -286,6 +346,66 @@ static void audio_sample_and_mixin_channel(const AudioChannel *channel, break; } + 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; + + 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); + + 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 * 2; + } + + break; + } + case AUDIO_FILE_TYPE_XM: { for (int i = 0; i < stream_frames; ) { const int n_frames = (stream_frames - i) > float_buffer_frames ? @@ -301,9 +421,11 @@ static void audio_sample_and_mixin_channel(const AudioChannel *channel, /* seek to start and try sampling some more */ xm_restart(channel->context.xm.handle); continue; - } else + } else { + channel->finished = true; /* leave silence */ break; + } } /* panning and mixing */ @@ -346,8 +468,23 @@ void audio_callback(void *userdata, uint8_t *stream, int len) { sanity_check_channel(&ctx.audio_channels[i].value); audio_sample_and_mixin_channel(&ctx.audio_channels[i].value, stream, len); } + + for (int i = 0; i < arrlen(ctx.unnamed_audio_channels); ++i) { + sanity_check_channel(&ctx.unnamed_audio_channels[i]); + audio_sample_and_mixin_channel(&ctx.unnamed_audio_channels[i], stream, len); + } + + /* ditch finished unnamed */ + int i = 0; + while (i < arrlen(ctx.unnamed_audio_channels)) { + 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); diff --git a/src/twn_audio_c.h b/src/twn_audio_c.h index 01f6b1b..a352442 100644 --- a/src/twn_audio_c.h +++ b/src/twn_audio_c.h @@ -13,9 +13,12 @@ #define AUDIO_FREQUENCY 48000 - +/* TODO: specify which PCM formats are usable with WAV */ +/* TODO: specify limitations of libxm */ +/* TODO: specify limitations of stb_vorbis */ typedef enum AudioFileType { AUDIO_FILE_TYPE_OGG, + AUDIO_FILE_TYPE_WAV, AUDIO_FILE_TYPE_XM, AUDIO_FILE_TYPE_COUNT, AUDIO_FILE_TYPE_UNKNOWN, @@ -30,6 +33,12 @@ union AudioContext { uint8_t channel_count; } vorbis; + struct { + void *samples; + SDL_AudioSpec spec; + size_t position; + } wav; + struct { xm_context_t *handle; } xm; @@ -44,6 +53,7 @@ typedef struct AudioChannel { bool repeat; float volume; float panning; + bool finished; } AudioChannel; diff --git a/src/twn_engine_context_c.h b/src/twn_engine_context_c.h index 0036bcd..4ea240a 100644 --- a/src/twn_engine_context_c.h +++ b/src/twn_engine_context_c.h @@ -53,6 +53,7 @@ typedef struct EngineContext { /* audio */ AudioChannelItem *audio_channels; + AudioChannel *unnamed_audio_channels; SDL_AudioDeviceID audio_device; int audio_stream_frequency; SDL_AudioFormat audio_stream_format;