.ogg playback
This commit is contained in:
parent
adcf638e2a
commit
8a8f62dc25
@ -19,10 +19,13 @@ add_subdirectory(third-party/physfs)
|
|||||||
|
|
||||||
set(SOURCE_FILES
|
set(SOURCE_FILES
|
||||||
third-party/physfs/extras/physfsrwops.c
|
third-party/physfs/extras/physfsrwops.c
|
||||||
|
third-party/stb/stb_vorbis.c
|
||||||
|
|
||||||
src/config.h
|
src/config.h
|
||||||
src/context.h
|
src/context.h
|
||||||
src/context.c
|
src/context.c
|
||||||
src/main.c
|
src/main.c
|
||||||
|
src/audio.c
|
||||||
src/util.c src/util.h
|
src/util.c src/util.h
|
||||||
src/rendering.c src/rendering.h
|
src/rendering.c src/rendering.h
|
||||||
src/textures.c src/textures.h
|
src/textures.c src/textures.h
|
||||||
@ -75,8 +78,7 @@ else()
|
|||||||
-Wshadow
|
-Wshadow
|
||||||
-Wdouble-promotion
|
-Wdouble-promotion
|
||||||
-Wconversion -Wno-sign-conversion
|
-Wconversion -Wno-sign-conversion
|
||||||
-Werror=vla
|
-Werror=vla)
|
||||||
-Werror=alloca)
|
|
||||||
set(BUILD_FLAGS
|
set(BUILD_FLAGS
|
||||||
# these SHOULDN'T break anything...
|
# these SHOULDN'T break anything...
|
||||||
-fno-math-errno
|
-fno-math-errno
|
||||||
@ -88,6 +90,7 @@ else()
|
|||||||
-flto)
|
-flto)
|
||||||
set(BUILD_FLAGS_DEBUG
|
set(BUILD_FLAGS_DEBUG
|
||||||
-g3
|
-g3
|
||||||
|
-gdwarf
|
||||||
-fsanitize-trap=undefined)
|
-fsanitize-trap=undefined)
|
||||||
target_compile_options(${PROJECT_NAME} PRIVATE
|
target_compile_options(${PROJECT_NAME} PRIVATE
|
||||||
${WARNING_FLAGS}
|
${WARNING_FLAGS}
|
||||||
|
BIN
data/music/test.ogg
(Stored with Git LFS)
Normal file
BIN
data/music/test.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
228
src/audio.c
Normal file
228
src/audio.c
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
#include "private/audio.h"
|
||||||
|
#include "audio.h"
|
||||||
|
#include "context.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
#include <SDL2/SDL.h>
|
||||||
|
#include <stb_ds.h>
|
||||||
|
#include <physfs.h>
|
||||||
|
|
||||||
|
#define STB_VORBIS_HEADER_ONLY
|
||||||
|
#include <stb_vorbis.c>
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* TODO: default to float sampling format? */
|
||||||
|
|
||||||
|
static const char *audio_exts[audio_file_type_count] = {
|
||||||
|
".ogg", /* audio_file_type_ogg */
|
||||||
|
".xm", /* audio_file_type_xm */
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/* TODO: count frames without use, free the memory when threshold is met */
|
||||||
|
/* stores path to data hash, useful for sound effects */
|
||||||
|
static struct audio_file_cache {
|
||||||
|
char *key;
|
||||||
|
struct audio_file_cache_value {
|
||||||
|
unsigned char *data;
|
||||||
|
size_t len;
|
||||||
|
} value;
|
||||||
|
} *audio_file_cache;
|
||||||
|
|
||||||
|
|
||||||
|
static int64_t get_audio_data(const char *path, unsigned char **data) {
|
||||||
|
const struct audio_file_cache *cache = shgetp_null(audio_file_cache, path);
|
||||||
|
if (!cache) {
|
||||||
|
unsigned char *file;
|
||||||
|
int64_t len = file_to_bytes(path, &file);
|
||||||
|
if (len == -1) {
|
||||||
|
CRY("Audio error", "Error reading file");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const struct audio_file_cache_value value = { file, (size_t)len };
|
||||||
|
shput(audio_file_cache, path, value);
|
||||||
|
|
||||||
|
*data = file;
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
*data = cache->value.data;
|
||||||
|
return (int64_t)cache->value.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void play_audio(const char *path, const char *channel) {
|
||||||
|
const struct audio_channel_pair *pair = shgetp_null(ctx.audio_channels, channel);
|
||||||
|
if (!pair)
|
||||||
|
play_audio_ex(path, channel, get_default_audio_args());
|
||||||
|
else
|
||||||
|
play_audio_ex(path, channel, pair->value.args);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static t_audio_file_type infer_audio_file_type(const char *path) {
|
||||||
|
size_t path_len = strlen(path);
|
||||||
|
|
||||||
|
for (int i = 0; i < audio_file_type_count; ++i) {
|
||||||
|
size_t ext_length = strlen(audio_exts[i]);
|
||||||
|
if (path_len <= ext_length)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (strcmp(&path[path_len - ext_length], audio_exts[i]) == 0)
|
||||||
|
return (t_audio_file_type)i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return audio_file_type_unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* TODO: error propagation and clearing of resources on partial success? */
|
||||||
|
/* or should we expect things to simply fail? */
|
||||||
|
static union audio_context init_audio_context(const char *path, t_audio_file_type type) {
|
||||||
|
switch (type) {
|
||||||
|
case audio_file_type_ogg: {
|
||||||
|
unsigned char *data;
|
||||||
|
int64_t len = get_audio_data(path, &data);
|
||||||
|
if (len == -1) {
|
||||||
|
CRY("Audio error", "Error reading file");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int error = 0;
|
||||||
|
stb_vorbis* handle = stb_vorbis_open_memory(data, (int)len, &error, NULL);
|
||||||
|
if (error != 0) {
|
||||||
|
CRY("Audio error", "Error reading .ogg file");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
stb_vorbis_info info = stb_vorbis_get_info(handle);
|
||||||
|
|
||||||
|
return (union audio_context){
|
||||||
|
.vorbis = {
|
||||||
|
.data = data,
|
||||||
|
.handle = handle,
|
||||||
|
.frequency = info.sample_rate,
|
||||||
|
.channel_count = (uint8_t)info.channels,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
CRY("Audio error", "Unhandled audio format (in init)");
|
||||||
|
return (union audio_context){0};
|
||||||
|
}
|
||||||
|
|
||||||
|
return (union audio_context){0};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void repeat_audio(struct audio_channel *channel) {
|
||||||
|
(void)channel;
|
||||||
|
|
||||||
|
/* TODO */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void play_audio_ex(const char *path, const char *channel, t_play_audio_args args) {
|
||||||
|
struct audio_channel_pair *pair = shgetp_null(ctx.audio_channels, channel);
|
||||||
|
|
||||||
|
/* create a channel if it doesn't exist */
|
||||||
|
if (!pair) {
|
||||||
|
t_audio_file_type file_type = infer_audio_file_type(path);
|
||||||
|
struct audio_channel new_channel = {
|
||||||
|
.args = args,
|
||||||
|
.file_type = file_type,
|
||||||
|
.context = init_audio_context(path, file_type),
|
||||||
|
.path = path,
|
||||||
|
};
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
t_play_audio_args get_default_audio_args(void) {
|
||||||
|
return (t_play_audio_args){
|
||||||
|
.repeat = false,
|
||||||
|
.crossfade = false,
|
||||||
|
.volume = 1.0f,
|
||||||
|
.panning = 0.0f,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void audio_sample_and_mixin_channel(struct audio_channel *channel,
|
||||||
|
uint8_t *stream,
|
||||||
|
int len) {
|
||||||
|
int16_t *const sstream = (int16_t *)stream;
|
||||||
|
static uint8_t buffer[16384];
|
||||||
|
const int buffer_frames = sizeof (buffer) / sizeof (int16_t);
|
||||||
|
|
||||||
|
switch (channel->file_type) {
|
||||||
|
case audio_file_type_ogg: {
|
||||||
|
/* feed stream for needed conversions */
|
||||||
|
for (int i = 0; i < (len / 2);) {
|
||||||
|
const int n_frames = ((len / 2) - i) > buffer_frames ? buffer_frames : (len / 2) - i;
|
||||||
|
|
||||||
|
/* TODO: handle end of file */
|
||||||
|
const int samples_per_channel = stb_vorbis_get_samples_short_interleaved(
|
||||||
|
channel->context.vorbis.handle,
|
||||||
|
channel->context.vorbis.channel_count,
|
||||||
|
(int16_t *)buffer,
|
||||||
|
n_frames);
|
||||||
|
|
||||||
|
/* panning and mixing */
|
||||||
|
|
||||||
|
#if AUDIO_N_CHANNELS == 2
|
||||||
|
|
||||||
|
for (int s = 0; s < samples_per_channel * ctx.audio_stream_channel_count; s += 2) {
|
||||||
|
/* left channel */
|
||||||
|
{
|
||||||
|
const float panning = fminf(fabsf(channel->args.panning - 1.0f), 1.0f);
|
||||||
|
const float volume = channel->args.volume * panning;
|
||||||
|
sstream[i + s] += (int16_t)(((int16_t *)buffer)[s] * volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* right channel */
|
||||||
|
{
|
||||||
|
const float panning = fminf(fabsf(channel->args.panning + 1.0f), 1.0f);
|
||||||
|
const float volume = channel->args.volume * panning;
|
||||||
|
sstream[i + s + 1] += (int16_t)(((int16_t *)buffer)[s + 1] * volume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
#error "Unimplemented channel count"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
i += samples_per_channel * ctx.audio_stream_channel_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
CRY("Audio error", "Unhandled audio format (in sampling)");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void audio_callback(void *userdata, uint8_t *stream, int len) {
|
||||||
|
(void)userdata;
|
||||||
|
|
||||||
|
/* prepare for mixing */
|
||||||
|
SDL_memset(stream, sizeof (uint16_t), len * ctx.audio_stream_channel_count);
|
||||||
|
|
||||||
|
for (int i = 0; i < shlen(ctx.audio_channels); ++i) {
|
||||||
|
audio_sample_and_mixin_channel(&ctx.audio_channels[i].value, stream, len);
|
||||||
|
}
|
||||||
|
}
|
36
src/audio.h
Normal file
36
src/audio.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#ifndef AUDIO_H
|
||||||
|
#define AUDIO_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct play_audio_args {
|
||||||
|
/* default: false */
|
||||||
|
bool repeat;
|
||||||
|
/* crossfade between already playing audio on a given channel, if any */
|
||||||
|
/* default: false */
|
||||||
|
bool crossfade;
|
||||||
|
/* range: 0.0f to 1.0f */
|
||||||
|
/* default: 100.0f */
|
||||||
|
float volume;
|
||||||
|
/* range: -1.0 to 1.0f */
|
||||||
|
/* default: 0.0f */
|
||||||
|
float panning;
|
||||||
|
} t_play_audio_args;
|
||||||
|
|
||||||
|
|
||||||
|
/* plays audio file at specified channel or anywhere if NULL is passed */
|
||||||
|
/* path must contain valid file extension to infer which file format it is */
|
||||||
|
/* supported formats: .ogg, .xm */
|
||||||
|
/* preserves args that are already specified on the channel */
|
||||||
|
void play_audio(const char *path, const char *channel);
|
||||||
|
|
||||||
|
void play_audio_ex(const char *path, const char *channel, t_play_audio_args args);
|
||||||
|
|
||||||
|
void set_audio_args(const char *channel, t_play_audio_args args);
|
||||||
|
|
||||||
|
t_play_audio_args get_audio_args(const char *channel);
|
||||||
|
|
||||||
|
t_play_audio_args get_default_audio_args(void);
|
||||||
|
|
||||||
|
#endif
|
@ -24,6 +24,9 @@
|
|||||||
|
|
||||||
#define NUM_KEYBIND_SLOTS 8
|
#define NUM_KEYBIND_SLOTS 8
|
||||||
|
|
||||||
|
#define AUDIO_FREQUENCY 48000
|
||||||
|
#define AUDIO_N_CHANNELS 2
|
||||||
|
|
||||||
/* 1024 * 1024 */
|
/* 1024 * 1024 */
|
||||||
/* #define UMKA_STACK_SIZE 1048576 */
|
/* #define UMKA_STACK_SIZE 1048576 */
|
||||||
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
#include "context.h"
|
#include "context.h"
|
||||||
|
|
||||||
t_ctx ctx;
|
t_ctx ctx = {0};
|
||||||
|
@ -26,7 +26,11 @@ typedef struct context {
|
|||||||
struct rect_primitive *render_queue_rectangles;
|
struct rect_primitive *render_queue_rectangles;
|
||||||
struct circle_primitive *render_queue_circles;
|
struct circle_primitive *render_queue_circles;
|
||||||
|
|
||||||
struct audio_channel *audio_channels;
|
struct audio_channel_pair *audio_channels;
|
||||||
|
SDL_AudioDeviceID audio_device;
|
||||||
|
int audio_stream_frequency;
|
||||||
|
SDL_AudioFormat audio_stream_format;
|
||||||
|
uint8_t audio_stream_channel_count;
|
||||||
|
|
||||||
struct circle_radius_cache_item *circle_radius_hash;
|
struct circle_radius_cache_item *circle_radius_hash;
|
||||||
|
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
#include "title.h"
|
#include "title.h"
|
||||||
#include "scene.h"
|
#include "scene.h"
|
||||||
|
|
||||||
|
#include "../../audio.h"
|
||||||
|
|
||||||
|
|
||||||
static void ingame_tick(struct state *state) {
|
static void ingame_tick(struct state *state) {
|
||||||
struct scene_ingame *scn = (struct scene_ingame *)state->scene;
|
struct scene_ingame *scn = (struct scene_ingame *)state->scene;
|
||||||
@ -28,5 +30,10 @@ struct scene *ingame_scene(struct state *state) {
|
|||||||
new_scene->world = world_create();
|
new_scene->world = world_create();
|
||||||
new_scene->player = player_create(new_scene->world);
|
new_scene->player = player_create(new_scene->world);
|
||||||
|
|
||||||
|
play_audio_ex("music/test.ogg", "soundtrack", (t_play_audio_args){
|
||||||
|
.volume = 0.8f,
|
||||||
|
.panning = -0.5f
|
||||||
|
});
|
||||||
|
|
||||||
return (struct scene *)new_scene;
|
return (struct scene *)new_scene;
|
||||||
}
|
}
|
||||||
|
23
src/main.c
23
src/main.c
@ -4,10 +4,11 @@
|
|||||||
#include "util.h"
|
#include "util.h"
|
||||||
#include "textures.h"
|
#include "textures.h"
|
||||||
#include "game/game.h"
|
#include "game/game.h"
|
||||||
|
#include "private/audio.h"
|
||||||
|
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
#include <SDL2/SDL_image.h>
|
|
||||||
#include <SDL2/SDL_ttf.h>
|
#include <SDL2/SDL_ttf.h>
|
||||||
|
#include <SDL2/SDL_image.h>
|
||||||
#include <physfs.h>
|
#include <physfs.h>
|
||||||
#include <stb_ds.h>
|
#include <stb_ds.h>
|
||||||
|
|
||||||
@ -179,6 +180,26 @@ static bool initialize(void) {
|
|||||||
SDL_RenderSetLogicalSize(ctx.renderer, RENDER_BASE_WIDTH, RENDER_BASE_HEIGHT);
|
SDL_RenderSetLogicalSize(ctx.renderer, RENDER_BASE_WIDTH, RENDER_BASE_HEIGHT);
|
||||||
SDL_GetRendererOutputSize(ctx.renderer, &ctx.window_w, &ctx.window_h);
|
SDL_GetRendererOutputSize(ctx.renderer, &ctx.window_w, &ctx.window_h);
|
||||||
|
|
||||||
|
/* audio initialization */
|
||||||
|
{
|
||||||
|
|
||||||
|
SDL_AudioSpec request, got;
|
||||||
|
SDL_zero(request);
|
||||||
|
|
||||||
|
request.freq = AUDIO_FREQUENCY;
|
||||||
|
request.format = AUDIO_S16;
|
||||||
|
request.channels = AUDIO_N_CHANNELS;
|
||||||
|
request.callback = audio_callback;
|
||||||
|
/* 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;
|
||||||
|
|
||||||
|
SDL_PauseAudioDevice(ctx.audio_device, 0);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/* images */
|
/* images */
|
||||||
if (IMG_Init(IMG_INIT_PNG) == 0) {
|
if (IMG_Init(IMG_INIT_PNG) == 0) {
|
||||||
CRY_SDL("SDL_image initialization failed.");
|
CRY_SDL("SDL_image initialization failed.");
|
||||||
|
49
src/private/audio.h
Normal file
49
src/private/audio.h
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
#ifndef PRIVATE_AUDIO_H
|
||||||
|
#define PRIVATE_AUDIO_H
|
||||||
|
|
||||||
|
#include "../audio.h"
|
||||||
|
|
||||||
|
#include <SDL2/SDL_audio.h>
|
||||||
|
|
||||||
|
#define STB_VORBIS_HEADER_ONLY
|
||||||
|
#include <stb_vorbis.c>
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
|
||||||
|
typedef enum audio_file_type {
|
||||||
|
audio_file_type_ogg,
|
||||||
|
audio_file_type_xm,
|
||||||
|
audio_file_type_count,
|
||||||
|
audio_file_type_unknown,
|
||||||
|
} t_audio_file_type;
|
||||||
|
|
||||||
|
|
||||||
|
union audio_context {
|
||||||
|
struct {
|
||||||
|
stb_vorbis *handle;
|
||||||
|
unsigned char *data;
|
||||||
|
int frequency;
|
||||||
|
uint8_t channel_count;
|
||||||
|
} vorbis;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
struct audio_channel {
|
||||||
|
t_play_audio_args args;
|
||||||
|
enum audio_file_type file_type;
|
||||||
|
union audio_context context; /* interpreted by `file_type` value */
|
||||||
|
const char *path;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
struct audio_channel_pair {
|
||||||
|
char *key;
|
||||||
|
struct audio_channel value;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
void audio_callback(void *userdata, uint8_t *stream, int len);
|
||||||
|
|
||||||
|
#endif
|
@ -101,7 +101,7 @@ void *ccalloc(size_t num, size_t size) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int64_t file_to_bytes(char *path, unsigned char **buf_out) {
|
int64_t file_to_bytes(const char *path, unsigned char **buf_out) {
|
||||||
SDL_RWops *handle = PHYSFSRWOPS_openRead(path);
|
SDL_RWops *handle = PHYSFSRWOPS_openRead(path);
|
||||||
|
|
||||||
if (handle == NULL) {
|
if (handle == NULL) {
|
||||||
@ -119,7 +119,7 @@ int64_t file_to_bytes(char *path, unsigned char **buf_out) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
char *file_to_str(char *path) {
|
char *file_to_str(const char *path) {
|
||||||
SDL_RWops *handle = PHYSFSRWOPS_openRead(path);
|
SDL_RWops *handle = PHYSFSRWOPS_openRead(path);
|
||||||
|
|
||||||
if (handle == NULL) {
|
if (handle == NULL) {
|
||||||
|
@ -51,10 +51,10 @@ void *ccalloc(size_t num, size_t size);
|
|||||||
|
|
||||||
/* sets buf_out to a pointer to a byte buffer which must be freed. */
|
/* sets buf_out to a pointer to a byte buffer which must be freed. */
|
||||||
/* returns the size of this buffer. */
|
/* returns the size of this buffer. */
|
||||||
int64_t file_to_bytes(char *path, unsigned char **buf_out);
|
int64_t file_to_bytes(const char *path, unsigned char **buf_out);
|
||||||
|
|
||||||
/* returns a pointer to a string which must be freed */
|
/* returns a pointer to a string which must be freed */
|
||||||
char *file_to_str(char *path);
|
char *file_to_str(const char *path);
|
||||||
|
|
||||||
|
|
||||||
/* returns true if str ends with suffix */
|
/* returns true if str ends with suffix */
|
||||||
|
Loading…
Reference in New Issue
Block a user