twn_audio.c: .wav support and scratch channels
This commit is contained in:
parent
87ae1a7312
commit
eefd53a630
171
src/twn_audio.c
171
src/twn_audio.c
@ -7,6 +7,7 @@
|
|||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
#include <stb_ds.h>
|
#include <stb_ds.h>
|
||||||
#include <physfs.h>
|
#include <physfs.h>
|
||||||
|
#include <physfsrwops.h>
|
||||||
|
|
||||||
#define STB_VORBIS_NO_PUSHDATA_API
|
#define STB_VORBIS_NO_PUSHDATA_API
|
||||||
#define STB_VORBIS_HEADER_ONLY
|
#define STB_VORBIS_HEADER_ONLY
|
||||||
@ -17,6 +18,7 @@
|
|||||||
|
|
||||||
static const char *audio_exts[AUDIO_FILE_TYPE_COUNT] = {
|
static const char *audio_exts[AUDIO_FILE_TYPE_COUNT] = {
|
||||||
".ogg", /* AUDIO_FILE_TYPE_OGG */
|
".ogg", /* AUDIO_FILE_TYPE_OGG */
|
||||||
|
".wav", /* AUDIO_FILE_TYPE_WAV */
|
||||||
".xm", /* AUDIO_FILE_TYPE_XM */
|
".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? */
|
/* TODO: error propagation and clearing of resources on partial success? */
|
||||||
/* or should we expect things to simply fail? */
|
/* 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) {
|
static union AudioContext init_audio_context(const char *path, AudioFileType type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case AUDIO_FILE_TYPE_OGG: {
|
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: {
|
case AUDIO_FILE_TYPE_XM: {
|
||||||
unsigned char *data;
|
unsigned char *data;
|
||||||
int64_t len = get_audio_data(path, &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) {
|
static void repeat_audio(AudioChannel *channel) {
|
||||||
switch (channel->file_type) {
|
switch (channel->file_type) {
|
||||||
case AUDIO_FILE_TYPE_OGG: {
|
case AUDIO_FILE_TYPE_OGG: {
|
||||||
@ -145,6 +179,11 @@ static void repeat_audio(AudioChannel *channel) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case AUDIO_FILE_TYPE_WAV: {
|
||||||
|
channel->context.wav.position = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case AUDIO_FILE_TYPE_XM: {
|
case AUDIO_FILE_TYPE_XM: {
|
||||||
xm_restart(channel->context.xm.handle);
|
xm_restart(channel->context.xm.handle);
|
||||||
break;
|
break;
|
||||||
@ -165,29 +204,48 @@ void audio_play(const char *path,
|
|||||||
float volume,
|
float volume,
|
||||||
float panning)
|
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 */
|
/* create a channel if it doesn't exist */
|
||||||
if (!pair) {
|
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),
|
||||||
|
.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 = {
|
AudioChannel new_channel = {
|
||||||
.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 = channel,
|
||||||
.repeat = repeat,
|
.repeat = false,
|
||||||
.volume = volume,
|
.volume = volume,
|
||||||
.panning = panning,
|
.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 */
|
/* 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,
|
uint8_t *stream,
|
||||||
int len)
|
int len)
|
||||||
{
|
{
|
||||||
static uint8_t buffer[16384];
|
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));
|
const int stream_frames = len / (int)(sizeof (float));
|
||||||
|
|
||||||
switch (channel->file_type) {
|
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 */
|
/* seek to start and try sampling some more */
|
||||||
stb_vorbis_seek_start(channel->context.vorbis.handle);
|
stb_vorbis_seek_start(channel->context.vorbis.handle);
|
||||||
continue;
|
continue;
|
||||||
} else
|
} else {
|
||||||
/* leave silence */
|
/* leave silence */
|
||||||
|
channel->finished = true;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* panning and mixing */
|
/* panning and mixing */
|
||||||
@ -286,6 +346,66 @@ static void audio_sample_and_mixin_channel(const AudioChannel *channel,
|
|||||||
break;
|
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: {
|
case AUDIO_FILE_TYPE_XM: {
|
||||||
for (int i = 0; i < stream_frames; ) {
|
for (int i = 0; i < stream_frames; ) {
|
||||||
const int n_frames = (stream_frames - i) > float_buffer_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 */
|
/* seek to start and try sampling some more */
|
||||||
xm_restart(channel->context.xm.handle);
|
xm_restart(channel->context.xm.handle);
|
||||||
continue;
|
continue;
|
||||||
} else
|
} else {
|
||||||
|
channel->finished = true;
|
||||||
/* leave silence */
|
/* leave silence */
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* panning and mixing */
|
/* 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);
|
sanity_check_channel(&ctx.audio_channels[i].value);
|
||||||
audio_sample_and_mixin_channel(&ctx.audio_channels[i].value, stream, len);
|
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) {
|
TWN_API void audio_play_args(PlayAudioArgs args) {
|
||||||
const char *channel = m_or(args, channel, NULL);
|
const char *channel = m_or(args, channel, NULL);
|
||||||
const bool repeat = m_or(args, repeat, false);
|
const bool repeat = m_or(args, repeat, false);
|
||||||
|
@ -13,9 +13,12 @@
|
|||||||
|
|
||||||
#define AUDIO_FREQUENCY 48000
|
#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 {
|
typedef enum AudioFileType {
|
||||||
AUDIO_FILE_TYPE_OGG,
|
AUDIO_FILE_TYPE_OGG,
|
||||||
|
AUDIO_FILE_TYPE_WAV,
|
||||||
AUDIO_FILE_TYPE_XM,
|
AUDIO_FILE_TYPE_XM,
|
||||||
AUDIO_FILE_TYPE_COUNT,
|
AUDIO_FILE_TYPE_COUNT,
|
||||||
AUDIO_FILE_TYPE_UNKNOWN,
|
AUDIO_FILE_TYPE_UNKNOWN,
|
||||||
@ -30,6 +33,12 @@ union AudioContext {
|
|||||||
uint8_t channel_count;
|
uint8_t channel_count;
|
||||||
} vorbis;
|
} vorbis;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
void *samples;
|
||||||
|
SDL_AudioSpec spec;
|
||||||
|
size_t position;
|
||||||
|
} wav;
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
xm_context_t *handle;
|
xm_context_t *handle;
|
||||||
} xm;
|
} xm;
|
||||||
@ -44,6 +53,7 @@ typedef struct AudioChannel {
|
|||||||
bool repeat;
|
bool repeat;
|
||||||
float volume;
|
float volume;
|
||||||
float panning;
|
float panning;
|
||||||
|
bool finished;
|
||||||
} AudioChannel;
|
} AudioChannel;
|
||||||
|
|
||||||
|
|
||||||
|
@ -53,6 +53,7 @@ typedef struct EngineContext {
|
|||||||
|
|
||||||
/* audio */
|
/* audio */
|
||||||
AudioChannelItem *audio_channels;
|
AudioChannelItem *audio_channels;
|
||||||
|
AudioChannel *unnamed_audio_channels;
|
||||||
SDL_AudioDeviceID audio_device;
|
SDL_AudioDeviceID audio_device;
|
||||||
int audio_stream_frequency;
|
int audio_stream_frequency;
|
||||||
SDL_AudioFormat audio_stream_format;
|
SDL_AudioFormat audio_stream_format;
|
||||||
|
Loading…
Reference in New Issue
Block a user