Skip to content

Commit

Permalink
Audio implementation for SDL2_mixer + libxmp
Browse files Browse the repository at this point in the history
  • Loading branch information
katajakasa committed Jan 7, 2024
1 parent a0624b3 commit 2d526b9
Showing 1 changed file with 291 additions and 61 deletions.
352 changes: 291 additions & 61 deletions src/audio/audio.c
Original file line number Diff line number Diff line change
@@ -1,94 +1,324 @@
#include <assert.h>
#include <stdlib.h>

#include <SDL.h>
#include <SDL_mixer.h>
#include <xmp.h>

#include "audio/audio.h"
#include "audio/sink.h"
#include "audio/sinks/openal_sink.h"
#include "resources/pathmanager.h"
#include "resources/sounds_loader.h"
#include "utils/allocator.h"
#include "utils/log.h"
#include <stdlib.h>
#include <string.h>
#include "utils/miscmath.h"

typedef struct {
int (*sink_init_fn)(audio_sink *sink);
const char *name;
} sink_info;
#define CHANNEL_MAX 8

static audio_sink *_global_sink = NULL;
const audio_freq output_freqs[] = {
{11025, 0, "11025Hz"},
{22050, 0, "22050Hz"},
{44100, 0, "44100Hz"},
{48000, 1, "48000Hz"},
{0, 0, 0 } // Guard
};

const sink_info sinks[] = {
#ifdef USE_OPENAL
{openal_sink_init, "openal"},
#endif // USE_OPENAL
const audio_mod_resampler music_resamplers[] = {
{XMP_INTERP_NEAREST, 0, "Nearest"},
{XMP_INTERP_LINEAR, 1, "Linear" },
{XMP_INTERP_SPLINE, 0, "Cubic" },
{0, 0, 0 } // Guard
};
#define SINK_COUNT (sizeof(sinks) / sizeof(sink_info))

const char *audio_get_first_sink_name() {
if(SINK_COUNT > 0) {
return sinks[0].name;
typedef struct audio_system {
int freq;
Uint16 format;
int channels;
int resampler;
float music_volume;
resource_id music_id;
xmp_context xmp_context;
Mix_Chunk *channel_chunks[CHANNEL_MAX];
} audio_system;

static audio_system *audio = NULL;

static const char *get_sdl_audio_format_string(SDL_AudioFormat format) {
switch(format) {
case AUDIO_U8:
return "AUDIO_U8";
case AUDIO_S8:
return "AUDIO_S8";
case AUDIO_U16LSB:
return "AUDIO_U16LSB";
case AUDIO_S16LSB:
return "AUDIO_S16LSB";
case AUDIO_U16MSB:
return "AUDIO_U16MSB";
case AUDIO_S16MSB:
return "AUDIO_S16MSB";
case AUDIO_S32LSB:
return "AUDIO_S32LSB";
case AUDIO_S32MSB:
return "AUDIO_S32MSB";
case AUDIO_F32LSB:
return "AUDIO_F32LSB";
case AUDIO_F32MSB:
return "AUDIO_F32MSB";
}
return NULL;
return "UNKNOWN";
}

int audio_is_sink_available(const char *sink_name) {
for(unsigned i = 0; i < SINK_COUNT; i++) {
if(strcmp(sink_name, sinks[i].name) == 0) {
return 1;
}
static Mix_Chunk *audio_get_chunk(int id, float volume, float pitch) {
char *src_buf;
int src_len;
Uint8 *dst_buf;
SDL_AudioCVT cvt;

// Load sample (8000Hz, mono, 8bit)
if(sounds_loader_get(id, &src_buf, &src_len) != 0) {
PERROR("Requested sound sample %d not found", id);
return NULL;
}
if(src_len == 0) {
DEBUG("Requested sound sample %d has nothing to play", id);
return NULL;
}

// Converter for sound samples.
int src_freq = 8000 * pitch;
if(SDL_BuildAudioCVT(&cvt, AUDIO_U8, 1, src_freq, audio->format, audio->channels, audio->freq) < 0) {
PERROR("Unable to build audio converter: %s", SDL_GetError());
return NULL;
}

// Create a buffer that can hold the source data and final converted data.
if((dst_buf = SDL_malloc(src_len * cvt.len_mult + 1)) == NULL) {
PERROR("Unable to allocate memory for sound buffer");
return NULL;
}
return 0;
SDL_memcpy((void *)dst_buf, (void *)src_buf, src_len);

// Convert!
cvt.buf = dst_buf;
cvt.len = src_len;
if(SDL_ConvertAudio(&cvt) != 0) {
PERROR("Unable to convert audio sample: %s", SDL_GetError());
return NULL;
}

Mix_Chunk *chunk = SDL_malloc(sizeof(Mix_Chunk));
chunk->volume = volume * MIX_MAX_VOLUME;
chunk->abuf = dst_buf;
chunk->alen = cvt.len_cvt;
chunk->allocated = 1;
return chunk;
}

void audio_render() {
if(_global_sink != NULL) {
sink_render(_global_sink);
static void audio_sound_finished(int channel) {
assert(audio);
if(audio->channel_chunks[channel] != NULL) {
Mix_FreeChunk(audio->channel_chunks[channel]);
audio->channel_chunks[channel] = NULL;
}
}

int audio_init(const char *sink_name) {
sink_info si;
memset(&si, 0, sizeof(sink_info));
static bool audio_load_module(const char *file) {
assert(audio);

// If null sink given, disable audio
if(sink_name == NULL || strlen(sink_name) <= 0) {
INFO("Audio sink NOT initialized; audio not available.");
return 0;
// Load the module file
if(xmp_load_module(audio->xmp_context, (char *)file) < 0) {
PERROR("Unable to open module file");
goto exit_0;
}

// Find requested sink
for(unsigned c = 0; c < SINK_COUNT; ++c) {
if(strcmp(sink_name, sinks[c].name) == 0) {
si = sinks[c];
break;
}
// Show some information
struct xmp_module_info mi;
xmp_get_module_info(audio->xmp_context, &mi);
DEBUG("Loaded music track %s (%s)", mi.mod->name, mi.mod->type);

// Start the player
int flags = 0;
if(audio->channels == 1)
flags |= XMP_FORMAT_MONO;
if(xmp_start_player(audio->xmp_context, audio->freq, flags) != 0) {
PERROR("Unable to start module playback");
goto exit_1;
}
if(xmp_set_player(audio->xmp_context, XMP_PLAYER_INTERP, audio->resampler) != 0) {
PERROR("Unable to set music resampler");
goto exit_2;
}
if(xmp_set_player(audio->xmp_context, XMP_PLAYER_VOLUME, audio->music_volume * 100) != 0) {
PERROR("Unable to set music volume");
goto exit_2;
}
return true;

exit_2:
xmp_end_player(audio->xmp_context);
exit_1:
xmp_release_module(audio->xmp_context);
exit_0:
return false;
}

// Callback function for SDL_Mixer
void audio_xmp_render(void *userdata, Uint8 *stream, int len) {
assert(audio);
xmp_play_buffer(audio->xmp_context, stream, len, INT_MAX);
}

static void audio_close_module() {
if(is_music(audio->music_id)) {
xmp_end_player(audio->xmp_context);
xmp_release_module(audio->xmp_context);
audio->music_id = NUMBER_OF_RESOURCES;
}
if(si.name == NULL) {
PERROR("Requested audio sink was not found!");
return 1;
}

bool audio_init(int freq, bool mono, int resampler, float music_volume, float sound_volume) {
if(!(audio = omf_calloc(1, sizeof(audio_system)))) {
PERROR("Unable to allocate audio subsystem");
goto error_0;
}
if(SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
PERROR("Unable to initialize audio subsystem: %s", SDL_GetError());
goto error_1;
}
if(Mix_Init(0) != 0) {
PERROR("Unable to initialize mixer subsystem: %s", Mix_GetError());
goto error_2;
}
if((audio->xmp_context = xmp_create_context()) == NULL) {
PERROR("Unable to initialize XMP context.");
goto error_3;
}

// Inform user
INFO("Using audio sink '%s'.", si.name);
INFO("Requested audio device with options:");
INFO(" * Rate: %dHz", freq);
INFO(" * Channels: %d", mono ? 1 : 2);
INFO(" * Format: %s", get_sdl_audio_format_string(AUDIO_S16SYS));

// Init sink
_global_sink = omf_calloc(1, sizeof(audio_sink));
sink_init(_global_sink);
if(si.sink_init_fn(_global_sink) != 0) {
omf_free(_global_sink);
return 1;
// Setup audio. We request for configuration, but we're not sure what we get.
if(Mix_OpenAudio(freq, AUDIO_S16SYS, mono ? 1 : 2, 2048) != 0) {
PERROR("Unable to initialize audio device: %s", SDL_GetError());
goto error_4;
}

// Success
INFO("Audio system initialized.");
return 0;
// Initialize playback parameters.
audio_set_sound_volume(sound_volume);
audio_set_music_volume(music_volume);
audio->resampler = resampler;
audio->music_id = NUMBER_OF_RESOURCES;

Mix_ChannelFinished(audio_sound_finished);

// Get the actual device configuration we got.
Mix_QuerySpec(&audio->freq, &audio->format, &audio->channels);
INFO("Opened audio device:");
INFO(" * Rate: %dHz", audio->freq);
INFO(" * Channels: %d", audio->channels);
INFO(" * Format: %s", get_sdl_audio_format_string(audio->format));
return true;

error_4:
xmp_free_context(audio->xmp_context);
error_3:
Mix_Quit();
error_2:
SDL_QuitSubSystem(SDL_INIT_AUDIO);
error_1:
omf_free(audio);
error_0:
audio = NULL;
return false;
}

void audio_close() {
if(_global_sink != NULL) {
sink_free(_global_sink);
omf_free(_global_sink);
_global_sink = NULL;
INFO("Audio system closed.");
if(audio != NULL) {
audio_close_module();
if(audio->xmp_context) {
xmp_free_context(audio->xmp_context);
audio->xmp_context = NULL;
}
Mix_ChannelFinished(NULL);
Mix_CloseAudio();
for(int i = 0; i < CHANNEL_MAX; i++) {
if(audio->channel_chunks[i] != NULL) {
Mix_FreeChunk(audio->channel_chunks[i]);
audio->channel_chunks[i] = NULL;
}
}
omf_free(audio);
audio = NULL;
}
Mix_Quit();
SDL_QuitSubSystem(SDL_INIT_AUDIO);
}

audio_sink *audio_get_sink() {
return _global_sink;
void audio_play_sound(int id, float volume, float panning, float pitch) {
assert(audio);
int channel;
float pan_left, pan_right;

// Anything beyond these are invalid
if(id < 0 || id > 299)
return;

volume = clampf(volume, VOLUME_MIN, VOLUME_MAX);
panning = clampf(panning, PANNING_MIN, PANNING_MAX);
pitch = clampf(pitch, PITCH_MIN, PITCH_MAX);
pan_left = (panning > 0) ? 1.0f - panning : 1.0f;
pan_right = (panning < 0) ? 1.0f + panning : 1.0f;

Mix_Chunk *chunk;
if(!(chunk = audio_get_chunk(id, volume, pitch)))
return;
channel = Mix_GroupAvailable(-1);
audio->channel_chunks[channel] = chunk;
Mix_SetPanning(channel, clamp(pan_left * 255, 0, 255), clamp(pan_right * 255, 0, 255));
if(Mix_PlayChannel(channel, chunk, 0) == -1) {
PERROR("Unable to play sound: %s", Mix_GetError());
}
}

void audio_play_music(resource_id id) {
assert(audio);
assert(is_music(id));
if(audio->music_id != id) {
audio_close_module();
const char *music_file = pm_get_resource_path(id);
if(!audio_load_module(music_file)) {
PERROR("Unable to load music track: %s", music_file);
return;
}
audio->music_id = id;
Mix_HookMusic(audio_xmp_render, NULL);
}
}

void audio_stop_music() {
assert(audio);
Mix_HaltMusic();
Mix_HookMusic(NULL, NULL);
}

void audio_set_music_volume(float volume) {
assert(audio);
audio->music_volume = clampf(volume, VOLUME_MIN, VOLUME_MAX);
xmp_set_player(audio->xmp_context, XMP_PLAYER_VOLUME, audio->music_volume * 100);
}

void audio_set_sound_volume(float volume) {
assert(audio);
volume = clampf(volume, VOLUME_MIN, VOLUME_MAX);
Mix_MasterVolume(volume * MIX_MAX_VOLUME);
}

const audio_freq *audio_get_freqs() {
return output_freqs;
}

const audio_mod_resampler *audio_get_resamplers() {
return music_resamplers;
}

0 comments on commit 2d526b9

Please sign in to comment.