From: Richard Kettlewell Date: Sat, 28 Feb 2009 17:50:53 +0000 (+0000) Subject: Uniform audio backend for Core Audio. There is an untested OSS one X-Git-Tag: 5.0~168^2~19 X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/disorder/commitdiff_plain/7a2c706849ecf6cee19d9e502f8491ffac3322e0 Uniform audio backend for Core Audio. There is an untested OSS one and a placeholder RTP one lurking around too. disorder-playrtp now uses the uniform audio interface (and is considerably simplified as a result). --- diff --git a/clients/Makefile.am b/clients/Makefile.am index 91d4448..5456bd1 100644 --- a/clients/Makefile.am +++ b/clients/Makefile.am @@ -33,8 +33,7 @@ disorderfm_SOURCES=disorderfm.c \ disorderfm_LDADD=$(LIBOBJS) ../lib/libdisorder.a $(LIBGC) $(LIBICONV) disorderfm_DEPENDENCIES=$(LIBOBJS) ../lib/libdisorder.a -disorder_playrtp_SOURCES=playrtp.c playrtp.h playrtp-mem.c \ - playrtp-alsa.c playrtp-coreaudio.c playrtp-oss.c +disorder_playrtp_SOURCES=playrtp.c playrtp.h playrtp-mem.c disorder_playrtp_LDADD=$(LIBOBJS) ../lib/libdisorder.a \ $(LIBASOUND) $(LIBPCRE) $(LIBICONV) $(LIBGCRYPT) $(COREAUDIO) \ $(LIBDB) $(LIBPTHREAD) diff --git a/clients/playrtp-alsa.c b/clients/playrtp-alsa.c deleted file mode 100644 index a407b2f..0000000 --- a/clients/playrtp-alsa.c +++ /dev/null @@ -1,110 +0,0 @@ -/* - * This file is part of DisOrder. - * Copyright (C) 2008 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 clients/playrtp-alsa.c - * @brief RTP player - ALSA support - * - * This has been rewritten to use the @ref alsabg.h interface and is therefore - * now closely modelled on @ref playrtp-coreaudio.c. Given a similar interface - * wrapping OSS the whole of playrtp could probably be greatly simplified. - */ - -#include "common.h" - -#if HAVE_ALSA_ASOUNDLIB_H - -#include -#include -#include -#include - -#include "mem.h" -#include "log.h" -#include "vector.h" -#include "heap.h" -#include "playrtp.h" -#include "alsabg.h" - -/** @brief Callback from alsa_bg_collect() */ -static int playrtp_alsa_supply(void *dst, - unsigned supply_nsamples) { - unsigned samples_available; - const struct packet *p; - - pthread_mutex_lock(&lock); - p = playrtp_next_packet(); - if(p && contains(p, next_timestamp)) { - /* This packet is ready to play */ - const uint32_t packet_end = p->timestamp + p->nsamples; - const uint32_t offset = next_timestamp - p->timestamp; - const uint16_t *src = (void *)(p->samples_raw + offset); - samples_available = packet_end - next_timestamp; - if(samples_available > supply_nsamples) - samples_available = supply_nsamples; - next_timestamp += samples_available; - memcpy(dst, src, samples_available * sizeof (int16_t)); - /* We don't bother junking the packet - that'll be dealt with next time - * round */ - } else { - /* No packet is ready to play (and there might be no packet at all) */ - samples_available = p ? p->timestamp - next_timestamp : supply_nsamples; - if(samples_available > supply_nsamples) - samples_available = supply_nsamples; - /*info("infill %d", samples_available);*/ - next_timestamp += samples_available; - /* Unlike Core Audio the buffer is not guaranteed to be 0-filled */ - memset(dst, 0, samples_available * sizeof (int16_t)); - } - pthread_mutex_unlock(&lock); - return samples_available; -} - -void playrtp_alsa(void) { - alsa_bg_init(device ? device : "default", - playrtp_alsa_supply); - pthread_mutex_lock(&lock); - for(;;) { - /* Wait for the buffer to fill up a bit */ - playrtp_fill_buffer(); - /* Start playing now */ - info("Playing..."); - next_timestamp = pheap_first(&packets)->timestamp; - active = 1; - alsa_bg_enable(); - /* Wait until the buffer empties out */ - while(nsamples >= minbuffer - || (nsamples > 0 - && contains(pheap_first(&packets), next_timestamp))) { - pthread_cond_wait(&cond, &lock); - } - /* Stop playing for a bit until the buffer re-fills */ - alsa_bg_disable(); - active = 0; - /* Go back round */ - } -} - -#endif - -/* -Local Variables: -c-basic-offset:2 -comment-column:40 -fill-column:79 -indent-tabs-mode:nil -End: -*/ diff --git a/clients/playrtp-coreaudio.c b/clients/playrtp-coreaudio.c deleted file mode 100644 index dae7027..0000000 --- a/clients/playrtp-coreaudio.c +++ /dev/null @@ -1,171 +0,0 @@ -/* - * This file is part of DisOrder. - * Copyright (C) 2007 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 clients/playrtp-coreaudio.c - * @brief RTP player - Core Audio support - */ - -#include "common.h" - -#if HAVE_COREAUDIO_AUDIOHARDWARE_H -#include - -#include "mem.h" -#include "log.h" -#include "vector.h" -#include "heap.h" -#include "playrtp.h" -#include "coreaudio.h" - -/** @brief Callback from Core Audio */ -static OSStatus adioproc - (AudioDeviceID attribute((unused)) inDevice, - const AudioTimeStamp attribute((unused)) *inNow, - const AudioBufferList attribute((unused)) *inInputData, - const AudioTimeStamp attribute((unused)) *inInputTime, - AudioBufferList *outOutputData, - const AudioTimeStamp attribute((unused)) *inOutputTime, - void attribute((unused)) *inClientData) { - UInt32 nbuffers = outOutputData->mNumberBuffers; - AudioBuffer *ab = outOutputData->mBuffers; - uint32_t samples_available; - - pthread_mutex_lock(&lock); - while(nbuffers > 0) { - float *samplesOut = ab->mData; - size_t samplesOutLeft = ab->mDataByteSize / sizeof (float); - - while(samplesOutLeft > 0) { - const struct packet *p = playrtp_next_packet(); - if(p && contains(p, next_timestamp)) { - /* This packet is ready to play */ - const uint32_t packet_end = p->timestamp + p->nsamples; - const uint32_t offset = next_timestamp - p->timestamp; - const uint16_t *ptr = (void *)(p->samples_raw + offset); - - samples_available = packet_end - next_timestamp; - if(samples_available > samplesOutLeft) - samples_available = samplesOutLeft; - next_timestamp += samples_available; - samplesOutLeft -= samples_available; - if(dump_buffer) { - size_t n; - - for(n = 0; n < samples_available; ++n) { - dump_buffer[dump_index++] = (int16_t)ntohs(ptr[n]); - dump_index %= dump_size; - } - } - while(samples_available-- > 0) - *samplesOut++ = (int16_t)ntohs(*ptr++) * (0.5 / 32767); - /* We don't bother junking the packet - that'll be dealt with next time - * round */ - } else { - /* No packet is ready to play (and there might be no packet at all) */ - samples_available = p ? p->timestamp - next_timestamp - : samplesOutLeft; - if(samples_available > samplesOutLeft) - samples_available = samplesOutLeft; - //info("infill by %"PRIu32, samples_available); - /* Conveniently the buffer is 0 to start with */ - next_timestamp += samples_available; - samplesOut += samples_available; - samplesOutLeft -= samples_available; - if(dump_buffer) { - size_t n; - - for(n = 0; n < samples_available; ++n) { - dump_buffer[dump_index++] = 0; - dump_index %= dump_size; - } - } - } - } - ++ab; - --nbuffers; - } - pthread_mutex_unlock(&lock); - return 0; -} - -void playrtp_coreaudio(void) { - OSStatus status; - UInt32 propertySize; - AudioDeviceID adid; - AudioStreamBasicDescription asbd; - - /* If this looks suspiciously like libao's macosx driver there's an - * excellent reason for that... */ - - /* TODO report errors as strings not numbers */ - /* Identify the device to use */ - adid = coreaudio_getdevice(device); - propertySize = sizeof asbd; - status = AudioDeviceGetProperty(adid, 0, false, - kAudioDevicePropertyStreamFormat, - &propertySize, &asbd); - if(status) - fatal(0, "AudioHardwareGetProperty: %d", (int)status); - D(("mSampleRate %f", asbd.mSampleRate)); - D(("mFormatID %08lx", asbd.mFormatID)); - D(("mFormatFlags %08lx", asbd.mFormatFlags)); - D(("mBytesPerPacket %08lx", asbd.mBytesPerPacket)); - D(("mFramesPerPacket %08lx", asbd.mFramesPerPacket)); - D(("mBytesPerFrame %08lx", asbd.mBytesPerFrame)); - D(("mChannelsPerFrame %08lx", asbd.mChannelsPerFrame)); - D(("mBitsPerChannel %08lx", asbd.mBitsPerChannel)); - D(("mReserved %08lx", asbd.mReserved)); - if(asbd.mFormatID != kAudioFormatLinearPCM) - fatal(0, "audio device does not support kAudioFormatLinearPCM"); - status = AudioDeviceAddIOProc(adid, adioproc, 0); - if(status) - fatal(0, "AudioDeviceAddIOProc: %d", (int)status); - pthread_mutex_lock(&lock); - for(;;) { - /* Wait for the buffer to fill up a bit */ - playrtp_fill_buffer(); - /* Start playing now */ - info("Playing..."); - next_timestamp = pheap_first(&packets)->timestamp; - active = 1; - status = AudioDeviceStart(adid, adioproc); - if(status) - fatal(0, "AudioDeviceStart: %d", (int)status); - /* Wait until the buffer empties out */ - while(nsamples >= minbuffer - || (nsamples > 0 - && contains(pheap_first(&packets), next_timestamp))) - pthread_cond_wait(&cond, &lock); - /* Stop playing for a bit until the buffer re-fills */ - status = AudioDeviceStop(adid, adioproc); - if(status) - fatal(0, "AudioDeviceStop: %d", (int)status); - active = 0; - /* Go back round */ - } -} - -#endif - -/* -Local Variables: -c-basic-offset:2 -comment-column:40 -fill-column:79 -indent-tabs-mode:nil -End: -*/ diff --git a/clients/playrtp-oss.c b/clients/playrtp-oss.c deleted file mode 100644 index 4b0ab83..0000000 --- a/clients/playrtp-oss.c +++ /dev/null @@ -1,278 +0,0 @@ -/* - * This file is part of DisOrder. - * Copyright (C) 2007 Richard Kettlewell - * Portions copyright (C) 2007 Ross Younger - * - * 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 clients/playrtp-oss.c - * @brief RTP player - OSS and empeg support - */ - -#include "common.h" - -#if HAVE_SYS_SOUNDCARD_H || EMPEG_HOST - -#include -#include -#if !EMPEG_HOST -#include -#endif -#include -#include -#include -#include -#include - -#include "mem.h" -#include "log.h" -#include "vector.h" -#include "heap.h" -#include "syscalls.h" -#include "playrtp.h" - -/** @brief /dev/dsp (or whatever) */ -static int playrtp_oss_fd = -1; - -/** @brief Audio buffer */ -static char *playrtp_oss_buffer; - -/** @brief Size of @ref playrtp_oss_buffer in bytes */ -static int playrtp_oss_bufsize; - -/** @brief Number of bytes used in @ref playrtp_oss_buffer */ -static int playrtp_oss_bufused; - -/** @brief Open and configure the OSS audio device */ -static void playrtp_oss_enable(void) { - if(playrtp_oss_fd == -1) { -#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. */ - if(!device) - device = "/dev/audio"; - if((playrtp_oss_fd = open(device, O_WRONLY)) < 0) - fatal(errno, "error opening %s", device); - playrtp_oss_bufsize = 4608; -#else - int rate = 44100, stereo = 1, format = AFMT_S16_BE; - if(!device) { - if(access("/dev/dsp", W_OK) == 0) - device = "/dev/dsp"; - else if(access("/dev/audio", W_OK) == 0) - device = "/dev/audio"; - else - fatal(0, "cannot determine default audio device"); - } - if((playrtp_oss_fd = open(device, O_WRONLY)) < 0) - fatal(errno, "error opening %s", device); - if(ioctl(playrtp_oss_fd, SNDCTL_DSP_SETFMT, &format) < 0) - fatal(errno, "ioctl SNDCTL_DSP_SETFMT"); - if(ioctl(playrtp_oss_fd, SNDCTL_DSP_STEREO, &stereo) < 0) - fatal(errno, "ioctl SNDCTL_DSP_STEREO"); - if(ioctl(playrtp_oss_fd, SNDCTL_DSP_SPEED, &rate) < 0) - fatal(errno, "ioctl SNDCTL_DSP_SPEED"); - if(rate != 44100) - error(0, "asking for 44100Hz, got %dHz", rate); - if(ioctl(playrtp_oss_fd, SNDCTL_DSP_GETBLKSIZE, &playrtp_oss_bufsize) < 0) - fatal(errno, "ioctl SNDCTL_DSP_GETBLKSIZE"); - info("OSS buffer size %d", playrtp_oss_bufsize); -#endif - playrtp_oss_buffer = xmalloc(playrtp_oss_bufsize); - playrtp_oss_bufused = 0; - nonblock(playrtp_oss_fd); - } -} - -/** @brief Flush the OSS output buffer - * @return 0 on success, non-0 on error - */ -static int playrtp_oss_flush(void) { - int nbyteswritten; - - if(!playrtp_oss_bufused) - return 0; /* nothing to do */ - /* 0 out the unused portion of the buffer */ - memset(playrtp_oss_buffer + playrtp_oss_bufused, 0, - playrtp_oss_bufsize - playrtp_oss_bufused); -#if EMPEG_HOST - /* empeg audio driver insists on native-endian samples */ - { - uint16_t *ptr, - *const limit = (uint16_t *)(playrtp_oss_buffer + playrtp_oss_bufused); - - for(ptr = (uint16_t *)playrtp_oss_buffer; ptr < limit; ++ptr) - *ptr = ntohs(*ptr); - } -#endif - for(;;) { - nbyteswritten = write(playrtp_oss_fd, - playrtp_oss_buffer, playrtp_oss_bufsize); - if(nbyteswritten < 0) { - switch(errno) { - case EINTR: - break; /* try again */ - case EAGAIN: - return 0; /* try later */ - default: - error(errno, "error writing to %s", device); - return -1; - } - } else { - if(nbyteswritten < playrtp_oss_bufsize) - error(0, "%s: short write (%d/%d)", - device, nbyteswritten, playrtp_oss_bufsize); - if(dump_buffer) { - int count; - const int16_t *sp = (const int16_t *)playrtp_oss_buffer; - - for(count = 0; count < playrtp_oss_bufsize; count += sizeof(int16_t)) { - dump_buffer[dump_index++] = (int16_t)ntohs(*sp++); - dump_index %= dump_size; - } - } - playrtp_oss_bufused = 0; - return 0; - } - } -} - -/** @brief Wait until the audio device can accept more data */ -static void playrtp_oss_wait(void) { - struct pollfd fds[1]; - int n; - - do { - fds[0].fd = playrtp_oss_fd; - fds[0].events = POLLOUT; - while((n = poll(fds, 1, -1)) < 0 && errno == EINTR) - ; - if(n < 0) - fatal(errno, "calling poll"); - } while(!(fds[0].revents & (POLLOUT|POLLERR))); -} - -/** @brief Close the OSS output device - * @param hard If nonzero, drop pending data - */ -static void playrtp_oss_disable(int hard) { - if(hard) { -#if !EMPEG_HOST - /* No SNDCTL_DSP_ ioctls on empeg */ - if(ioctl(playrtp_oss_fd, SNDCTL_DSP_RESET, 0) < 0) - error(errno, "ioctl SNDCTL_DSP_RESET"); -#endif - } else - playrtp_oss_flush(); - xclose(playrtp_oss_fd); - playrtp_oss_fd = -1; - free(playrtp_oss_buffer); - playrtp_oss_buffer = 0; -} - -/** @brief Write samples to OSS output device - * @param data Pointer to sample data - * @param samples Number of samples - * @return 0 on success, non-0 on error - */ -static int playrtp_oss_write(const char *data, size_t samples) { - long bytes = samples * sizeof(int16_t); - while(bytes > 0) { - int n = playrtp_oss_bufsize - playrtp_oss_bufused; - - if(n > bytes) - n = bytes; - memcpy(playrtp_oss_buffer + playrtp_oss_bufused, data, n); - bytes -= n; - data += n; - playrtp_oss_bufused += n; - if(playrtp_oss_bufused == playrtp_oss_bufsize) - if(playrtp_oss_flush()) - return -1; - } - next_timestamp += samples; - return 0; -} - -/** @brief Play some data from packet @p p - * - * @p p is assumed to contain @ref next_timestamp. - */ -static int playrtp_oss_play(const struct packet *p) { - return playrtp_oss_write - ((const char *)(p->samples_raw + next_timestamp - p->timestamp), - (p->timestamp + p->nsamples) - next_timestamp); -} - -/** @brief Play some silence before packet @p p - * - * @p p is assumed to be entirely before @ref next_timestamp. - */ -static int playrtp_oss_infill(const struct packet *p) { - static const char zeros[INFILL_SAMPLES * sizeof(int16_t)]; - size_t samples_available = INFILL_SAMPLES; - - if(p && samples_available > p->timestamp - next_timestamp) - samples_available = p->timestamp - next_timestamp; - return playrtp_oss_write(zeros, samples_available); -} - -/** @brief OSS backend for playrtp */ -void playrtp_oss(void) { - int escape; - const struct packet *p; - - pthread_mutex_lock(&lock); - for(;;) { - /* Wait for the buffer to fill up a bit */ - playrtp_fill_buffer(); - playrtp_oss_enable(); - escape = 0; - info("Playing..."); - /* Keep playing until the buffer empties out, we get an error */ - while((nsamples >= minbuffer - || (nsamples > 0 - && contains(pheap_first(&packets), next_timestamp))) - && !escape) { - /* Wait until we can play more */ - pthread_mutex_unlock(&lock); - playrtp_oss_wait(); - pthread_mutex_lock(&lock); - /* Device is ready for more data, find something to play */ - p = playrtp_next_packet(); - /* Play it or play some silence */ - if(contains(p, next_timestamp)) - escape = playrtp_oss_play(p); - else - escape = playrtp_oss_infill(p); - } - active = 0; - /* We stop playing for a bit until the buffer re-fills */ - pthread_mutex_unlock(&lock); - playrtp_oss_disable(escape); - pthread_mutex_lock(&lock); - } -} - -#endif - -/* -Local Variables: -c-basic-offset:2 -comment-column:40 -fill-column:79 -indent-tabs-mode:nil -End: -*/ diff --git a/clients/playrtp.c b/clients/playrtp.c index aca1532..f1eaebc 100644 --- a/clients/playrtp.c +++ b/clients/playrtp.c @@ -79,6 +79,7 @@ #include "playrtp.h" #include "inputline.h" #include "version.h" +#include "uaudio.h" #define readahead linux_headers_are_borked @@ -94,7 +95,6 @@ static int rtpfd; static FILE *logfp; /** @brief Output device */ -const char *device; /** @brief Minimum low watermark * @@ -168,16 +168,8 @@ pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; /** @brief Condition variable signalled whenever @ref packets is changed */ pthread_cond_t cond = PTHREAD_COND_INITIALIZER; -#if DEFAULT_BACKEND == BACKEND_ALSA -# define DEFAULT_PLAYRTP_BACKEND playrtp_alsa -#elif DEFAULT_BACKEND == BACKEND_OSS -# define DEFAULT_PLAYRTP_BACKEND playrtp_oss -#elif DEFAULT_BACKEND == BACKEND_COREAUDIO -# define DEFAULT_PLAYRTP_BACKEND playrtp_coreaudio -#endif - /** @brief Backend to play with */ -static void (*backend)(void) = DEFAULT_PLAYRTP_BACKEND; +static const struct uaudio *backend; HEAP_DEFINE(pheap, struct packet *, lt_packet); @@ -479,29 +471,6 @@ struct packet *playrtp_next_packet(void) { return 0; } -/** @brief Play an RTP stream - * - * This is the guts of the program. It is responsible for: - * - starting the listening thread - * - opening the audio device - * - reading ahead to build up a buffer - * - arranging for audio to be played - * - detecting when the buffer has got too small and re-buffering - */ -static void play_rtp(void) { - pthread_t ltid; - int err; - - /* We receive and convert audio data in a background thread */ - if((err = pthread_create(<id, 0, listen_thread, 0))) - fatal(err, "pthread_create listen_thread"); - /* We have a second thread to add received packets to the queue */ - if((err = pthread_create(<id, 0, queue_thread, 0))) - fatal(err, "pthread_create queue_thread"); - /* The rest of the work is backend-specific */ - backend(); -} - /* display usage message and terminate */ static void help(void) { xprintf("Usage:\n" @@ -529,6 +498,66 @@ static void help(void) { exit(0); } +static size_t playrtp_callback(int16_t *buffer, + size_t max_samples, + void attribute((unused)) *userdata) { + size_t samples; + + pthread_mutex_lock(&lock); + /* Get the next packet, junking any that are now in the past */ + const struct packet *p = playrtp_next_packet(); + if(p && contains(p, next_timestamp)) { + /* This packet is ready to play; the desired next timestamp points + * somewhere into it. */ + + /* Timestamp of end of packet */ + const uint32_t packet_end = p->timestamp + p->nsamples; + + /* Offset of desired next timestamp into current packet */ + const uint32_t offset = next_timestamp - p->timestamp; + + /* Pointer to audio data */ + const uint16_t *ptr = (void *)(p->samples_raw + offset); + + /* Compute number of samples left in packet, limited to output buffer + * size */ + samples = packet_end - next_timestamp; + if(samples > max_samples) + samples = max_samples; + + /* Copy into buffer, converting to native endianness */ + size_t i = samples; + int16_t *bufptr = buffer; + while(i > 0) { + *bufptr++ = (int16_t)ntohs(*ptr++); + --i; + } + /* We don't junk the packet here; a subsequent call to + * playrtp_next_packet() will dispose of it (if it's actually done with). */ + } else { + /* There is no suitable packet. We introduce 0s up to the next packet, or + * to fill the buffer if there's no next packet or that's too many. The + * comparison with max_samples deals with the otherwise troubling overflow + * case. */ + samples = p ? p->timestamp - next_timestamp : max_samples; + if(samples > max_samples) + samples = max_samples; + //info("infill by %zu", samples); + memset(buffer, 0, samples * sizeof *buffer); + } + /* Debug dump */ + if(dump_buffer) { + for(size_t i = 0; i < samples; ++i) { + dump_buffer[dump_index++] = buffer[i]; + dump_index %= dump_size; + } + } + /* Advance timestamp */ + next_timestamp += samples; + pthread_mutex_unlock(&lock); + return samples; +} + int main(int argc, char **argv) { int n, err; struct addrinfo *res; @@ -548,6 +577,8 @@ int main(int argc, char **argv) { }; union any_sockaddr mgroup; const char *dumpfile = 0; + const char *device = 0; + pthread_t ltid; static const struct addrinfo prefs = { .ai_flags = AI_PASSIVE, @@ -558,6 +589,7 @@ int main(int argc, char **argv) { mem_init(); if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale"); + backend = uaudio_apis[0]; while((n = getopt_long(argc, argv, "hVdD:m:b:x:L:R:M:aocC:r", options, 0)) >= 0) { switch(n) { case 'h': help(); @@ -570,13 +602,13 @@ int main(int argc, char **argv) { case 'L': logfp = fopen(optarg, "w"); break; case 'R': target_rcvbuf = atoi(optarg); break; #if HAVE_ALSA_ASOUNDLIB_H - case 'a': backend = playrtp_alsa; break; + case 'a': backend = &uaudio_alsa; break; #endif #if HAVE_SYS_SOUNDCARD_H || EMPEG_HOST - case 'o': backend = playrtp_oss; break; + case 'o': backend = &uaudio_oss; break; #endif #if HAVE_COREAUDIO_AUDIOHARDWARE_H - case 'c': backend = playrtp_coreaudio; break; + case 'c': backend = &uaudio_coreaudio; break; #endif case 'C': configfile = optarg; break; case 's': control_socket = optarg; break; @@ -710,7 +742,36 @@ int main(int argc, char **argv) { fatal(errno, "mapping %s", dumpfile); info("dumping to %s", dumpfile); } - play_rtp(); + /* Choose output device */ + if(device) + uaudio_set("device", device); + /* Set up output */ + backend->start(playrtp_callback, NULL); + /* We receive and convert audio data in a background thread */ + if((err = pthread_create(<id, 0, listen_thread, 0))) + fatal(err, "pthread_create listen_thread"); + /* We have a second thread to add received packets to the queue */ + if((err = pthread_create(<id, 0, queue_thread, 0))) + fatal(err, "pthread_create queue_thread"); + pthread_mutex_lock(&lock); + for(;;) { + /* Wait for the buffer to fill up a bit */ + playrtp_fill_buffer(); + /* Start playing now */ + info("Playing..."); + next_timestamp = pheap_first(&packets)->timestamp; + active = 1; + backend->activate(); + /* Wait until the buffer empties out */ + while(nsamples >= minbuffer + || (nsamples > 0 + && 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; + /* Go back round */ + } return 0; } diff --git a/clients/playrtp.h b/clients/playrtp.h index 900ff50..d5caa4c 100644 --- a/clients/playrtp.h +++ b/clients/playrtp.h @@ -128,7 +128,6 @@ void playrtp_free_packet(struct packet *p); void playrtp_fill_buffer(void); struct packet *playrtp_next_packet(void); -extern const char *device; extern struct packet *received_packets; extern struct packet **received_tail; extern pthread_mutex_t receive_lock; diff --git a/lib/Makefile.am b/lib/Makefile.am index 9df1dfd..4ac8cbd 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -1,6 +1,6 @@ # # This file is part of DisOrder. -# Copyright (C) 2004-2008 Richard Kettlewell +# Copyright (C) 2004-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 @@ -82,6 +82,8 @@ 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 \ url.h url.c \ user.h user.c \ unicode.h unicode.c \ diff --git a/lib/uaudio-coreaudio.c b/lib/uaudio-coreaudio.c new file mode 100644 index 0000000..129a66e --- /dev/null +++ b/lib/uaudio-coreaudio.c @@ -0,0 +1,162 @@ +/* + * 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-coreaudio.c + * @brief Support for Core Audio backend */ +#include "common.h" + +#if HAVE_COREAUDIO_AUDIOHARDWARE_H + +#include "coreaudio.h" +#include "uaudio.h" +#include "mem.h" +#include "log.h" +#include "syscalls.h" + +/** @brief Callback to request sample data */ +static uaudio_callback *coreaudio_callback; + +/** @brief Userdata for @ref coreaudio_callback */ +static void *coreaudio_userdata; + +/** @brief Core Audio device ID */ +static AudioDeviceID coreaudio_adid; + +/** @brief Core Audio option names */ +static const char *const coreaudio_options[] = { + "device", + NULL +}; + +/** @brief Callback from Core Audio + * + * Core Audio demands floating point samples but we provide integers. + * So there is a conversion step in here. + */ +static OSStatus coreaudio_adioproc + (AudioDeviceID attribute((unused)) inDevice, + const AudioTimeStamp attribute((unused)) *inNow, + const AudioBufferList attribute((unused)) *inInputData, + const AudioTimeStamp attribute((unused)) *inInputTime, + AudioBufferList *outOutputData, + const AudioTimeStamp attribute((unused)) *inOutputTime, + void attribute((unused)) *inClientData) { + /* Number of buffers we must fill */ + unsigned nbuffers = outOutputData->mNumberBuffers; + /* Pointer to buffer to fill */ + AudioBuffer *ab = outOutputData->mBuffers; + + while(nbuffers > 0) { + /* Where to store converted sample data */ + float *samples = ab->mData; + /* Number of samples left to fill */ + size_t nsamples = ab->mDataByteSize / sizeof (float); + + while(nsamples > 0) { + /* Integer-format input buffer */ + int16_t input[1024], *ptr = input; + /* How many samples we'll ask for */ + const int ask = nsamples > 1024 ? 1024 : (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); + } + } + /* Move on to the next buffer */ + ++ab; + --nbuffers; + } + return 0; +} + +static void coreaudio_start(uaudio_callback *callback, + void *userdata) { + OSStatus status; + UInt32 propertySize; + AudioStreamBasicDescription asbd; + const char *device; + + coreaudio_callback = callback; + coreaudio_userdata = userdata; + device = uaudio_get("device"); + coreaudio_adid = coreaudio_getdevice(device); + propertySize = sizeof asbd; + status = AudioDeviceGetProperty(coreaudio_adid, 0, false, + kAudioDevicePropertyStreamFormat, + &propertySize, &asbd); + if(status) + coreaudio_fatal(status, "AudioHardwareGetProperty"); + D(("mSampleRate %f", asbd.mSampleRate)); + D(("mFormatID %08lx", asbd.mFormatID)); + D(("mFormatFlags %08lx", asbd.mFormatFlags)); + D(("mBytesPerPacket %08lx", asbd.mBytesPerPacket)); + D(("mFramesPerPacket %08lx", asbd.mFramesPerPacket)); + D(("mBytesPerFrame %08lx", asbd.mBytesPerFrame)); + D(("mChannelsPerFrame %08lx", asbd.mChannelsPerFrame)); + D(("mBitsPerChannel %08lx", asbd.mBitsPerChannel)); + D(("mReserved %08lx", asbd.mReserved)); + if(asbd.mFormatID != kAudioFormatLinearPCM) + disorder_fatal(0, "audio device does not support kAudioFormatLinearPCM"); + status = AudioDeviceAddIOProc(coreaudio_adid, coreaudio_adioproc, 0); + if(status) + coreaudio_fatal(status, "AudioDeviceAddIOProc"); +} + +static void coreaudio_stop(void) { +} + +static void coreaudio_activate(void) { + OSStatus status; + + status = AudioDeviceStart(coreaudio_adid, coreaudio_adioproc); + if(status) + coreaudio_fatal(status, "AudioDeviceStart"); +} + +static void coreaudio_deactivate(void) { + OSStatus status; + + status = AudioDeviceStop(coreaudio_adid, coreaudio_adioproc); + if(status) + coreaudio_fatal(status, "AudioDeviceStop"); +} + +const struct uaudio uaudio_coreaudio = { + .name = "coreaudio", + .options = coreaudio_options, + .start = coreaudio_start, + .stop = coreaudio_stop, + .activate = coreaudio_activate, + .deactivate = coreaudio_deactivate +}; + +#endif + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/lib/uaudio-oss.c b/lib/uaudio-oss.c new file mode 100644 index 0000000..06ffb73 --- /dev/null +++ b/lib/uaudio-oss.c @@ -0,0 +1,189 @@ +/* + * 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-oss.c + * @brief Support for OSS backend */ +#include "common.h" + +#if HAVE_SYS_SOUNDCARD_H || EMPEG_HOST + +#if HAVE_SYS_SOUNDCARD_H +# include +#endif +#include +#include +#include +#include +#include +#include + +#include "mem.h" +#include "log.h" +#include "syscalls.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; +static int oss_bufsize; + +static const char *const oss_options[] = { + "device", + NULL +}; + +static void *oss_thread(void *userdata) { + int16_t *buffer; + pthread_mutex_lock(&oss_lock); + buffer = xmalloc(XXX); + 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_bufsizeXXX, userdata);; + if(write(oss_fd, buffer, XXX) < 0) + fatal(errno, "error playing audio"); + // TODO short writes! + } + oss_going = 0; + pthread_cond_signal(&oss_cond); + } + pthread_mutex_unlock(&oss_lock); + return NULL; +} + +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"); +} + +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); +} + +static void oss_activate(void) { + pthread_mutex_lock(&oss_lock); + if(!oss_activated) { + const char *device = uaudio_get(&uadiuo_oss, "device"); + +#if EMPEG_HOST + 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(ossfd, SNDCTL_DSP_STEREO, &stereo) < 0) { + error(errno, "error calling ioctl SNDCTL_DSP_STEREO %d", stereo); + goto failed; + } + /* 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); + 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); + } + pthread_mutex_unlock(&oss_lock); +} + +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); +} + +const struct uaudio uaudio_oss = { + .name = "oss", + .options = oss_options, + .start = oss_start, + .stop = oss_stop, + .activate = oss_activate, + .deactivate = oss_deactivate +}; + +#endif + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/lib/uaudio-rtp.c b/lib/uaudio-rtp.c new file mode 100644 index 0000000..1bbfa9c --- /dev/null +++ b/lib/uaudio-rtp.c @@ -0,0 +1,68 @@ +/* + * 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-oss.c + * @brief Support for RTP network play backend */ +#include "common.h" + +#include + +#include "uaudio.h" +#include "mem.h" +#include "log.h" +#include "syscalls.h" + +static const char *const rtp_options[] = { + NULL +}; + +static void rtp_start(uaudio_callback *callback, + void *userdata) { + (void)callback; + (void)userdata; + /* TODO */ +} + +static void rtp_stop(void) { + /* TODO */ +} + +static void rtp_activate(void) { + /* TODO */ +} + +static void rtp_deactivate(void) { + /* TODO */ +} + +const struct uaudio uaudio_rtp = { + .name = "rtp", + .options = rtp_options, + .start = rtp_start, + .stop = rtp_stop, + .activate = rtp_activate, + .deactivate = rtp_deactivate +}; + +/* +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 new file mode 100644 index 0000000..aa00f66 --- /dev/null +++ b/lib/uaudio.c @@ -0,0 +1,75 @@ +/* + * 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.c + * @brief Uniform audio interface + */ + +#include "common.h" +#include "uaudio.h" +#include "hash.h" +#include "mem.h" + +/** @brief Options for chosen uaudio API */ +static hash *uaudio_options; + +/** @brief Set a uaudio option */ +void uaudio_set(const char *name, const char *value) { + if(!uaudio_options) + uaudio_options = hash_new(sizeof(char *)); + value = xstrdup(value); + hash_add(uaudio_options, name, &value, HASH_INSERT_OR_REPLACE); +} + +/** @brief Set a uaudio option */ +const char *uaudio_get(const char *name) { + const char *value = (uaudio_options ? + *(char **)hash_find(uaudio_options, name) + : NULL); + return value ? xstrdup(value) : NULL; +} + +/** @brief List of known APIs + * + * Terminated by a null pointer. + * + * The first one will be used as a default, so putting ALSA before OSS + * constitutes a policy decision. + */ +const struct uaudio *uaudio_apis[] = { +#if HAVE_COREAUDIO_AUDIOHARDWARE_H + &uaudio_coreaudio, +#endif +#if HAVE_ALSA_ASOUNDLIB_H + &uaudio_alsa, +#endif +#if HAVE_SYS_SOUNDCARD_H || EMPEG_HOST + &uaudio_oss, +#endif + &uaudio_rtp, + NULL, +}; + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/lib/uaudio.h b/lib/uaudio.h new file mode 100644 index 0000000..6b0dea1 --- /dev/null +++ b/lib/uaudio.h @@ -0,0 +1,110 @@ +/* + * 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.h + * @brief Uniform audio interface + */ + +#ifndef UAUDIO_H +#define UAUDIO_H + +/** @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, + size_t max_samples, + void *userdata); + +/** @brief Audio API definition */ +struct uaudio { + /** @brief Name of this API */ + const char *name; + + /** @brief List of options, terminated by NULL */ + const char *const *options; + + /** @brief Do slow setup + * @param ua Handle returned by uaudio_open() + * @param callback Called for audio data + * @param userdata Passed to @p callback + * + * This does resource-intensive setup for the output device. + * + * For instance it might open mixable audio devices or network sockets. It + * will create any background thread required. However, it must not exclude + * other processes from outputting sound. + */ + void (*start)(uaudio_callback *callback, + void *userdata); + + /** @brief Tear down + * @param ua Handle returned by uaudio_open() + * + * This undoes the effect of @c start. + */ + void (*stop)(void); + + /** @brief Enable output + * + * A background thread will start calling @c callback as set by @c + * start and playing the audio data received from it. + */ + void (*activate)(void); + + /** @brief Disable output + * + * The background thread will stop calling @c callback. + */ + void (*deactivate)(void); + +}; + +void uaudio_set(const char *name, const char *value); +const char *uaudio_get(const char *name); + +#if HAVE_COREAUDIO_AUDIOHARDWARE_H +extern const struct uaudio uaudio_coreaudio; +#endif + +#if HAVE_ALSA_ASOUNDLIB_H +extern const struct uaudio uaudio_alsa; +#endif + +#if HAVE_SYS_SOUNDCARD_H || EMPEG_HOST +extern const struct uaudio uaudio_oss; +#endif + +extern const struct uaudio uaudio_rtp; + +extern const struct uaudio *uaudio_apis[]; + +#endif /* UAUDIO_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/