twn_audio.c: .wav support and scratch channels
This commit is contained in:
parent
87ae1a7312
commit
eefd53a630
147
src/twn_audio.c
147
src/twn_audio.c
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user