swgysnd.c

/**
 * 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;
}