X-Git-Url: https://git.distorted.org.uk/~mdw/jog/blobdiff_plain/b1ffb19d137241e62b4eea0e6db834bd624bdda2..e9060e7e4c0de71ef6d4b7d7b05c3167790073e3:/ausys-sdl.c diff --git a/ausys-sdl.c b/ausys-sdl.c new file mode 100644 index 0000000..c54d378 --- /dev/null +++ b/ausys-sdl.c @@ -0,0 +1,472 @@ +/* -*-c-*- + * + * $Id: ausys-sdl.c,v 1.1 2002/02/02 19:16:28 mdw Exp $ + * + * Unix-specific (SDL) audio handling + * + * (c) 2002 Mark Wooding + */ + +/*----- Licensing notice --------------------------------------------------* + * + * This file is part of Jog: Programming for a jogging machine. + * + * Jog 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 2 of the License, or + * (at your option) any later version. + * + * Jog 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 Jog; if not, write to the Free Software Foundation, + * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/*----- Revision history --------------------------------------------------* + * + * $Log: ausys-sdl.c,v $ + * Revision 1.1 2002/02/02 19:16:28 mdw + * New audio subsystem. + * + */ + +/*----- Header files ------------------------------------------------------*/ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include "au.h" +#include "ausys.h" +#include "err.h" +#include "jog.h" + +/*----- Data structures ---------------------------------------------------*/ + +/* --- Queue of samples to play --- */ + +typedef struct qnode { + struct qnode *next; /* Next item in the queue */ + au_data *a; /* Pointer to sample data */ +} qnode; + +/*----- Global variables --------------------------------------------------*/ + +const char *const ausys_suffix = "wav"; + +/*----- Static data -------------------------------------------------------*/ + +static SDL_AudioSpec auspec; /* Driver's selected parameters */ +static qnode *qhead = 0, *qtail = 0; /* Queue of samples to play */ +static qnode *qfree = 0; /* Queue of samples to free */ + +static pthread_t tid_free; /* The sample-freeing thread */ +static pthread_mutex_t mx_free; /* Mutex for @qfree@ */ +static pthread_cond_t cv_free; /* More sample data to free */ + +static pthread_mutex_t mx_sub; /* Protects mLib @sub@ functions */ + +/*----- Thread structure --------------------------------------------------* + * + * SDL makes a buffer-stuffing thread that @fill@ is run from. This function + * reads samples to play from the queue at @qhead@. Hence @qhead@ must be + * locked when it's modified, using @SDL_LockAudio@. + * + * In order to keep latency down when new sample data is wanted, we don't do + * potentially tedious things like freeing sample data blocks in the @fill@ + * thread. Instead, there's a queue of sample blocks which need freeing, and + * a separate thread @dofree@ which processes the queue. The queue, @qfree@ + * is locked by @mx_free@. When new nodes are added to @qfree@, the + * condition @cv_free@ is signalled. + * + * Finally, the mLib `sub' module isn't thread-safe, so it's locked + * separately by @mx_sub@. + * + * There's an ordering on mutexes. If you want more than one mutex at a + * time, you must lock the greatest first. The ordering is: + * + * SDL_Audio > free > sub + */ + +/*----- Main code ---------------------------------------------------------*/ + +/* --- @fill@ --- * + * + * Arguments: @void *u@ = user data (ignored) + * @octet *q@ = pointer to buffer to fill in + * @int qsz@ = size of the buffer + * + * Returns: --- + * + * Use: Fills an audio buffer with precomputed stuff. + */ + +static void fill(void *u, octet *q, int qsz) +{ + static const octet *p = 0; /* Current place in current sample */ + static size_t sz = 0; /* How far to go in current sample */ + qnode *qf = 0, **qft = &qf; /* Samples we've finished with */ + + /* --- If @p@ is null we need to get a new sample --- */ + + if (!p && qhead) { + T( trace(T_AUSYS, "ausys: begin playing `%s'", SYM_NAME(qhead->a->s)); ) + p = qhead->a->p; + sz = qhead->a->sz; + } + + /* --- Main queue-filling loop --- */ + + while (qsz) { + size_t n; + + /* --- If the queue is empty, play silence --- */ + + if (!p) { + memset(q, auspec.silence, qsz); + break; + } + + /* --- Fill the buffer with my sample --- */ +exit(0); + n = qsz; + if (n > sz) + n = sz; + memcpy(q, p, n); + p += n; q += n; + sz -= n; qsz -= n; + + /* --- If the sample is used up, throw it away --- */ + + if (!sz) { + qnode *qq = qhead; + qhead = qq->next; + if (!qhead) + qtail = 0; + T( trace(T_AUSYS, "ausys: put `%s' on free list", + SYM_NAME(qq->a->s)); ) + *qft = qq; + qft = &qq->next; + if (qhead) { + T( trace(T_AUSYS, "ausys: begin playing `%s'", + SYM_NAME(qhead->a->s)); ) + p = qhead->a->p; + sz = qhead->a->sz; + } else { + T( trace(T_AUSYS, "ausys: sample queue is empty"); ) + p = 0; + sz = 0; + } + } + } + + /* --- Finally dump freed samples, all in one go --- */ + + if (qf) { + pthread_mutex_lock(&mx_free); + *qft = qfree; + qfree = qf; + pthread_mutex_unlock(&mx_free); + pthread_cond_signal(&cv_free); + } +} + +/* --- @dofree@ --- * + * + * Arguments: @void *u@ = unused pointer + * + * Returns: An unused pointer. + * + * Use: Frees unwanted sample queue nodes. + */ + +static void *dofree(void *u) +{ + qnode *q, *qq; + + pthread_mutex_lock(&mx_free); + for (;;) { + T( trace(T_AUSYS, "ausys: dofree sleeping"); ) + pthread_cond_wait(&cv_free, &mx_free); + T( trace(T_AUSYS, "ausys: dofree woken: work to do"); ) + + while (qfree) { + q = qfree; + qfree = 0; + pthread_mutex_lock(&mx_sub); + while (q) { + qq = q->next; + T( trace(T_AUSYS, "ausys: freeing `%s'", SYM_NAME(q->a->s)); ) + au_free_unlocked(q->a); + DESTROY(q); + q = qq; + } + pthread_mutex_unlock(&mx_sub); + } + } +} + +/* --- @ausys_init@ --- * + * + * Arguments: --- + * + * Returns: --- + * + * Use: Does any initialization required by the system-specific audio + * handler. + */ + +void ausys_init(void) +{ + SDL_AudioSpec want; + pthread_attr_t ta; + int e; + + /* --- Crank up the sample-freeing thread --- */ + + if ((e = pthread_mutex_init(&mx_free, 0)) != 0 || + (e = pthread_mutex_init(&mx_sub, 0)) != 0 || + (e = pthread_cond_init(&cv_free, 0)) != 0 || + (e = pthread_attr_init(&ta)) != 0 || + (e = pthread_attr_setdetachstate(&ta, PTHREAD_CREATE_DETACHED)) != 0 || + (e = pthread_create(&tid_free, &ta, dofree, 0)) != 0) { + err_report(ERR_AUDIO, ERRAU_INIT, e, + "couldn't create audio threads: %s", strerror(errno)); + exit(EXIT_FAILURE); + } + pthread_attr_destroy(&ta); + + /* --- Crank up the SDL audio subsystem --- */ + + if (SDL_Init(SDL_INIT_AUDIO | SDL_INIT_NOPARACHUTE)) { + err_report(ERR_AUDIO, ERRAU_INIT, 0, + "couldn't initialize SDL audio: %s", SDL_GetError()); + exit(EXIT_FAILURE); + } + + /* --- Open the audio device --- */ + + want.freq = 8000; + want.format = AUDIO_U8; + want.channels = 1; + want.samples = 4096; + want.callback = fill; + want.userdata = 0; + + if (SDL_OpenAudio(&want, &auspec)) { + err_report(ERR_AUDIO, ERRAU_INIT, 0, + "couldn't open audio device: %s", SDL_GetError()); + exit(EXIT_FAILURE); + } + + /* --- Prepare whatever needs to be done --- */ + + SDL_PauseAudio(0); + + T( trace(T_AUSYS, "ausys: initalized ok"); ) +} + +/* --- @ausys_shutdown@ --- * + * + * Arguments: --- + * + * Returns: --- + * + * Use: Does any tidying up required. + */ + +void ausys_shutdown(void) +{ + /* --- Busy-wait until sound buffer empties --- */ + + while (qhead) { + struct timeval tv = { 0, 100000 }; + select(0, 0, 0, 0, &tv); + } + + /* --- Shut stuff down --- */ + + pthread_cancel(tid_free); + SDL_CloseAudio(); + SDL_Quit(); + + T( trace(T_AUSYS, "ausys: shut down ok"); ) +} + +/* --- @ausys_lock@, @ausys_unlock@ --- * + * + * Arguments: --- + * + * Returns: --- + * + * Use: Locks or unlocks the audio subsystem. This protects the + * audio queue from becoming corrupted during all the tedious + * asynchronous stuff. + */ + +void ausys_lock(void) +{ + pthread_mutex_lock(&mx_free); + pthread_mutex_lock(&mx_sub); + T( trace(T_AUSYS, "ausys: acquired lock"); ) +} + +void ausys_unlock(void) +{ + pthread_mutex_unlock(&mx_sub); + pthread_mutex_unlock(&mx_free); + T( trace(T_AUSYS, "ausys: released lock"); ) +} + +/* --- @ausys_decode@ --- * + * + * Arguments: @au_sample *s@ = pointer to sample block + * @const void *p@ = pointer to sample file contents + * @size_t sz@ = size of sample file contents + * + * Returns: Pointer to a sample data structure. + * + * Use: Decodes a WAV file into something the system-specific layer + * actually wants to deal with. + */ + +au_data *ausys_decode(au_sample *s, const void *p, size_t sz) +{ + au_data *a; + SDL_AudioCVT cvt; + SDL_AudioSpec spec; + SDL_RWops *rw; + Uint8 *buf; + Uint32 len; + void *q; + + /* --- Firstly, extract the audio data from the WAV file --- */ + + rw = SDL_RWFromMem((void *)p, sz); + if (!SDL_LoadWAV_RW(rw, 1, &spec, &buf, &len)) { + err_report(ERR_AUDIO, ERRAU_OPEN, 0, + "can't read WAV file for `%s': %s", + SYM_NAME(s), SDL_GetError()); + goto fail_0; + } + + /* --- Now convert that to the driver's expected format --- */ + + if (SDL_BuildAudioCVT(&cvt, spec.format, spec.channels, spec.freq, + auspec.format, auspec.channels, auspec.freq) == -1) { + err_report(ERR_AUDIO, ERRAU_OPEN, 0, + "can't build conversion structure for `%s': %s", + SYM_NAME(s), SDL_GetError()); + goto fail_1; + } + + cvt.buf = xmalloc(len * cvt.len_mult); + cvt.len = len; + memcpy(cvt.buf, buf, len); + SDL_FreeWAV(buf); + + if (SDL_ConvertAudio(&cvt)) { + err_report(ERR_AUDIO, ERRAU_OPEN, 0, + "can't convert `%s': %s", SYM_NAME(s), SDL_GetError()); + goto fail_2; + } + + /* --- Now fiddle with buffers --- */ + + sz = len * cvt.len_ratio; + if (cvt.len_ratio >= 1) + q = cvt.buf; + else { + q = xmalloc(sz); + memcpy(q, cvt.buf, sz); + xfree(cvt.buf); + } + + /* --- Construct a new node --- */ + + pthread_mutex_lock(&mx_sub); + a = CREATE(au_data); + pthread_mutex_unlock(&mx_sub); + a->p = q; + a->sz = sz; + + T( trace(T_AUSYS, "ausys: decoded `%s' ok", SYM_NAME(s)); ) + return (a); + + /* --- Tidy up after errors --- */ + +fail_2: + xfree(cvt.buf); + goto fail_0; +fail_1: + SDL_FreeWAV(buf); +fail_0: + return (0); +} + +/* --- @ausys_queue@ --- * + * + * Arguments: @au_data *a@ = an audio thingy to play + * + * Returns: --- + * + * Use: Queues an audio sample to be played. The sample should be + * freed (with @au_free@) when it's no longer wanted. + */ + +void ausys_queue(au_data *a) +{ + qnode *q; + + pthread_mutex_lock(&mx_sub); + q = CREATE(qnode); + pthread_mutex_unlock(&mx_sub); + q->next = 0; + q->a = a; + SDL_LockAudio(); + if (qtail) + qtail->next = q; + else + qhead = q; + qtail = q; + SDL_UnlockAudio(); + T( trace(T_AUSYS, "ausys: queuing `%s'", SYM_NAME(a->s)); ) +} + +/* --- @ausys_free@ --- * + * + * Arguments: @au_data *a@ = an audio thingy to free + * + * Returns: --- + * + * Use: Frees a decoded audio sample. + */ + +void ausys_free(au_data *a) +{ + xfree(a->p); + DESTROY(a); + T( trace(T_AUSYS, "ausys: freeing data for `%s' ok", SYM_NAME(a->s)); ) +} + +/*----- That's all, folks -------------------------------------------------*/