/**
* Copyright (c) 2024, SWGY, Inc. <ron@sw.gy>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or (at
* your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include <assert.h>
#include <err.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_audio.h>
#include "swgysnd.h"
#define MAX_WAVS 128
#define MAX_CHANNELS 8
extern int verbose;
struct wav_data {
/* Audio data format */
SDL_AudioSpec wav_spec;
/* size of audio buffer */
Uint32 wav_length;
/* Total number of stereo S16 frames in the sample */
Uint32 frame_count;
/* malloc'd buffer - free with SDL_FreeWAV when finished */
Uint8 *wav_buffer;
/* zero if free, non-zero if in use */
int in_use;
};
struct channel {
/* True if channel is actively playing */
SDL_bool active;
/* Index into the wavs array */
int wav_index;
/* Current frame */
Uint32 position;
/* -1 = full left, 0 = centered, 1 = full right */
double balance;
};
static const char *version = "1.2.0";
static SDL_AudioDeviceID audio_device = 0;
static struct wav_data wavs[MAX_WAVS];
static struct channel channels[MAX_CHANNELS];
/* Forward declaration */
static void audio_callback(void *userdata, Uint8 *stream, int len);
/*
* Return a version string for the library.
*/
const char *
swgysnd_version_str()
{
return version;
}
/*
* Must be called first to initialize the library.
*/
int
swgysnd_init()
{
if (SDL_Init(SDL_INIT_AUDIO) != 0) {
fprintf(stderr, "SDL_Init(SDL_INIT_AUDIO) failed: %s\n",
SDL_GetError());
return -1;
}
bzero(wavs, sizeof(wavs));
bzero(channels, sizeof(channels));
return 0;
}
static void
audio_callback(void *userdate, Uint8 *stream, int len)
{
int total_frames, c, wav_idx, i;
Uint32 frames_to_mix, frames_left;
Sint16 left_sample, right_sample;
Sint16 *out, *wav_ptr;
struct wav_data *wp;
double bal;
/* assume S16 stereo format */
bzero(stream, len);
out = (Sint16 *)stream;
total_frames = len / (2 * sizeof(Sint16));
for (c = 0; c < MAX_CHANNELS; ++c) {
if (!channels[c].active)
continue;
wav_idx = channels[c].wav_index;
wp = &wavs[wav_idx];
if (!wp->in_use) {
channels[c].active = SDL_FALSE;
continue;
}
frames_left = wp->frame_count - channels[c].position;
if (frames_left == 0) {
/* channel finished playing */
channels[c].active = SDL_FALSE;
continue;
}
frames_to_mix = (frames_left < (Uint32) total_frames) ?
frames_left : (Uint32)total_frames;
wav_ptr = (Sint16*)(wp->wav_buffer) + 2 * channels[c].position;
/* clamp balance */
bal = channels[c].balance;
if (bal < -1.0)
bal = -1.0;
if (bal > 1.0)
bal = 1.0;
for (i = 0; i < frames_to_mix; i++) {
left_sample = wav_ptr[i * wp->wav_spec.channels];
right_sample = wav_ptr[i * wp->wav_spec.channels + 1];
/* apply panning */
if (bal < 0.0) {
/* pan left */
right_sample = (Sint16)(right_sample *
(1.0 + bal));
} else if (bal > 0.0) {
/* pan right */
left_sample = (Sint16)(left_sample *
(1.0 - bal));
}
out[2*i] += left_sample;
out[2*i + 1] += right_sample;
}
channels[c].position += frames_to_mix;
if (channels[c].position >= wp->frame_count) {
channels[c].active = SDL_FALSE;
}
}
}
/*
* Load a wav for playing later. The handle is written to handle_out.
* Returns -1 on error. The value of handle_out in an error condition
* is unusable.
*/
int
swgysnd_loadwav(const char *path, int *handle_out)
{
int i;
struct wav_data *wp;
SDL_AudioSpec desired, have;
SDL_AudioCVT cvt;
SDL_AudioFormat src_format, dst_format;
Uint8 *buffer;
assert(path != NULL);
assert(handle_out != NULL);
if (verbose > 1)
fprintf(stderr, "Loading %s... ", path);
wp = NULL;
for (i = 0; i < MAX_WAVS; ++i) {
if (!wavs[i].in_use) {
wp = &wavs[i];
break;
}
}
*handle_out = i;
if (wp == NULL) {
fprintf(stderr, "No available slots, try freeing a WAV\n");
return 1;
}
if (SDL_LoadWAV(path, &wp->wav_spec, &wp->wav_buffer, &wp->wav_length)
== NULL) {
fprintf(stderr, "Problem loading %s\n", path);
return 1;
}
if (verbose)
fprintf(stderr, "loaded wav: Spec{ .freq: %d, .format: %d, "
".channels %d, .samples %d }\n",
wp->wav_spec.freq, wp->wav_spec.format,
wp->wav_spec.channels, wp->wav_spec.samples);
if (verbose > 1)
fprintf(stderr, "DONE.");
/* convert everything to stereo */
src_format = wp->wav_spec.format;
dst_format = AUDIO_S16SYS;
/*dst_format = AUDIO_S16LSB;*/
if (SDL_BuildAudioCVT(&cvt, src_format, wp->wav_spec.channels,
wp->wav_spec.freq, dst_format, 2,
wp->wav_spec.freq) <= 0) {
fprintf(stderr, "SDL_BuildAudioCVT failed.\n");
SDL_FreeWAV(wp->wav_buffer);
return 1;
}
cvt.len = wp->wav_length;
if ((cvt.buf = malloc(cvt.len * cvt.len_mult)) == NULL) {
fprintf(stderr, "MALLOC FAILURE\n");
SDL_FreeWAV(wp->wav_buffer);
return 1;
}
buffer = wp->wav_buffer;
memcpy(cvt.buf, buffer, wp->wav_length);
if (SDL_ConvertAudio(&cvt) != 0) {
fprintf(stderr, "SDL_ConvertAudio failed\n");
SDL_FreeWAV(wp->wav_buffer);
free(cvt.buf);
return 1;
}
SDL_FreeWAV(wp->wav_buffer);
wp->wav_buffer = cvt.buf;
wp->wav_length = cvt.len_cvt;
wp->wav_spec.format = dst_format;
wp->wav_spec.channels = 2;
wp->frame_count = wp->wav_length / (wp->wav_spec.channels * sizeof(Sint16));
wp->in_use = 1;
if (audio_device == 0) {
bzero(&desired, sizeof(desired));
desired.freq = wp->wav_spec.freq;
desired.format = wp->wav_spec.format;
desired.channels = 2;
desired.samples = 4096;
desired.callback = audio_callback;
desired.userdata = NULL;
if ((audio_device = SDL_OpenAudioDevice(NULL, 0, &desired,
&have, 0)) == 0) {
fprintf(stderr, "SDL_OpenAudioDevice failed.");
return -1;
}
if (verbose) {
fprintf(stderr, "Setting device spec have: "
"Spec{ .freq: %d, .format: %d, "
".channels %d, .samples %d }\n",
have.freq, have.format, have.channels,
have.samples);
}
SDL_PauseAudioDevice(audio_device, 0);
}
return 0;
}
/*
* Free a previously loaded wav referenced by the provided handle.
*/
int swgysnd_freewav(int handle)
{
assert(handle >= 0);
assert(handle < MAX_WAVS);
if (wavs[handle].in_use) {
free(wavs[handle].wav_buffer);
wavs[handle].wav_buffer = NULL;
wavs[handle].in_use = SDL_FALSE;
} else if (verbose) {
fprintf(stderr, "Asked to free handle %d but it was not in use",
handle);
return -1;
}
return 0;
}
/*
* Play the wav associated with the indicated handle. The balance parameter
* indicates the left-right balance with zero being centered, -1 being
* full-left, and +1 being full-right. Values outside this range are truncated
* to -1/+1.
*/
int
swgysnd_playwav(int handle, double balance)
{
struct wav_data *wp;
int ch;
assert(handle >= 0);
assert(handle < MAX_WAVS);
assert(audio_device != 0);
wp = &wavs[handle];
if (!wp->in_use) {
fprintf(stderr, "handle %d not loaded.\n", handle);
return -1;
}
SDL_LockAudioDevice(audio_device);
for (ch = 0; ch < MAX_CHANNELS; ch++) {
if (!channels[ch].active)
break;
}
if (ch == MAX_CHANNELS) {
SDL_UnlockAudioDevice(audio_device);
fprintf(stderr, "No free channels to play.\n");
return -1;
}
channels[ch].active = SDL_TRUE;
channels[ch].wav_index = handle;
channels[ch].position = 0;
channels[ch].balance = balance;
SDL_UnlockAudioDevice(audio_device);
return 0;
}
/*
* Call to release all dynamic resources.
*/
int
swgysnd_shutdown()
{
int i;
/* Mark all channels inactive. */
if (audio_device != 0) {
SDL_LockAudioDevice(audio_device);
for (int c = 0; c < MAX_CHANNELS; c++) {
channels[c].active = SDL_FALSE;
}
SDL_UnlockAudioDevice(audio_device);
}
/* loop through and free all WAVs */
for (i = 0; i < MAX_WAVS; ++i) {
if (wavs[i].in_use) {
/* use "free" as all wavs are now sitting on malloc'd
* memory. */
free(wavs[i].wav_buffer);
wavs[i].in_use = 0;
}
}
if (audio_device != 0) {
SDL_CloseAudioDevice(audio_device);
audio_device = 0;
}
SDL_Quit();
return 0;
}