twn_audio.c: .wav support and scratch channels

This commit is contained in:
veclavtalica 2025-01-13 19:56:20 +03:00
parent 87ae1a7312
commit eefd53a630
3 changed files with 166 additions and 18 deletions

View File

@ -7,6 +7,7 @@
#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
@ -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,11 +204,12 @@ void audio_play(const char *path,
float volume,
float panning)
{
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);
AudioFileType const file_type = infer_audio_file_type(path);
AudioChannel new_channel = {
.file_type = file_type,
.context = init_audio_context(path, file_type),
@ -188,6 +228,24 @@ void audio_play(const char *path,
/* 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 = 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);
}
}
@ -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,10 +328,12 @@ 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 */
audio_mixin_streams(channel,
@ -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,10 +421,12 @@ 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 */
audio_mixin_streams(channel,
@ -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);

View File

@ -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;

View File

@ -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;