From 4fd3886810d93a7d3d2c2505e8b9ac38df2430d1 Mon Sep 17 00:00:00 2001 Message-Id: <4fd3886810d93a7d3d2c2505e8b9ac38df2430d1.1713898615.git.mdw@distorted.org.uk> From: Mark Wooding Date: Sun, 1 Mar 2009 12:01:34 +0000 Subject: [PATCH] ALSA support for uniform audio. ALSA and OSS support now shares the threading work, which is split into a collecter and a player thread communicating via a ring of buffers. Organization: Straylight/Edgeware From: Richard Kettlewell New uaudio_set_format() allows caller to declare what the sample format will be, giving us flexibility to do things other than 16/44100/2 (although not tested with other formats). Core audio uniform audio support now supports more sample formats, and checks that the channel count and rate are right. It doesn't (currently) attempt to ask for different channel counts or rates. --- clients/playrtp.c | 16 ++- lib/Makefile.am | 6 +- lib/uaudio-alsa.c | 147 ++++++++++++++++++++++ lib/uaudio-coreaudio.c | 51 +++++++- lib/uaudio-oss.c | 189 +++++++++++------------------ lib/uaudio-thread.c | 268 +++++++++++++++++++++++++++++++++++++++++ lib/uaudio.c | 45 +++++++ lib/uaudio.h | 30 ++++- 8 files changed, 616 insertions(+), 136 deletions(-) create mode 100644 lib/uaudio-alsa.c create mode 100644 lib/uaudio-thread.c diff --git a/clients/playrtp.c b/clients/playrtp.c index f1eaebc..eb879fe 100644 --- a/clients/playrtp.c +++ b/clients/playrtp.c @@ -399,7 +399,7 @@ static void *listen_thread(void attribute((unused)) *arg) { if(header.mpt & 0x80) p->flags |= IDLE; switch(header.mpt & 0x7F) { - case 10: + case 10: /* L16 */ p->nsamples = (n - sizeof header) / sizeof(uint16_t); break; /* TODO support other RFC3551 media types (when the speaker does) */ @@ -498,7 +498,7 @@ static void help(void) { exit(0); } -static size_t playrtp_callback(int16_t *buffer, +static size_t playrtp_callback(void *buffer, size_t max_samples, void attribute((unused)) *userdata) { size_t samples; @@ -543,12 +543,12 @@ static size_t playrtp_callback(int16_t *buffer, if(samples > max_samples) samples = max_samples; //info("infill by %zu", samples); - memset(buffer, 0, samples * sizeof *buffer); + memset(buffer, 0, samples * uaudio_sample_size); } /* Debug dump */ if(dump_buffer) { for(size_t i = 0; i < samples; ++i) { - dump_buffer[dump_index++] = buffer[i]; + dump_buffer[dump_index++] = ((int16_t *)buffer)[i]; dump_index %= dump_size; } } @@ -745,7 +745,10 @@ int main(int argc, char **argv) { /* Choose output device */ if(device) uaudio_set("device", device); - /* Set up output */ + /* Set up output. Currently we only support L16 so there's no harm setting + * the format before we know what it is! */ + uaudio_set_format(44100/*Hz*/, 2/*channels*/, + 16/*bits/channel*/, 1/*signed*/); backend->start(playrtp_callback, NULL); /* We receive and convert audio data in a background thread */ if((err = pthread_create(<id, 0, listen_thread, 0))) @@ -765,8 +768,9 @@ int main(int argc, char **argv) { /* Wait until the buffer empties out */ while(nsamples >= minbuffer || (nsamples > 0 - && contains(pheap_first(&packets), next_timestamp))) + && contains(pheap_first(&packets), next_timestamp))) { pthread_cond_wait(&cond, &lock); + } /* Stop playing for a bit until the buffer re-fills */ backend->deactivate(); active = 0; diff --git a/lib/Makefile.am b/lib/Makefile.am index 4ac8cbd..fe6a661 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -82,8 +82,10 @@ libdisorder_a_SOURCES=charset.c charset.h \ $(TRACKDB) trackdb.h trackdb-int.h \ trackname.c trackorder.c trackname.h \ tracksort.c \ - uaudio.c uaudio.h \ - uaudio-oss.c uaudio-coreaudio.c uaudio-rtp.c \ + uaudio.c uaudio-thread.c uaudio.h \ + uaudio-oss.c uaudio-alsa.c \ + uaudio-coreaudio.c \ + uaudio-rtp.c \ url.h url.c \ user.h user.c \ unicode.h unicode.c \ diff --git a/lib/uaudio-alsa.c b/lib/uaudio-alsa.c new file mode 100644 index 0000000..343d1aa --- /dev/null +++ b/lib/uaudio-alsa.c @@ -0,0 +1,147 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2009 Richard Kettlewell + * + * 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, see . + */ +/** @file lib/uaudio-alsa.c + * @brief Support for ALSA backend */ +#include "common.h" + +#if HAVE_ALSA_ASOUNDLIB_H + +#include + +#include "mem.h" +#include "log.h" +#include "uaudio.h" + +/** @brief The current PCM handle */ +static snd_pcm_t *alsa_pcm; + +static const char *const alsa_options[] = { + "device", + NULL +}; + +/** @brief Actually play sound via ALSA */ +static size_t alsa_play(void *buffer, size_t samples) { + int err; + /* ALSA wants 'frames', where frame = several concurrently played samples */ + const snd_pcm_uframes_t frames = samples / uaudio_channels; + + snd_pcm_sframes_t rc = snd_pcm_writei(alsa_pcm, buffer, frames); + if(rc < 0) { + switch(rc) { + case -EPIPE: + if((err = snd_pcm_prepare(alsa_pcm))) + fatal(0, "error calling snd_pcm_prepare: %d", err); + return 0; + case -EAGAIN: + return 0; + default: + fatal(0, "error calling snd_pcm_writei: %d", (int)rc); + } + } + return rc * uaudio_channels; +} + +/** @brief Open the ALSA sound device */ +static void alsa_open(void) { + const char *device = uaudio_get("device"); + int err; + + if(!device || !*device) + device = "default"; + if((err = snd_pcm_open(&alsa_pcm, + device, + SND_PCM_STREAM_PLAYBACK, + 0))) + fatal(0, "error from snd_pcm_open: %d", err); + snd_pcm_hw_params_t *hwparams; + snd_pcm_hw_params_alloca(&hwparams); + if((err = snd_pcm_hw_params_any(alsa_pcm, hwparams)) < 0) + fatal(0, "error from snd_pcm_hw_params_any: %d", err); + if((err = snd_pcm_hw_params_set_access(alsa_pcm, hwparams, + SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) + fatal(0, "error from snd_pcm_hw_params_set_access: %d", err); + int sample_format; + if(uaudio_bits == 16) + sample_format = uaudio_signed ? SND_PCM_FORMAT_S16 : SND_PCM_FORMAT_U16; + else + sample_format = uaudio_signed ? SND_PCM_FORMAT_S8 : SND_PCM_FORMAT_U8; + if((err = snd_pcm_hw_params_set_format(alsa_pcm, hwparams, + sample_format)) < 0) + fatal(0, "error from snd_pcm_hw_params_set_format (%d): %d", + sample_format, err); + unsigned rate = uaudio_rate; + if((err = snd_pcm_hw_params_set_rate_near(alsa_pcm, hwparams, &rate, 0)) < 0) + fatal(0, "error from snd_pcm_hw_params_set_rate_near (%d): %d", + rate, err); + if((err = snd_pcm_hw_params_set_channels(alsa_pcm, hwparams, + uaudio_channels)) < 0) + fatal(0, "error from snd_pcm_hw_params_set_channels (%d): %d", + uaudio_channels, err); + if((err = snd_pcm_hw_params(alsa_pcm, hwparams)) < 0) + fatal(0, "error calling snd_pcm_hw_params: %d", err); + +} + +static void alsa_activate(void) { + uaudio_thread_activate(); +} + +static void alsa_deactivate(void) { + uaudio_thread_deactivate(); +} + +static void alsa_start(uaudio_callback *callback, + void *userdata) { + if(uaudio_channels != 1 && uaudio_channels != 2) + fatal(0, "asked for %d channels but only support 1 or 2", + uaudio_channels); + if(uaudio_bits != 8 && uaudio_bits != 16) + fatal(0, "asked for %d bits/channel but only support 8 or 16", + uaudio_bits); + alsa_open(); + uaudio_thread_start(callback, userdata, alsa_play, + 32 / uaudio_sample_size, + 4096 / uaudio_sample_size); +} + +static void alsa_stop(void) { + uaudio_thread_stop(); + snd_pcm_close(alsa_pcm); + alsa_pcm = 0; +} + +const struct uaudio uaudio_alsa = { + .name = "alsa", + .options = alsa_options, + .start = alsa_start, + .stop = alsa_stop, + .activate = alsa_activate, + .deactivate = alsa_deactivate +}; + +#endif + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/lib/uaudio-coreaudio.c b/lib/uaudio-coreaudio.c index 129a66e..5c11ce4 100644 --- a/lib/uaudio-coreaudio.c +++ b/lib/uaudio-coreaudio.c @@ -68,18 +68,44 @@ static OSStatus coreaudio_adioproc while(nsamples > 0) { /* Integer-format input buffer */ - int16_t input[1024], *ptr = input; + unsigned char input[1024]; + const int maxsamples = sizeof input / uaudio_sample_size; /* How many samples we'll ask for */ - const int ask = nsamples > 1024 ? 1024 : (int)nsamples; + const int ask = nsamples > maxsamples ? maxsamples : (int)nsamples; /* How many we get */ int got; got = coreaudio_callback(input, ask, coreaudio_userdata); /* Convert the samples and store in the output buffer */ nsamples -= got; - while(got > 0) { - --got; - *samples++ = *ptr++ * (0.5 / 32767); + if(uaudio_signed) { + if(uaudio_bits == 16) { + const int16_t *ptr = input; + while(got > 0) { + --got; + *samples++ = *ptr++ * (0.5 / 32767); + } + } else { + const int8_t *ptr = input; + while(got > 0) { + --got; + *samples++ = *ptr++ * (0.5 / 127); + } + } + } else { + if(uaudio_bits == 16) { + const uint16_t *ptr = input; + while(got > 0) { + --got; + *samples++ = ((int)*ptr++ - 32768) * (0.5 / 32767); + } + } else { + const uint8_t *ptr = input; + while(got > 0) { + --got; + *samples++ = ((int)*ptr++ - 128) * (0.5 / 127); + } + } } } /* Move on to the next buffer */ @@ -96,10 +122,14 @@ static void coreaudio_start(uaudio_callback *callback, AudioStreamBasicDescription asbd; const char *device; + if(uaudio_bits != 8 && uaudio_bits != 16) + disorder_fatal("asked for %d bits/channel but only support 8 and 16", + uaudio_bits); coreaudio_callback = callback; coreaudio_userdata = userdata; device = uaudio_get("device"); coreaudio_adid = coreaudio_getdevice(device); + /* Get the device properties */ propertySize = sizeof asbd; status = AudioDeviceGetProperty(coreaudio_adid, 0, false, kAudioDevicePropertyStreamFormat, @@ -115,8 +145,19 @@ static void coreaudio_start(uaudio_callback *callback, D(("mChannelsPerFrame %08lx", asbd.mChannelsPerFrame)); D(("mBitsPerChannel %08lx", asbd.mBitsPerChannel)); D(("mReserved %08lx", asbd.mReserved)); + /* Check that everything adds up */ if(asbd.mFormatID != kAudioFormatLinearPCM) disorder_fatal(0, "audio device does not support kAudioFormatLinearPCM"); + if(asbd.mSampleRate != uaudio_rate + || asbd.mChannelsPerFrame != uaudio_channels) { + disorder_fatal(0, "want %dHz %d channels " + "but got %"PRIu32"Hz %"PRIu32" channels", + uaudio_rate, + uaudio_channels, + asbd.mSampleRate, + asbd.mChannelsPerFrame); + } + /* Add a collector callback */ status = AudioDeviceAddIOProc(coreaudio_adid, coreaudio_adioproc, 0); if(status) coreaudio_fatal(status, "AudioDeviceAddIOProc"); diff --git a/lib/uaudio-oss.c b/lib/uaudio-oss.c index 072188e..7bdfb35 100644 --- a/lib/uaudio-oss.c +++ b/lib/uaudio-oss.c @@ -25,157 +25,108 @@ # include #endif #include -#include #include #include #include -#include #include "mem.h" #include "log.h" -#include "syscalls.h" #include "uaudio.h" -#include "configuration.h" -static int oss_fd = -1; -static pthread_t oss_thread; -static uaudio_callback *oss_callback; -static int oss_started; -static int oss_activated; -static int oss_going; -static pthread_mutex_t oss_lock = PTHREAD_MUTEX_INITIALIZER; -static pthread_cond_t oss_cond = PTHREAD_COND_INITIALIZER; +#ifndef AFMT_U16_NE +# if BYTE_ORDER == BIG_ENDIAN +# define AFMT_U16_NE AFMT_U16_BE +# else +# define AFMT_U16_NE AFMT_U16_LE +# endif +#endif -/** @brief Buffer size in bytes */ -static int oss_bufsize; +static int oss_fd = -1; static const char *const oss_options[] = { "device", NULL }; -static void *oss_thread_fn(void *userdata) { - int16_t *buffer; - pthread_mutex_lock(&oss_lock); - buffer = xmalloc(oss_bufsize); - while(oss_started) { - while(oss_started && !oss_activated) - pthread_cond_wait(&oss_cond, &oss_lock); - if(!oss_started) - break; - /* We are definitely active now */ - oss_going = 1; - pthread_cond_signal(&oss_cond); - while(oss_activated) { - const int nsamples = oss_callback(buffer, - oss_bufsize / sizeof(int16_t), - userdata); - const int nbytes = nsamples * sizeof(int16_t); - int written = 0; - - while(written < nsamples) { - const int rc = write(oss_fd, buffer + written, nbytes - written); - if(rc < 0) - fatal(errno, "error playing audio"); - written += rc; - } - } - oss_going = 0; - pthread_cond_signal(&oss_cond); - } - pthread_mutex_unlock(&oss_lock); - return NULL; +/** @brief Actually play sound via OSS */ +static size_t oss_play(void *buffer, size_t samples) { + const size_t bytes = samples * uaudio_sample_size; + int rc = write(oss_fd, buffer, bytes); + if(rc < 0) + fatal(errno, "error writing to sound device"); + return rc / uaudio_sample_size; } -static void oss_activate(void) { - pthread_mutex_lock(&oss_lock); - if(!oss_activated) { - const char *device = uaudio_get("device"); +/** @brief Open the OSS sound device */ +static void oss_open(void) { + const char *device = uaudio_get("device"); #if EMPEG_HOST - if(!device || !*device || !strcmp(device, "default")) { - device "/dev/audio"; + if(!device || !*device || !strcmp(device, "default")) + device "/dev/audio"; #else - if(!device || !*device || !strcmp(device, "default")) { - if(access("/dev/dsp", W_OK) == 0) - device = "/dev/dsp"; - else - device = "/dev/audio"; - } -#endif - if((oss_fd = open(device, O_WRONLY, 0)) < 0) - fatal(errno, "error opening %s", device); -#if EMPEG_HOST - /* empeg audio driver only knows /dev/audio, only supports the equivalent - * of AFMT_S16_NE, has a fixed buffer size, and does not support the - * SNDCTL_ ioctls. */ - oss_bufsize = 4608; -#else - int stereo = (config->sample_format.channels == 2), format, rate; - if(ioctl(oss_fd, SNDCTL_DSP_STEREO, &stereo) < 0) - fatal(errno, "error calling ioctl SNDCTL_DSP_STEREO %d", stereo); - /* TODO we need to think about where we decide this */ - if(config->sample_format.bits == 8) - format = AFMT_U8; - else if(config->sample_format.bits == 16) - format = (config->sample_format.endian == ENDIAN_LITTLE - ? AFMT_S16_LE : AFMT_S16_BE); + if(!device || !*device || !strcmp(device, "default")) { + if(access("/dev/dsp", W_OK) == 0) + device = "/dev/dsp"; else - fatal(0, "unsupported sample_format for oss backend"); - if(ioctl(oss_fd, SNDCTL_DSP_SETFMT, &format) < 0) - fatal(errno, "error calling ioctl SNDCTL_DSP_SETFMT %#x", format); - rate = config->sample_format.rate; - if(ioctl(oss_fd, SNDCTL_DSP_SPEED, &rate) < 0) - fatal(errno, "error calling ioctl SNDCTL_DSP_SPEED %d", rate); - if((unsigned)rate != config->sample_format.rate) - error(0, "asked for %luHz, got %dHz", - (unsigned long)config->sample_format.rate, rate); - if(ioctl(oss_fd, SNDCTL_DSP_GETBLKSIZE, &oss_bufsize) < 0) { - error(errno, "ioctl SNDCTL_DSP_GETBLKSIZE"); - oss_bufsize = 2048; /* guess */ - } -#endif - oss_activated = 1; - pthread_cond_signal(&oss_cond); - while(!oss_going) - pthread_cond_wait(&oss_cond, &oss_lock); + device = "/dev/audio"; } - pthread_mutex_unlock(&oss_lock); +#endif + if((oss_fd = open(device, O_WRONLY, 0)) < 0) + fatal(errno, "error opening %s", device); +#if !EMPEG_HOST + int stereo = (uaudio_channels == 2), format; + if(ioctl(oss_fd, SNDCTL_DSP_STEREO, &stereo) < 0) + fatal(errno, "error calling ioctl SNDCTL_DSP_STEREO %d", stereo); + if(uaudio_bits == 16) + format = uaudio_signed ? AFMT_S16_NE : AFMT_U16_NE; + else + format = uaudio_signed ? AFMT_S8 : AFMT_U8; + if(ioctl(oss_fd, SNDCTL_DSP_SETFMT, &format) < 0) + fatal(errno, "error calling ioctl SNDCTL_DSP_SETFMT %#x", format); + int rate = uaudio_rate; + if(ioctl(oss_fd, SNDCTL_DSP_SPEED, &rate) < 0) + fatal(errno, "error calling ioctl SNDCTL_DSP_SPEED %d", rate); + if(rate != uaudio_rate) + error(0, "asked for %dHz, got %dHz", uaudio_rate, rate); +#endif +} + +static void oss_activate(void) { + oss_open(); + uaudio_thread_activate(); } static void oss_deactivate(void) { - pthread_mutex_lock(&oss_lock); - if(oss_activated) { - oss_activated = 0; - pthread_cond_signal(&oss_cond); - while(oss_going) - pthread_cond_wait(&oss_cond, &oss_lock); - close(oss_fd); - oss_fd = -1; - } - pthread_mutex_unlock(&oss_lock); + uaudio_thread_deactivate(); + close(oss_fd); + oss_fd = -1; } static void oss_start(uaudio_callback *callback, void *userdata) { - int e; - oss_callback = callback; - if((e = pthread_create(&oss_thread, - NULL, - oss_thread_fn, - userdata))) - fatal(e, "pthread_create"); + if(uaudio_channels != 1 && uaudio_channels != 2) + fatal(0, "asked for %d channels but only support 1 or 2", + uaudio_channels); + if(uaudio_bits != 8 && uaudio_bits != 16) + fatal(0, "asked for %d bits/channel but only support 8 or 16", + uaudio_bits); +#if EMPEG_HOST + /* Very specific buffer size requirements here apparently */ + uaudio_thread_start(callback, userdata, oss_play, + 4608 / uaudio_sample_size, + 4608 / uaudio_sample_size); +#else + /* We could SNDCTL_DSP_GETBLKSIZE but only when the device is already open, + * which is kind of inconvenient. We go with 1-4Kbyte for now. */ + uaudio_thread_start(callback, userdata, oss_play, + 32 / uaudio_sample_size, + 4096 / uaudio_sample_size); +#endif } static void oss_stop(void) { - void *result; - - oss_deactivate(); - pthread_mutex_lock(&oss_lock); - oss_started = 0; - pthread_cond_signal(&oss_cond); - pthread_mutex_unlock(&oss_lock); - pthread_join(oss_thread, &result); + uaudio_thread_stop(); } const struct uaudio uaudio_oss = { diff --git a/lib/uaudio-thread.c b/lib/uaudio-thread.c new file mode 100644 index 0000000..3253a1d --- /dev/null +++ b/lib/uaudio-thread.c @@ -0,0 +1,268 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2009 Richard Kettlewell + * + * 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, see . + */ +/** @file lib/uaudio-thread.c + * @brief Background thread for audio processing */ +#include "common.h" + +#include +#include + +#include "uaudio.h" +#include "log.h" +#include "mem.h" + +/** @brief Number of buffers + * + * Must be at least 2 and should normally be at least 3. We maintain multiple + * buffers so that we can read new data into one while the previous is being + * played. + */ +#define UAUDIO_THREAD_BUFFERS 4 + +/** @brief Buffer data structure */ +struct uaudio_buffer { + /** @brief Pointer to sample data */ + void *samples; + + /** @brief Count of samples */ + size_t nsamples; +}; + +/** @brief Input buffers + * + * This is actually a ring buffer, managed by @ref uaudio_collect_buffer and + * @ref uaudio_play_buffer. + * + * Initially both pointers are 0. Whenever the pointers are equal, we + * interpreted this as meaning that there is no data stored at all. A + * consequence of this is that maximal occupancy is when the collect point is + * just before the play point, so at least one buffer is always empty (hence it + * being good for @ref UAUDIO_THREAD_BUFFERS to be at least 3). + */ +static struct uaudio_buffer uaudio_buffers[UAUDIO_THREAD_BUFFERS]; + +/** @brief Buffer to read into */ +static unsigned uaudio_collect_buffer; + +/** @brief Buffer to play from */ +static unsigned uaudio_play_buffer; + +/** @brief Collection thread ID */ +static pthread_t uaudio_collect_thread; + +/** @brief Playing thread ID */ +static pthread_t uaudio_play_thread; + +static uaudio_callback *uaudio_thread_collect_callback; +static uaudio_playcallback *uaudio_thread_play_callback; +static void *uaudio_thread_userdata; +static int uaudio_thread_started; +static int uaudio_thread_activated; +static int uaudio_thread_collecting; +static pthread_mutex_t uaudio_thread_lock = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t uaudio_thread_cond = PTHREAD_COND_INITIALIZER; + +/** @brief Minimum number of samples per chunk */ +static size_t uaudio_thread_min; + +/** @brief Maximum number of samples per chunk */ +static size_t uaudio_thread_max; + +/** @brief Return number of buffers currently in use */ +static int uaudio_buffers_used(void) { + return (uaudio_collect_buffer - uaudio_play_buffer) % UAUDIO_THREAD_BUFFERS; +} + +/** @brief Background thread for audio collection + * + * Collects data while activated and communicates its status via @ref + * uaudio_thread_collecting. + */ +static void *uaudio_collect_thread_fn(void attribute((unused)) *arg) { + pthread_mutex_lock(&uaudio_thread_lock); + while(uaudio_thread_started) { + /* Wait until we're activatd */ + if(!uaudio_thread_activated) { + pthread_cond_wait(&uaudio_thread_cond, &uaudio_thread_lock); + continue; + } + /* We are definitely active now */ + uaudio_thread_collecting = 1; + pthread_cond_broadcast(&uaudio_thread_cond); + while(uaudio_thread_activated) { + if(uaudio_buffers_used() < UAUDIO_THREAD_BUFFERS - 1) { + /* At least one buffer is available. We release the lock while + * collecting data so that other already-filled buffers can be played + * without delay. */ + struct uaudio_buffer *const b = &uaudio_buffers[uaudio_collect_buffer]; + pthread_mutex_unlock(&uaudio_thread_lock); + //fprintf(stderr, "C%d.", uaudio_collect_buffer); + + /* Keep on trying until we get the minimum required amount of data */ + b->nsamples = 0; + while(b->nsamples < uaudio_thread_min) { + b->nsamples += uaudio_thread_collect_callback + ((char *)b->samples + + b->nsamples * uaudio_sample_size, + uaudio_thread_max - b->nsamples, + uaudio_thread_userdata); + } + pthread_mutex_lock(&uaudio_thread_lock); + /* Advance to next buffer */ + uaudio_collect_buffer = (1 + uaudio_collect_buffer) % UAUDIO_THREAD_BUFFERS; + /* Awaken player */ + pthread_cond_broadcast(&uaudio_thread_cond); + } else + /* No space, wait for player */ + pthread_cond_wait(&uaudio_thread_cond, &uaudio_thread_lock); + } + uaudio_thread_collecting = 0; + pthread_cond_broadcast(&uaudio_thread_cond); + } + pthread_mutex_unlock(&uaudio_thread_lock); + return NULL; +} + +/** @brief Background thread for audio playing + * + * This thread plays data as long as there is something to play. So the + * buffers will drain to empty before deactivation completes. + */ +static void *uaudio_play_thread_fn(void attribute((unused)) *arg) { + int resync = 1; + + pthread_mutex_lock(&uaudio_thread_lock); + while(uaudio_thread_started) { + const int used = uaudio_buffers_used(); + int go; + + if(resync) + go = (used == UAUDIO_THREAD_BUFFERS - 1); + else + go = (used > 0); + if(go) { + /* At least one buffer is filled. We release the lock while playing so + * that more collection can go on. */ + struct uaudio_buffer *const b = &uaudio_buffers[uaudio_play_buffer]; + pthread_mutex_unlock(&uaudio_thread_lock); + //fprintf(stderr, "P%d.", uaudio_play_buffer); + size_t played = 0; + while(played < b->nsamples) + played += uaudio_thread_play_callback((char *)b->samples + + played * uaudio_sample_size, + b->nsamples - played); + pthread_mutex_lock(&uaudio_thread_lock); + /* Move to next buffer */ + uaudio_play_buffer = (1 + uaudio_play_buffer) % UAUDIO_THREAD_BUFFERS; + /* Awaken collector */ + pthread_cond_broadcast(&uaudio_thread_cond); + resync = 0; + } else { + /* Insufficient data to play, wait for collector */ + pthread_cond_wait(&uaudio_thread_cond, &uaudio_thread_lock); + /* (Still) re-synchronizing */ + resync = 1; + } + } + pthread_mutex_unlock(&uaudio_thread_lock); + return NULL; +} + +/** @brief Create background threads for audio processing + * @param callback Callback to collect audio data + * @param userdata Passed to @p callback + * @param playcallback Callback to play audio data + * @param min Minimum number of samples to play in a chunk + * @param max Maximum number of samples to play in a chunk + * + * @p callback will be called multiple times in quick succession if necessary + * to gather at least @p min samples. Equally @p playcallback may be called + * repeatedly in quick succession to play however much was received in a single + * chunk. + */ +void uaudio_thread_start(uaudio_callback *callback, + void *userdata, + uaudio_playcallback *playcallback, + size_t min, + size_t max) { + int e; + uaudio_thread_collect_callback = callback; + uaudio_thread_userdata = userdata; + uaudio_thread_play_callback = playcallback; + uaudio_thread_min = min; + uaudio_thread_max = max; + uaudio_thread_started = 1; + for(int n = 0; n < UAUDIO_THREAD_BUFFERS; ++n) + uaudio_buffers[n].samples = xcalloc(uaudio_thread_max, uaudio_sample_size); + uaudio_collect_buffer = uaudio_play_buffer = 0; + if((e = pthread_create(&uaudio_collect_thread, + NULL, + uaudio_collect_thread_fn, + NULL))) + fatal(e, "pthread_create"); + if((e = pthread_create(&uaudio_play_thread, + NULL, + uaudio_play_thread_fn, + NULL))) + fatal(e, "pthread_create"); +} + +/** @brief Shut down background threads for audio processing */ +void uaudio_thread_stop(void) { + void *result; + + pthread_mutex_lock(&uaudio_thread_lock); + uaudio_thread_activated = 0; + uaudio_thread_started = 0; + pthread_cond_broadcast(&uaudio_thread_cond); + pthread_mutex_unlock(&uaudio_thread_lock); + pthread_join(uaudio_collect_thread, &result); + pthread_join(uaudio_play_thread, &result); + for(int n = 0; n < UAUDIO_THREAD_BUFFERS; ++n) + xfree(uaudio_buffers[n].samples); +} + +/** @brief Activate audio output */ +void uaudio_thread_activate(void) { + pthread_mutex_lock(&uaudio_thread_lock); + uaudio_thread_activated = 1; + pthread_cond_broadcast(&uaudio_thread_cond); + while(!uaudio_thread_collecting) + pthread_cond_wait(&uaudio_thread_cond, &uaudio_thread_lock); + pthread_mutex_unlock(&uaudio_thread_lock); +} + +/** @brief Deactivate audio output */ +void uaudio_thread_deactivate(void) { + pthread_mutex_lock(&uaudio_thread_lock); + uaudio_thread_activated = 0; + pthread_cond_broadcast(&uaudio_thread_cond); + + while(uaudio_thread_collecting || uaudio_buffers_used()) + pthread_cond_wait(&uaudio_thread_cond, &uaudio_thread_lock); + pthread_mutex_unlock(&uaudio_thread_lock); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/lib/uaudio.c b/lib/uaudio.c index aa00f66..f18af09 100644 --- a/lib/uaudio.c +++ b/lib/uaudio.c @@ -28,6 +28,26 @@ /** @brief Options for chosen uaudio API */ static hash *uaudio_options; +/** @brief Sample rate (Hz) */ +int uaudio_rate; + +/** @brief Bits per channel */ +int uaudio_bits; + +/** @brief Number of channels */ +int uaudio_channels; + +/** @brief Whether samples are signed or unsigned */ +int uaudio_signed; + +/** @brief Sample size in bytes + * + * NB one sample is a single point sample; up to @c uaudio_channels samples may + * play at the same time through different speakers. Thus this value is + * independent of @ref uaudio_channels. + */ +size_t uaudio_sample_size; + /** @brief Set a uaudio option */ void uaudio_set(const char *name, const char *value) { if(!uaudio_options) @@ -44,6 +64,31 @@ const char *uaudio_get(const char *name) { return value ? xstrdup(value) : NULL; } +/** @brief Set sample format + * @param rate Sample rate in KHz + * @param channels Number of channels (i.e. 2 for stereo) + * @param bits Number of bits per channel (typically 8 or 16) + * @param signed_ True for signed samples, false for unsigned + * + * Sets @ref uaudio_rate, @ref uaudio_channels, @ref uaudio_bits, @ref + * uaudio_signed and @ref uaudio_sample_size. + * + * Currently there is no way to specify non-native endian formats even if the + * underlying API can conveniently handle them. Actually this would be quite + * convenient for playrtp, so it might be added at some point. + * + * Not all APIs can support all sample formats. Generally the @c start + * function will do some error checking but some may be deferred to the point + * the device is opened (which might be @c activate). + */ +void uaudio_set_format(int rate, int channels, int bits, int signed_) { + uaudio_rate = rate; + uaudio_channels = channels; + uaudio_bits = bits; + uaudio_signed = signed_; + uaudio_sample_size = bits / CHAR_BIT; +} + /** @brief List of known APIs * * Terminated by a null pointer. diff --git a/lib/uaudio.h b/lib/uaudio.h index 6b0dea1..ac88b3f 100644 --- a/lib/uaudio.h +++ b/lib/uaudio.h @@ -23,18 +23,31 @@ #ifndef UAUDIO_H #define UAUDIO_H +extern int uaudio_rate; +extern int uaudio_bits; +extern int uaudio_channels; +extern int uaudio_signed; +extern size_t uaudio_sample_size; + /** @brief Callback to get audio data * @param buffer Where to put audio data * @param max_samples How many samples to supply * @param userdata As passed to uaudio_open() * @return Number of samples filled - * - * One sample is a single 16-bit signed value. */ -typedef size_t uaudio_callback(int16_t *buffer, +typedef size_t uaudio_callback(void *buffer, size_t max_samples, void *userdata); +/** @brief Callback to play audio data + * @param buffer Pointer to audio buffer + * @param samples Number of samples to play + * @return Number of samples played + * + * Used with uaudio_thread_start() etc. + */ +typedef size_t uaudio_playcallback(void *buffer, size_t samples); + /** @brief Audio API definition */ struct uaudio { /** @brief Name of this API */ @@ -78,9 +91,18 @@ struct uaudio { void (*deactivate)(void); }; - + +void uaudio_set_format(int rate, int channels, int samplesize, int signed_); void uaudio_set(const char *name, const char *value); const char *uaudio_get(const char *name); +void uaudio_thread_start(uaudio_callback *callback, + void *userdata, + uaudio_playcallback *playcallback, + size_t min, + size_t max); +void uaudio_thread_stop(void); +void uaudio_thread_activate(void); +void uaudio_thread_deactivate(void); #if HAVE_COREAUDIO_AUDIOHARDWARE_H extern const struct uaudio uaudio_coreaudio; -- [mdw]