From 3fe80106bf0dc11987be660db2a39f2cc0a692a4 Mon Sep 17 00:00:00 2001 From: Andre Noll Date: Sat, 5 Mar 2011 12:36:34 +0100 Subject: [PATCH] Implement support for libao via the new ao writer. This adds support for another writer to paraslash. The new ao writer allows to play an audio stream via one of libao's output plugins. Two new configure options are provided and the documentation is updated accordingly. --- FEATURES | 4 +- Makefile.in | 3 + ao_write.c | 423 ++++++++++++++++++++++++++++++++++++++++++++++++ configure.ac | 61 +++++++ error.h | 13 ++ ggo/.gitignore | 1 + ggo/ao_write.m4 | 26 +++ web/manual.m4 | 9 ++ 8 files changed, 539 insertions(+), 1 deletion(-) create mode 100644 ao_write.c create mode 100644 ggo/ao_write.m4 diff --git a/FEATURES b/FEATURES index 5b5f4699..2839d79c 100644 --- a/FEATURES +++ b/FEATURES @@ -6,7 +6,9 @@ Features * Runs on Linux, Mac OS, FreeBSD, NetBSD, Solaris and probably other Unixes * Mp3, ogg/vorbis, ogg/speex, aac (m4a) and wma support - * Local or remote http, dccp, and udp network audio streaming + * Native Alsa, OSS, CoreAudio output support + * Support for ESD, Pulseaudio, AIX, Solaris, IRIX through libao + * Local or remote http, dccp and udp network audio streaming * IPv6 support * Forward error correction allows receivers to recover from packet losses * Volume normalizer diff --git a/Makefile.in b/Makefile.in index 8b33aaac..a440e4f6 100644 --- a/Makefile.in +++ b/Makefile.in @@ -193,6 +193,9 @@ $(object_dir)/aac_afh.o: aac_afh.c | $(object_dir) $(object_dir)/gui%.o: gui%.c | $(object_dir) @[ -z "$(Q)" ] || echo 'CC $<' $(Q) $(CC) -c -o $@ $(CPPFLAGS) $(DEBUG_CPPFLAGS) @curses_cppflags@ $< +$(object_dir)/ao_write.o: ao_write.c | $(object_dir) + @[ -z "$(Q)" ] || echo 'CC $<' + $(Q) $(CC) -c -o $@ $(CPPFLAGS) $(DEBUG_CPPFLAGS) @ao_cppflags@ $< $(object_dir)/%.cmdline.o: $(cmdline_dir)/%.cmdline.c $(cmdline_dir)/%.cmdline.h | $(object_dir) @[ -z "$(Q)" ] || echo 'CC $<' diff --git a/ao_write.c b/ao_write.c new file mode 100644 index 00000000..307588f9 --- /dev/null +++ b/ao_write.c @@ -0,0 +1,423 @@ +/* + * Copyright (C) 2011 Andre Noll + * + * Licensed under the GPL v2. For licencing details see COPYING. + */ + +/** \file ao_write.c Paraslash's libao output plugin. */ + +#include +#include +#include +#include + +#include "para.h" +#include "fd.h" +#include "string.h" +#include "list.h" +#include "sched.h" +#include "ggo.h" +#include "buffer_tree.h" +#include "write.h" +#include "write_common.h" +#include "ao_write.cmdline.h" +#include "error.h" + +struct private_aow_data { + ao_device *dev; + int bytes_per_frame; + + pthread_t thread; + pthread_attr_t attr; + pthread_mutex_t mutex; + pthread_cond_t data_available; + struct btr_node *thread_btrn; +}; + +static void aow_close(struct writer_node *wn) +{ + struct private_aow_data *pawd = wn->private_data; + + if (!pawd) + return; + ao_close(pawd->dev); + free(pawd); + wn->private_data = NULL; + ao_shutdown(); +} + +static void aow_pre_select(struct sched *s, struct task *t) +{ + struct writer_node *wn = container_of(t, struct writer_node, task); + int ret = btr_node_status(wn->btrn, wn->min_iqs, BTR_NT_LEAF); + + if (ret == 0) + return; + sched_min_delay(s); +} + +static int aow_set_sample_format(unsigned sample_rate, unsigned channels, + int sample_format, ao_sample_format *result) +{ + memset(result, 0, sizeof(*result)); + switch (sample_format) { + case SF_U8: + case SF_U16_LE: + case SF_U16_BE: + return -E_AO_BAD_SAMPLE_FORMAT; + case SF_S8: + /* no need to set byte_format */ + result->bits = 8; + break; + case SF_S16_LE: + result->bits = 16; + result->byte_format = AO_FMT_LITTLE; + break; + case SF_S16_BE: + result->bits = 16; + result->byte_format = AO_FMT_BIG; + break; + default: + PARA_EMERG_LOG("bug: invalid sample format\n"); + exit(EXIT_FAILURE); + } + result->channels = channels; + result->rate = sample_rate; + return 1; +} + +static int aow_open_device(int id, ao_sample_format *asf, ao_option *options, + ao_device **result) +{ + const char *msg; + ao_device *dev = ao_open_live(id, asf, options); + + if (dev) { + *result = dev; + return 1; + } + switch (errno) { + case AO_ENODRIVER: + msg = "No driver corresponds to driver_id"; + break; + case AO_ENOTLIVE: + msg = "This driver is not a live output device"; + break; + case AO_EBADOPTION: + msg = "A valid option key has an invalid value"; + break; + case AO_EOPENDEVICE: + msg = "Cannot open the device"; + break; + case AO_EFAIL: + msg = "General libao error"; + break; + default: + msg = "Unknown ao error"; + break; + } + PARA_ERROR_LOG("%s\n", msg); + return -E_AO_OPEN_LIVE; +} + +static int aow_init(struct writer_node *wn, unsigned sample_rate, + unsigned channels, int sample_format) +{ + int id, ret, i; + ao_option *aoo = NULL; + ao_sample_format asf; + ao_info *info; + struct private_aow_data *pawd = para_malloc(sizeof(*pawd)); + struct ao_write_args_info *conf = wn->conf; + + ao_initialize(); + if (conf->driver_given) { + ret = -E_AO_BAD_DRIVER; + id = ao_driver_id(conf->driver_arg); + } else { + ret = -E_AO_DEFAULT_DRIVER; + id = ao_default_driver_id(); + } + if (id < 0) + goto fail; + info = ao_driver_info(id); + assert(info && info->short_name); + if (info->type == AO_TYPE_FILE) { + ret = -E_AO_FILE_NOT_SUPP; + goto fail; + } + PARA_INFO_LOG("using %s driver\n", info->short_name); + for (i = 0; i < conf->ao_option_given; i++) { + char *o = para_strdup(conf->ao_option_arg[i]), *value; + + ret = -E_AO_BAD_OPTION; + value = strchr(o, ':'); + if (!value) { + free(o); + goto fail; + } + *value = '\0'; + value++; + PARA_INFO_LOG("appending option: key=%s, value=%s\n", o, value); + ret = ao_append_option(&aoo, o, value); + free(o); + if (ret == 0) { + ret = -E_AO_APPEND_OPTION; + goto fail; + } + } + ret = aow_set_sample_format(sample_rate, channels, sample_format, &asf); + if (ret < 0) + goto fail; + if (sample_format == SF_S8 || sample_format == SF_U8) + pawd->bytes_per_frame = channels; + else + pawd->bytes_per_frame = channels * 2; + ret = aow_open_device(id, &asf, aoo, &pawd->dev); + if (ret < 0) + goto fail; + PARA_INFO_LOG("successfully opened %s\n", info->short_name); + wn->private_data = pawd; + return 1; +fail: + free(pawd); + return ret; +} + +__noreturn static void *aow_play(void *priv) +{ + struct writer_node *wn = priv; + struct private_aow_data *pawd = wn->private_data; + struct btr_node *btrn = pawd->thread_btrn; + size_t frames, bytes; + char *data; + int ret; + + for (;;) { + /* + * Lock mutex and wait for signal. pthread_cond_wait() will + * automatically and atomically unlock mutex while it waits. + */ + pthread_mutex_lock(&pawd->mutex); + for (;;) { + ret = btr_node_status(btrn, wn->min_iqs, BTR_NT_LEAF); + if (ret < 0) + goto unlock; + if (ret > 0) { + btr_merge(btrn, wn->min_iqs); + bytes = btr_next_buffer(btrn, &data); + frames = bytes / pawd->bytes_per_frame; + if (frames > 0) + break; + /* eof and less than a single frame available */ + ret = -E_AO_EOF; + goto unlock; + } + //PARA_CRIT_LOG("waiting for data\n"); + //usleep(1000); + //pthread_mutex_unlock(&pawd->mutex); + pthread_cond_wait(&pawd->data_available, &pawd->mutex); + } + pthread_mutex_unlock(&pawd->mutex); + assert(frames > 0); + bytes = frames * pawd->bytes_per_frame; + ret = -E_AO_PLAY; + if (ao_play(pawd->dev, data, bytes) == 0) /* failure */ + goto out; + btr_consume(btrn, bytes); + } +unlock: + pthread_mutex_unlock(&pawd->mutex); +out: + assert(ret < 0); + PARA_NOTICE_LOG("%s\n", para_strerror(-ret)); + pthread_exit(NULL); +} + +static int aow_create_thread(struct writer_node *wn) +{ + struct private_aow_data *pawd = wn->private_data; + int ret; + const char *msg; + + /* initialize with default attributes */ + msg = "could not init mutex"; + ret = pthread_mutex_init(&pawd->mutex, NULL); + if (ret < 0) + goto fail; + + msg = "could not initialize condition variable"; + ret = pthread_cond_init(&pawd->data_available, NULL); + if (ret < 0) + goto fail; + + msg = "could not initialize thread attributes"; + ret = pthread_attr_init(&pawd->attr); + if (ret < 0) + goto fail; + + /* schedule this thread under the real-time policy SCHED_FIFO */ + msg = "could not set sched policy"; + ret = pthread_attr_setschedpolicy(&pawd->attr, SCHED_FIFO); + if (ret < 0) + goto fail; + + msg = "could not set detach state to joinable"; + ret = pthread_attr_setdetachstate(&pawd->attr, PTHREAD_CREATE_JOINABLE); + if (ret < 0) + goto fail; + + msg = "could not create thread"; + ret = pthread_create(&pawd->thread, &pawd->attr, aow_play, wn); + if (ret < 0) + goto fail; + return 1; +fail: + PARA_ERROR_LOG("%s (%s)\n", msg, strerror(ret)); + return -E_AO_PTHREAD; +} + +static void aow_post_select(__a_unused struct sched *s, + struct task *t) +{ + struct writer_node *wn = container_of(t, struct writer_node, task); + struct btr_node *btrn = wn->btrn; + struct private_aow_data *pawd = wn->private_data; + int ret; + + if (!pawd) { + int32_t rate, ch, format; + struct btr_node_description bnd; + + ret = btr_node_status(btrn, wn->min_iqs, BTR_NT_LEAF); + if (ret < 0) + goto remove_btrn; + if (ret == 0) + return; + get_btr_sample_rate(btrn, &rate); + get_btr_channels(btrn, &ch); + get_btr_sample_format(btrn, &format); + ret = aow_init(wn, rate, ch, format); + if (ret < 0) + goto remove_btrn; + pawd = wn->private_data; + + /* set up thread btr node */ + bnd.name = "ao_thread_btrn"; + bnd.parent = btrn; + bnd.child = NULL; + bnd.handler = NULL; + bnd.context = pawd; + pawd->thread_btrn = btr_new_node(&bnd); + wn->private_data = pawd; + + ret = aow_create_thread(wn); + if (ret < 0) + goto remove_thread_btrn; + return; + } + pthread_mutex_lock(&pawd->mutex); + ret = btr_node_status(btrn, wn->min_iqs, BTR_NT_LEAF); + if (ret > 0) { + btr_pushdown(btrn); + pthread_cond_signal(&pawd->data_available); + } + pthread_mutex_unlock(&pawd->mutex); + if (ret >= 0) + goto out; + pthread_mutex_lock(&pawd->mutex); + btr_remove_node(btrn); + btrn = NULL; + PARA_INFO_LOG("waiting for thread to terminate\n"); + pthread_cond_signal(&pawd->data_available); + pthread_mutex_unlock(&pawd->mutex); + pthread_join(pawd->thread, NULL); +remove_thread_btrn: + btr_remove_node(pawd->thread_btrn); + btr_free_node(pawd->thread_btrn); +remove_btrn: + if (btrn) + btr_remove_node(btrn); +out: + t->error = ret; +} + +__malloc static void *aow_parse_config_or_die(const char *options) +{ + struct ao_write_args_info *conf = para_calloc(sizeof(*conf)); + + /* exits on errors */ + ao_cmdline_parser_string(options, conf, "ao_write"); + return conf; +} + +static void aow_free_config(void *conf) +{ + ao_cmdline_parser_free(conf); +} + +/** + * The init function of the ao writer. + * + * \param w Pointer to the writer to initialize. + * + * \sa struct writer. + */ +void ao_write_init(struct writer *w) +{ + struct ao_write_args_info dummy; + int i, j, num_drivers, num_lines; + ao_info **driver_list; + char **dh; /* detailed help */ + + ao_cmdline_parser_init(&dummy); + w->close = aow_close; + w->pre_select = aow_pre_select; + w->post_select = aow_post_select; + w->parse_config_or_die = aow_parse_config_or_die; + w->free_config = aow_free_config; + w->shutdown = NULL; + w->help = (struct ggo_help) { + .short_help = ao_write_args_info_help, + }; + /* create detailed help containing all supported drivers/options */ + for (i = 0; ao_write_args_info_detailed_help[i]; i++) + ; /* nothing */ + num_lines = i; + dh = para_malloc((num_lines + 3) * sizeof(char *)); + for (i = 0; i < num_lines; i++) + dh[i] = para_strdup(ao_write_args_info_detailed_help[i]); + dh[num_lines++] = para_strdup("libao drivers available on this host:"); + dh[num_lines++] = para_strdup("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); + + ao_initialize(); + driver_list = ao_driver_info_list(&num_drivers); + + for (i = 0; i < num_drivers; i++) { + ao_info *info = driver_list[i]; + char *keys = NULL, *tmp = NULL; + + if (info->type == AO_TYPE_FILE) + continue; + for (j = 0; j < info->option_count; j++) { + tmp = make_message("%s%s%s", keys? keys : "", + keys? ", " : "", + info->options[j]); + free(keys); + keys = tmp; + } + dh = para_realloc(dh, (num_lines + 6) * sizeof(char *)); + dh[num_lines++] = make_message("%s: %s", info->short_name, info->name); + dh[num_lines++] = make_message("priority: %d", info->priority); + dh[num_lines++] = make_message("keys: %s", keys? keys : "[none]"); + dh[num_lines++] = make_message("comment: %s", info->comment? + info->comment : "[none]"); + dh[num_lines++] = para_strdup(NULL); + free(keys); + } + dh[num_lines] = NULL; + w->help.detailed_help = (const char **)dh; + ao_cmdline_parser_free(&dummy); + ao_shutdown(); +} + diff --git a/configure.ac b/configure.ac index e5a23438..ac840618 100644 --- a/configure.ac +++ b/configure.ac @@ -721,6 +721,67 @@ fi CPPFLAGS="$OLD_CPPFLAGS" LDFLAGS="$OLD_LDFLAGS" LIBS="$OLD_LIBS" +########################################################################### libao +OLD_CPPFLAGS="$CPPFLAGS" +OLD_LD_FLAGS="$LDFLAGS" +OLD_LIBS="$LIBS" + +have_ao="yes" +AC_ARG_WITH(ao_headers, [AC_HELP_STRING(--with-ao-headers=dir, + [look for ao/ao.h also in dir])]) +if test -n "$with_ao_headers"; then + ao_cppflags="-I$with_ao_headers" + CPPFLAGS="$CPPFLAGS $ao_cppflags" +fi +AC_ARG_WITH(ao_libs, [AC_HELP_STRING(--with-ao-libs=dir, + [look for libao also in dir])]) +if test -n "$with_ao_libs"; then + ao_libs="-L$with_ao_libs" + LDFLAGS="$LDFLAGS $ao_libs" +fi +msg="no libao support for para_audiod/para_write" +AC_CHECK_HEADERS([ao/ao.h], [ + ], [ + have_ao="no" + AC_MSG_WARN([ao.h not found, $msg]) +]) +if test "$have_ao" = "yes"; then + AC_CHECK_LIB([ao], [ao_initialize], [], [ + have_ao="no" + AC_MSG_WARN([ao lib not found or not working, $msg]) + ]) +fi +if test "$have_ao" = "yes"; then + AC_CHECK_HEADERS([pthread.h], [ + ], [ + have_ao="no" + AC_MSG_WARN([pthread.h not found, $msg]) + ]) +fi +if test "$have_ao" = "yes"; then + AC_CHECK_LIB([pthread], [pthread_create], [], [ + have_ao="no" + AC_MSG_WARN([pthread lib not found or not working, $msg]) + ]) +fi +if test "$have_ao" = "yes"; then + all_errlist_objs="$all_errlist_objs ao_write" + audiod_errlist_objs="$audiod_errlist_objs ao_write" + audiod_cmdline_objs="$audiod_cmdline_objs add_cmdline(ao_write)" + audiod_ldflags="$audiod_ldflags -lao -lpthread" + + write_errlist_objs="$write_errlist_objs ao_write" + write_cmdline_objs="$write_cmdline_objs add_cmdline(ao_write)" + write_ldflags="$write_ldflags $ao_libs -lao -lpthread" + writers="$writers ao" + AC_SUBST(ao_cppflags) +fi + +CPPFLAGS="$OLD_CPPFLAGS" +LDFLAGS="$OLD_LDFLAGS" +LIBS="$OLD_LIBS" + + AC_SUBST(install_sh, [$INSTALL]) AC_CONFIG_FILES([Makefile]) diff --git a/error.h b/error.h index fdd2d3b7..26e6fe00 100644 --- a/error.h +++ b/error.h @@ -105,6 +105,19 @@ extern const char **para_errlist[]; PARA_ERROR(BAD_SAMPLERATE, "sample rate not supported"), \ +#define AO_WRITE_ERRORS \ + PARA_ERROR(AO_DEFAULT_DRIVER, "ao: no usable output device"), \ + PARA_ERROR(AO_BAD_DRIVER, "ao: invalid driver"), \ + PARA_ERROR(AO_BAD_OPTION, "ao option is not of type key:value"), \ + PARA_ERROR(AO_APPEND_OPTION, "ao append option: memory allocation failure"), \ + PARA_ERROR(AO_OPEN_LIVE, "ao: could not open audio device"), \ + PARA_ERROR(AO_FILE_NOT_SUPP, "ao: file io drivers not supported"), \ + PARA_ERROR(AO_EOF, "ao: end of file"), \ + PARA_ERROR(AO_PLAY, "ao_play() failed"), \ + PARA_ERROR(AO_BAD_SAMPLE_FORMAT, "ao: unsigned sample formats not supported"), \ + PARA_ERROR(AO_PTHREAD, "pthread error"), \ + + #define COMPRESS_FILTER_ERRORS \ PARA_ERROR(COMPRESS_SYNTAX, "syntax error in compress filter config"), \ PARA_ERROR(COMPRESS_EOF, "compress: end of file"), \ diff --git a/ggo/.gitignore b/ggo/.gitignore index a0824cf8..8b70bd4f 100644 --- a/ggo/.gitignore +++ b/ggo/.gitignore @@ -8,3 +8,4 @@ gui.ggo recv.ggo server.ggo write.ggo +ao_write.ggo diff --git a/ggo/ao_write.m4 b/ggo/ao_write.m4 new file mode 100644 index 00000000..baccc57f --- /dev/null +++ b/ggo/ao_write.m4 @@ -0,0 +1,26 @@ +include(header.m4) + + +option "driver" d +#~~~~~~~~~~~~~~~~ +"Select a output driver by name" +string typestr = "name" +optional +details = " + If this is not given, the driver with the highest priority + (see below) will be used. +" + +option "ao-option" o +#~~~~~~~~~~~~~~~~~~~ +"Pass a key-value pair to the libao driver" +string typestr = "key:value" +optional +multiple +details = " + For each time this option is given, the supplied key-value + pair is appended to the list of options for the driver. Invalid + keys are silently ignored. +" + + diff --git a/web/manual.m4 b/web/manual.m4 index 30fe922f..03726956 100644 --- a/web/manual.m4 +++ b/web/manual.m4 @@ -251,6 +251,10 @@ Optional: Linux, you'll need to have ALSA's development package libasound2-dev installed. + - XREFERENCE(http://downloads.xiph.org/releases/ao/, + libao). Needed to build the ao writer (ESD, PulseAudio,...). + Debian package: libao-dev. + Installation ~~~~~~~~~~~~ @@ -1678,6 +1682,11 @@ write the PCM data to a file on the file system rather than playing it through a sound device. It is supported on all platforms and is always compiled in. +*AO*. _Libao_ is a cross-platform audio library which supports a wide +variety of platforms including PulseAudio (gnome), ESD (Enlightened +Sound Daemon), AIX, Solaris and IRIX. The ao writer plays audio +through an output plugin of libao. + Examples ~~~~~~~~ -- 2.39.5