aboutsummaryrefslogtreecommitdiffstats
path: root/src/output/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'src/output/plugins')
-rw-r--r--src/output/plugins/AlsaOutputPlugin.cxx895
-rw-r--r--src/output/plugins/AlsaOutputPlugin.hxx25
-rw-r--r--src/output/plugins/AoOutputPlugin.cxx282
-rw-r--r--src/output/plugins/AoOutputPlugin.hxx25
-rw-r--r--src/output/plugins/FifoOutputPlugin.cxx307
-rw-r--r--src/output/plugins/FifoOutputPlugin.hxx25
-rw-r--r--src/output/plugins/JackOutputPlugin.cxx762
-rw-r--r--src/output/plugins/JackOutputPlugin.hxx25
-rw-r--r--src/output/plugins/NullOutputPlugin.cxx138
-rw-r--r--src/output/plugins/NullOutputPlugin.hxx25
-rw-r--r--src/output/plugins/OSXOutputPlugin.cxx431
-rw-r--r--src/output/plugins/OSXOutputPlugin.hxx25
-rw-r--r--src/output/plugins/OpenALOutputPlugin.cxx282
-rw-r--r--src/output/plugins/OpenALOutputPlugin.hxx25
-rw-r--r--src/output/plugins/OssOutputPlugin.cxx779
-rw-r--r--src/output/plugins/OssOutputPlugin.hxx25
-rw-r--r--src/output/plugins/PipeOutputPlugin.cxx143
-rw-r--r--src/output/plugins/PipeOutputPlugin.hxx25
-rw-r--r--src/output/plugins/PulseOutputPlugin.cxx882
-rw-r--r--src/output/plugins/PulseOutputPlugin.hxx46
-rw-r--r--src/output/plugins/RecorderOutputPlugin.cxx258
-rw-r--r--src/output/plugins/RecorderOutputPlugin.hxx25
-rw-r--r--src/output/plugins/RoarOutputPlugin.cxx429
-rw-r--r--src/output/plugins/RoarOutputPlugin.hxx33
-rw-r--r--src/output/plugins/ShoutOutputPlugin.cxx537
-rw-r--r--src/output/plugins/ShoutOutputPlugin.hxx25
-rw-r--r--src/output/plugins/SolarisOutputPlugin.cxx198
-rw-r--r--src/output/plugins/SolarisOutputPlugin.hxx25
-rw-r--r--src/output/plugins/WinmmOutputPlugin.cxx352
-rw-r--r--src/output/plugins/WinmmOutputPlugin.hxx42
-rw-r--r--src/output/plugins/httpd/HttpdClient.cxx484
-rw-r--r--src/output/plugins/httpd/HttpdClient.hxx193
-rw-r--r--src/output/plugins/httpd/HttpdInternal.hxx268
-rw-r--r--src/output/plugins/httpd/HttpdOutputPlugin.cxx601
-rw-r--r--src/output/plugins/httpd/HttpdOutputPlugin.hxx25
-rw-r--r--src/output/plugins/httpd/IcyMetaDataServer.cxx134
-rw-r--r--src/output/plugins/httpd/IcyMetaDataServer.hxx39
-rw-r--r--src/output/plugins/httpd/Page.cxx70
-rw-r--r--src/output/plugins/httpd/Page.hxx102
-rw-r--r--src/output/plugins/sles/AndroidSimpleBufferQueue.hxx67
-rw-r--r--src/output/plugins/sles/Engine.hxx68
-rw-r--r--src/output/plugins/sles/Object.hxx64
-rw-r--r--src/output/plugins/sles/Play.hxx52
-rw-r--r--src/output/plugins/sles/SlesOutputPlugin.cxx539
-rw-r--r--src/output/plugins/sles/SlesOutputPlugin.hxx25
45 files changed, 9827 insertions, 0 deletions
diff --git a/src/output/plugins/AlsaOutputPlugin.cxx b/src/output/plugins/AlsaOutputPlugin.cxx
new file mode 100644
index 000000000..33a090274
--- /dev/null
+++ b/src/output/plugins/AlsaOutputPlugin.cxx
@@ -0,0 +1,895 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "AlsaOutputPlugin.hxx"
+#include "../OutputAPI.hxx"
+#include "mixer/MixerList.hxx"
+#include "pcm/PcmExport.hxx"
+#include "config/ConfigError.hxx"
+#include "util/Manual.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+#include "util/ConstBuffer.hxx"
+#include "Log.hxx"
+
+#include <alsa/asoundlib.h>
+
+#include <string>
+
+#if SND_LIB_VERSION >= 0x1001c
+/* alsa-lib supports DSD since version 1.0.27.1 */
+#define HAVE_ALSA_DSD
+#endif
+
+static const char default_device[] = "default";
+
+static constexpr unsigned MPD_ALSA_BUFFER_TIME_US = 500000;
+
+static constexpr unsigned MPD_ALSA_RETRY_NR = 5;
+
+typedef snd_pcm_sframes_t alsa_writei_t(snd_pcm_t * pcm, const void *buffer,
+ snd_pcm_uframes_t size);
+
+struct AlsaOutput {
+ AudioOutput base;
+
+ Manual<PcmExport> pcm_export;
+
+ /**
+ * The configured name of the ALSA device; empty for the
+ * default device
+ */
+ std::string device;
+
+ /** use memory mapped I/O? */
+ bool use_mmap;
+
+ /**
+ * Enable DSD over PCM according to the DoP standard standard?
+ *
+ * @see http://dsd-guide.com/dop-open-standard
+ */
+ bool dop;
+
+ /** libasound's buffer_time setting (in microseconds) */
+ unsigned int buffer_time;
+
+ /** libasound's period_time setting (in microseconds) */
+ unsigned int period_time;
+
+ /** the mode flags passed to snd_pcm_open */
+ int mode;
+
+ /** the libasound PCM device handle */
+ snd_pcm_t *pcm;
+
+ /**
+ * a pointer to the libasound writei() function, which is
+ * snd_pcm_writei() or snd_pcm_mmap_writei(), depending on the
+ * use_mmap configuration
+ */
+ alsa_writei_t *writei;
+
+ /**
+ * The size of one audio frame passed to method play().
+ */
+ size_t in_frame_size;
+
+ /**
+ * The size of one audio frame passed to libasound.
+ */
+ size_t out_frame_size;
+
+ /**
+ * The size of one period, in number of frames.
+ */
+ snd_pcm_uframes_t period_frames;
+
+ /**
+ * The number of frames written in the current period.
+ */
+ snd_pcm_uframes_t period_position;
+
+ /**
+ * Do we need to call snd_pcm_prepare() before the next write?
+ * It means that we put the device to SND_PCM_STATE_SETUP by
+ * calling snd_pcm_drop().
+ *
+ * Without this flag, we could easily recover after a failed
+ * optimistic write (returning -EBADFD), but the Raspberry Pi
+ * audio driver is infamous for generating ugly artefacts from
+ * this.
+ */
+ bool must_prepare;
+
+ /**
+ * This buffer gets allocated after opening the ALSA device.
+ * It contains silence samples, enough to fill one period (see
+ * #period_frames).
+ */
+ uint8_t *silence;
+
+ AlsaOutput()
+ :base(alsa_output_plugin),
+ mode(0), writei(snd_pcm_writei) {
+ }
+
+ bool Configure(const config_param &param, Error &error);
+};
+
+static constexpr Domain alsa_output_domain("alsa_output");
+
+static const char *
+alsa_device(const AlsaOutput *ad)
+{
+ return ad->device.empty() ? default_device : ad->device.c_str();
+}
+
+inline bool
+AlsaOutput::Configure(const config_param &param, Error &error)
+{
+ if (!base.Configure(param, error))
+ return false;
+
+ device = param.GetBlockValue("device", "");
+
+ use_mmap = param.GetBlockValue("use_mmap", false);
+
+ dop = param.GetBlockValue("dop", false) ||
+ /* legacy name from MPD 0.18 and older: */
+ param.GetBlockValue("dsd_usb", false);
+
+ buffer_time = param.GetBlockValue("buffer_time",
+ MPD_ALSA_BUFFER_TIME_US);
+ period_time = param.GetBlockValue("period_time", 0u);
+
+#ifdef SND_PCM_NO_AUTO_RESAMPLE
+ if (!param.GetBlockValue("auto_resample", true))
+ mode |= SND_PCM_NO_AUTO_RESAMPLE;
+#endif
+
+#ifdef SND_PCM_NO_AUTO_CHANNELS
+ if (!param.GetBlockValue("auto_channels", true))
+ mode |= SND_PCM_NO_AUTO_CHANNELS;
+#endif
+
+#ifdef SND_PCM_NO_AUTO_FORMAT
+ if (!param.GetBlockValue("auto_format", true))
+ mode |= SND_PCM_NO_AUTO_FORMAT;
+#endif
+
+ return true;
+}
+
+static AudioOutput *
+alsa_init(const config_param &param, Error &error)
+{
+ AlsaOutput *ad = new AlsaOutput();
+
+ if (!ad->Configure(param, error)) {
+ delete ad;
+ return nullptr;
+ }
+
+ return &ad->base;
+}
+
+static void
+alsa_finish(AudioOutput *ao)
+{
+ AlsaOutput *ad = (AlsaOutput *)ao;
+
+ delete ad;
+
+ /* free libasound's config cache */
+ snd_config_update_free_global();
+}
+
+static bool
+alsa_output_enable(AudioOutput *ao, gcc_unused Error &error)
+{
+ AlsaOutput *ad = (AlsaOutput *)ao;
+
+ ad->pcm_export.Construct();
+ return true;
+}
+
+static void
+alsa_output_disable(AudioOutput *ao)
+{
+ AlsaOutput *ad = (AlsaOutput *)ao;
+
+ ad->pcm_export.Destruct();
+}
+
+static bool
+alsa_test_default_device()
+{
+ snd_pcm_t *handle;
+
+ int ret = snd_pcm_open(&handle, default_device,
+ SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
+ if (ret) {
+ FormatError(alsa_output_domain,
+ "Error opening default ALSA device: %s",
+ snd_strerror(-ret));
+ return false;
+ } else
+ snd_pcm_close(handle);
+
+ return true;
+}
+
+/**
+ * Convert MPD's #SampleFormat enum to libasound's snd_pcm_format_t
+ * enum. Returns SND_PCM_FORMAT_UNKNOWN if there is no according ALSA
+ * PCM format.
+ */
+static snd_pcm_format_t
+get_bitformat(SampleFormat sample_format)
+{
+ switch (sample_format) {
+ case SampleFormat::UNDEFINED:
+ return SND_PCM_FORMAT_UNKNOWN;
+
+ case SampleFormat::DSD:
+#ifdef HAVE_ALSA_DSD
+ return SND_PCM_FORMAT_DSD_U8;
+#else
+ return SND_PCM_FORMAT_UNKNOWN;
+#endif
+
+ case SampleFormat::S8:
+ return SND_PCM_FORMAT_S8;
+
+ case SampleFormat::S16:
+ return SND_PCM_FORMAT_S16;
+
+ case SampleFormat::S24_P32:
+ return SND_PCM_FORMAT_S24;
+
+ case SampleFormat::S32:
+ return SND_PCM_FORMAT_S32;
+
+ case SampleFormat::FLOAT:
+ return SND_PCM_FORMAT_FLOAT;
+ }
+
+ assert(false);
+ gcc_unreachable();
+}
+
+/**
+ * Determine the byte-swapped PCM format. Returns
+ * SND_PCM_FORMAT_UNKNOWN if the format cannot be byte-swapped.
+ */
+static snd_pcm_format_t
+byteswap_bitformat(snd_pcm_format_t fmt)
+{
+ switch (fmt) {
+ case SND_PCM_FORMAT_S16_LE: return SND_PCM_FORMAT_S16_BE;
+ case SND_PCM_FORMAT_S24_LE: return SND_PCM_FORMAT_S24_BE;
+ case SND_PCM_FORMAT_S32_LE: return SND_PCM_FORMAT_S32_BE;
+ case SND_PCM_FORMAT_S16_BE: return SND_PCM_FORMAT_S16_LE;
+ case SND_PCM_FORMAT_S24_BE: return SND_PCM_FORMAT_S24_LE;
+
+ case SND_PCM_FORMAT_S24_3BE:
+ return SND_PCM_FORMAT_S24_3LE;
+
+ case SND_PCM_FORMAT_S24_3LE:
+ return SND_PCM_FORMAT_S24_3BE;
+
+ case SND_PCM_FORMAT_S32_BE: return SND_PCM_FORMAT_S32_LE;
+ default: return SND_PCM_FORMAT_UNKNOWN;
+ }
+}
+
+/**
+ * Check if there is a "packed" version of the give PCM format.
+ * Returns SND_PCM_FORMAT_UNKNOWN if not.
+ */
+static snd_pcm_format_t
+alsa_to_packed_format(snd_pcm_format_t fmt)
+{
+ switch (fmt) {
+ case SND_PCM_FORMAT_S24_LE:
+ return SND_PCM_FORMAT_S24_3LE;
+
+ case SND_PCM_FORMAT_S24_BE:
+ return SND_PCM_FORMAT_S24_3BE;
+
+ default:
+ return SND_PCM_FORMAT_UNKNOWN;
+ }
+}
+
+/**
+ * Attempts to configure the specified sample format. On failure,
+ * fall back to the packed version.
+ */
+static int
+alsa_try_format_or_packed(snd_pcm_t *pcm, snd_pcm_hw_params_t *hwparams,
+ snd_pcm_format_t fmt, bool *packed_r)
+{
+ int err = snd_pcm_hw_params_set_format(pcm, hwparams, fmt);
+ if (err == 0)
+ *packed_r = false;
+
+ if (err != -EINVAL)
+ return err;
+
+ fmt = alsa_to_packed_format(fmt);
+ if (fmt == SND_PCM_FORMAT_UNKNOWN)
+ return -EINVAL;
+
+ err = snd_pcm_hw_params_set_format(pcm, hwparams, fmt);
+ if (err == 0)
+ *packed_r = true;
+
+ return err;
+}
+
+/**
+ * Attempts to configure the specified sample format, and tries the
+ * reversed host byte order if was not supported.
+ */
+static int
+alsa_output_try_format(snd_pcm_t *pcm, snd_pcm_hw_params_t *hwparams,
+ SampleFormat sample_format,
+ bool *packed_r, bool *reverse_endian_r)
+{
+ snd_pcm_format_t alsa_format = get_bitformat(sample_format);
+ if (alsa_format == SND_PCM_FORMAT_UNKNOWN)
+ return -EINVAL;
+
+ int err = alsa_try_format_or_packed(pcm, hwparams, alsa_format,
+ packed_r);
+ if (err == 0)
+ *reverse_endian_r = false;
+
+ if (err != -EINVAL)
+ return err;
+
+ alsa_format = byteswap_bitformat(alsa_format);
+ if (alsa_format == SND_PCM_FORMAT_UNKNOWN)
+ return -EINVAL;
+
+ err = alsa_try_format_or_packed(pcm, hwparams, alsa_format, packed_r);
+ if (err == 0)
+ *reverse_endian_r = true;
+
+ return err;
+}
+
+/**
+ * Configure a sample format, and probe other formats if that fails.
+ */
+static int
+alsa_output_setup_format(snd_pcm_t *pcm, snd_pcm_hw_params_t *hwparams,
+ AudioFormat &audio_format,
+ bool *packed_r, bool *reverse_endian_r)
+{
+ /* try the input format first */
+
+ int err = alsa_output_try_format(pcm, hwparams,
+ audio_format.format,
+ packed_r, reverse_endian_r);
+
+ /* if unsupported by the hardware, try other formats */
+
+ static constexpr SampleFormat probe_formats[] = {
+ SampleFormat::S24_P32,
+ SampleFormat::S32,
+ SampleFormat::S16,
+ SampleFormat::S8,
+ SampleFormat::UNDEFINED,
+ };
+
+ for (unsigned i = 0;
+ err == -EINVAL && probe_formats[i] != SampleFormat::UNDEFINED;
+ ++i) {
+ const SampleFormat mpd_format = probe_formats[i];
+ if (mpd_format == audio_format.format)
+ continue;
+
+ err = alsa_output_try_format(pcm, hwparams, mpd_format,
+ packed_r, reverse_endian_r);
+ if (err == 0)
+ audio_format.format = mpd_format;
+ }
+
+ return err;
+}
+
+/**
+ * Set up the snd_pcm_t object which was opened by the caller. Set up
+ * the configured settings and the audio format.
+ */
+static bool
+alsa_setup(AlsaOutput *ad, AudioFormat &audio_format,
+ bool *packed_r, bool *reverse_endian_r, Error &error)
+{
+ unsigned int sample_rate = audio_format.sample_rate;
+ unsigned int channels = audio_format.channels;
+ int err;
+ const char *cmd = nullptr;
+ unsigned retry = MPD_ALSA_RETRY_NR;
+ unsigned int period_time, period_time_ro;
+ unsigned int buffer_time;
+
+ period_time_ro = period_time = ad->period_time;
+configure_hw:
+ /* configure HW params */
+ snd_pcm_hw_params_t *hwparams;
+ snd_pcm_hw_params_alloca(&hwparams);
+ cmd = "snd_pcm_hw_params_any";
+ err = snd_pcm_hw_params_any(ad->pcm, hwparams);
+ if (err < 0)
+ goto error;
+
+ if (ad->use_mmap) {
+ err = snd_pcm_hw_params_set_access(ad->pcm, hwparams,
+ SND_PCM_ACCESS_MMAP_INTERLEAVED);
+ if (err < 0) {
+ FormatWarning(alsa_output_domain,
+ "Cannot set mmap'ed mode on ALSA device \"%s\": %s",
+ alsa_device(ad), snd_strerror(-err));
+ LogWarning(alsa_output_domain,
+ "Falling back to direct write mode");
+ ad->use_mmap = false;
+ } else
+ ad->writei = snd_pcm_mmap_writei;
+ }
+
+ if (!ad->use_mmap) {
+ cmd = "snd_pcm_hw_params_set_access";
+ err = snd_pcm_hw_params_set_access(ad->pcm, hwparams,
+ SND_PCM_ACCESS_RW_INTERLEAVED);
+ if (err < 0)
+ goto error;
+ ad->writei = snd_pcm_writei;
+ }
+
+ err = alsa_output_setup_format(ad->pcm, hwparams, audio_format,
+ packed_r, reverse_endian_r);
+ if (err < 0) {
+ error.Format(alsa_output_domain, err,
+ "ALSA device \"%s\" does not support format %s: %s",
+ alsa_device(ad),
+ sample_format_to_string(audio_format.format),
+ snd_strerror(-err));
+ return false;
+ }
+
+ snd_pcm_format_t format;
+ if (snd_pcm_hw_params_get_format(hwparams, &format) == 0)
+ FormatDebug(alsa_output_domain,
+ "format=%s (%s)", snd_pcm_format_name(format),
+ snd_pcm_format_description(format));
+
+ err = snd_pcm_hw_params_set_channels_near(ad->pcm, hwparams,
+ &channels);
+ if (err < 0) {
+ error.Format(alsa_output_domain, err,
+ "ALSA device \"%s\" does not support %i channels: %s",
+ alsa_device(ad), (int)audio_format.channels,
+ snd_strerror(-err));
+ return false;
+ }
+ audio_format.channels = (int8_t)channels;
+
+ err = snd_pcm_hw_params_set_rate_near(ad->pcm, hwparams,
+ &sample_rate, nullptr);
+ if (err < 0 || sample_rate == 0) {
+ error.Format(alsa_output_domain, err,
+ "ALSA device \"%s\" does not support %u Hz audio",
+ alsa_device(ad), audio_format.sample_rate);
+ return false;
+ }
+ audio_format.sample_rate = sample_rate;
+
+ snd_pcm_uframes_t buffer_size_min, buffer_size_max;
+ snd_pcm_hw_params_get_buffer_size_min(hwparams, &buffer_size_min);
+ snd_pcm_hw_params_get_buffer_size_max(hwparams, &buffer_size_max);
+ unsigned buffer_time_min, buffer_time_max;
+ snd_pcm_hw_params_get_buffer_time_min(hwparams, &buffer_time_min, 0);
+ snd_pcm_hw_params_get_buffer_time_max(hwparams, &buffer_time_max, 0);
+ FormatDebug(alsa_output_domain, "buffer: size=%u..%u time=%u..%u",
+ (unsigned)buffer_size_min, (unsigned)buffer_size_max,
+ buffer_time_min, buffer_time_max);
+
+ snd_pcm_uframes_t period_size_min, period_size_max;
+ snd_pcm_hw_params_get_period_size_min(hwparams, &period_size_min, 0);
+ snd_pcm_hw_params_get_period_size_max(hwparams, &period_size_max, 0);
+ unsigned period_time_min, period_time_max;
+ snd_pcm_hw_params_get_period_time_min(hwparams, &period_time_min, 0);
+ snd_pcm_hw_params_get_period_time_max(hwparams, &period_time_max, 0);
+ FormatDebug(alsa_output_domain, "period: size=%u..%u time=%u..%u",
+ (unsigned)period_size_min, (unsigned)period_size_max,
+ period_time_min, period_time_max);
+
+ if (ad->buffer_time > 0) {
+ buffer_time = ad->buffer_time;
+ cmd = "snd_pcm_hw_params_set_buffer_time_near";
+ err = snd_pcm_hw_params_set_buffer_time_near(ad->pcm, hwparams,
+ &buffer_time, nullptr);
+ if (err < 0)
+ goto error;
+ } else {
+ err = snd_pcm_hw_params_get_buffer_time(hwparams, &buffer_time,
+ nullptr);
+ if (err < 0)
+ buffer_time = 0;
+ }
+
+ if (period_time_ro == 0 && buffer_time >= 10000) {
+ period_time_ro = period_time = buffer_time / 4;
+
+ FormatDebug(alsa_output_domain,
+ "default period_time = buffer_time/4 = %u/4 = %u",
+ buffer_time, period_time);
+ }
+
+ if (period_time_ro > 0) {
+ period_time = period_time_ro;
+ cmd = "snd_pcm_hw_params_set_period_time_near";
+ err = snd_pcm_hw_params_set_period_time_near(ad->pcm, hwparams,
+ &period_time, nullptr);
+ if (err < 0)
+ goto error;
+ }
+
+ cmd = "snd_pcm_hw_params";
+ err = snd_pcm_hw_params(ad->pcm, hwparams);
+ if (err == -EPIPE && --retry > 0 && period_time_ro > 0) {
+ period_time_ro = period_time_ro >> 1;
+ goto configure_hw;
+ } else if (err < 0)
+ goto error;
+ if (retry != MPD_ALSA_RETRY_NR)
+ FormatDebug(alsa_output_domain,
+ "ALSA period_time set to %d", period_time);
+
+ snd_pcm_uframes_t alsa_buffer_size;
+ cmd = "snd_pcm_hw_params_get_buffer_size";
+ err = snd_pcm_hw_params_get_buffer_size(hwparams, &alsa_buffer_size);
+ if (err < 0)
+ goto error;
+
+ snd_pcm_uframes_t alsa_period_size;
+ cmd = "snd_pcm_hw_params_get_period_size";
+ err = snd_pcm_hw_params_get_period_size(hwparams, &alsa_period_size,
+ nullptr);
+ if (err < 0)
+ goto error;
+
+ /* configure SW params */
+ snd_pcm_sw_params_t *swparams;
+ snd_pcm_sw_params_alloca(&swparams);
+
+ cmd = "snd_pcm_sw_params_current";
+ err = snd_pcm_sw_params_current(ad->pcm, swparams);
+ if (err < 0)
+ goto error;
+
+ cmd = "snd_pcm_sw_params_set_start_threshold";
+ err = snd_pcm_sw_params_set_start_threshold(ad->pcm, swparams,
+ alsa_buffer_size -
+ alsa_period_size);
+ if (err < 0)
+ goto error;
+
+ cmd = "snd_pcm_sw_params_set_avail_min";
+ err = snd_pcm_sw_params_set_avail_min(ad->pcm, swparams,
+ alsa_period_size);
+ if (err < 0)
+ goto error;
+
+ cmd = "snd_pcm_sw_params";
+ err = snd_pcm_sw_params(ad->pcm, swparams);
+ if (err < 0)
+ goto error;
+
+ FormatDebug(alsa_output_domain, "buffer_size=%u period_size=%u",
+ (unsigned)alsa_buffer_size, (unsigned)alsa_period_size);
+
+ if (alsa_period_size == 0)
+ /* this works around a SIGFPE bug that occurred when
+ an ALSA driver indicated period_size==0; this
+ caused a division by zero in alsa_play(). By using
+ the fallback "1", we make sure that this won't
+ happen again. */
+ alsa_period_size = 1;
+
+ ad->period_frames = alsa_period_size;
+ ad->period_position = 0;
+
+ ad->silence = new uint8_t[snd_pcm_frames_to_bytes(ad->pcm,
+ alsa_period_size)];
+ snd_pcm_format_set_silence(format, ad->silence,
+ alsa_period_size * channels);
+
+ return true;
+
+error:
+ error.Format(alsa_output_domain, err,
+ "Error opening ALSA device \"%s\" (%s): %s",
+ alsa_device(ad), cmd, snd_strerror(-err));
+ return false;
+}
+
+static bool
+alsa_setup_dsd(AlsaOutput *ad, const AudioFormat audio_format,
+ bool *shift8_r, bool *packed_r, bool *reverse_endian_r,
+ Error &error)
+{
+ assert(ad->dop);
+ assert(audio_format.format == SampleFormat::DSD);
+
+ /* pass 24 bit to alsa_setup() */
+
+ AudioFormat dop_format = audio_format;
+ dop_format.format = SampleFormat::S24_P32;
+ dop_format.sample_rate /= 2;
+
+ const AudioFormat check = dop_format;
+
+ if (!alsa_setup(ad, dop_format, packed_r, reverse_endian_r, error))
+ return false;
+
+ /* if the device allows only 32 bit, shift all DoP
+ samples left by 8 bit and leave the lower 8 bit cleared;
+ the DSD-over-USB documentation does not specify whether
+ this is legal, but there is anecdotical evidence that this
+ is possible (and the only option for some devices) */
+ *shift8_r = dop_format.format == SampleFormat::S32;
+ if (dop_format.format == SampleFormat::S32)
+ dop_format.format = SampleFormat::S24_P32;
+
+ if (dop_format != check) {
+ /* no bit-perfect playback, which is required
+ for DSD over USB */
+ error.Format(alsa_output_domain,
+ "Failed to configure DSD-over-PCM on ALSA device \"%s\"",
+ alsa_device(ad));
+ delete[] ad->silence;
+ return false;
+ }
+
+ return true;
+}
+
+static bool
+alsa_setup_or_dsd(AlsaOutput *ad, AudioFormat &audio_format,
+ Error &error)
+{
+ bool shift8 = false, packed, reverse_endian;
+
+ const bool dop = ad->dop &&
+ audio_format.format == SampleFormat::DSD;
+ const bool success = dop
+ ? alsa_setup_dsd(ad, audio_format,
+ &shift8, &packed, &reverse_endian,
+ error)
+ : alsa_setup(ad, audio_format, &packed, &reverse_endian,
+ error);
+ if (!success)
+ return false;
+
+ ad->pcm_export->Open(audio_format.format,
+ audio_format.channels,
+ dop, shift8, packed, reverse_endian);
+ return true;
+}
+
+static bool
+alsa_open(AudioOutput *ao, AudioFormat &audio_format, Error &error)
+{
+ AlsaOutput *ad = (AlsaOutput *)ao;
+
+ int err = snd_pcm_open(&ad->pcm, alsa_device(ad),
+ SND_PCM_STREAM_PLAYBACK, ad->mode);
+ if (err < 0) {
+ error.Format(alsa_output_domain, err,
+ "Failed to open ALSA device \"%s\": %s",
+ alsa_device(ad), snd_strerror(err));
+ return false;
+ }
+
+ FormatDebug(alsa_output_domain, "opened %s type=%s",
+ snd_pcm_name(ad->pcm),
+ snd_pcm_type_name(snd_pcm_type(ad->pcm)));
+
+ if (!alsa_setup_or_dsd(ad, audio_format, error)) {
+ snd_pcm_close(ad->pcm);
+ return false;
+ }
+
+ ad->in_frame_size = audio_format.GetFrameSize();
+ ad->out_frame_size = ad->pcm_export->GetFrameSize(audio_format);
+
+ ad->must_prepare = false;
+
+ return true;
+}
+
+/**
+ * Write silence to the ALSA device.
+ */
+static void
+alsa_write_silence(AlsaOutput *ad, snd_pcm_uframes_t nframes)
+{
+ ad->writei(ad->pcm, ad->silence, nframes);
+}
+
+static int
+alsa_recover(AlsaOutput *ad, int err)
+{
+ if (err == -EPIPE) {
+ FormatDebug(alsa_output_domain,
+ "Underrun on ALSA device \"%s\"", alsa_device(ad));
+ } else if (err == -ESTRPIPE) {
+ FormatDebug(alsa_output_domain,
+ "ALSA device \"%s\" was suspended",
+ alsa_device(ad));
+ }
+
+ switch (snd_pcm_state(ad->pcm)) {
+ case SND_PCM_STATE_PAUSED:
+ err = snd_pcm_pause(ad->pcm, /* disable */ 0);
+ break;
+ case SND_PCM_STATE_SUSPENDED:
+ err = snd_pcm_resume(ad->pcm);
+ if (err == -EAGAIN)
+ return 0;
+ /* fall-through to snd_pcm_prepare: */
+ case SND_PCM_STATE_SETUP:
+ case SND_PCM_STATE_XRUN:
+ ad->period_position = 0;
+ err = snd_pcm_prepare(ad->pcm);
+ break;
+ case SND_PCM_STATE_DISCONNECTED:
+ break;
+ /* this is no error, so just keep running */
+ case SND_PCM_STATE_RUNNING:
+ err = 0;
+ break;
+ default:
+ /* unknown state, do nothing */
+ break;
+ }
+
+ return err;
+}
+
+static void
+alsa_drain(AudioOutput *ao)
+{
+ AlsaOutput *ad = (AlsaOutput *)ao;
+
+ if (snd_pcm_state(ad->pcm) != SND_PCM_STATE_RUNNING)
+ return;
+
+ if (ad->period_position > 0) {
+ /* generate some silence to finish the partial
+ period */
+ snd_pcm_uframes_t nframes =
+ ad->period_frames - ad->period_position;
+ alsa_write_silence(ad, nframes);
+ }
+
+ snd_pcm_drain(ad->pcm);
+
+ ad->period_position = 0;
+}
+
+static void
+alsa_cancel(AudioOutput *ao)
+{
+ AlsaOutput *ad = (AlsaOutput *)ao;
+
+ ad->period_position = 0;
+ ad->must_prepare = true;
+
+ snd_pcm_drop(ad->pcm);
+}
+
+static void
+alsa_close(AudioOutput *ao)
+{
+ AlsaOutput *ad = (AlsaOutput *)ao;
+
+ snd_pcm_close(ad->pcm);
+ delete[] ad->silence;
+}
+
+static size_t
+alsa_play(AudioOutput *ao, const void *chunk, size_t size,
+ Error &error)
+{
+ AlsaOutput *ad = (AlsaOutput *)ao;
+
+ assert(size > 0);
+ assert(size % ad->in_frame_size == 0);
+
+ if (ad->must_prepare) {
+ ad->must_prepare = false;
+
+ int err = snd_pcm_prepare(ad->pcm);
+ if (err < 0) {
+ error.Set(alsa_output_domain, err, snd_strerror(-err));
+ return 0;
+ }
+ }
+
+ const auto e = ad->pcm_export->Export({chunk, size});
+ if (e.size == 0)
+ /* the DoP (DSD over PCM) filter converts two frames
+ at a time and ignores the last odd frame; if there
+ was only one frame (e.g. the last frame in the
+ file), the result is empty; to avoid an endless
+ loop, bail out here, and pretend the one frame has
+ been played */
+ return size;
+
+ chunk = e.data;
+ size = e.size;
+
+ assert(size % ad->out_frame_size == 0);
+
+ size /= ad->out_frame_size;
+ assert(size > 0);
+
+ while (true) {
+ snd_pcm_sframes_t ret = ad->writei(ad->pcm, chunk, size);
+ if (ret > 0) {
+ ad->period_position = (ad->period_position + ret)
+ % ad->period_frames;
+
+ size_t bytes_written = ret * ad->out_frame_size;
+ return ad->pcm_export->CalcSourceSize(bytes_written);
+ }
+
+ if (ret < 0 && ret != -EAGAIN && ret != -EINTR &&
+ alsa_recover(ad, ret) < 0) {
+ error.Set(alsa_output_domain, ret, snd_strerror(-ret));
+ return 0;
+ }
+ }
+}
+
+const struct AudioOutputPlugin alsa_output_plugin = {
+ "alsa",
+ alsa_test_default_device,
+ alsa_init,
+ alsa_finish,
+ alsa_output_enable,
+ alsa_output_disable,
+ alsa_open,
+ alsa_close,
+ nullptr,
+ nullptr,
+ alsa_play,
+ alsa_drain,
+ alsa_cancel,
+ nullptr,
+
+ &alsa_mixer_plugin,
+};
diff --git a/src/output/plugins/AlsaOutputPlugin.hxx b/src/output/plugins/AlsaOutputPlugin.hxx
new file mode 100644
index 000000000..f72116f91
--- /dev/null
+++ b/src/output/plugins/AlsaOutputPlugin.hxx
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_ALSA_OUTPUT_PLUGIN_HXX
+#define MPD_ALSA_OUTPUT_PLUGIN_HXX
+
+extern const struct AudioOutputPlugin alsa_output_plugin;
+
+#endif
diff --git a/src/output/plugins/AoOutputPlugin.cxx b/src/output/plugins/AoOutputPlugin.cxx
new file mode 100644
index 000000000..af8c88fa1
--- /dev/null
+++ b/src/output/plugins/AoOutputPlugin.cxx
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "AoOutputPlugin.hxx"
+#include "../OutputAPI.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+#include "Log.hxx"
+
+#include <ao/ao.h>
+#include <glib.h>
+
+#include <string.h>
+
+/* An ao_sample_format, with all fields set to zero: */
+static ao_sample_format OUR_AO_FORMAT_INITIALIZER;
+
+static unsigned ao_output_ref;
+
+struct AoOutput {
+ AudioOutput base;
+
+ size_t write_size;
+ int driver;
+ ao_option *options;
+ ao_device *device;
+
+ AoOutput()
+ :base(ao_output_plugin) {}
+
+ bool Initialize(const config_param &param, Error &error) {
+ return base.Configure(param, error);
+ }
+
+ bool Configure(const config_param &param, Error &error);
+};
+
+static constexpr Domain ao_output_domain("ao_output");
+
+static void
+ao_output_error(Error &error_r)
+{
+ const char *error;
+
+ switch (errno) {
+ case AO_ENODRIVER:
+ error = "No such libao driver";
+ break;
+
+ case AO_ENOTLIVE:
+ error = "This driver is not a libao live device";
+ break;
+
+ case AO_EBADOPTION:
+ error = "Invalid libao option";
+ break;
+
+ case AO_EOPENDEVICE:
+ error = "Cannot open the libao device";
+ break;
+
+ case AO_EFAIL:
+ error = "Generic libao failure";
+ break;
+
+ default:
+ error_r.SetErrno();
+ return;
+ }
+
+ error_r.Set(ao_output_domain, errno, error);
+}
+
+inline bool
+AoOutput::Configure(const config_param &param, Error &error)
+{
+ const char *value;
+
+ options = nullptr;
+
+ write_size = param.GetBlockValue("write_size", 1024u);
+
+ if (ao_output_ref == 0) {
+ ao_initialize();
+ }
+ ao_output_ref++;
+
+ value = param.GetBlockValue("driver", "default");
+ if (0 == strcmp(value, "default"))
+ driver = ao_default_driver_id();
+ else
+ driver = ao_driver_id(value);
+
+ if (driver < 0) {
+ error.Format(ao_output_domain,
+ "\"%s\" is not a valid ao driver",
+ value);
+ return false;
+ }
+
+ ao_info *ai = ao_driver_info(driver);
+ if (ai == nullptr) {
+ error.Set(ao_output_domain, "problems getting driver info");
+ return false;
+ }
+
+ FormatDebug(ao_output_domain, "using ao driver \"%s\" for \"%s\"\n",
+ ai->short_name, param.GetBlockValue("name", nullptr));
+
+ value = param.GetBlockValue("options", nullptr);
+ if (value != nullptr) {
+ gchar **_options = g_strsplit(value, ";", 0);
+
+ for (unsigned i = 0; _options[i] != nullptr; ++i) {
+ gchar **key_value = g_strsplit(_options[i], "=", 2);
+
+ if (key_value[0] == nullptr || key_value[1] == nullptr) {
+ error.Format(ao_output_domain,
+ "problems parsing options \"%s\"",
+ _options[i]);
+ return false;
+ }
+
+ ao_append_option(&options, key_value[0],
+ key_value[1]);
+
+ g_strfreev(key_value);
+ }
+
+ g_strfreev(_options);
+ }
+
+ return true;
+}
+
+static AudioOutput *
+ao_output_init(const config_param &param, Error &error)
+{
+ AoOutput *ad = new AoOutput();
+
+ if (!ad->Initialize(param, error)) {
+ delete ad;
+ return nullptr;
+ }
+
+ if (!ad->Configure(param, error)) {
+ delete ad;
+ return nullptr;
+ }
+
+ return &ad->base;
+}
+
+static void
+ao_output_finish(AudioOutput *ao)
+{
+ AoOutput *ad = (AoOutput *)ao;
+
+ ao_free_options(ad->options);
+ delete ad;
+
+ ao_output_ref--;
+
+ if (ao_output_ref == 0)
+ ao_shutdown();
+}
+
+static void
+ao_output_close(AudioOutput *ao)
+{
+ AoOutput *ad = (AoOutput *)ao;
+
+ ao_close(ad->device);
+}
+
+static bool
+ao_output_open(AudioOutput *ao, AudioFormat &audio_format,
+ Error &error)
+{
+ ao_sample_format format = OUR_AO_FORMAT_INITIALIZER;
+ AoOutput *ad = (AoOutput *)ao;
+
+ switch (audio_format.format) {
+ case SampleFormat::S8:
+ format.bits = 8;
+ break;
+
+ case SampleFormat::S16:
+ format.bits = 16;
+ break;
+
+ default:
+ /* support for 24 bit samples in libao is currently
+ dubious, and until we have sorted that out,
+ convert everything to 16 bit */
+ audio_format.format = SampleFormat::S16;
+ format.bits = 16;
+ break;
+ }
+
+ format.rate = audio_format.sample_rate;
+ format.byte_format = AO_FMT_NATIVE;
+ format.channels = audio_format.channels;
+
+ ad->device = ao_open_live(ad->driver, &format, ad->options);
+
+ if (ad->device == nullptr) {
+ ao_output_error(error);
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * For whatever reason, libao wants a non-const pointer. Let's hope
+ * it does not write to the buffer, and use the union deconst hack to
+ * work around this API misdesign.
+ */
+static int ao_play_deconst(ao_device *device, const void *output_samples,
+ uint_32 num_bytes)
+{
+ union {
+ const void *in;
+ char *out;
+ } u;
+
+ u.in = output_samples;
+ return ao_play(device, u.out, num_bytes);
+}
+
+static size_t
+ao_output_play(AudioOutput *ao, const void *chunk, size_t size,
+ Error &error)
+{
+ AoOutput *ad = (AoOutput *)ao;
+
+ if (size > ad->write_size)
+ size = ad->write_size;
+
+ if (ao_play_deconst(ad->device, chunk, size) == 0) {
+ ao_output_error(error);
+ return 0;
+ }
+
+ return size;
+}
+
+const struct AudioOutputPlugin ao_output_plugin = {
+ "ao",
+ nullptr,
+ ao_output_init,
+ ao_output_finish,
+ nullptr,
+ nullptr,
+ ao_output_open,
+ ao_output_close,
+ nullptr,
+ nullptr,
+ ao_output_play,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+};
diff --git a/src/output/plugins/AoOutputPlugin.hxx b/src/output/plugins/AoOutputPlugin.hxx
new file mode 100644
index 000000000..07c2ba16b
--- /dev/null
+++ b/src/output/plugins/AoOutputPlugin.hxx
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_AO_OUTPUT_PLUGIN_HXX
+#define MPD_AO_OUTPUT_PLUGIN_HXX
+
+extern const struct AudioOutputPlugin ao_output_plugin;
+
+#endif
diff --git a/src/output/plugins/FifoOutputPlugin.cxx b/src/output/plugins/FifoOutputPlugin.cxx
new file mode 100644
index 000000000..9df5a74dd
--- /dev/null
+++ b/src/output/plugins/FifoOutputPlugin.cxx
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "FifoOutputPlugin.hxx"
+#include "config/ConfigError.hxx"
+#include "../OutputAPI.hxx"
+#include "../Timer.hxx"
+#include "fs/AllocatedPath.hxx"
+#include "fs/FileSystem.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+#include "Log.hxx"
+#include "open.h"
+
+#include <sys/stat.h>
+#include <errno.h>
+#include <unistd.h>
+
+#define FIFO_BUFFER_SIZE 65536 /* pipe capacity on Linux >= 2.6.11 */
+
+struct FifoOutput {
+ AudioOutput base;
+
+ AllocatedPath path;
+ std::string path_utf8;
+
+ int input;
+ int output;
+ bool created;
+ Timer *timer;
+
+ FifoOutput()
+ :base(fifo_output_plugin),
+ path(AllocatedPath::Null()), input(-1), output(-1),
+ created(false) {}
+
+ bool Initialize(const config_param &param, Error &error) {
+ return base.Configure(param, error);
+ }
+
+ bool Create(Error &error);
+ bool Check(Error &error);
+ void Delete();
+
+ bool Open(Error &error);
+ void Close();
+};
+
+static constexpr Domain fifo_output_domain("fifo_output");
+
+inline void
+FifoOutput::Delete()
+{
+ FormatDebug(fifo_output_domain,
+ "Removing FIFO \"%s\"", path_utf8.c_str());
+
+ if (!RemoveFile(path)) {
+ FormatErrno(fifo_output_domain,
+ "Could not remove FIFO \"%s\"",
+ path_utf8.c_str());
+ return;
+ }
+
+ created = false;
+}
+
+void
+FifoOutput::Close()
+{
+ if (input >= 0) {
+ close(input);
+ input = -1;
+ }
+
+ if (output >= 0) {
+ close(output);
+ output = -1;
+ }
+
+ struct stat st;
+ if (created && StatFile(path, st))
+ Delete();
+}
+
+inline bool
+FifoOutput::Create(Error &error)
+{
+ if (!MakeFifo(path, 0666)) {
+ error.FormatErrno("Couldn't create FIFO \"%s\"",
+ path_utf8.c_str());
+ return false;
+ }
+
+ created = true;
+ return true;
+}
+
+inline bool
+FifoOutput::Check(Error &error)
+{
+ struct stat st;
+ if (!StatFile(path, st)) {
+ if (errno == ENOENT) {
+ /* Path doesn't exist */
+ return Create(error);
+ }
+
+ error.FormatErrno("Failed to stat FIFO \"%s\"",
+ path_utf8.c_str());
+ return false;
+ }
+
+ if (!S_ISFIFO(st.st_mode)) {
+ error.Format(fifo_output_domain,
+ "\"%s\" already exists, but is not a FIFO",
+ path_utf8.c_str());
+ return false;
+ }
+
+ return true;
+}
+
+inline bool
+FifoOutput::Open(Error &error)
+{
+ if (!Check(error))
+ return false;
+
+ input = OpenFile(path, O_RDONLY|O_NONBLOCK|O_BINARY, 0);
+ if (input < 0) {
+ error.FormatErrno("Could not open FIFO \"%s\" for reading",
+ path_utf8.c_str());
+ Close();
+ return false;
+ }
+
+ output = OpenFile(path, O_WRONLY|O_NONBLOCK|O_BINARY, 0);
+ if (output < 0) {
+ error.FormatErrno("Could not open FIFO \"%s\" for writing",
+ path_utf8.c_str());
+ Close();
+ return false;
+ }
+
+ return true;
+}
+
+static bool
+fifo_open(FifoOutput *fd, Error &error)
+{
+ return fd->Open(error);
+}
+
+static AudioOutput *
+fifo_output_init(const config_param &param, Error &error)
+{
+ FifoOutput *fd = new FifoOutput();
+
+ fd->path = param.GetBlockPath("path", error);
+ if (fd->path.IsNull()) {
+ delete fd;
+
+ if (!error.IsDefined())
+ error.Set(config_domain,
+ "No \"path\" parameter specified");
+ return nullptr;
+ }
+
+ fd->path_utf8 = fd->path.ToUTF8();
+
+ if (!fd->Initialize(param, error)) {
+ delete fd;
+ return nullptr;
+ }
+
+ if (!fifo_open(fd, error)) {
+ delete fd;
+ return nullptr;
+ }
+
+ return &fd->base;
+}
+
+static void
+fifo_output_finish(AudioOutput *ao)
+{
+ FifoOutput *fd = (FifoOutput *)ao;
+
+ fd->Close();
+ delete fd;
+}
+
+static bool
+fifo_output_open(AudioOutput *ao, AudioFormat &audio_format,
+ gcc_unused Error &error)
+{
+ FifoOutput *fd = (FifoOutput *)ao;
+
+ fd->timer = new Timer(audio_format);
+
+ return true;
+}
+
+static void
+fifo_output_close(AudioOutput *ao)
+{
+ FifoOutput *fd = (FifoOutput *)ao;
+
+ delete fd->timer;
+}
+
+static void
+fifo_output_cancel(AudioOutput *ao)
+{
+ FifoOutput *fd = (FifoOutput *)ao;
+ char buf[FIFO_BUFFER_SIZE];
+ int bytes = 1;
+
+ fd->timer->Reset();
+
+ while (bytes > 0 && errno != EINTR)
+ bytes = read(fd->input, buf, FIFO_BUFFER_SIZE);
+
+ if (bytes < 0 && errno != EAGAIN) {
+ FormatErrno(fifo_output_domain,
+ "Flush of FIFO \"%s\" failed",
+ fd->path_utf8.c_str());
+ }
+}
+
+static unsigned
+fifo_output_delay(AudioOutput *ao)
+{
+ FifoOutput *fd = (FifoOutput *)ao;
+
+ return fd->timer->IsStarted()
+ ? fd->timer->GetDelay()
+ : 0;
+}
+
+static size_t
+fifo_output_play(AudioOutput *ao, const void *chunk, size_t size,
+ Error &error)
+{
+ FifoOutput *fd = (FifoOutput *)ao;
+ ssize_t bytes;
+
+ if (!fd->timer->IsStarted())
+ fd->timer->Start();
+ fd->timer->Add(size);
+
+ while (true) {
+ bytes = write(fd->output, chunk, size);
+ if (bytes > 0)
+ return (size_t)bytes;
+
+ if (bytes < 0) {
+ switch (errno) {
+ case EAGAIN:
+ /* The pipe is full, so empty it */
+ fifo_output_cancel(&fd->base);
+ continue;
+ case EINTR:
+ continue;
+ }
+
+ error.FormatErrno("Failed to write to FIFO %s",
+ fd->path_utf8.c_str());
+ return 0;
+ }
+ }
+}
+
+const struct AudioOutputPlugin fifo_output_plugin = {
+ "fifo",
+ nullptr,
+ fifo_output_init,
+ fifo_output_finish,
+ nullptr,
+ nullptr,
+ fifo_output_open,
+ fifo_output_close,
+ fifo_output_delay,
+ nullptr,
+ fifo_output_play,
+ nullptr,
+ fifo_output_cancel,
+ nullptr,
+ nullptr,
+};
diff --git a/src/output/plugins/FifoOutputPlugin.hxx b/src/output/plugins/FifoOutputPlugin.hxx
new file mode 100644
index 000000000..f41ceded6
--- /dev/null
+++ b/src/output/plugins/FifoOutputPlugin.hxx
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_FIFO_OUTPUT_PLUGIN_HXX
+#define MPD_FIFO_OUTPUT_PLUGIN_HXX
+
+extern const struct AudioOutputPlugin fifo_output_plugin;
+
+#endif
diff --git a/src/output/plugins/JackOutputPlugin.cxx b/src/output/plugins/JackOutputPlugin.cxx
new file mode 100644
index 000000000..e1dad7893
--- /dev/null
+++ b/src/output/plugins/JackOutputPlugin.cxx
@@ -0,0 +1,762 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "JackOutputPlugin.hxx"
+#include "../OutputAPI.hxx"
+#include "config/ConfigError.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+#include "Log.hxx"
+
+#include <assert.h>
+
+#include <glib.h>
+#include <jack/jack.h>
+#include <jack/types.h>
+#include <jack/ringbuffer.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+enum {
+ MAX_PORTS = 16,
+};
+
+static const size_t jack_sample_size = sizeof(jack_default_audio_sample_t);
+
+struct JackOutput {
+ AudioOutput base;
+
+ /**
+ * libjack options passed to jack_client_open().
+ */
+ jack_options_t options;
+
+ const char *name;
+
+ const char *server_name;
+
+ /* configuration */
+
+ char *source_ports[MAX_PORTS];
+ unsigned num_source_ports;
+
+ char *destination_ports[MAX_PORTS];
+ unsigned num_destination_ports;
+
+ size_t ringbuffer_size;
+
+ /* the current audio format */
+ AudioFormat audio_format;
+
+ /* jack library stuff */
+ jack_port_t *ports[MAX_PORTS];
+ jack_client_t *client;
+ jack_ringbuffer_t *ringbuffer[MAX_PORTS];
+
+ bool shutdown;
+
+ /**
+ * While this flag is set, the "process" callback generates
+ * silence.
+ */
+ bool pause;
+
+ JackOutput()
+ :base(jack_output_plugin) {}
+
+ bool Initialize(const config_param &param, Error &error_r) {
+ return base.Configure(param, error_r);
+ }
+};
+
+static constexpr Domain jack_output_domain("jack_output");
+
+/**
+ * Determine the number of frames guaranteed to be available on all
+ * channels.
+ */
+static jack_nframes_t
+mpd_jack_available(const JackOutput *jd)
+{
+ size_t min = jack_ringbuffer_read_space(jd->ringbuffer[0]);
+
+ for (unsigned i = 1; i < jd->audio_format.channels; ++i) {
+ size_t current = jack_ringbuffer_read_space(jd->ringbuffer[i]);
+ if (current < min)
+ min = current;
+ }
+
+ assert(min % jack_sample_size == 0);
+
+ return min / jack_sample_size;
+}
+
+static int
+mpd_jack_process(jack_nframes_t nframes, void *arg)
+{
+ JackOutput *jd = (JackOutput *) arg;
+
+ if (nframes <= 0)
+ return 0;
+
+ if (jd->pause) {
+ /* empty the ring buffers */
+
+ const jack_nframes_t available = mpd_jack_available(jd);
+ for (unsigned i = 0; i < jd->audio_format.channels; ++i)
+ jack_ringbuffer_read_advance(jd->ringbuffer[i],
+ available * jack_sample_size);
+
+ /* generate silence while MPD is paused */
+
+ for (unsigned i = 0; i < jd->audio_format.channels; ++i) {
+ jack_default_audio_sample_t *out =
+ (jack_default_audio_sample_t *)
+ jack_port_get_buffer(jd->ports[i], nframes);
+
+ for (jack_nframes_t f = 0; f < nframes; ++f)
+ out[f] = 0.0;
+ }
+
+ return 0;
+ }
+
+ jack_nframes_t available = mpd_jack_available(jd);
+ if (available > nframes)
+ available = nframes;
+
+ for (unsigned i = 0; i < jd->audio_format.channels; ++i) {
+ jack_default_audio_sample_t *out =
+ (jack_default_audio_sample_t *)
+ jack_port_get_buffer(jd->ports[i], nframes);
+ if (out == nullptr)
+ /* workaround for libjack1 bug: if the server
+ connection fails, the process callback is
+ invoked anyway, but unable to get a
+ buffer */
+ continue;
+
+ jack_ringbuffer_read(jd->ringbuffer[i],
+ (char *)out, available * jack_sample_size);
+
+ for (jack_nframes_t f = available; f < nframes; ++f)
+ /* ringbuffer underrun, fill with silence */
+ out[f] = 0.0;
+ }
+
+ /* generate silence for the unused source ports */
+
+ for (unsigned i = jd->audio_format.channels;
+ i < jd->num_source_ports; ++i) {
+ jack_default_audio_sample_t *out =
+ (jack_default_audio_sample_t *)
+ jack_port_get_buffer(jd->ports[i], nframes);
+ if (out == nullptr)
+ /* workaround for libjack1 bug: if the server
+ connection fails, the process callback is
+ invoked anyway, but unable to get a
+ buffer */
+ continue;
+
+ for (jack_nframes_t f = 0; f < nframes; ++f)
+ out[f] = 0.0;
+ }
+
+ return 0;
+}
+
+static void
+mpd_jack_shutdown(void *arg)
+{
+ JackOutput *jd = (JackOutput *) arg;
+ jd->shutdown = true;
+}
+
+static void
+set_audioformat(JackOutput *jd, AudioFormat &audio_format)
+{
+ audio_format.sample_rate = jack_get_sample_rate(jd->client);
+
+ if (jd->num_source_ports == 1)
+ audio_format.channels = 1;
+ else if (audio_format.channels > jd->num_source_ports)
+ audio_format.channels = 2;
+
+ if (audio_format.format != SampleFormat::S16 &&
+ audio_format.format != SampleFormat::S24_P32)
+ audio_format.format = SampleFormat::S24_P32;
+}
+
+static void
+mpd_jack_error(const char *msg)
+{
+ LogError(jack_output_domain, msg);
+}
+
+#ifdef HAVE_JACK_SET_INFO_FUNCTION
+static void
+mpd_jack_info(const char *msg)
+{
+ LogDefault(jack_output_domain, msg);
+}
+#endif
+
+/**
+ * Disconnect the JACK client.
+ */
+static void
+mpd_jack_disconnect(JackOutput *jd)
+{
+ assert(jd != nullptr);
+ assert(jd->client != nullptr);
+
+ jack_deactivate(jd->client);
+ jack_client_close(jd->client);
+ jd->client = nullptr;
+}
+
+/**
+ * Connect the JACK client and performs some basic setup
+ * (e.g. register callbacks).
+ */
+static bool
+mpd_jack_connect(JackOutput *jd, Error &error)
+{
+ jack_status_t status;
+
+ assert(jd != nullptr);
+
+ jd->shutdown = false;
+
+ jd->client = jack_client_open(jd->name, jd->options, &status,
+ jd->server_name);
+ if (jd->client == nullptr) {
+ error.Format(jack_output_domain, status,
+ "Failed to connect to JACK server, status=%d",
+ status);
+ return false;
+ }
+
+ jack_set_process_callback(jd->client, mpd_jack_process, jd);
+ jack_on_shutdown(jd->client, mpd_jack_shutdown, jd);
+
+ for (unsigned i = 0; i < jd->num_source_ports; ++i) {
+ jd->ports[i] = jack_port_register(jd->client,
+ jd->source_ports[i],
+ JACK_DEFAULT_AUDIO_TYPE,
+ JackPortIsOutput, 0);
+ if (jd->ports[i] == nullptr) {
+ error.Format(jack_output_domain,
+ "Cannot register output port \"%s\"",
+ jd->source_ports[i]);
+ mpd_jack_disconnect(jd);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static bool
+mpd_jack_test_default_device(void)
+{
+ return true;
+}
+
+static unsigned
+parse_port_list(const char *source, char **dest, Error &error)
+{
+ char **list = g_strsplit(source, ",", 0);
+ unsigned n = 0;
+
+ for (n = 0; list[n] != nullptr; ++n) {
+ if (n >= MAX_PORTS) {
+ error.Set(config_domain,
+ "too many port names");
+ return 0;
+ }
+
+ dest[n] = list[n];
+ }
+
+ g_free(list);
+
+ if (n == 0) {
+ error.Format(config_domain,
+ "at least one port name expected");
+ return 0;
+ }
+
+ return n;
+}
+
+static AudioOutput *
+mpd_jack_init(const config_param &param, Error &error)
+{
+ JackOutput *jd = new JackOutput();
+
+ if (!jd->Initialize(param, error)) {
+ delete jd;
+ return nullptr;
+ }
+
+ const char *value;
+
+ jd->options = JackNullOption;
+
+ jd->name = param.GetBlockValue("client_name", nullptr);
+ if (jd->name != nullptr)
+ jd->options = jack_options_t(jd->options | JackUseExactName);
+ else
+ /* if there's a no configured client name, we don't
+ care about the JackUseExactName option */
+ jd->name = "Music Player Daemon";
+
+ jd->server_name = param.GetBlockValue("server_name", nullptr);
+ if (jd->server_name != nullptr)
+ jd->options = jack_options_t(jd->options | JackServerName);
+
+ if (!param.GetBlockValue("autostart", false))
+ jd->options = jack_options_t(jd->options | JackNoStartServer);
+
+ /* configure the source ports */
+
+ value = param.GetBlockValue("source_ports", "left,right");
+ jd->num_source_ports = parse_port_list(value,
+ jd->source_ports, error);
+ if (jd->num_source_ports == 0)
+ return nullptr;
+
+ /* configure the destination ports */
+
+ value = param.GetBlockValue("destination_ports", nullptr);
+ if (value == nullptr) {
+ /* compatibility with MPD < 0.16 */
+ value = param.GetBlockValue("ports", nullptr);
+ if (value != nullptr)
+ FormatWarning(jack_output_domain,
+ "deprecated option 'ports' in line %d",
+ param.line);
+ }
+
+ if (value != nullptr) {
+ jd->num_destination_ports =
+ parse_port_list(value,
+ jd->destination_ports, error);
+ if (jd->num_destination_ports == 0)
+ return nullptr;
+ } else {
+ jd->num_destination_ports = 0;
+ }
+
+ if (jd->num_destination_ports > 0 &&
+ jd->num_destination_ports != jd->num_source_ports)
+ FormatWarning(jack_output_domain,
+ "number of source ports (%u) mismatches the "
+ "number of destination ports (%u) in line %d",
+ jd->num_source_ports, jd->num_destination_ports,
+ param.line);
+
+ jd->ringbuffer_size = param.GetBlockValue("ringbuffer_size", 32768u);
+
+ jack_set_error_function(mpd_jack_error);
+
+#ifdef HAVE_JACK_SET_INFO_FUNCTION
+ jack_set_info_function(mpd_jack_info);
+#endif
+
+ return &jd->base;
+}
+
+static void
+mpd_jack_finish(AudioOutput *ao)
+{
+ JackOutput *jd = (JackOutput *)ao;
+
+ for (unsigned i = 0; i < jd->num_source_ports; ++i)
+ g_free(jd->source_ports[i]);
+
+ for (unsigned i = 0; i < jd->num_destination_ports; ++i)
+ g_free(jd->destination_ports[i]);
+
+ delete jd;
+}
+
+static bool
+mpd_jack_enable(AudioOutput *ao, Error &error)
+{
+ JackOutput *jd = (JackOutput *)ao;
+
+ for (unsigned i = 0; i < jd->num_source_ports; ++i)
+ jd->ringbuffer[i] = nullptr;
+
+ return mpd_jack_connect(jd, error);
+}
+
+static void
+mpd_jack_disable(AudioOutput *ao)
+{
+ JackOutput *jd = (JackOutput *)ao;
+
+ if (jd->client != nullptr)
+ mpd_jack_disconnect(jd);
+
+ for (unsigned i = 0; i < jd->num_source_ports; ++i) {
+ if (jd->ringbuffer[i] != nullptr) {
+ jack_ringbuffer_free(jd->ringbuffer[i]);
+ jd->ringbuffer[i] = nullptr;
+ }
+ }
+}
+
+/**
+ * Stops the playback on the JACK connection.
+ */
+static void
+mpd_jack_stop(JackOutput *jd)
+{
+ assert(jd != nullptr);
+
+ if (jd->client == nullptr)
+ return;
+
+ if (jd->shutdown)
+ /* the connection has failed; close it */
+ mpd_jack_disconnect(jd);
+ else
+ /* the connection is alive: just stop playback */
+ jack_deactivate(jd->client);
+}
+
+static bool
+mpd_jack_start(JackOutput *jd, Error &error)
+{
+ const char *destination_ports[MAX_PORTS], **jports;
+ const char *duplicate_port = nullptr;
+ unsigned num_destination_ports;
+
+ assert(jd->client != nullptr);
+ assert(jd->audio_format.channels <= jd->num_source_ports);
+
+ /* allocate the ring buffers on the first open(); these
+ persist until MPD exits. It's too unsafe to delete them
+ because we can never know when mpd_jack_process() gets
+ called */
+ for (unsigned i = 0; i < jd->num_source_ports; ++i) {
+ if (jd->ringbuffer[i] == nullptr)
+ jd->ringbuffer[i] =
+ jack_ringbuffer_create(jd->ringbuffer_size);
+
+ /* clear the ring buffer to be sure that data from
+ previous playbacks are gone */
+ jack_ringbuffer_reset(jd->ringbuffer[i]);
+ }
+
+ if ( jack_activate(jd->client) ) {
+ error.Set(jack_output_domain, "cannot activate client");
+ mpd_jack_stop(jd);
+ return false;
+ }
+
+ if (jd->num_destination_ports == 0) {
+ /* no output ports were configured - ask libjack for
+ defaults */
+ jports = jack_get_ports(jd->client, nullptr, nullptr,
+ JackPortIsPhysical | JackPortIsInput);
+ if (jports == nullptr) {
+ error.Set(jack_output_domain, "no ports found");
+ mpd_jack_stop(jd);
+ return false;
+ }
+
+ assert(*jports != nullptr);
+
+ for (num_destination_ports = 0;
+ num_destination_ports < MAX_PORTS &&
+ jports[num_destination_ports] != nullptr;
+ ++num_destination_ports) {
+ FormatDebug(jack_output_domain,
+ "destination_port[%u] = '%s'\n",
+ num_destination_ports,
+ jports[num_destination_ports]);
+ destination_ports[num_destination_ports] =
+ jports[num_destination_ports];
+ }
+ } else {
+ /* use the configured output ports */
+
+ num_destination_ports = jd->num_destination_ports;
+ memcpy(destination_ports, jd->destination_ports,
+ num_destination_ports * sizeof(*destination_ports));
+
+ jports = nullptr;
+ }
+
+ assert(num_destination_ports > 0);
+
+ if (jd->audio_format.channels >= 2 && num_destination_ports == 1) {
+ /* mix stereo signal on one speaker */
+
+ while (num_destination_ports < jd->audio_format.channels)
+ destination_ports[num_destination_ports++] =
+ destination_ports[0];
+ } else if (num_destination_ports > jd->audio_format.channels) {
+ if (jd->audio_format.channels == 1 && num_destination_ports > 2) {
+ /* mono input file: connect the one source
+ channel to the both destination channels */
+ duplicate_port = destination_ports[1];
+ num_destination_ports = 1;
+ } else
+ /* connect only as many ports as we need */
+ num_destination_ports = jd->audio_format.channels;
+ }
+
+ assert(num_destination_ports <= jd->num_source_ports);
+
+ for (unsigned i = 0; i < num_destination_ports; ++i) {
+ int ret;
+
+ ret = jack_connect(jd->client, jack_port_name(jd->ports[i]),
+ destination_ports[i]);
+ if (ret != 0) {
+ error.Format(jack_output_domain,
+ "Not a valid JACK port: %s",
+ destination_ports[i]);
+
+ if (jports != nullptr)
+ free(jports);
+
+ mpd_jack_stop(jd);
+ return false;
+ }
+ }
+
+ if (duplicate_port != nullptr) {
+ /* mono input file: connect the one source channel to
+ the both destination channels */
+ int ret;
+
+ ret = jack_connect(jd->client, jack_port_name(jd->ports[0]),
+ duplicate_port);
+ if (ret != 0) {
+ error.Format(jack_output_domain,
+ "Not a valid JACK port: %s",
+ duplicate_port);
+
+ if (jports != nullptr)
+ free(jports);
+
+ mpd_jack_stop(jd);
+ return false;
+ }
+ }
+
+ if (jports != nullptr)
+ free(jports);
+
+ return true;
+}
+
+static bool
+mpd_jack_open(AudioOutput *ao, AudioFormat &audio_format,
+ Error &error)
+{
+ JackOutput *jd = (JackOutput *)ao;
+
+ assert(jd != nullptr);
+
+ jd->pause = false;
+
+ if (jd->client != nullptr && jd->shutdown)
+ mpd_jack_disconnect(jd);
+
+ if (jd->client == nullptr && !mpd_jack_connect(jd, error))
+ return false;
+
+ set_audioformat(jd, audio_format);
+ jd->audio_format = audio_format;
+
+ if (!mpd_jack_start(jd, error))
+ return false;
+
+ return true;
+}
+
+static void
+mpd_jack_close(gcc_unused AudioOutput *ao)
+{
+ JackOutput *jd = (JackOutput *)ao;
+
+ mpd_jack_stop(jd);
+}
+
+static unsigned
+mpd_jack_delay(AudioOutput *ao)
+{
+ JackOutput *jd = (JackOutput *)ao;
+
+ return jd->base.pause && jd->pause && !jd->shutdown
+ ? 1000
+ : 0;
+}
+
+static inline jack_default_audio_sample_t
+sample_16_to_jack(int16_t sample)
+{
+ return sample / (jack_default_audio_sample_t)(1 << (16 - 1));
+}
+
+static void
+mpd_jack_write_samples_16(JackOutput *jd, const int16_t *src,
+ unsigned num_samples)
+{
+ jack_default_audio_sample_t sample;
+ unsigned i;
+
+ while (num_samples-- > 0) {
+ for (i = 0; i < jd->audio_format.channels; ++i) {
+ sample = sample_16_to_jack(*src++);
+ jack_ringbuffer_write(jd->ringbuffer[i],
+ (const char *)&sample,
+ sizeof(sample));
+ }
+ }
+}
+
+static inline jack_default_audio_sample_t
+sample_24_to_jack(int32_t sample)
+{
+ return sample / (jack_default_audio_sample_t)(1 << (24 - 1));
+}
+
+static void
+mpd_jack_write_samples_24(JackOutput *jd, const int32_t *src,
+ unsigned num_samples)
+{
+ jack_default_audio_sample_t sample;
+ unsigned i;
+
+ while (num_samples-- > 0) {
+ for (i = 0; i < jd->audio_format.channels; ++i) {
+ sample = sample_24_to_jack(*src++);
+ jack_ringbuffer_write(jd->ringbuffer[i],
+ (const char *)&sample,
+ sizeof(sample));
+ }
+ }
+}
+
+static void
+mpd_jack_write_samples(JackOutput *jd, const void *src,
+ unsigned num_samples)
+{
+ switch (jd->audio_format.format) {
+ case SampleFormat::S16:
+ mpd_jack_write_samples_16(jd, (const int16_t*)src,
+ num_samples);
+ break;
+
+ case SampleFormat::S24_P32:
+ mpd_jack_write_samples_24(jd, (const int32_t*)src,
+ num_samples);
+ break;
+
+ default:
+ assert(false);
+ gcc_unreachable();
+ }
+}
+
+static size_t
+mpd_jack_play(AudioOutput *ao, const void *chunk, size_t size,
+ Error &error)
+{
+ JackOutput *jd = (JackOutput *)ao;
+ const size_t frame_size = jd->audio_format.GetFrameSize();
+ size_t space = 0, space1;
+
+ jd->pause = false;
+
+ assert(size % frame_size == 0);
+ size /= frame_size;
+
+ while (true) {
+ if (jd->shutdown) {
+ error.Set(jack_output_domain,
+ "Refusing to play, because "
+ "there is no client thread");
+ return 0;
+ }
+
+ space = jack_ringbuffer_write_space(jd->ringbuffer[0]);
+ for (unsigned i = 1; i < jd->audio_format.channels; ++i) {
+ space1 = jack_ringbuffer_write_space(jd->ringbuffer[i]);
+ if (space > space1)
+ /* send data symmetrically */
+ space = space1;
+ }
+
+ if (space >= jack_sample_size)
+ break;
+
+ /* XXX do something more intelligent to
+ synchronize */
+ g_usleep(1000);
+ }
+
+ space /= jack_sample_size;
+ if (space < size)
+ size = space;
+
+ mpd_jack_write_samples(jd, chunk, size);
+ return size * frame_size;
+}
+
+static bool
+mpd_jack_pause(AudioOutput *ao)
+{
+ JackOutput *jd = (JackOutput *)ao;
+
+ if (jd->shutdown)
+ return false;
+
+ jd->pause = true;
+
+ return true;
+}
+
+const struct AudioOutputPlugin jack_output_plugin = {
+ "jack",
+ mpd_jack_test_default_device,
+ mpd_jack_init,
+ mpd_jack_finish,
+ mpd_jack_enable,
+ mpd_jack_disable,
+ mpd_jack_open,
+ mpd_jack_close,
+ mpd_jack_delay,
+ nullptr,
+ mpd_jack_play,
+ nullptr,
+ nullptr,
+ mpd_jack_pause,
+ nullptr,
+};
diff --git a/src/output/plugins/JackOutputPlugin.hxx b/src/output/plugins/JackOutputPlugin.hxx
new file mode 100644
index 000000000..6f1f7ecb9
--- /dev/null
+++ b/src/output/plugins/JackOutputPlugin.hxx
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_JACK_OUTPUT_PLUGIN_HXX
+#define MPD_JACK_OUTPUT_PLUGIN_HXX
+
+extern const struct AudioOutputPlugin jack_output_plugin;
+
+#endif
diff --git a/src/output/plugins/NullOutputPlugin.cxx b/src/output/plugins/NullOutputPlugin.cxx
new file mode 100644
index 000000000..098f58926
--- /dev/null
+++ b/src/output/plugins/NullOutputPlugin.cxx
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "NullOutputPlugin.hxx"
+#include "../OutputAPI.hxx"
+#include "../Timer.hxx"
+
+struct NullOutput {
+ AudioOutput base;
+
+ bool sync;
+
+ Timer *timer;
+
+ NullOutput()
+ :base(null_output_plugin) {}
+
+ bool Initialize(const config_param &param, Error &error) {
+ return base.Configure(param, error);
+ }
+};
+
+static AudioOutput *
+null_init(const config_param &param, Error &error)
+{
+ NullOutput *nd = new NullOutput();
+
+ if (!nd->Initialize(param, error)) {
+ delete nd;
+ return nullptr;
+ }
+
+ nd->sync = param.GetBlockValue("sync", true);
+
+ return &nd->base;
+}
+
+static void
+null_finish(AudioOutput *ao)
+{
+ NullOutput *nd = (NullOutput *)ao;
+
+ delete nd;
+}
+
+static bool
+null_open(AudioOutput *ao, AudioFormat &audio_format,
+ gcc_unused Error &error)
+{
+ NullOutput *nd = (NullOutput *)ao;
+
+ if (nd->sync)
+ nd->timer = new Timer(audio_format);
+
+ return true;
+}
+
+static void
+null_close(AudioOutput *ao)
+{
+ NullOutput *nd = (NullOutput *)ao;
+
+ if (nd->sync)
+ delete nd->timer;
+}
+
+static unsigned
+null_delay(AudioOutput *ao)
+{
+ NullOutput *nd = (NullOutput *)ao;
+
+ return nd->sync && nd->timer->IsStarted()
+ ? nd->timer->GetDelay()
+ : 0;
+}
+
+static size_t
+null_play(AudioOutput *ao, gcc_unused const void *chunk, size_t size,
+ gcc_unused Error &error)
+{
+ NullOutput *nd = (NullOutput *)ao;
+ Timer *timer = nd->timer;
+
+ if (!nd->sync)
+ return size;
+
+ if (!timer->IsStarted())
+ timer->Start();
+ timer->Add(size);
+
+ return size;
+}
+
+static void
+null_cancel(AudioOutput *ao)
+{
+ NullOutput *nd = (NullOutput *)ao;
+
+ if (!nd->sync)
+ return;
+
+ nd->timer->Reset();
+}
+
+const struct AudioOutputPlugin null_output_plugin = {
+ "null",
+ nullptr,
+ null_init,
+ null_finish,
+ nullptr,
+ nullptr,
+ null_open,
+ null_close,
+ null_delay,
+ nullptr,
+ null_play,
+ nullptr,
+ null_cancel,
+ nullptr,
+ nullptr,
+};
diff --git a/src/output/plugins/NullOutputPlugin.hxx b/src/output/plugins/NullOutputPlugin.hxx
new file mode 100644
index 000000000..f25f5b9f3
--- /dev/null
+++ b/src/output/plugins/NullOutputPlugin.hxx
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_NULL_OUTPUT_PLUGIN_HXX
+#define MPD_NULL_OUTPUT_PLUGIN_HXX
+
+extern const struct AudioOutputPlugin null_output_plugin;
+
+#endif
diff --git a/src/output/plugins/OSXOutputPlugin.cxx b/src/output/plugins/OSXOutputPlugin.cxx
new file mode 100644
index 000000000..13ac7b35e
--- /dev/null
+++ b/src/output/plugins/OSXOutputPlugin.cxx
@@ -0,0 +1,431 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "OSXOutputPlugin.hxx"
+#include "../OutputAPI.hxx"
+#include "util/DynamicFifoBuffer.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+#include "thread/Mutex.hxx"
+#include "thread/Cond.hxx"
+#include "system/ByteOrder.hxx"
+#include "Log.hxx"
+
+#include <CoreAudio/AudioHardware.h>
+#include <AudioUnit/AudioUnit.h>
+#include <CoreServices/CoreServices.h>
+
+struct OSXOutput {
+ AudioOutput base;
+
+ /* configuration settings */
+ OSType component_subtype;
+ /* only applicable with kAudioUnitSubType_HALOutput */
+ const char *device_name;
+
+ AudioUnit au;
+ Mutex mutex;
+ Cond condition;
+
+ DynamicFifoBuffer<uint8_t> *buffer;
+
+ OSXOutput()
+ :base(osx_output_plugin) {}
+};
+
+static constexpr Domain osx_output_domain("osx_output");
+
+static bool
+osx_output_test_default_device(void)
+{
+ /* on a Mac, this is always the default plugin, if nothing
+ else is configured */
+ return true;
+}
+
+static void
+osx_output_configure(OSXOutput *oo, const config_param &param)
+{
+ const char *device = param.GetBlockValue("device");
+
+ if (device == NULL || 0 == strcmp(device, "default")) {
+ oo->component_subtype = kAudioUnitSubType_DefaultOutput;
+ oo->device_name = NULL;
+ }
+ else if (0 == strcmp(device, "system")) {
+ oo->component_subtype = kAudioUnitSubType_SystemOutput;
+ oo->device_name = NULL;
+ }
+ else {
+ oo->component_subtype = kAudioUnitSubType_HALOutput;
+ /* XXX am I supposed to strdup() this? */
+ oo->device_name = device;
+ }
+}
+
+static AudioOutput *
+osx_output_init(const config_param &param, Error &error)
+{
+ OSXOutput *oo = new OSXOutput();
+ if (!oo->base.Configure(param, error)) {
+ delete oo;
+ return NULL;
+ }
+
+ osx_output_configure(oo, param);
+
+ return &oo->base;
+}
+
+static void
+osx_output_finish(AudioOutput *ao)
+{
+ OSXOutput *oo = (OSXOutput *)ao;
+
+ delete oo;
+}
+
+static bool
+osx_output_set_device(OSXOutput *oo, Error &error)
+{
+ bool ret = true;
+ OSStatus status;
+ UInt32 size, numdevices;
+ AudioDeviceID *deviceids = NULL;
+ char name[256];
+ unsigned int i;
+
+ if (oo->component_subtype != kAudioUnitSubType_HALOutput)
+ goto done;
+
+ /* how many audio devices are there? */
+ status = AudioHardwareGetPropertyInfo(kAudioHardwarePropertyDevices,
+ &size,
+ NULL);
+ if (status != noErr) {
+ error.Format(osx_output_domain, status,
+ "Unable to determine number of OS X audio devices: %s",
+ GetMacOSStatusCommentString(status));
+ ret = false;
+ goto done;
+ }
+
+ /* what are the available audio device IDs? */
+ numdevices = size / sizeof(AudioDeviceID);
+ deviceids = new AudioDeviceID[numdevices];
+ status = AudioHardwareGetProperty(kAudioHardwarePropertyDevices,
+ &size,
+ deviceids);
+ if (status != noErr) {
+ error.Format(osx_output_domain, status,
+ "Unable to determine OS X audio device IDs: %s",
+ GetMacOSStatusCommentString(status));
+ ret = false;
+ goto done;
+ }
+
+ /* which audio device matches oo->device_name? */
+ for (i = 0; i < numdevices; i++) {
+ size = sizeof(name);
+ status = AudioDeviceGetProperty(deviceids[i], 0, false,
+ kAudioDevicePropertyDeviceName,
+ &size, name);
+ if (status != noErr) {
+ error.Format(osx_output_domain, status,
+ "Unable to determine OS X device name "
+ "(device %u): %s",
+ (unsigned int) deviceids[i],
+ GetMacOSStatusCommentString(status));
+ ret = false;
+ goto done;
+ }
+ if (strcmp(oo->device_name, name) == 0) {
+ FormatDebug(osx_output_domain,
+ "found matching device: ID=%u, name=%s",
+ (unsigned)deviceids[i], name);
+ break;
+ }
+ }
+ if (i == numdevices) {
+ FormatWarning(osx_output_domain,
+ "Found no audio device with name '%s' "
+ "(will use default audio device)",
+ oo->device_name);
+ goto done;
+ }
+
+ status = AudioUnitSetProperty(oo->au,
+ kAudioOutputUnitProperty_CurrentDevice,
+ kAudioUnitScope_Global,
+ 0,
+ &(deviceids[i]),
+ sizeof(AudioDeviceID));
+ if (status != noErr) {
+ error.Format(osx_output_domain, status,
+ "Unable to set OS X audio output device: %s",
+ GetMacOSStatusCommentString(status));
+ ret = false;
+ goto done;
+ }
+
+ FormatDebug(osx_output_domain,
+ "set OS X audio output device ID=%u, name=%s",
+ (unsigned)deviceids[i], name);
+
+done:
+ delete[] deviceids;
+ return ret;
+}
+
+static OSStatus
+osx_render(void *vdata,
+ gcc_unused AudioUnitRenderActionFlags *io_action_flags,
+ gcc_unused const AudioTimeStamp *in_timestamp,
+ gcc_unused UInt32 in_bus_number,
+ gcc_unused UInt32 in_number_frames,
+ AudioBufferList *buffer_list)
+{
+ OSXOutput *od = (OSXOutput *) vdata;
+ AudioBuffer *buffer = &buffer_list->mBuffers[0];
+ size_t buffer_size = buffer->mDataByteSize;
+
+ assert(od->buffer != NULL);
+
+ od->mutex.lock();
+
+ auto src = od->buffer->Read();
+ if (!src.IsEmpty()) {
+ if (src.size > buffer_size)
+ src.size = buffer_size;
+
+ memcpy(buffer->mData, src.data, src.size);
+ od->buffer->Consume(src.size);
+ }
+
+ od->condition.signal();
+ od->mutex.unlock();
+
+ buffer->mDataByteSize = src.size;
+
+ unsigned i;
+ for (i = 1; i < buffer_list->mNumberBuffers; ++i) {
+ buffer = &buffer_list->mBuffers[i];
+ buffer->mDataByteSize = 0;
+ }
+
+ return 0;
+}
+
+static bool
+osx_output_enable(AudioOutput *ao, Error &error)
+{
+ OSXOutput *oo = (OSXOutput *)ao;
+
+ ComponentDescription desc;
+ desc.componentType = kAudioUnitType_Output;
+ desc.componentSubType = oo->component_subtype;
+ desc.componentManufacturer = kAudioUnitManufacturer_Apple;
+ desc.componentFlags = 0;
+ desc.componentFlagsMask = 0;
+
+ Component comp = FindNextComponent(NULL, &desc);
+ if (comp == 0) {
+ error.Set(osx_output_domain,
+ "Error finding OS X component");
+ return false;
+ }
+
+ OSStatus status = OpenAComponent(comp, &oo->au);
+ if (status != noErr) {
+ error.Format(osx_output_domain, status,
+ "Unable to open OS X component: %s",
+ GetMacOSStatusCommentString(status));
+ return false;
+ }
+
+ if (!osx_output_set_device(oo, error)) {
+ CloseComponent(oo->au);
+ return false;
+ }
+
+ AURenderCallbackStruct callback;
+ callback.inputProc = osx_render;
+ callback.inputProcRefCon = oo;
+
+ ComponentResult result =
+ AudioUnitSetProperty(oo->au,
+ kAudioUnitProperty_SetRenderCallback,
+ kAudioUnitScope_Input, 0,
+ &callback, sizeof(callback));
+ if (result != noErr) {
+ CloseComponent(oo->au);
+ error.Set(osx_output_domain, result,
+ "unable to set callback for OS X audio unit");
+ return false;
+ }
+
+ return true;
+}
+
+static void
+osx_output_disable(AudioOutput *ao)
+{
+ OSXOutput *oo = (OSXOutput *)ao;
+
+ CloseComponent(oo->au);
+}
+
+static void
+osx_output_cancel(AudioOutput *ao)
+{
+ OSXOutput *od = (OSXOutput *)ao;
+
+ const ScopeLock protect(od->mutex);
+ od->buffer->Clear();
+}
+
+static void
+osx_output_close(AudioOutput *ao)
+{
+ OSXOutput *od = (OSXOutput *)ao;
+
+ AudioOutputUnitStop(od->au);
+ AudioUnitUninitialize(od->au);
+
+ delete od->buffer;
+}
+
+static bool
+osx_output_open(AudioOutput *ao, AudioFormat &audio_format,
+ Error &error)
+{
+ OSXOutput *od = (OSXOutput *)ao;
+
+ AudioStreamBasicDescription stream_description;
+ stream_description.mSampleRate = audio_format.sample_rate;
+ stream_description.mFormatID = kAudioFormatLinearPCM;
+ stream_description.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
+
+ switch (audio_format.format) {
+ case SampleFormat::S8:
+ stream_description.mBitsPerChannel = 8;
+ break;
+
+ case SampleFormat::S16:
+ stream_description.mBitsPerChannel = 16;
+ break;
+
+ case SampleFormat::S32:
+ stream_description.mBitsPerChannel = 32;
+ break;
+
+ default:
+ audio_format.format = SampleFormat::S32;
+ stream_description.mBitsPerChannel = 32;
+ break;
+ }
+
+ if (IsBigEndian())
+ stream_description.mFormatFlags |= kLinearPCMFormatFlagIsBigEndian;
+
+ stream_description.mBytesPerPacket = audio_format.GetFrameSize();
+ stream_description.mFramesPerPacket = 1;
+ stream_description.mBytesPerFrame = stream_description.mBytesPerPacket;
+ stream_description.mChannelsPerFrame = audio_format.channels;
+
+ ComponentResult result =
+ AudioUnitSetProperty(od->au, kAudioUnitProperty_StreamFormat,
+ kAudioUnitScope_Input, 0,
+ &stream_description,
+ sizeof(stream_description));
+ if (result != noErr) {
+ error.Set(osx_output_domain, result,
+ "Unable to set format on OS X device");
+ return false;
+ }
+
+ OSStatus status = AudioUnitInitialize(od->au);
+ if (status != noErr) {
+ error.Format(osx_output_domain, status,
+ "Unable to initialize OS X audio unit: %s",
+ GetMacOSStatusCommentString(status));
+ return false;
+ }
+
+ /* create a buffer of 1s */
+ od->buffer = new DynamicFifoBuffer<uint8_t>(audio_format.sample_rate *
+ audio_format.GetFrameSize());
+
+ status = AudioOutputUnitStart(od->au);
+ if (status != 0) {
+ AudioUnitUninitialize(od->au);
+ error.Format(osx_output_domain, status,
+ "unable to start audio output: %s",
+ GetMacOSStatusCommentString(status));
+ return false;
+ }
+
+ return true;
+}
+
+static size_t
+osx_output_play(AudioOutput *ao, const void *chunk, size_t size,
+ gcc_unused Error &error)
+{
+ OSXOutput *od = (OSXOutput *)ao;
+
+ const ScopeLock protect(od->mutex);
+
+ DynamicFifoBuffer<uint8_t>::Range dest;
+ while (true) {
+ dest = od->buffer->Write();
+ if (!dest.IsEmpty())
+ break;
+
+ /* wait for some free space in the buffer */
+ od->condition.wait(od->mutex);
+ }
+
+ if (size > dest.size)
+ size = dest.size;
+
+ memcpy(dest.data, chunk, size);
+ od->buffer->Append(size);
+
+ return size;
+}
+
+const struct AudioOutputPlugin osx_output_plugin = {
+ "osx",
+ osx_output_test_default_device,
+ osx_output_init,
+ osx_output_finish,
+ osx_output_enable,
+ osx_output_disable,
+ osx_output_open,
+ osx_output_close,
+ nullptr,
+ nullptr,
+ osx_output_play,
+ nullptr,
+ osx_output_cancel,
+ nullptr,
+ nullptr,
+};
diff --git a/src/output/plugins/OSXOutputPlugin.hxx b/src/output/plugins/OSXOutputPlugin.hxx
new file mode 100644
index 000000000..d7aed40b6
--- /dev/null
+++ b/src/output/plugins/OSXOutputPlugin.hxx
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_OSX_OUTPUT_PLUGIN_HXX
+#define MPD_OSX_OUTPUT_PLUGIN_HXX
+
+extern const struct AudioOutputPlugin osx_output_plugin;
+
+#endif
diff --git a/src/output/plugins/OpenALOutputPlugin.cxx b/src/output/plugins/OpenALOutputPlugin.cxx
new file mode 100644
index 000000000..2f095c0a4
--- /dev/null
+++ b/src/output/plugins/OpenALOutputPlugin.cxx
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "OpenALOutputPlugin.hxx"
+#include "../OutputAPI.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+
+#include <unistd.h>
+
+#ifndef __APPLE__
+#include <AL/al.h>
+#include <AL/alc.h>
+#else
+#include <OpenAL/al.h>
+#include <OpenAL/alc.h>
+#endif
+
+/* should be enough for buffer size = 2048 */
+#define NUM_BUFFERS 16
+
+struct OpenALOutput {
+ AudioOutput base;
+
+ const char *device_name;
+ ALCdevice *device;
+ ALCcontext *context;
+ ALuint buffers[NUM_BUFFERS];
+ unsigned filled;
+ ALuint source;
+ ALenum format;
+ ALuint frequency;
+
+ OpenALOutput()
+ :base(openal_output_plugin) {}
+
+ bool Initialize(const config_param &param, Error &error_r) {
+ return base.Configure(param, error_r);
+ }
+};
+
+static constexpr Domain openal_output_domain("openal_output");
+
+static ALenum
+openal_audio_format(AudioFormat &audio_format)
+{
+ /* note: cannot map SampleFormat::S8 to AL_FORMAT_STEREO8 or
+ AL_FORMAT_MONO8 since OpenAL expects unsigned 8 bit
+ samples, while MPD uses signed samples */
+
+ switch (audio_format.format) {
+ case SampleFormat::S16:
+ if (audio_format.channels == 2)
+ return AL_FORMAT_STEREO16;
+ if (audio_format.channels == 1)
+ return AL_FORMAT_MONO16;
+
+ /* fall back to mono */
+ audio_format.channels = 1;
+ return openal_audio_format(audio_format);
+
+ default:
+ /* fall back to 16 bit */
+ audio_format.format = SampleFormat::S16;
+ return openal_audio_format(audio_format);
+ }
+}
+
+gcc_pure
+static inline ALint
+openal_get_source_i(const OpenALOutput *od, ALenum param)
+{
+ ALint value;
+ alGetSourcei(od->source, param, &value);
+ return value;
+}
+
+gcc_pure
+static inline bool
+openal_has_processed(const OpenALOutput *od)
+{
+ return openal_get_source_i(od, AL_BUFFERS_PROCESSED) > 0;
+}
+
+gcc_pure
+static inline ALint
+openal_is_playing(const OpenALOutput *od)
+{
+ return openal_get_source_i(od, AL_SOURCE_STATE) == AL_PLAYING;
+}
+
+static bool
+openal_setup_context(OpenALOutput *od, Error &error)
+{
+ od->device = alcOpenDevice(od->device_name);
+
+ if (od->device == nullptr) {
+ error.Format(openal_output_domain,
+ "Error opening OpenAL device \"%s\"",
+ od->device_name);
+ return false;
+ }
+
+ od->context = alcCreateContext(od->device, nullptr);
+
+ if (od->context == nullptr) {
+ error.Format(openal_output_domain,
+ "Error creating context for \"%s\"",
+ od->device_name);
+ alcCloseDevice(od->device);
+ return false;
+ }
+
+ return true;
+}
+
+static AudioOutput *
+openal_init(const config_param &param, Error &error)
+{
+ const char *device_name = param.GetBlockValue("device");
+ if (device_name == nullptr) {
+ device_name = alcGetString(nullptr, ALC_DEFAULT_DEVICE_SPECIFIER);
+ }
+
+ OpenALOutput *od = new OpenALOutput();
+ if (!od->Initialize(param, error)) {
+ delete od;
+ return nullptr;
+ }
+
+ od->device_name = device_name;
+
+ return &od->base;
+}
+
+static void
+openal_finish(AudioOutput *ao)
+{
+ OpenALOutput *od = (OpenALOutput *)ao;
+
+ delete od;
+}
+
+static bool
+openal_open(AudioOutput *ao, AudioFormat &audio_format,
+ Error &error)
+{
+ OpenALOutput *od = (OpenALOutput *)ao;
+
+ od->format = openal_audio_format(audio_format);
+
+ if (!openal_setup_context(od, error)) {
+ return false;
+ }
+
+ alcMakeContextCurrent(od->context);
+ alGenBuffers(NUM_BUFFERS, od->buffers);
+
+ if (alGetError() != AL_NO_ERROR) {
+ error.Set(openal_output_domain, "Failed to generate buffers");
+ return false;
+ }
+
+ alGenSources(1, &od->source);
+
+ if (alGetError() != AL_NO_ERROR) {
+ error.Set(openal_output_domain, "Failed to generate source");
+ alDeleteBuffers(NUM_BUFFERS, od->buffers);
+ return false;
+ }
+
+ od->filled = 0;
+ od->frequency = audio_format.sample_rate;
+
+ return true;
+}
+
+static void
+openal_close(AudioOutput *ao)
+{
+ OpenALOutput *od = (OpenALOutput *)ao;
+
+ alcMakeContextCurrent(od->context);
+ alDeleteSources(1, &od->source);
+ alDeleteBuffers(NUM_BUFFERS, od->buffers);
+ alcDestroyContext(od->context);
+ alcCloseDevice(od->device);
+}
+
+static unsigned
+openal_delay(AudioOutput *ao)
+{
+ OpenALOutput *od = (OpenALOutput *)ao;
+
+ return od->filled < NUM_BUFFERS || openal_has_processed(od)
+ ? 0
+ /* we don't know exactly how long we must wait for the
+ next buffer to finish, so this is a random
+ guess: */
+ : 50;
+}
+
+static size_t
+openal_play(AudioOutput *ao, const void *chunk, size_t size,
+ gcc_unused Error &error)
+{
+ OpenALOutput *od = (OpenALOutput *)ao;
+ ALuint buffer;
+
+ if (alcGetCurrentContext() != od->context) {
+ alcMakeContextCurrent(od->context);
+ }
+
+ if (od->filled < NUM_BUFFERS) {
+ /* fill all buffers */
+ buffer = od->buffers[od->filled];
+ od->filled++;
+ } else {
+ /* wait for processed buffer */
+ while (!openal_has_processed(od))
+ usleep(10);
+
+ alSourceUnqueueBuffers(od->source, 1, &buffer);
+ }
+
+ alBufferData(buffer, od->format, chunk, size, od->frequency);
+ alSourceQueueBuffers(od->source, 1, &buffer);
+
+ if (!openal_is_playing(od))
+ alSourcePlay(od->source);
+
+ return size;
+}
+
+static void
+openal_cancel(AudioOutput *ao)
+{
+ OpenALOutput *od = (OpenALOutput *)ao;
+
+ od->filled = 0;
+ alcMakeContextCurrent(od->context);
+ alSourceStop(od->source);
+
+ /* force-unqueue all buffers */
+ alSourcei(od->source, AL_BUFFER, 0);
+ od->filled = 0;
+}
+
+const struct AudioOutputPlugin openal_output_plugin = {
+ "openal",
+ nullptr,
+ openal_init,
+ openal_finish,
+ nullptr,
+ nullptr,
+ openal_open,
+ openal_close,
+ openal_delay,
+ nullptr,
+ openal_play,
+ nullptr,
+ openal_cancel,
+ nullptr,
+ nullptr,
+};
diff --git a/src/output/plugins/OpenALOutputPlugin.hxx b/src/output/plugins/OpenALOutputPlugin.hxx
new file mode 100644
index 000000000..a27e6b53c
--- /dev/null
+++ b/src/output/plugins/OpenALOutputPlugin.hxx
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_OPENAL_OUTPUT_PLUGIN_HXX
+#define MPD_OPENAL_OUTPUT_PLUGIN_HXX
+
+extern const struct AudioOutputPlugin openal_output_plugin;
+
+#endif
diff --git a/src/output/plugins/OssOutputPlugin.cxx b/src/output/plugins/OssOutputPlugin.cxx
new file mode 100644
index 000000000..39d87fc35
--- /dev/null
+++ b/src/output/plugins/OssOutputPlugin.cxx
@@ -0,0 +1,779 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "OssOutputPlugin.hxx"
+#include "../OutputAPI.hxx"
+#include "mixer/MixerList.hxx"
+#include "system/fd_util.h"
+#include "util/ConstBuffer.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+#include "util/Macros.hxx"
+#include "system/ByteOrder.hxx"
+#include "Log.hxx"
+
+#include <sys/stat.h>
+#include <sys/ioctl.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <assert.h>
+
+#if defined(__OpenBSD__) || defined(__NetBSD__)
+# include <soundcard.h>
+#else /* !(defined(__OpenBSD__) || defined(__NetBSD__) */
+# include <sys/soundcard.h>
+#endif /* !(defined(__OpenBSD__) || defined(__NetBSD__) */
+
+/* We got bug reports from FreeBSD users who said that the two 24 bit
+ formats generate white noise on FreeBSD, but 32 bit works. This is
+ a workaround until we know what exactly is expected by the kernel
+ audio drivers. */
+#ifndef __linux__
+#undef AFMT_S24_PACKED
+#undef AFMT_S24_NE
+#endif
+
+#ifdef AFMT_S24_PACKED
+#include "pcm/PcmExport.hxx"
+#include "util/Manual.hxx"
+#endif
+
+struct OssOutput {
+ AudioOutput base;
+
+#ifdef AFMT_S24_PACKED
+ Manual<PcmExport> pcm_export;
+#endif
+
+ int fd;
+ const char *device;
+
+ /**
+ * The current input audio format. This is needed to reopen
+ * the device after cancel().
+ */
+ AudioFormat audio_format;
+
+ /**
+ * The current OSS audio format. This is needed to reopen the
+ * device after cancel().
+ */
+ int oss_format;
+
+ OssOutput()
+ :base(oss_output_plugin),
+ fd(-1), device(nullptr) {}
+
+ bool Initialize(const config_param &param, Error &error_r) {
+ return base.Configure(param, error_r);
+ }
+};
+
+static constexpr Domain oss_output_domain("oss_output");
+
+enum oss_stat {
+ OSS_STAT_NO_ERROR = 0,
+ OSS_STAT_NOT_CHAR_DEV = -1,
+ OSS_STAT_NO_PERMS = -2,
+ OSS_STAT_DOESN_T_EXIST = -3,
+ OSS_STAT_OTHER = -4,
+};
+
+static enum oss_stat
+oss_stat_device(const char *device, int *errno_r)
+{
+ struct stat st;
+
+ if (0 == stat(device, &st)) {
+ if (!S_ISCHR(st.st_mode)) {
+ return OSS_STAT_NOT_CHAR_DEV;
+ }
+ } else {
+ *errno_r = errno;
+
+ switch (errno) {
+ case ENOENT:
+ case ENOTDIR:
+ return OSS_STAT_DOESN_T_EXIST;
+ case EACCES:
+ return OSS_STAT_NO_PERMS;
+ default:
+ return OSS_STAT_OTHER;
+ }
+ }
+
+ return OSS_STAT_NO_ERROR;
+}
+
+static const char *default_devices[] = { "/dev/sound/dsp", "/dev/dsp" };
+
+static bool
+oss_output_test_default_device(void)
+{
+ int fd, i;
+
+ for (i = ARRAY_SIZE(default_devices); --i >= 0; ) {
+ fd = open_cloexec(default_devices[i], O_WRONLY, 0);
+
+ if (fd >= 0) {
+ close(fd);
+ return true;
+ }
+
+ FormatErrno(oss_output_domain,
+ "Error opening OSS device \"%s\"",
+ default_devices[i]);
+ }
+
+ return false;
+}
+
+static AudioOutput *
+oss_open_default(Error &error)
+{
+ int err[ARRAY_SIZE(default_devices)];
+ enum oss_stat ret[ARRAY_SIZE(default_devices)];
+
+ const config_param empty;
+ for (int i = ARRAY_SIZE(default_devices); --i >= 0; ) {
+ ret[i] = oss_stat_device(default_devices[i], &err[i]);
+ if (ret[i] == OSS_STAT_NO_ERROR) {
+ OssOutput *od = new OssOutput();
+ if (!od->Initialize(empty, error)) {
+ delete od;
+ return NULL;
+ }
+
+ od->device = default_devices[i];
+ return &od->base;
+ }
+ }
+
+ for (int i = ARRAY_SIZE(default_devices); --i >= 0; ) {
+ const char *dev = default_devices[i];
+ switch(ret[i]) {
+ case OSS_STAT_NO_ERROR:
+ /* never reached */
+ break;
+ case OSS_STAT_DOESN_T_EXIST:
+ FormatWarning(oss_output_domain,
+ "%s not found", dev);
+ break;
+ case OSS_STAT_NOT_CHAR_DEV:
+ FormatWarning(oss_output_domain,
+ "%s is not a character device", dev);
+ break;
+ case OSS_STAT_NO_PERMS:
+ FormatWarning(oss_output_domain,
+ "%s: permission denied", dev);
+ break;
+ case OSS_STAT_OTHER:
+ FormatErrno(oss_output_domain, err[i],
+ "Error accessing %s", dev);
+ }
+ }
+
+ error.Set(oss_output_domain,
+ "error trying to open default OSS device");
+ return NULL;
+}
+
+static AudioOutput *
+oss_output_init(const config_param &param, Error &error)
+{
+ const char *device = param.GetBlockValue("device");
+ if (device != NULL) {
+ OssOutput *od = new OssOutput();
+ if (!od->Initialize(param, error)) {
+ delete od;
+ return NULL;
+ }
+
+ od->device = device;
+ return &od->base;
+ }
+
+ return oss_open_default(error);
+}
+
+static void
+oss_output_finish(AudioOutput *ao)
+{
+ OssOutput *od = (OssOutput *)ao;
+
+ delete od;
+}
+
+#ifdef AFMT_S24_PACKED
+
+static bool
+oss_output_enable(AudioOutput *ao, gcc_unused Error &error)
+{
+ OssOutput *od = (OssOutput *)ao;
+
+ od->pcm_export.Construct();
+ return true;
+}
+
+static void
+oss_output_disable(AudioOutput *ao)
+{
+ OssOutput *od = (OssOutput *)ao;
+
+ od->pcm_export.Destruct();
+}
+
+#endif
+
+static void
+oss_close(OssOutput *od)
+{
+ if (od->fd >= 0)
+ close(od->fd);
+ od->fd = -1;
+}
+
+/**
+ * A tri-state type for oss_try_ioctl().
+ */
+enum oss_setup_result {
+ SUCCESS,
+ ERROR,
+ UNSUPPORTED,
+};
+
+/**
+ * Invoke an ioctl on the OSS file descriptor. On success, SUCCESS is
+ * returned. If the parameter is not supported, UNSUPPORTED is
+ * returned. Any other failure returns ERROR and allocates an #Error.
+ */
+static enum oss_setup_result
+oss_try_ioctl_r(int fd, unsigned long request, int *value_r,
+ const char *msg, Error &error)
+{
+ assert(fd >= 0);
+ assert(value_r != NULL);
+ assert(msg != NULL);
+ assert(!error.IsDefined());
+
+ int ret = ioctl(fd, request, value_r);
+ if (ret >= 0)
+ return SUCCESS;
+
+ if (errno == EINVAL)
+ return UNSUPPORTED;
+
+ error.SetErrno(msg);
+ return ERROR;
+}
+
+/**
+ * Invoke an ioctl on the OSS file descriptor. On success, SUCCESS is
+ * returned. If the parameter is not supported, UNSUPPORTED is
+ * returned. Any other failure returns ERROR and allocates an #Error.
+ */
+static enum oss_setup_result
+oss_try_ioctl(int fd, unsigned long request, int value,
+ const char *msg, Error &error_r)
+{
+ return oss_try_ioctl_r(fd, request, &value, msg, error_r);
+}
+
+/**
+ * Set up the channel number, and attempts to find alternatives if the
+ * specified number is not supported.
+ */
+static bool
+oss_setup_channels(int fd, AudioFormat &audio_format, Error &error)
+{
+ const char *const msg = "Failed to set channel count";
+ int channels = audio_format.channels;
+ enum oss_setup_result result =
+ oss_try_ioctl_r(fd, SNDCTL_DSP_CHANNELS, &channels, msg, error);
+ switch (result) {
+ case SUCCESS:
+ if (!audio_valid_channel_count(channels))
+ break;
+
+ audio_format.channels = channels;
+ return true;
+
+ case ERROR:
+ return false;
+
+ case UNSUPPORTED:
+ break;
+ }
+
+ for (unsigned i = 1; i < 2; ++i) {
+ if (i == audio_format.channels)
+ /* don't try that again */
+ continue;
+
+ channels = i;
+ result = oss_try_ioctl_r(fd, SNDCTL_DSP_CHANNELS, &channels,
+ msg, error);
+ switch (result) {
+ case SUCCESS:
+ if (!audio_valid_channel_count(channels))
+ break;
+
+ audio_format.channels = channels;
+ return true;
+
+ case ERROR:
+ return false;
+
+ case UNSUPPORTED:
+ break;
+ }
+ }
+
+ error.Set(oss_output_domain, msg);
+ return false;
+}
+
+/**
+ * Set up the sample rate, and attempts to find alternatives if the
+ * specified sample rate is not supported.
+ */
+static bool
+oss_setup_sample_rate(int fd, AudioFormat &audio_format,
+ Error &error)
+{
+ const char *const msg = "Failed to set sample rate";
+ int sample_rate = audio_format.sample_rate;
+ enum oss_setup_result result =
+ oss_try_ioctl_r(fd, SNDCTL_DSP_SPEED, &sample_rate,
+ msg, error);
+ switch (result) {
+ case SUCCESS:
+ if (!audio_valid_sample_rate(sample_rate))
+ break;
+
+ audio_format.sample_rate = sample_rate;
+ return true;
+
+ case ERROR:
+ return false;
+
+ case UNSUPPORTED:
+ break;
+ }
+
+ static const int sample_rates[] = { 48000, 44100, 0 };
+ for (unsigned i = 0; sample_rates[i] != 0; ++i) {
+ sample_rate = sample_rates[i];
+ if (sample_rate == (int)audio_format.sample_rate)
+ continue;
+
+ result = oss_try_ioctl_r(fd, SNDCTL_DSP_SPEED, &sample_rate,
+ msg, error);
+ switch (result) {
+ case SUCCESS:
+ if (!audio_valid_sample_rate(sample_rate))
+ break;
+
+ audio_format.sample_rate = sample_rate;
+ return true;
+
+ case ERROR:
+ return false;
+
+ case UNSUPPORTED:
+ break;
+ }
+ }
+
+ error.Set(oss_output_domain, msg);
+ return false;
+}
+
+/**
+ * Convert a MPD sample format to its OSS counterpart. Returns
+ * AFMT_QUERY if there is no direct counterpart.
+ */
+static int
+sample_format_to_oss(SampleFormat format)
+{
+ switch (format) {
+ case SampleFormat::UNDEFINED:
+ case SampleFormat::FLOAT:
+ case SampleFormat::DSD:
+ return AFMT_QUERY;
+
+ case SampleFormat::S8:
+ return AFMT_S8;
+
+ case SampleFormat::S16:
+ return AFMT_S16_NE;
+
+ case SampleFormat::S24_P32:
+#ifdef AFMT_S24_NE
+ return AFMT_S24_NE;
+#else
+ return AFMT_QUERY;
+#endif
+
+ case SampleFormat::S32:
+#ifdef AFMT_S32_NE
+ return AFMT_S32_NE;
+#else
+ return AFMT_QUERY;
+#endif
+ }
+
+ return AFMT_QUERY;
+}
+
+/**
+ * Convert an OSS sample format to its MPD counterpart. Returns
+ * SampleFormat::UNDEFINED if there is no direct counterpart.
+ */
+static SampleFormat
+sample_format_from_oss(int format)
+{
+ switch (format) {
+ case AFMT_S8:
+ return SampleFormat::S8;
+
+ case AFMT_S16_NE:
+ return SampleFormat::S16;
+
+#ifdef AFMT_S24_PACKED
+ case AFMT_S24_PACKED:
+ return SampleFormat::S24_P32;
+#endif
+
+#ifdef AFMT_S24_NE
+ case AFMT_S24_NE:
+ return SampleFormat::S24_P32;
+#endif
+
+#ifdef AFMT_S32_NE
+ case AFMT_S32_NE:
+ return SampleFormat::S32;
+#endif
+
+ default:
+ return SampleFormat::UNDEFINED;
+ }
+}
+
+/**
+ * Probe one sample format.
+ *
+ * @return the selected sample format or SampleFormat::UNDEFINED on
+ * error
+ */
+static enum oss_setup_result
+oss_probe_sample_format(int fd, SampleFormat sample_format,
+ SampleFormat *sample_format_r,
+ int *oss_format_r,
+#ifdef AFMT_S24_PACKED
+ PcmExport &pcm_export,
+#endif
+ Error &error)
+{
+ int oss_format = sample_format_to_oss(sample_format);
+ if (oss_format == AFMT_QUERY)
+ return UNSUPPORTED;
+
+ enum oss_setup_result result =
+ oss_try_ioctl_r(fd, SNDCTL_DSP_SAMPLESIZE,
+ &oss_format,
+ "Failed to set sample format", error);
+
+#ifdef AFMT_S24_PACKED
+ if (result == UNSUPPORTED && sample_format == SampleFormat::S24_P32) {
+ /* if the driver doesn't support padded 24 bit, try
+ packed 24 bit */
+ oss_format = AFMT_S24_PACKED;
+ result = oss_try_ioctl_r(fd, SNDCTL_DSP_SAMPLESIZE,
+ &oss_format,
+ "Failed to set sample format", error);
+ }
+#endif
+
+ if (result != SUCCESS)
+ return result;
+
+ sample_format = sample_format_from_oss(oss_format);
+ if (sample_format == SampleFormat::UNDEFINED)
+ return UNSUPPORTED;
+
+ *sample_format_r = sample_format;
+ *oss_format_r = oss_format;
+
+#ifdef AFMT_S24_PACKED
+ pcm_export.Open(sample_format, 0, false, false,
+ oss_format == AFMT_S24_PACKED,
+ oss_format == AFMT_S24_PACKED &&
+ !IsLittleEndian());
+#endif
+
+ return SUCCESS;
+}
+
+/**
+ * Set up the sample format, and attempts to find alternatives if the
+ * specified format is not supported.
+ */
+static bool
+oss_setup_sample_format(int fd, AudioFormat &audio_format,
+ int *oss_format_r,
+#ifdef AFMT_S24_PACKED
+ PcmExport &pcm_export,
+#endif
+ Error &error)
+{
+ SampleFormat mpd_format;
+ enum oss_setup_result result =
+ oss_probe_sample_format(fd, audio_format.format,
+ &mpd_format, oss_format_r,
+#ifdef AFMT_S24_PACKED
+ pcm_export,
+#endif
+ error);
+ switch (result) {
+ case SUCCESS:
+ audio_format.format = mpd_format;
+ return true;
+
+ case ERROR:
+ return false;
+
+ case UNSUPPORTED:
+ break;
+ }
+
+ if (result != UNSUPPORTED)
+ return result == SUCCESS;
+
+ /* the requested sample format is not available - probe for
+ other formats supported by MPD */
+
+ static const SampleFormat sample_formats[] = {
+ SampleFormat::S24_P32,
+ SampleFormat::S32,
+ SampleFormat::S16,
+ SampleFormat::S8,
+ SampleFormat::UNDEFINED /* sentinel */
+ };
+
+ for (unsigned i = 0; sample_formats[i] != SampleFormat::UNDEFINED; ++i) {
+ mpd_format = sample_formats[i];
+ if (mpd_format == audio_format.format)
+ /* don't try that again */
+ continue;
+
+ result = oss_probe_sample_format(fd, mpd_format,
+ &mpd_format, oss_format_r,
+#ifdef AFMT_S24_PACKED
+ pcm_export,
+#endif
+ error);
+ switch (result) {
+ case SUCCESS:
+ audio_format.format = mpd_format;
+ return true;
+
+ case ERROR:
+ return false;
+
+ case UNSUPPORTED:
+ break;
+ }
+ }
+
+ error.Set(oss_output_domain, "Failed to set sample format");
+ return false;
+}
+
+/**
+ * Sets up the OSS device which was opened before.
+ */
+static bool
+oss_setup(OssOutput *od, AudioFormat &audio_format,
+ Error &error)
+{
+ return oss_setup_channels(od->fd, audio_format, error) &&
+ oss_setup_sample_rate(od->fd, audio_format, error) &&
+ oss_setup_sample_format(od->fd, audio_format, &od->oss_format,
+#ifdef AFMT_S24_PACKED
+ od->pcm_export,
+#endif
+ error);
+}
+
+/**
+ * Reopen the device with the saved audio_format, without any probing.
+ */
+static bool
+oss_reopen(OssOutput *od, Error &error)
+{
+ assert(od->fd < 0);
+
+ od->fd = open_cloexec(od->device, O_WRONLY, 0);
+ if (od->fd < 0) {
+ error.FormatErrno("Error opening OSS device \"%s\"",
+ od->device);
+ return false;
+ }
+
+ enum oss_setup_result result;
+
+ const char *const msg1 = "Failed to set channel count";
+ result = oss_try_ioctl(od->fd, SNDCTL_DSP_CHANNELS,
+ od->audio_format.channels, msg1, error);
+ if (result != SUCCESS) {
+ oss_close(od);
+ if (result == UNSUPPORTED)
+ error.Set(oss_output_domain, msg1);
+ return false;
+ }
+
+ const char *const msg2 = "Failed to set sample rate";
+ result = oss_try_ioctl(od->fd, SNDCTL_DSP_SPEED,
+ od->audio_format.sample_rate, msg2, error);
+ if (result != SUCCESS) {
+ oss_close(od);
+ if (result == UNSUPPORTED)
+ error.Set(oss_output_domain, msg2);
+ return false;
+ }
+
+ const char *const msg3 = "Failed to set sample format";
+ result = oss_try_ioctl(od->fd, SNDCTL_DSP_SAMPLESIZE,
+ od->oss_format,
+ msg3, error);
+ if (result != SUCCESS) {
+ oss_close(od);
+ if (result == UNSUPPORTED)
+ error.Set(oss_output_domain, msg3);
+ return false;
+ }
+
+ return true;
+}
+
+static bool
+oss_output_open(AudioOutput *ao, AudioFormat &audio_format,
+ Error &error)
+{
+ OssOutput *od = (OssOutput *)ao;
+
+ od->fd = open_cloexec(od->device, O_WRONLY, 0);
+ if (od->fd < 0) {
+ error.FormatErrno("Error opening OSS device \"%s\"",
+ od->device);
+ return false;
+ }
+
+ if (!oss_setup(od, audio_format, error)) {
+ oss_close(od);
+ return false;
+ }
+
+ od->audio_format = audio_format;
+ return true;
+}
+
+static void
+oss_output_close(AudioOutput *ao)
+{
+ OssOutput *od = (OssOutput *)ao;
+
+ oss_close(od);
+}
+
+static void
+oss_output_cancel(AudioOutput *ao)
+{
+ OssOutput *od = (OssOutput *)ao;
+
+ if (od->fd >= 0) {
+ ioctl(od->fd, SNDCTL_DSP_RESET, 0);
+ oss_close(od);
+ }
+}
+
+static size_t
+oss_output_play(AudioOutput *ao, const void *chunk, size_t size,
+ Error &error)
+{
+ OssOutput *od = (OssOutput *)ao;
+ ssize_t ret;
+
+ assert(size > 0);
+
+ /* reopen the device since it was closed by dropBufferedAudio */
+ if (od->fd < 0 && !oss_reopen(od, error))
+ return 0;
+
+#ifdef AFMT_S24_PACKED
+ const auto e = od->pcm_export->Export({chunk, size});
+ chunk = e.data;
+ size = e.size;
+#endif
+
+ assert(size > 0);
+
+ while (true) {
+ ret = write(od->fd, chunk, size);
+ if (ret > 0) {
+#ifdef AFMT_S24_PACKED
+ ret = od->pcm_export->CalcSourceSize(ret);
+#endif
+ return ret;
+ }
+
+ if (ret < 0 && errno != EINTR) {
+ error.FormatErrno("Write error on %s", od->device);
+ return 0;
+ }
+ }
+}
+
+const struct AudioOutputPlugin oss_output_plugin = {
+ "oss",
+ oss_output_test_default_device,
+ oss_output_init,
+ oss_output_finish,
+#ifdef AFMT_S24_PACKED
+ oss_output_enable,
+ oss_output_disable,
+#else
+ nullptr,
+ nullptr,
+#endif
+ oss_output_open,
+ oss_output_close,
+ nullptr,
+ nullptr,
+ oss_output_play,
+ nullptr,
+ oss_output_cancel,
+ nullptr,
+
+ &oss_mixer_plugin,
+};
diff --git a/src/output/plugins/OssOutputPlugin.hxx b/src/output/plugins/OssOutputPlugin.hxx
new file mode 100644
index 000000000..f9970c8f0
--- /dev/null
+++ b/src/output/plugins/OssOutputPlugin.hxx
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_OSS_OUTPUT_PLUGIN_HXX
+#define MPD_OSS_OUTPUT_PLUGIN_HXX
+
+extern const struct AudioOutputPlugin oss_output_plugin;
+
+#endif
diff --git a/src/output/plugins/PipeOutputPlugin.cxx b/src/output/plugins/PipeOutputPlugin.cxx
new file mode 100644
index 000000000..7a1f32258
--- /dev/null
+++ b/src/output/plugins/PipeOutputPlugin.cxx
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "PipeOutputPlugin.hxx"
+#include "../OutputAPI.hxx"
+#include "config/ConfigError.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+
+#include <string>
+
+#include <stdio.h>
+
+struct PipeOutput {
+ AudioOutput base;
+
+ std::string cmd;
+ FILE *fh;
+
+ PipeOutput()
+ :base(pipe_output_plugin) {}
+
+ bool Initialize(const config_param &param, Error &error) {
+ return base.Configure(param, error);
+ }
+
+ bool Configure(const config_param &param, Error &error);
+};
+
+static constexpr Domain pipe_output_domain("pipe_output");
+
+inline bool
+PipeOutput::Configure(const config_param &param, Error &error)
+{
+ cmd = param.GetBlockValue("command", "");
+ if (cmd.empty()) {
+ error.Set(config_domain,
+ "No \"command\" parameter specified");
+ return false;
+ }
+
+ return true;
+}
+
+static AudioOutput *
+pipe_output_init(const config_param &param, Error &error)
+{
+ PipeOutput *pd = new PipeOutput();
+
+ if (!pd->Initialize(param, error)) {
+ delete pd;
+ return nullptr;
+ }
+
+ if (!pd->Configure(param, error)) {
+ delete pd;
+ return nullptr;
+ }
+
+ return &pd->base;
+}
+
+static void
+pipe_output_finish(AudioOutput *ao)
+{
+ PipeOutput *pd = (PipeOutput *)ao;
+
+ delete pd;
+}
+
+static bool
+pipe_output_open(AudioOutput *ao,
+ gcc_unused AudioFormat &audio_format,
+ Error &error)
+{
+ PipeOutput *pd = (PipeOutput *)ao;
+
+ pd->fh = popen(pd->cmd.c_str(), "w");
+ if (pd->fh == nullptr) {
+ error.FormatErrno("Error opening pipe \"%s\"",
+ pd->cmd.c_str());
+ return false;
+ }
+
+ return true;
+}
+
+static void
+pipe_output_close(AudioOutput *ao)
+{
+ PipeOutput *pd = (PipeOutput *)ao;
+
+ pclose(pd->fh);
+}
+
+static size_t
+pipe_output_play(AudioOutput *ao, const void *chunk, size_t size,
+ Error &error)
+{
+ PipeOutput *pd = (PipeOutput *)ao;
+ size_t ret;
+
+ ret = fwrite(chunk, 1, size, pd->fh);
+ if (ret == 0)
+ error.SetErrno("Write error on pipe");
+
+ return ret;
+}
+
+const struct AudioOutputPlugin pipe_output_plugin = {
+ "pipe",
+ nullptr,
+ pipe_output_init,
+ pipe_output_finish,
+ nullptr,
+ nullptr,
+ pipe_output_open,
+ pipe_output_close,
+ nullptr,
+ nullptr,
+ pipe_output_play,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+};
diff --git a/src/output/plugins/PipeOutputPlugin.hxx b/src/output/plugins/PipeOutputPlugin.hxx
new file mode 100644
index 000000000..bdaf2ecfd
--- /dev/null
+++ b/src/output/plugins/PipeOutputPlugin.hxx
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_PIPE_OUTPUT_PLUGIN_HXX
+#define MPD_PIPE_OUTPUT_PLUGIN_HXX
+
+extern const struct AudioOutputPlugin pipe_output_plugin;
+
+#endif
diff --git a/src/output/plugins/PulseOutputPlugin.cxx b/src/output/plugins/PulseOutputPlugin.cxx
new file mode 100644
index 000000000..120bad090
--- /dev/null
+++ b/src/output/plugins/PulseOutputPlugin.cxx
@@ -0,0 +1,882 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "PulseOutputPlugin.hxx"
+#include "../OutputAPI.hxx"
+#include "mixer/MixerList.hxx"
+#include "mixer/plugins/PulseMixerPlugin.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+#include "Log.hxx"
+
+#include <pulse/thread-mainloop.h>
+#include <pulse/context.h>
+#include <pulse/stream.h>
+#include <pulse/introspect.h>
+#include <pulse/subscribe.h>
+#include <pulse/error.h>
+#include <pulse/version.h>
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdlib.h>
+
+#define MPD_PULSE_NAME "Music Player Daemon"
+
+struct PulseOutput {
+ AudioOutput base;
+
+ const char *name;
+ const char *server;
+ const char *sink;
+
+ PulseMixer *mixer;
+
+ struct pa_threaded_mainloop *mainloop;
+ struct pa_context *context;
+ struct pa_stream *stream;
+
+ size_t writable;
+
+ PulseOutput()
+ :base(pulse_output_plugin) {}
+};
+
+static constexpr Domain pulse_output_domain("pulse_output");
+
+static void
+SetError(Error &error, pa_context *context, const char *msg)
+{
+ const int e = pa_context_errno(context);
+ error.Format(pulse_output_domain, e, "%s: %s", msg, pa_strerror(e));
+}
+
+void
+pulse_output_lock(PulseOutput &po)
+{
+ pa_threaded_mainloop_lock(po.mainloop);
+}
+
+void
+pulse_output_unlock(PulseOutput &po)
+{
+ pa_threaded_mainloop_unlock(po.mainloop);
+}
+
+void
+pulse_output_set_mixer(PulseOutput &po, PulseMixer &pm)
+{
+ assert(po.mixer == nullptr);
+
+ po.mixer = &pm;
+
+ if (po.mainloop == nullptr)
+ return;
+
+ pa_threaded_mainloop_lock(po.mainloop);
+
+ if (po.context != nullptr &&
+ pa_context_get_state(po.context) == PA_CONTEXT_READY) {
+ pulse_mixer_on_connect(pm, po.context);
+
+ if (po.stream != nullptr &&
+ pa_stream_get_state(po.stream) == PA_STREAM_READY)
+ pulse_mixer_on_change(pm, po.context, po.stream);
+ }
+
+ pa_threaded_mainloop_unlock(po.mainloop);
+}
+
+void
+pulse_output_clear_mixer(PulseOutput &po, gcc_unused PulseMixer &pm)
+{
+ assert(po.mixer == &pm);
+
+ po.mixer = nullptr;
+}
+
+bool
+pulse_output_set_volume(PulseOutput &po, const pa_cvolume *volume,
+ Error &error)
+{
+ pa_operation *o;
+
+ if (po.context == nullptr || po.stream == nullptr ||
+ pa_stream_get_state(po.stream) != PA_STREAM_READY) {
+ error.Set(pulse_output_domain, "disconnected");
+ return false;
+ }
+
+ o = pa_context_set_sink_input_volume(po.context,
+ pa_stream_get_index(po.stream),
+ volume, nullptr, nullptr);
+ if (o == nullptr) {
+ SetError(error, po.context,
+ "failed to set PulseAudio volume");
+ return false;
+ }
+
+ pa_operation_unref(o);
+ return true;
+}
+
+/**
+ * \brief waits for a pulseaudio operation to finish, frees it and
+ * unlocks the mainloop
+ * \param operation the operation to wait for
+ * \return true if operation has finished normally (DONE state),
+ * false otherwise
+ */
+static bool
+pulse_wait_for_operation(struct pa_threaded_mainloop *mainloop,
+ struct pa_operation *operation)
+{
+ assert(mainloop != nullptr);
+ assert(operation != nullptr);
+
+ pa_operation_state_t state;
+ while ((state = pa_operation_get_state(operation))
+ == PA_OPERATION_RUNNING)
+ pa_threaded_mainloop_wait(mainloop);
+
+ pa_operation_unref(operation);
+
+ return state == PA_OPERATION_DONE;
+}
+
+/**
+ * Callback function for stream operation. It just sends a signal to
+ * the caller thread, to wake pulse_wait_for_operation() up.
+ */
+static void
+pulse_output_stream_success_cb(gcc_unused pa_stream *s,
+ gcc_unused int success, void *userdata)
+{
+ PulseOutput *po = (PulseOutput *)userdata;
+
+ pa_threaded_mainloop_signal(po->mainloop, 0);
+}
+
+static void
+pulse_output_context_state_cb(struct pa_context *context, void *userdata)
+{
+ PulseOutput *po = (PulseOutput *)userdata;
+
+ switch (pa_context_get_state(context)) {
+ case PA_CONTEXT_READY:
+ if (po->mixer != nullptr)
+ pulse_mixer_on_connect(*po->mixer, context);
+
+ pa_threaded_mainloop_signal(po->mainloop, 0);
+ break;
+
+ case PA_CONTEXT_TERMINATED:
+ case PA_CONTEXT_FAILED:
+ if (po->mixer != nullptr)
+ pulse_mixer_on_disconnect(*po->mixer);
+
+ /* the caller thread might be waiting for these
+ states */
+ pa_threaded_mainloop_signal(po->mainloop, 0);
+ break;
+
+ case PA_CONTEXT_UNCONNECTED:
+ case PA_CONTEXT_CONNECTING:
+ case PA_CONTEXT_AUTHORIZING:
+ case PA_CONTEXT_SETTING_NAME:
+ break;
+ }
+}
+
+static void
+pulse_output_subscribe_cb(pa_context *context,
+ pa_subscription_event_type_t t,
+ uint32_t idx, void *userdata)
+{
+ PulseOutput *po = (PulseOutput *)userdata;
+ pa_subscription_event_type_t facility =
+ pa_subscription_event_type_t(t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK);
+ pa_subscription_event_type_t type =
+ pa_subscription_event_type_t(t & PA_SUBSCRIPTION_EVENT_TYPE_MASK);
+
+ if (po->mixer != nullptr &&
+ facility == PA_SUBSCRIPTION_EVENT_SINK_INPUT &&
+ po->stream != nullptr &&
+ pa_stream_get_state(po->stream) == PA_STREAM_READY &&
+ idx == pa_stream_get_index(po->stream) &&
+ (type == PA_SUBSCRIPTION_EVENT_NEW ||
+ type == PA_SUBSCRIPTION_EVENT_CHANGE))
+ pulse_mixer_on_change(*po->mixer, context, po->stream);
+}
+
+/**
+ * Attempt to connect asynchronously to the PulseAudio server.
+ *
+ * @return true on success, false on error
+ */
+static bool
+pulse_output_connect(PulseOutput *po, Error &error)
+{
+ assert(po != nullptr);
+ assert(po->context != nullptr);
+
+ if (pa_context_connect(po->context, po->server,
+ (pa_context_flags_t)0, nullptr) < 0) {
+ SetError(error, po->context,
+ "pa_context_connect() has failed");
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Frees and clears the stream.
+ */
+static void
+pulse_output_delete_stream(PulseOutput *po)
+{
+ assert(po != nullptr);
+ assert(po->stream != nullptr);
+
+ pa_stream_set_suspended_callback(po->stream, nullptr, nullptr);
+
+ pa_stream_set_state_callback(po->stream, nullptr, nullptr);
+ pa_stream_set_write_callback(po->stream, nullptr, nullptr);
+
+ pa_stream_disconnect(po->stream);
+ pa_stream_unref(po->stream);
+ po->stream = nullptr;
+}
+
+/**
+ * Frees and clears the context.
+ *
+ * Caller must lock the main loop.
+ */
+static void
+pulse_output_delete_context(PulseOutput *po)
+{
+ assert(po != nullptr);
+ assert(po->context != nullptr);
+
+ pa_context_set_state_callback(po->context, nullptr, nullptr);
+ pa_context_set_subscribe_callback(po->context, nullptr, nullptr);
+
+ pa_context_disconnect(po->context);
+ pa_context_unref(po->context);
+ po->context = nullptr;
+}
+
+/**
+ * Create, set up and connect a context.
+ *
+ * Caller must lock the main loop.
+ *
+ * @return true on success, false on error
+ */
+static bool
+pulse_output_setup_context(PulseOutput *po, Error &error)
+{
+ assert(po != nullptr);
+ assert(po->mainloop != nullptr);
+
+ po->context = pa_context_new(pa_threaded_mainloop_get_api(po->mainloop),
+ MPD_PULSE_NAME);
+ if (po->context == nullptr) {
+ error.Set(pulse_output_domain, "pa_context_new() has failed");
+ return false;
+ }
+
+ pa_context_set_state_callback(po->context,
+ pulse_output_context_state_cb, po);
+ pa_context_set_subscribe_callback(po->context,
+ pulse_output_subscribe_cb, po);
+
+ if (!pulse_output_connect(po, error)) {
+ pulse_output_delete_context(po);
+ return false;
+ }
+
+ return true;
+}
+
+static AudioOutput *
+pulse_output_init(const config_param &param, Error &error)
+{
+ PulseOutput *po;
+
+ setenv("PULSE_PROP_media.role", "music", true);
+ setenv("PULSE_PROP_application.icon_name", "mpd", true);
+
+ po = new PulseOutput();
+ if (!po->base.Configure(param, error)) {
+ delete po;
+ return nullptr;
+ }
+
+ po->name = param.GetBlockValue("name", "mpd_pulse");
+ po->server = param.GetBlockValue("server");
+ po->sink = param.GetBlockValue("sink");
+
+ po->mixer = nullptr;
+ po->mainloop = nullptr;
+ po->context = nullptr;
+ po->stream = nullptr;
+
+ return &po->base;
+}
+
+static void
+pulse_output_finish(AudioOutput *ao)
+{
+ PulseOutput *po = (PulseOutput *)ao;
+
+ delete po;
+}
+
+static bool
+pulse_output_enable(AudioOutput *ao, Error &error)
+{
+ PulseOutput *po = (PulseOutput *)ao;
+
+ assert(po->mainloop == nullptr);
+ assert(po->context == nullptr);
+
+ /* create the libpulse mainloop and start the thread */
+
+ po->mainloop = pa_threaded_mainloop_new();
+ if (po->mainloop == nullptr) {
+ error.Set(pulse_output_domain,
+ "pa_threaded_mainloop_new() has failed");
+ return false;
+ }
+
+ pa_threaded_mainloop_lock(po->mainloop);
+
+ if (pa_threaded_mainloop_start(po->mainloop) < 0) {
+ pa_threaded_mainloop_unlock(po->mainloop);
+ pa_threaded_mainloop_free(po->mainloop);
+ po->mainloop = nullptr;
+
+ error.Set(pulse_output_domain,
+ "pa_threaded_mainloop_start() has failed");
+ return false;
+ }
+
+ /* create the libpulse context and connect it */
+
+ if (!pulse_output_setup_context(po, error)) {
+ pa_threaded_mainloop_unlock(po->mainloop);
+ pa_threaded_mainloop_stop(po->mainloop);
+ pa_threaded_mainloop_free(po->mainloop);
+ po->mainloop = nullptr;
+ return false;
+ }
+
+ pa_threaded_mainloop_unlock(po->mainloop);
+
+ return true;
+}
+
+static void
+pulse_output_disable(AudioOutput *ao)
+{
+ PulseOutput *po = (PulseOutput *)ao;
+
+ assert(po->mainloop != nullptr);
+
+ pa_threaded_mainloop_stop(po->mainloop);
+ if (po->context != nullptr)
+ pulse_output_delete_context(po);
+ pa_threaded_mainloop_free(po->mainloop);
+ po->mainloop = nullptr;
+}
+
+/**
+ * Check if the context is (already) connected, and waits if not. If
+ * the context has been disconnected, retry to connect.
+ *
+ * Caller must lock the main loop.
+ *
+ * @return true on success, false on error
+ */
+static bool
+pulse_output_wait_connection(PulseOutput *po, Error &error)
+{
+ assert(po->mainloop != nullptr);
+
+ pa_context_state_t state;
+
+ if (po->context == nullptr && !pulse_output_setup_context(po, error))
+ return false;
+
+ while (true) {
+ state = pa_context_get_state(po->context);
+ switch (state) {
+ case PA_CONTEXT_READY:
+ /* nothing to do */
+ return true;
+
+ case PA_CONTEXT_UNCONNECTED:
+ case PA_CONTEXT_TERMINATED:
+ case PA_CONTEXT_FAILED:
+ /* failure */
+ SetError(error, po->context, "failed to connect");
+ pulse_output_delete_context(po);
+ return false;
+
+ case PA_CONTEXT_CONNECTING:
+ case PA_CONTEXT_AUTHORIZING:
+ case PA_CONTEXT_SETTING_NAME:
+ /* wait some more */
+ pa_threaded_mainloop_wait(po->mainloop);
+ break;
+ }
+ }
+}
+
+static void
+pulse_output_stream_suspended_cb(gcc_unused pa_stream *stream, void *userdata)
+{
+ PulseOutput *po = (PulseOutput *)userdata;
+
+ assert(stream == po->stream || po->stream == nullptr);
+ assert(po->mainloop != nullptr);
+
+ /* wake up the main loop to break out of the loop in
+ pulse_output_play() */
+ pa_threaded_mainloop_signal(po->mainloop, 0);
+}
+
+static void
+pulse_output_stream_state_cb(pa_stream *stream, void *userdata)
+{
+ PulseOutput *po = (PulseOutput *)userdata;
+
+ assert(stream == po->stream || po->stream == nullptr);
+ assert(po->mainloop != nullptr);
+ assert(po->context != nullptr);
+
+ switch (pa_stream_get_state(stream)) {
+ case PA_STREAM_READY:
+ if (po->mixer != nullptr)
+ pulse_mixer_on_change(*po->mixer, po->context, stream);
+
+ pa_threaded_mainloop_signal(po->mainloop, 0);
+ break;
+
+ case PA_STREAM_FAILED:
+ case PA_STREAM_TERMINATED:
+ if (po->mixer != nullptr)
+ pulse_mixer_on_disconnect(*po->mixer);
+
+ pa_threaded_mainloop_signal(po->mainloop, 0);
+ break;
+
+ case PA_STREAM_UNCONNECTED:
+ case PA_STREAM_CREATING:
+ break;
+ }
+}
+
+static void
+pulse_output_stream_write_cb(gcc_unused pa_stream *stream, size_t nbytes,
+ void *userdata)
+{
+ PulseOutput *po = (PulseOutput *)userdata;
+
+ assert(po->mainloop != nullptr);
+
+ po->writable = nbytes;
+ pa_threaded_mainloop_signal(po->mainloop, 0);
+}
+
+/**
+ * Create, set up and connect a context.
+ *
+ * Caller must lock the main loop.
+ *
+ * @return true on success, false on error
+ */
+static bool
+pulse_output_setup_stream(PulseOutput *po, const pa_sample_spec *ss,
+ Error &error)
+{
+ assert(po != nullptr);
+ assert(po->context != nullptr);
+
+ po->stream = pa_stream_new(po->context, po->name, ss, nullptr);
+ if (po->stream == nullptr) {
+ SetError(error, po->context, "pa_stream_new() has failed");
+ return false;
+ }
+
+ pa_stream_set_suspended_callback(po->stream,
+ pulse_output_stream_suspended_cb, po);
+
+ pa_stream_set_state_callback(po->stream,
+ pulse_output_stream_state_cb, po);
+ pa_stream_set_write_callback(po->stream,
+ pulse_output_stream_write_cb, po);
+
+ return true;
+}
+
+static bool
+pulse_output_open(AudioOutput *ao, AudioFormat &audio_format,
+ Error &error)
+{
+ PulseOutput *po = (PulseOutput *)ao;
+ pa_sample_spec ss;
+
+ assert(po->mainloop != nullptr);
+
+ pa_threaded_mainloop_lock(po->mainloop);
+
+ if (po->context != nullptr) {
+ switch (pa_context_get_state(po->context)) {
+ case PA_CONTEXT_UNCONNECTED:
+ case PA_CONTEXT_TERMINATED:
+ case PA_CONTEXT_FAILED:
+ /* the connection was closed meanwhile; delete
+ it, and pulse_output_wait_connection() will
+ reopen it */
+ pulse_output_delete_context(po);
+ break;
+
+ case PA_CONTEXT_READY:
+ case PA_CONTEXT_CONNECTING:
+ case PA_CONTEXT_AUTHORIZING:
+ case PA_CONTEXT_SETTING_NAME:
+ break;
+ }
+ }
+
+ if (!pulse_output_wait_connection(po, error)) {
+ pa_threaded_mainloop_unlock(po->mainloop);
+ return false;
+ }
+
+ /* MPD doesn't support the other pulseaudio sample formats, so
+ we just force MPD to send us everything as 16 bit */
+ audio_format.format = SampleFormat::S16;
+
+ ss.format = PA_SAMPLE_S16NE;
+ ss.rate = audio_format.sample_rate;
+ ss.channels = audio_format.channels;
+
+ /* create a stream .. */
+
+ if (!pulse_output_setup_stream(po, &ss, error)) {
+ pa_threaded_mainloop_unlock(po->mainloop);
+ return false;
+ }
+
+ /* .. and connect it (asynchronously) */
+
+ if (pa_stream_connect_playback(po->stream, po->sink,
+ nullptr, pa_stream_flags_t(0),
+ nullptr, nullptr) < 0) {
+ pulse_output_delete_stream(po);
+
+ SetError(error, po->context,
+ "pa_stream_connect_playback() has failed");
+ pa_threaded_mainloop_unlock(po->mainloop);
+ return false;
+ }
+
+ pa_threaded_mainloop_unlock(po->mainloop);
+
+ return true;
+}
+
+static void
+pulse_output_close(AudioOutput *ao)
+{
+ PulseOutput *po = (PulseOutput *)ao;
+ pa_operation *o;
+
+ assert(po->mainloop != nullptr);
+
+ pa_threaded_mainloop_lock(po->mainloop);
+
+ if (pa_stream_get_state(po->stream) == PA_STREAM_READY) {
+ o = pa_stream_drain(po->stream,
+ pulse_output_stream_success_cb, po);
+ if (o == nullptr) {
+ FormatWarning(pulse_output_domain,
+ "pa_stream_drain() has failed: %s",
+ pa_strerror(pa_context_errno(po->context)));
+ } else
+ pulse_wait_for_operation(po->mainloop, o);
+ }
+
+ pulse_output_delete_stream(po);
+
+ if (po->context != nullptr &&
+ pa_context_get_state(po->context) != PA_CONTEXT_READY)
+ pulse_output_delete_context(po);
+
+ pa_threaded_mainloop_unlock(po->mainloop);
+}
+
+/**
+ * Check if the stream is (already) connected, and waits if not. The
+ * mainloop must be locked before calling this function.
+ *
+ * @return true on success, false on error
+ */
+static bool
+pulse_output_wait_stream(PulseOutput *po, Error &error)
+{
+ while (true) {
+ switch (pa_stream_get_state(po->stream)) {
+ case PA_STREAM_READY:
+ return true;
+
+ case PA_STREAM_FAILED:
+ case PA_STREAM_TERMINATED:
+ case PA_STREAM_UNCONNECTED:
+ SetError(error, po->context,
+ "failed to connect the stream");
+ return false;
+
+ case PA_STREAM_CREATING:
+ pa_threaded_mainloop_wait(po->mainloop);
+ break;
+ }
+ }
+}
+
+/**
+ * Sets cork mode on the stream.
+ */
+static bool
+pulse_output_stream_pause(PulseOutput *po, bool pause,
+ Error &error)
+{
+ pa_operation *o;
+
+ assert(po->mainloop != nullptr);
+ assert(po->context != nullptr);
+ assert(po->stream != nullptr);
+
+ o = pa_stream_cork(po->stream, pause,
+ pulse_output_stream_success_cb, po);
+ if (o == nullptr) {
+ SetError(error, po->context, "pa_stream_cork() has failed");
+ return false;
+ }
+
+ if (!pulse_wait_for_operation(po->mainloop, o)) {
+ SetError(error, po->context, "pa_stream_cork() has failed");
+ return false;
+ }
+
+ return true;
+}
+
+static unsigned
+pulse_output_delay(AudioOutput *ao)
+{
+ PulseOutput *po = (PulseOutput *)ao;
+ unsigned result = 0;
+
+ pa_threaded_mainloop_lock(po->mainloop);
+
+ if (po->base.pause && pa_stream_is_corked(po->stream) &&
+ pa_stream_get_state(po->stream) == PA_STREAM_READY)
+ /* idle while paused */
+ result = 1000;
+
+ pa_threaded_mainloop_unlock(po->mainloop);
+
+ return result;
+}
+
+static size_t
+pulse_output_play(AudioOutput *ao, const void *chunk, size_t size,
+ Error &error)
+{
+ PulseOutput *po = (PulseOutput *)ao;
+
+ assert(po->mainloop != nullptr);
+ assert(po->stream != nullptr);
+
+ pa_threaded_mainloop_lock(po->mainloop);
+
+ /* check if the stream is (already) connected */
+
+ if (!pulse_output_wait_stream(po, error)) {
+ pa_threaded_mainloop_unlock(po->mainloop);
+ return 0;
+ }
+
+ assert(po->context != nullptr);
+
+ /* unpause if previously paused */
+
+ if (pa_stream_is_corked(po->stream) &&
+ !pulse_output_stream_pause(po, false, error)) {
+ pa_threaded_mainloop_unlock(po->mainloop);
+ return 0;
+ }
+
+ /* wait until the server allows us to write */
+
+ while (po->writable == 0) {
+ if (pa_stream_is_suspended(po->stream)) {
+ pa_threaded_mainloop_unlock(po->mainloop);
+ error.Set(pulse_output_domain, "suspended");
+ return 0;
+ }
+
+ pa_threaded_mainloop_wait(po->mainloop);
+
+ if (pa_stream_get_state(po->stream) != PA_STREAM_READY) {
+ pa_threaded_mainloop_unlock(po->mainloop);
+ error.Set(pulse_output_domain, "disconnected");
+ return 0;
+ }
+ }
+
+ /* now write */
+
+ if (size > po->writable)
+ /* don't send more than possible */
+ size = po->writable;
+
+ po->writable -= size;
+
+ int result = pa_stream_write(po->stream, chunk, size, nullptr,
+ 0, PA_SEEK_RELATIVE);
+ pa_threaded_mainloop_unlock(po->mainloop);
+ if (result < 0) {
+ SetError(error, po->context, "pa_stream_write() failed");
+ return 0;
+ }
+
+ return size;
+}
+
+static void
+pulse_output_cancel(AudioOutput *ao)
+{
+ PulseOutput *po = (PulseOutput *)ao;
+ pa_operation *o;
+
+ assert(po->mainloop != nullptr);
+ assert(po->stream != nullptr);
+
+ pa_threaded_mainloop_lock(po->mainloop);
+
+ if (pa_stream_get_state(po->stream) != PA_STREAM_READY) {
+ /* no need to flush when the stream isn't connected
+ yet */
+ pa_threaded_mainloop_unlock(po->mainloop);
+ return;
+ }
+
+ assert(po->context != nullptr);
+
+ o = pa_stream_flush(po->stream, pulse_output_stream_success_cb, po);
+ if (o == nullptr) {
+ FormatWarning(pulse_output_domain,
+ "pa_stream_flush() has failed: %s",
+ pa_strerror(pa_context_errno(po->context)));
+ pa_threaded_mainloop_unlock(po->mainloop);
+ return;
+ }
+
+ pulse_wait_for_operation(po->mainloop, o);
+ pa_threaded_mainloop_unlock(po->mainloop);
+}
+
+static bool
+pulse_output_pause(AudioOutput *ao)
+{
+ PulseOutput *po = (PulseOutput *)ao;
+
+ assert(po->mainloop != nullptr);
+ assert(po->stream != nullptr);
+
+ pa_threaded_mainloop_lock(po->mainloop);
+
+ /* check if the stream is (already/still) connected */
+
+ Error error;
+ if (!pulse_output_wait_stream(po, error)) {
+ pa_threaded_mainloop_unlock(po->mainloop);
+ LogError(error);
+ return false;
+ }
+
+ assert(po->context != nullptr);
+
+ /* cork the stream */
+
+ if (!pa_stream_is_corked(po->stream) &&
+ !pulse_output_stream_pause(po, true, error)) {
+ pa_threaded_mainloop_unlock(po->mainloop);
+ LogError(error);
+ return false;
+ }
+
+ pa_threaded_mainloop_unlock(po->mainloop);
+
+ return true;
+}
+
+static bool
+pulse_output_test_default_device(void)
+{
+ bool success;
+
+ const config_param empty;
+ PulseOutput *po = (PulseOutput *)
+ pulse_output_init(empty, IgnoreError());
+ if (po == nullptr)
+ return false;
+
+ success = pulse_output_wait_connection(po, IgnoreError());
+ pulse_output_finish(&po->base);
+
+ return success;
+}
+
+const struct AudioOutputPlugin pulse_output_plugin = {
+ "pulse",
+ pulse_output_test_default_device,
+ pulse_output_init,
+ pulse_output_finish,
+ pulse_output_enable,
+ pulse_output_disable,
+ pulse_output_open,
+ pulse_output_close,
+ pulse_output_delay,
+ nullptr,
+ pulse_output_play,
+ nullptr,
+ pulse_output_cancel,
+ pulse_output_pause,
+
+ &pulse_mixer_plugin,
+};
diff --git a/src/output/plugins/PulseOutputPlugin.hxx b/src/output/plugins/PulseOutputPlugin.hxx
new file mode 100644
index 000000000..9219780a5
--- /dev/null
+++ b/src/output/plugins/PulseOutputPlugin.hxx
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_PULSE_OUTPUT_PLUGIN_HXX
+#define MPD_PULSE_OUTPUT_PLUGIN_HXX
+
+struct PulseOutput;
+class PulseMixer;
+struct pa_cvolume;
+class Error;
+
+extern const struct AudioOutputPlugin pulse_output_plugin;
+
+void
+pulse_output_lock(PulseOutput &po);
+
+void
+pulse_output_unlock(PulseOutput &po);
+
+void
+pulse_output_set_mixer(PulseOutput &po, PulseMixer &pm);
+
+void
+pulse_output_clear_mixer(PulseOutput &po, PulseMixer &pm);
+
+bool
+pulse_output_set_volume(PulseOutput &po,
+ const pa_cvolume *volume, Error &error);
+
+#endif
diff --git a/src/output/plugins/RecorderOutputPlugin.cxx b/src/output/plugins/RecorderOutputPlugin.cxx
new file mode 100644
index 000000000..87e23f55a
--- /dev/null
+++ b/src/output/plugins/RecorderOutputPlugin.cxx
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "RecorderOutputPlugin.hxx"
+#include "../OutputAPI.hxx"
+#include "encoder/EncoderPlugin.hxx"
+#include "encoder/EncoderList.hxx"
+#include "config/ConfigError.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+#include "system/fd_util.h"
+#include "open.h"
+
+#include <assert.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <errno.h>
+
+struct RecorderOutput {
+ AudioOutput base;
+
+ /**
+ * The configured encoder plugin.
+ */
+ Encoder *encoder;
+
+ /**
+ * The destination file name.
+ */
+ const char *path;
+
+ /**
+ * The destination file descriptor.
+ */
+ int fd;
+
+ /**
+ * The buffer for encoder_read().
+ */
+ char buffer[32768];
+
+ RecorderOutput()
+ :base(recorder_output_plugin) {}
+
+ bool Initialize(const config_param &param, Error &error_r) {
+ return base.Configure(param, error_r);
+ }
+
+ bool Configure(const config_param &param, Error &error);
+
+ bool WriteToFile(const void *data, size_t length, Error &error);
+
+ /**
+ * Writes pending data from the encoder to the output file.
+ */
+ bool EncoderToFile(Error &error);
+};
+
+static constexpr Domain recorder_output_domain("recorder_output");
+
+inline bool
+RecorderOutput::Configure(const config_param &param, Error &error)
+{
+ /* read configuration */
+
+ const char *encoder_name =
+ param.GetBlockValue("encoder", "vorbis");
+ const auto encoder_plugin = encoder_plugin_get(encoder_name);
+ if (encoder_plugin == nullptr) {
+ error.Format(config_domain,
+ "No such encoder: %s", encoder_name);
+ return false;
+ }
+
+ path = param.GetBlockValue("path");
+ if (path == nullptr) {
+ error.Set(config_domain, "'path' not configured");
+ return false;
+ }
+
+ /* initialize encoder */
+
+ encoder = encoder_init(*encoder_plugin, param, error);
+ if (encoder == nullptr)
+ return false;
+
+ return true;
+}
+
+static AudioOutput *
+recorder_output_init(const config_param &param, Error &error)
+{
+ RecorderOutput *recorder = new RecorderOutput();
+
+ if (!recorder->Initialize(param, error)) {
+ delete recorder;
+ return nullptr;
+ }
+
+ if (!recorder->Configure(param, error)) {
+ delete recorder;
+ return nullptr;
+ }
+
+ return &recorder->base;
+}
+
+static void
+recorder_output_finish(AudioOutput *ao)
+{
+ RecorderOutput *recorder = (RecorderOutput *)ao;
+
+ encoder_finish(recorder->encoder);
+ delete recorder;
+}
+
+inline bool
+RecorderOutput::WriteToFile(const void *_data, size_t length, Error &error)
+{
+ assert(length > 0);
+
+ const uint8_t *data = (const uint8_t *)_data, *end = data + length;
+
+ while (true) {
+ ssize_t nbytes = write(fd, data, end - data);
+ if (nbytes > 0) {
+ data += nbytes;
+ if (data == end)
+ return true;
+ } else if (nbytes == 0) {
+ /* shouldn't happen for files */
+ error.Set(recorder_output_domain,
+ "write() returned 0");
+ return false;
+ } else if (errno != EINTR) {
+ error.FormatErrno("Failed to write to '%s'", path);
+ return false;
+ }
+ }
+}
+
+inline bool
+RecorderOutput::EncoderToFile(Error &error)
+{
+ assert(fd >= 0);
+
+ while (true) {
+ /* read from the encoder */
+
+ size_t size = encoder_read(encoder, buffer, sizeof(buffer));
+ if (size == 0)
+ return true;
+
+ /* write everything into the file */
+
+ if (!WriteToFile(buffer, size, error))
+ return false;
+ }
+}
+
+static bool
+recorder_output_open(AudioOutput *ao,
+ AudioFormat &audio_format,
+ Error &error)
+{
+ RecorderOutput *recorder = (RecorderOutput *)ao;
+
+ /* create the output file */
+
+ recorder->fd = open_cloexec(recorder->path,
+ O_CREAT|O_WRONLY|O_TRUNC|O_BINARY,
+ 0666);
+ if (recorder->fd < 0) {
+ error.FormatErrno("Failed to create '%s'", recorder->path);
+ return false;
+ }
+
+ /* open the encoder */
+
+ if (!encoder_open(recorder->encoder, audio_format, error)) {
+ close(recorder->fd);
+ unlink(recorder->path);
+ return false;
+ }
+
+ if (!recorder->EncoderToFile(error)) {
+ encoder_close(recorder->encoder);
+ close(recorder->fd);
+ unlink(recorder->path);
+ return false;
+ }
+
+ return true;
+}
+
+static void
+recorder_output_close(AudioOutput *ao)
+{
+ RecorderOutput *recorder = (RecorderOutput *)ao;
+
+ /* flush the encoder and write the rest to the file */
+
+ if (encoder_end(recorder->encoder, IgnoreError()))
+ recorder->EncoderToFile(IgnoreError());
+
+ /* now really close everything */
+
+ encoder_close(recorder->encoder);
+
+ close(recorder->fd);
+}
+
+static size_t
+recorder_output_play(AudioOutput *ao, const void *chunk, size_t size,
+ Error &error)
+{
+ RecorderOutput *recorder = (RecorderOutput *)ao;
+
+ return encoder_write(recorder->encoder, chunk, size, error) &&
+ recorder->EncoderToFile(error)
+ ? size : 0;
+}
+
+const struct AudioOutputPlugin recorder_output_plugin = {
+ "recorder",
+ nullptr,
+ recorder_output_init,
+ recorder_output_finish,
+ nullptr,
+ nullptr,
+ recorder_output_open,
+ recorder_output_close,
+ nullptr,
+ nullptr,
+ recorder_output_play,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+};
diff --git a/src/output/plugins/RecorderOutputPlugin.hxx b/src/output/plugins/RecorderOutputPlugin.hxx
new file mode 100644
index 000000000..ea1044e0f
--- /dev/null
+++ b/src/output/plugins/RecorderOutputPlugin.hxx
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_RECORDER_OUTPUT_PLUGIN_HXX
+#define MPD_RECORDER_OUTPUT_PLUGIN_HXX
+
+extern const struct AudioOutputPlugin recorder_output_plugin;
+
+#endif
diff --git a/src/output/plugins/RoarOutputPlugin.cxx b/src/output/plugins/RoarOutputPlugin.cxx
new file mode 100644
index 000000000..04836cc46
--- /dev/null
+++ b/src/output/plugins/RoarOutputPlugin.cxx
@@ -0,0 +1,429 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * Copyright (C) 2010-2011 Philipp 'ph3-der-loewe' Schafft
+ * Copyright (C) 2010-2011 Hans-Kristian 'maister' Arntzen
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "RoarOutputPlugin.hxx"
+#include "../OutputAPI.hxx"
+#include "mixer/MixerList.hxx"
+#include "thread/Mutex.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+#include "Log.hxx"
+
+#include <string>
+
+/* libroar/services.h declares roar_service_stream::new - work around
+ this C++ problem */
+#define new _new
+#include <roaraudio.h>
+#undef new
+
+class RoarOutput {
+ AudioOutput base;
+
+ std::string host, name;
+
+ roar_vs_t * vss;
+ int err;
+ int role;
+ struct roar_connection con;
+ struct roar_audio_info info;
+ mutable Mutex mutex;
+ volatile bool alive;
+
+public:
+ RoarOutput()
+ :base(roar_output_plugin),
+ err(ROAR_ERROR_NONE) {}
+
+ operator AudioOutput *() {
+ return &base;
+ }
+
+ bool Initialize(const config_param &param, Error &error) {
+ return base.Configure(param, error);
+ }
+
+ void Configure(const config_param &param);
+
+ bool Open(AudioFormat &audio_format, Error &error);
+ void Close();
+
+ void SendTag(const Tag &tag);
+ size_t Play(const void *chunk, size_t size, Error &error);
+ void Cancel();
+
+ int GetVolume() const;
+ bool SetVolume(unsigned volume);
+};
+
+static constexpr Domain roar_output_domain("roar_output");
+
+inline int
+RoarOutput::GetVolume() const
+{
+ const ScopeLock protect(mutex);
+
+ if (vss == nullptr || !alive)
+ return -1;
+
+ float l, r;
+ int error;
+ if (roar_vs_volume_get(vss, &l, &r, &error) < 0)
+ return -1;
+
+ return (l + r) * 50;
+}
+
+int
+roar_output_get_volume(RoarOutput &roar)
+{
+ return roar.GetVolume();
+}
+
+bool
+RoarOutput::SetVolume(unsigned volume)
+{
+ assert(volume <= 100);
+
+ const ScopeLock protect(mutex);
+ if (vss == nullptr || !alive)
+ return false;
+
+ int error;
+ float level = volume / 100.0;
+
+ roar_vs_volume_mono(vss, level, &error);
+ return true;
+}
+
+bool
+roar_output_set_volume(RoarOutput &roar, unsigned volume)
+{
+ return roar.SetVolume(volume);
+}
+
+inline void
+RoarOutput::Configure(const config_param &param)
+{
+ host = param.GetBlockValue("server", "");
+ name = param.GetBlockValue("name", "MPD");
+
+ const char *_role = param.GetBlockValue("role", "music");
+ role = _role != nullptr
+ ? roar_str2role(_role)
+ : ROAR_ROLE_MUSIC;
+}
+
+static AudioOutput *
+roar_init(const config_param &param, Error &error)
+{
+ RoarOutput *self = new RoarOutput();
+
+ if (!self->Initialize(param, error)) {
+ delete self;
+ return nullptr;
+ }
+
+ self->Configure(param);
+ return *self;
+}
+
+static void
+roar_finish(AudioOutput *ao)
+{
+ RoarOutput *self = (RoarOutput *)ao;
+
+ delete self;
+}
+
+static void
+roar_use_audio_format(struct roar_audio_info *info,
+ AudioFormat &audio_format)
+{
+ info->rate = audio_format.sample_rate;
+ info->channels = audio_format.channels;
+ info->codec = ROAR_CODEC_PCM_S;
+
+ switch (audio_format.format) {
+ case SampleFormat::UNDEFINED:
+ case SampleFormat::FLOAT:
+ case SampleFormat::DSD:
+ info->bits = 16;
+ audio_format.format = SampleFormat::S16;
+ break;
+
+ case SampleFormat::S8:
+ info->bits = 8;
+ break;
+
+ case SampleFormat::S16:
+ info->bits = 16;
+ break;
+
+ case SampleFormat::S24_P32:
+ info->bits = 32;
+ audio_format.format = SampleFormat::S32;
+ break;
+
+ case SampleFormat::S32:
+ info->bits = 32;
+ break;
+ }
+}
+
+inline bool
+RoarOutput::Open(AudioFormat &audio_format, Error &error)
+{
+ const ScopeLock protect(mutex);
+
+ if (roar_simple_connect(&con,
+ host.empty() ? nullptr : host.c_str(),
+ name.c_str()) < 0) {
+ error.Set(roar_output_domain,
+ "Failed to connect to Roar server");
+ return false;
+ }
+
+ vss = roar_vs_new_from_con(&con, &err);
+
+ if (vss == nullptr || err != ROAR_ERROR_NONE) {
+ error.Set(roar_output_domain, "Failed to connect to server");
+ return false;
+ }
+
+ roar_use_audio_format(&info, audio_format);
+
+ if (roar_vs_stream(vss, &info, ROAR_DIR_PLAY, &err) < 0) {
+ error.Set(roar_output_domain, "Failed to start stream");
+ return false;
+ }
+
+ roar_vs_role(vss, role, &err);
+ alive = true;
+ return true;
+}
+
+static bool
+roar_open(AudioOutput *ao, AudioFormat &audio_format, Error &error)
+{
+ RoarOutput *self = (RoarOutput *)ao;
+
+ return self->Open(audio_format, error);
+}
+
+inline void
+RoarOutput::Close()
+{
+ const ScopeLock protect(mutex);
+
+ alive = false;
+
+ if (vss != nullptr)
+ roar_vs_close(vss, ROAR_VS_TRUE, &err);
+ vss = nullptr;
+ roar_disconnect(&con);
+}
+
+static void
+roar_close(AudioOutput *ao)
+{
+ RoarOutput *self = (RoarOutput *)ao;
+ self->Close();
+}
+
+inline void
+RoarOutput::Cancel()
+{
+ const ScopeLock protect(mutex);
+
+ if (vss == nullptr)
+ return;
+
+ roar_vs_t *_vss = vss;
+ vss = nullptr;
+ roar_vs_close(_vss, ROAR_VS_TRUE, &err);
+ alive = false;
+
+ _vss = roar_vs_new_from_con(&con, &err);
+ if (_vss == nullptr)
+ return;
+
+ if (roar_vs_stream(_vss, &info, ROAR_DIR_PLAY, &err) < 0) {
+ roar_vs_close(_vss, ROAR_VS_TRUE, &err);
+ LogError(roar_output_domain, "Failed to start stream");
+ return;
+ }
+
+ roar_vs_role(_vss, role, &err);
+ vss = _vss;
+ alive = true;
+}
+
+static void
+roar_cancel(AudioOutput *ao)
+{
+ RoarOutput *self = (RoarOutput *)ao;
+
+ self->Cancel();
+}
+
+inline size_t
+RoarOutput::Play(const void *chunk, size_t size, Error &error)
+{
+ if (vss == nullptr) {
+ error.Set(roar_output_domain, "Connection is invalid");
+ return 0;
+ }
+
+ ssize_t nbytes = roar_vs_write(vss, chunk, size, &err);
+ if (nbytes <= 0) {
+ error.Set(roar_output_domain, "Failed to play data");
+ return 0;
+ }
+
+ return nbytes;
+}
+
+static size_t
+roar_play(AudioOutput *ao, const void *chunk, size_t size,
+ Error &error)
+{
+ RoarOutput *self = (RoarOutput *)ao;
+ return self->Play(chunk, size, error);
+}
+
+static const char*
+roar_tag_convert(TagType type, bool *is_uuid)
+{
+ *is_uuid = false;
+ switch (type)
+ {
+ case TAG_ARTIST:
+ case TAG_ALBUM_ARTIST:
+ return "AUTHOR";
+ case TAG_ALBUM:
+ return "ALBUM";
+ case TAG_TITLE:
+ return "TITLE";
+ case TAG_TRACK:
+ return "TRACK";
+ case TAG_NAME:
+ return "NAME";
+ case TAG_GENRE:
+ return "GENRE";
+ case TAG_DATE:
+ return "DATE";
+ case TAG_PERFORMER:
+ return "PERFORMER";
+ case TAG_COMMENT:
+ return "COMMENT";
+ case TAG_DISC:
+ return "DISCID";
+ case TAG_COMPOSER:
+#ifdef ROAR_META_TYPE_COMPOSER
+ return "COMPOSER";
+#else
+ return "AUTHOR";
+#endif
+ case TAG_MUSICBRAINZ_ARTISTID:
+ case TAG_MUSICBRAINZ_ALBUMID:
+ case TAG_MUSICBRAINZ_ALBUMARTISTID:
+ case TAG_MUSICBRAINZ_TRACKID:
+ *is_uuid = true;
+ return "HASH";
+
+ default:
+ return nullptr;
+ }
+}
+
+inline void
+RoarOutput::SendTag(const Tag &tag)
+{
+ if (vss == nullptr)
+ return;
+
+ const ScopeLock protect(mutex);
+
+ size_t cnt = 0;
+ struct roar_keyval vals[32];
+ char uuid_buf[32][64];
+
+ char timebuf[16];
+ if (!tag.duration.IsNegative()) {
+ const unsigned seconds = tag.duration.ToS();
+ snprintf(timebuf, sizeof(timebuf), "%02d:%02d:%02d",
+ seconds / 3600, (seconds % 3600) / 60, seconds % 60);
+
+ vals[cnt].key = const_cast<char *>("LENGTH");
+ vals[cnt].value = timebuf;
+ ++cnt;
+ }
+
+ for (const auto &item : tag) {
+ if (cnt >= 32)
+ break;
+
+ bool is_uuid = false;
+ const char *key = roar_tag_convert(item.type,
+ &is_uuid);
+ if (key != nullptr) {
+ vals[cnt].key = const_cast<char *>(key);
+
+ if (is_uuid) {
+ snprintf(uuid_buf[cnt], sizeof(uuid_buf[0]), "{UUID}%s",
+ item.value);
+ vals[cnt].value = uuid_buf[cnt];
+ } else {
+ vals[cnt].value = const_cast<char *>(item.value);
+ }
+
+ cnt++;
+ }
+ }
+
+ roar_vs_meta(vss, vals, cnt, &(err));
+}
+
+static void
+roar_send_tag(AudioOutput *ao, const Tag *meta)
+{
+ RoarOutput *self = (RoarOutput *)ao;
+ self->SendTag(*meta);
+}
+
+const struct AudioOutputPlugin roar_output_plugin = {
+ "roar",
+ nullptr,
+ roar_init,
+ roar_finish,
+ nullptr,
+ nullptr,
+ roar_open,
+ roar_close,
+ nullptr,
+ roar_send_tag,
+ roar_play,
+ nullptr,
+ roar_cancel,
+ nullptr,
+ &roar_mixer_plugin,
+};
diff --git a/src/output/plugins/RoarOutputPlugin.hxx b/src/output/plugins/RoarOutputPlugin.hxx
new file mode 100644
index 000000000..5f5a9246e
--- /dev/null
+++ b/src/output/plugins/RoarOutputPlugin.hxx
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_ROAR_OUTPUT_PLUGIN_H
+#define MPD_ROAR_OUTPUT_PLUGIN_H
+
+class RoarOutput;
+
+extern const struct AudioOutputPlugin roar_output_plugin;
+
+int
+roar_output_get_volume(RoarOutput &roar);
+
+bool
+roar_output_set_volume(RoarOutput &roar, unsigned volume);
+
+#endif
diff --git a/src/output/plugins/ShoutOutputPlugin.cxx b/src/output/plugins/ShoutOutputPlugin.cxx
new file mode 100644
index 000000000..0341e1cf7
--- /dev/null
+++ b/src/output/plugins/ShoutOutputPlugin.cxx
@@ -0,0 +1,537 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "ShoutOutputPlugin.hxx"
+#include "../OutputAPI.hxx"
+#include "encoder/EncoderPlugin.hxx"
+#include "encoder/EncoderList.hxx"
+#include "config/ConfigError.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+#include "system/FatalError.hxx"
+#include "Log.hxx"
+
+#include <shout/shout.h>
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+static constexpr unsigned DEFAULT_CONN_TIMEOUT = 2;
+
+struct ShoutOutput final {
+ AudioOutput base;
+
+ shout_t *shout_conn;
+ shout_metadata_t *shout_meta;
+
+ Encoder *encoder;
+
+ float quality;
+ int bitrate;
+
+ int timeout;
+
+ uint8_t buffer[32768];
+
+ ShoutOutput()
+ :base(shout_output_plugin),
+ shout_conn(shout_new()),
+ shout_meta(shout_metadata_new()),
+ quality(-2.0),
+ bitrate(-1),
+ timeout(DEFAULT_CONN_TIMEOUT) {}
+
+ ~ShoutOutput() {
+ if (shout_meta != nullptr)
+ shout_metadata_free(shout_meta);
+ if (shout_conn != nullptr)
+ shout_free(shout_conn);
+ }
+
+ bool Initialize(const config_param &param, Error &error) {
+ return base.Configure(param, error);
+ }
+
+ bool Configure(const config_param &param, Error &error);
+};
+
+static int shout_init_count;
+
+static constexpr Domain shout_output_domain("shout_output");
+
+static const EncoderPlugin *
+shout_encoder_plugin_get(const char *name)
+{
+ if (strcmp(name, "ogg") == 0)
+ name = "vorbis";
+ else if (strcmp(name, "mp3") == 0)
+ name = "lame";
+
+ return encoder_plugin_get(name);
+}
+
+gcc_pure
+static const char *
+require_block_string(const config_param &param, const char *name)
+{
+ const char *value = param.GetBlockValue(name);
+ if (value == nullptr)
+ FormatFatalError("no \"%s\" defined for shout device defined "
+ "at line %u\n", name, param.line);
+
+ return value;
+}
+
+inline bool
+ShoutOutput::Configure(const config_param &param, Error &error)
+{
+
+ const AudioFormat audio_format = base.config_audio_format;
+ if (!audio_format.IsFullyDefined()) {
+ error.Set(config_domain,
+ "Need full audio format specification");
+ return nullptr;
+ }
+
+ const char *host = require_block_string(param, "host");
+ const char *mount = require_block_string(param, "mount");
+ unsigned port = param.GetBlockValue("port", 0u);
+ if (port == 0) {
+ error.Set(config_domain, "shout port must be configured");
+ return false;
+ }
+
+ const char *passwd = require_block_string(param, "password");
+ const char *name = require_block_string(param, "name");
+
+ bool is_public = param.GetBlockValue("public", false);
+
+ const char *user = param.GetBlockValue("user", "source");
+
+ const char *value = param.GetBlockValue("quality");
+ if (value != nullptr) {
+ char *test;
+ quality = strtod(value, &test);
+
+ if (*test != '\0' || quality < -1.0 || quality > 10.0) {
+ error.Format(config_domain,
+ "shout quality \"%s\" is not a number in the "
+ "range -1 to 10",
+ value);
+ return false;
+ }
+
+ if (param.GetBlockValue("bitrate") != nullptr) {
+ error.Set(config_domain,
+ "quality and bitrate are "
+ "both defined");
+ return false;
+ }
+ } else {
+ value = param.GetBlockValue("bitrate");
+ if (value == nullptr) {
+ error.Set(config_domain,
+ "neither bitrate nor quality defined");
+ return false;
+ }
+
+ char *test;
+ bitrate = strtol(value, &test, 10);
+
+ if (*test != '\0' || bitrate <= 0) {
+ error.Set(config_domain,
+ "bitrate must be a positive integer");
+ return false;
+ }
+ }
+
+ const char *encoding = param.GetBlockValue("encoding", "ogg");
+ const auto encoder_plugin = shout_encoder_plugin_get(encoding);
+ if (encoder_plugin == nullptr) {
+ error.Format(config_domain,
+ "couldn't find shout encoder plugin \"%s\"",
+ encoding);
+ return false;
+ }
+
+ encoder = encoder_init(*encoder_plugin, param, error);
+ if (encoder == nullptr)
+ return false;
+
+ unsigned shout_format;
+ if (strcmp(encoding, "mp3") == 0 || strcmp(encoding, "lame") == 0)
+ shout_format = SHOUT_FORMAT_MP3;
+ else
+ shout_format = SHOUT_FORMAT_OGG;
+
+ unsigned protocol;
+ value = param.GetBlockValue("protocol");
+ if (value != nullptr) {
+ if (0 == strcmp(value, "shoutcast") &&
+ 0 != strcmp(encoding, "mp3")) {
+ error.Format(config_domain,
+ "you cannot stream \"%s\" to shoutcast, use mp3",
+ encoding);
+ return false;
+ } else if (0 == strcmp(value, "shoutcast"))
+ protocol = SHOUT_PROTOCOL_ICY;
+ else if (0 == strcmp(value, "icecast1"))
+ protocol = SHOUT_PROTOCOL_XAUDIOCAST;
+ else if (0 == strcmp(value, "icecast2"))
+ protocol = SHOUT_PROTOCOL_HTTP;
+ else {
+ error.Format(config_domain,
+ "shout protocol \"%s\" is not \"shoutcast\" or "
+ "\"icecast1\"or \"icecast2\"",
+ value);
+ return false;
+ }
+ } else {
+ protocol = SHOUT_PROTOCOL_HTTP;
+ }
+
+ if (shout_set_host(shout_conn, host) != SHOUTERR_SUCCESS ||
+ shout_set_port(shout_conn, port) != SHOUTERR_SUCCESS ||
+ shout_set_password(shout_conn, passwd) != SHOUTERR_SUCCESS ||
+ shout_set_mount(shout_conn, mount) != SHOUTERR_SUCCESS ||
+ shout_set_name(shout_conn, name) != SHOUTERR_SUCCESS ||
+ shout_set_user(shout_conn, user) != SHOUTERR_SUCCESS ||
+ shout_set_public(shout_conn, is_public) != SHOUTERR_SUCCESS ||
+ shout_set_format(shout_conn, shout_format)
+ != SHOUTERR_SUCCESS ||
+ shout_set_protocol(shout_conn, protocol) != SHOUTERR_SUCCESS ||
+ shout_set_agent(shout_conn, "MPD") != SHOUTERR_SUCCESS) {
+ error.Set(shout_output_domain, shout_get_error(shout_conn));
+ return false;
+ }
+
+ /* optional paramters */
+ timeout = param.GetBlockValue("timeout", DEFAULT_CONN_TIMEOUT);
+
+ value = param.GetBlockValue("genre");
+ if (value != nullptr && shout_set_genre(shout_conn, value)) {
+ error.Set(shout_output_domain, shout_get_error(shout_conn));
+ return false;
+ }
+
+ value = param.GetBlockValue("description");
+ if (value != nullptr && shout_set_description(shout_conn, value)) {
+ error.Set(shout_output_domain, shout_get_error(shout_conn));
+ return false;
+ }
+
+ value = param.GetBlockValue("url");
+ if (value != nullptr && shout_set_url(shout_conn, value)) {
+ error.Set(shout_output_domain, shout_get_error(shout_conn));
+ return false;
+ }
+
+ {
+ char temp[11];
+ memset(temp, 0, sizeof(temp));
+
+ snprintf(temp, sizeof(temp), "%u", audio_format.channels);
+ shout_set_audio_info(shout_conn, SHOUT_AI_CHANNELS, temp);
+
+ snprintf(temp, sizeof(temp), "%u", audio_format.sample_rate);
+
+ shout_set_audio_info(shout_conn, SHOUT_AI_SAMPLERATE, temp);
+
+ if (quality >= -1.0) {
+ snprintf(temp, sizeof(temp), "%2.2f", quality);
+ shout_set_audio_info(shout_conn, SHOUT_AI_QUALITY,
+ temp);
+ } else {
+ snprintf(temp, sizeof(temp), "%d", bitrate);
+ shout_set_audio_info(shout_conn, SHOUT_AI_BITRATE,
+ temp);
+ }
+ }
+
+ return true;
+}
+
+static AudioOutput *
+my_shout_init_driver(const config_param &param, Error &error)
+{
+ ShoutOutput *sd = new ShoutOutput();
+ if (!sd->Initialize(param, error)) {
+ delete sd;
+ return nullptr;
+ }
+
+ if (!sd->Configure(param, error)) {
+ delete sd;
+ return nullptr;
+ }
+
+ if (shout_init_count == 0)
+ shout_init();
+
+ shout_init_count++;
+
+ return &sd->base;
+}
+
+static bool
+handle_shout_error(ShoutOutput *sd, int err, Error &error)
+{
+ switch (err) {
+ case SHOUTERR_SUCCESS:
+ break;
+
+ case SHOUTERR_UNCONNECTED:
+ case SHOUTERR_SOCKET:
+ error.Format(shout_output_domain, err,
+ "Lost shout connection to %s:%i: %s",
+ shout_get_host(sd->shout_conn),
+ shout_get_port(sd->shout_conn),
+ shout_get_error(sd->shout_conn));
+ return false;
+
+ default:
+ error.Format(shout_output_domain, err,
+ "connection to %s:%i error: %s",
+ shout_get_host(sd->shout_conn),
+ shout_get_port(sd->shout_conn),
+ shout_get_error(sd->shout_conn));
+ return false;
+ }
+
+ return true;
+}
+
+static bool
+write_page(ShoutOutput *sd, Error &error)
+{
+ assert(sd->encoder != nullptr);
+
+ while (true) {
+ size_t nbytes = encoder_read(sd->encoder,
+ sd->buffer, sizeof(sd->buffer));
+ if (nbytes == 0)
+ return true;
+
+ int err = shout_send(sd->shout_conn, sd->buffer, nbytes);
+ if (!handle_shout_error(sd, err, error))
+ return false;
+ }
+
+ return true;
+}
+
+static void close_shout_conn(ShoutOutput * sd)
+{
+ if (sd->encoder != nullptr) {
+ if (encoder_end(sd->encoder, IgnoreError()))
+ write_page(sd, IgnoreError());
+
+ encoder_close(sd->encoder);
+ }
+
+ if (shout_get_connected(sd->shout_conn) != SHOUTERR_UNCONNECTED &&
+ shout_close(sd->shout_conn) != SHOUTERR_SUCCESS) {
+ FormatWarning(shout_output_domain,
+ "problem closing connection to shout server: %s",
+ shout_get_error(sd->shout_conn));
+ }
+}
+
+static void
+my_shout_finish_driver(AudioOutput *ao)
+{
+ ShoutOutput *sd = (ShoutOutput *)ao;
+
+ encoder_finish(sd->encoder);
+
+ delete sd;
+
+ shout_init_count--;
+
+ if (shout_init_count == 0)
+ shout_shutdown();
+}
+
+static void
+my_shout_drop_buffered_audio(AudioOutput *ao)
+{
+ gcc_unused
+ ShoutOutput *sd = (ShoutOutput *)ao;
+
+ /* needs to be implemented for shout */
+}
+
+static void
+my_shout_close_device(AudioOutput *ao)
+{
+ ShoutOutput *sd = (ShoutOutput *)ao;
+
+ close_shout_conn(sd);
+}
+
+static bool
+shout_connect(ShoutOutput *sd, Error &error)
+{
+ switch (shout_open(sd->shout_conn)) {
+ case SHOUTERR_SUCCESS:
+ case SHOUTERR_CONNECTED:
+ return true;
+
+ default:
+ error.Format(shout_output_domain,
+ "problem opening connection to shout server %s:%i: %s",
+ shout_get_host(sd->shout_conn),
+ shout_get_port(sd->shout_conn),
+ shout_get_error(sd->shout_conn));
+ return false;
+ }
+}
+
+static bool
+my_shout_open_device(AudioOutput *ao, AudioFormat &audio_format,
+ Error &error)
+{
+ ShoutOutput *sd = (ShoutOutput *)ao;
+
+ if (!shout_connect(sd, error))
+ return false;
+
+ if (!encoder_open(sd->encoder, audio_format, error)) {
+ shout_close(sd->shout_conn);
+ return false;
+ }
+
+ if (!write_page(sd, error)) {
+ encoder_close(sd->encoder);
+ shout_close(sd->shout_conn);
+ return false;
+ }
+
+ return true;
+}
+
+static unsigned
+my_shout_delay(AudioOutput *ao)
+{
+ ShoutOutput *sd = (ShoutOutput *)ao;
+
+ int delay = shout_delay(sd->shout_conn);
+ if (delay < 0)
+ delay = 0;
+
+ return delay;
+}
+
+static size_t
+my_shout_play(AudioOutput *ao, const void *chunk, size_t size,
+ Error &error)
+{
+ ShoutOutput *sd = (ShoutOutput *)ao;
+
+ return encoder_write(sd->encoder, chunk, size, error) &&
+ write_page(sd, error)
+ ? size
+ : 0;
+}
+
+static bool
+my_shout_pause(AudioOutput *ao)
+{
+ static char silence[1020];
+
+ return my_shout_play(ao, silence, sizeof(silence), IgnoreError());
+}
+
+static void
+shout_tag_to_metadata(const Tag *tag, char *dest, size_t size)
+{
+ char artist[size];
+ char title[size];
+
+ artist[0] = 0;
+ title[0] = 0;
+
+ for (const auto &item : *tag) {
+ switch (item.type) {
+ case TAG_ARTIST:
+ strncpy(artist, item.value, size);
+ break;
+ case TAG_TITLE:
+ strncpy(title, item.value, size);
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ snprintf(dest, size, "%s - %s", artist, title);
+}
+
+static void my_shout_set_tag(AudioOutput *ao,
+ const Tag *tag)
+{
+ ShoutOutput *sd = (ShoutOutput *)ao;
+
+ if (sd->encoder->plugin.tag != nullptr) {
+ /* encoder plugin supports stream tags */
+
+ Error error;
+ if (!encoder_pre_tag(sd->encoder, error) ||
+ !write_page(sd, error) ||
+ !encoder_tag(sd->encoder, tag, error)) {
+ LogError(error);
+ return;
+ }
+ } else {
+ /* no stream tag support: fall back to icy-metadata */
+ char song[1024];
+ shout_tag_to_metadata(tag, song, sizeof(song));
+
+ shout_metadata_add(sd->shout_meta, "song", song);
+ if (SHOUTERR_SUCCESS != shout_set_metadata(sd->shout_conn,
+ sd->shout_meta)) {
+ LogWarning(shout_output_domain,
+ "error setting shout metadata");
+ }
+ }
+
+ write_page(sd, IgnoreError());
+}
+
+const struct AudioOutputPlugin shout_output_plugin = {
+ "shout",
+ nullptr,
+ my_shout_init_driver,
+ my_shout_finish_driver,
+ nullptr,
+ nullptr,
+ my_shout_open_device,
+ my_shout_close_device,
+ my_shout_delay,
+ my_shout_set_tag,
+ my_shout_play,
+ nullptr,
+ my_shout_drop_buffered_audio,
+ my_shout_pause,
+ nullptr,
+};
diff --git a/src/output/plugins/ShoutOutputPlugin.hxx b/src/output/plugins/ShoutOutputPlugin.hxx
new file mode 100644
index 000000000..9f706fc3b
--- /dev/null
+++ b/src/output/plugins/ShoutOutputPlugin.hxx
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_SHOUT_OUTPUT_PLUGIN_HXX
+#define MPD_SHOUT_OUTPUT_PLUGIN_HXX
+
+extern const struct AudioOutputPlugin shout_output_plugin;
+
+#endif
diff --git a/src/output/plugins/SolarisOutputPlugin.cxx b/src/output/plugins/SolarisOutputPlugin.cxx
new file mode 100644
index 000000000..30745f97c
--- /dev/null
+++ b/src/output/plugins/SolarisOutputPlugin.cxx
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "SolarisOutputPlugin.hxx"
+#include "../OutputAPI.hxx"
+#include "system/fd_util.h"
+#include "util/Error.hxx"
+
+#include <sys/stropts.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+
+#ifdef __sun
+#include <sys/audio.h>
+#else
+
+/* some fake declarations that allow build this plugin on systems
+ other than Solaris, just to see if it compiles */
+
+#define AUDIO_GETINFO 0
+#define AUDIO_SETINFO 0
+#define AUDIO_ENCODING_LINEAR 0
+
+struct audio_info {
+ struct {
+ unsigned sample_rate, channels, precision, encoding;
+ } play;
+};
+
+#endif
+
+struct SolarisOutput {
+ AudioOutput base;
+
+ /* configuration */
+ const char *device;
+
+ int fd;
+
+ SolarisOutput()
+ :base(solaris_output_plugin) {}
+
+ bool Initialize(const config_param &param, Error &error_r) {
+ return base.Configure(param, error_r);
+ }
+};
+
+static bool
+solaris_output_test_default_device(void)
+{
+ struct stat st;
+
+ return stat("/dev/audio", &st) == 0 && S_ISCHR(st.st_mode) &&
+ access("/dev/audio", W_OK) == 0;
+}
+
+static AudioOutput *
+solaris_output_init(const config_param &param, Error &error_r)
+{
+ SolarisOutput *so = new SolarisOutput();
+ if (!so->Initialize(param, error_r)) {
+ delete so;
+ return nullptr;
+ }
+
+ so->device = param.GetBlockValue("device", "/dev/audio");
+
+ return &so->base;
+}
+
+static void
+solaris_output_finish(AudioOutput *ao)
+{
+ SolarisOutput *so = (SolarisOutput *)ao;
+
+ delete so;
+}
+
+static bool
+solaris_output_open(AudioOutput *ao, AudioFormat &audio_format,
+ Error &error)
+{
+ SolarisOutput *so = (SolarisOutput *)ao;
+ struct audio_info info;
+ int ret, flags;
+
+ /* support only 16 bit mono/stereo for now; nothing else has
+ been tested */
+ audio_format.format = SampleFormat::S16;
+
+ /* open the device in non-blocking mode */
+
+ so->fd = open_cloexec(so->device, O_WRONLY|O_NONBLOCK, 0);
+ if (so->fd < 0) {
+ error.FormatErrno("Failed to open %s",
+ so->device);
+ return false;
+ }
+
+ /* restore blocking mode */
+
+ flags = fcntl(so->fd, F_GETFL);
+ if (flags > 0 && (flags & O_NONBLOCK) != 0)
+ fcntl(so->fd, F_SETFL, flags & ~O_NONBLOCK);
+
+ /* configure the audio device */
+
+ ret = ioctl(so->fd, AUDIO_GETINFO, &info);
+ if (ret < 0) {
+ error.SetErrno("AUDIO_GETINFO failed");
+ close(so->fd);
+ return false;
+ }
+
+ info.play.sample_rate = audio_format.sample_rate;
+ info.play.channels = audio_format.channels;
+ info.play.precision = 16;
+ info.play.encoding = AUDIO_ENCODING_LINEAR;
+
+ ret = ioctl(so->fd, AUDIO_SETINFO, &info);
+ if (ret < 0) {
+ error.SetErrno("AUDIO_SETINFO failed");
+ close(so->fd);
+ return false;
+ }
+
+ return true;
+}
+
+static void
+solaris_output_close(AudioOutput *ao)
+{
+ SolarisOutput *so = (SolarisOutput *)ao;
+
+ close(so->fd);
+}
+
+static size_t
+solaris_output_play(AudioOutput *ao, const void *chunk, size_t size,
+ Error &error)
+{
+ SolarisOutput *so = (SolarisOutput *)ao;
+ ssize_t nbytes;
+
+ nbytes = write(so->fd, chunk, size);
+ if (nbytes <= 0) {
+ error.SetErrno("Write failed");
+ return 0;
+ }
+
+ return nbytes;
+}
+
+static void
+solaris_output_cancel(AudioOutput *ao)
+{
+ SolarisOutput *so = (SolarisOutput *)ao;
+
+ ioctl(so->fd, I_FLUSH);
+}
+
+const struct AudioOutputPlugin solaris_output_plugin = {
+ "solaris",
+ solaris_output_test_default_device,
+ solaris_output_init,
+ solaris_output_finish,
+ nullptr,
+ nullptr,
+ solaris_output_open,
+ solaris_output_close,
+ nullptr,
+ nullptr,
+ solaris_output_play,
+ nullptr,
+ solaris_output_cancel,
+ nullptr,
+ nullptr,
+};
diff --git a/src/output/plugins/SolarisOutputPlugin.hxx b/src/output/plugins/SolarisOutputPlugin.hxx
new file mode 100644
index 000000000..3f9ede7a6
--- /dev/null
+++ b/src/output/plugins/SolarisOutputPlugin.hxx
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_SOLARIS_OUTPUT_PLUGIN_HXX
+#define MPD_SOLARIS_OUTPUT_PLUGIN_HXX
+
+extern const struct AudioOutputPlugin solaris_output_plugin;
+
+#endif
diff --git a/src/output/plugins/WinmmOutputPlugin.cxx b/src/output/plugins/WinmmOutputPlugin.cxx
new file mode 100644
index 000000000..e5c5a6f0c
--- /dev/null
+++ b/src/output/plugins/WinmmOutputPlugin.cxx
@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "WinmmOutputPlugin.hxx"
+#include "../OutputAPI.hxx"
+#include "pcm/PcmBuffer.hxx"
+#include "mixer/MixerList.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+#include "util/Macros.hxx"
+
+#include <stdlib.h>
+#include <string.h>
+
+struct WinmmBuffer {
+ PcmBuffer buffer;
+
+ WAVEHDR hdr;
+};
+
+struct WinmmOutput {
+ AudioOutput base;
+
+ UINT device_id;
+ HWAVEOUT handle;
+
+ /**
+ * This event is triggered by Windows when a buffer is
+ * finished.
+ */
+ HANDLE event;
+
+ WinmmBuffer buffers[8];
+ unsigned next_buffer;
+
+ WinmmOutput()
+ :base(winmm_output_plugin) {}
+};
+
+static constexpr Domain winmm_output_domain("winmm_output");
+
+HWAVEOUT
+winmm_output_get_handle(WinmmOutput &output)
+{
+ return output.handle;
+}
+
+static bool
+winmm_output_test_default_device(void)
+{
+ return waveOutGetNumDevs() > 0;
+}
+
+static bool
+get_device_id(const char *device_name, UINT *device_id, Error &error)
+{
+ /* if device is not specified use wave mapper */
+ if (device_name == nullptr) {
+ *device_id = WAVE_MAPPER;
+ return true;
+ }
+
+ UINT numdevs = waveOutGetNumDevs();
+
+ /* check for device id */
+ char *endptr;
+ UINT id = strtoul(device_name, &endptr, 0);
+ if (endptr > device_name && *endptr == 0) {
+ if (id >= numdevs)
+ goto fail;
+ *device_id = id;
+ return true;
+ }
+
+ /* check for device name */
+ for (UINT i = 0; i < numdevs; i++) {
+ WAVEOUTCAPS caps;
+ MMRESULT result = waveOutGetDevCaps(i, &caps, sizeof(caps));
+ if (result != MMSYSERR_NOERROR)
+ continue;
+ /* szPname is only 32 chars long, so it is often truncated.
+ Use partial match to work around this. */
+ if (strstr(device_name, caps.szPname) == device_name) {
+ *device_id = i;
+ return true;
+ }
+ }
+
+fail:
+ error.Format(winmm_output_domain,
+ "device \"%s\" is not found", device_name);
+ return false;
+}
+
+static AudioOutput *
+winmm_output_init(const config_param &param, Error &error)
+{
+ WinmmOutput *wo = new WinmmOutput();
+ if (!wo->base.Configure(param, error)) {
+ delete wo;
+ return nullptr;
+ }
+
+ const char *device = param.GetBlockValue("device");
+ if (!get_device_id(device, &wo->device_id, error)) {
+ delete wo;
+ return nullptr;
+ }
+
+ return &wo->base;
+}
+
+static void
+winmm_output_finish(AudioOutput *ao)
+{
+ WinmmOutput *wo = (WinmmOutput *)ao;
+
+ delete wo;
+}
+
+static bool
+winmm_output_open(AudioOutput *ao, AudioFormat &audio_format,
+ Error &error)
+{
+ WinmmOutput *wo = (WinmmOutput *)ao;
+
+ wo->event = CreateEvent(nullptr, false, false, nullptr);
+ if (wo->event == nullptr) {
+ error.Set(winmm_output_domain, "CreateEvent() failed");
+ return false;
+ }
+
+ switch (audio_format.format) {
+ case SampleFormat::S8:
+ case SampleFormat::S16:
+ break;
+
+ case SampleFormat::S24_P32:
+ case SampleFormat::S32:
+ case SampleFormat::FLOAT:
+ case SampleFormat::DSD:
+ case SampleFormat::UNDEFINED:
+ /* we havn't tested formats other than S16 */
+ audio_format.format = SampleFormat::S16;
+ break;
+ }
+
+ if (audio_format.channels > 2)
+ /* same here: more than stereo was not tested */
+ audio_format.channels = 2;
+
+ WAVEFORMATEX format;
+ format.wFormatTag = WAVE_FORMAT_PCM;
+ format.nChannels = audio_format.channels;
+ format.nSamplesPerSec = audio_format.sample_rate;
+ format.nBlockAlign = audio_format.GetFrameSize();
+ format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign;
+ format.wBitsPerSample = audio_format.GetSampleSize() * 8;
+ format.cbSize = 0;
+
+ MMRESULT result = waveOutOpen(&wo->handle, wo->device_id, &format,
+ (DWORD_PTR)wo->event, 0, CALLBACK_EVENT);
+ if (result != MMSYSERR_NOERROR) {
+ CloseHandle(wo->event);
+ error.Set(winmm_output_domain, "waveOutOpen() failed");
+ return false;
+ }
+
+ for (unsigned i = 0; i < ARRAY_SIZE(wo->buffers); ++i) {
+ memset(&wo->buffers[i].hdr, 0, sizeof(wo->buffers[i].hdr));
+ }
+
+ wo->next_buffer = 0;
+
+ return true;
+}
+
+static void
+winmm_output_close(AudioOutput *ao)
+{
+ WinmmOutput *wo = (WinmmOutput *)ao;
+
+ for (unsigned i = 0; i < ARRAY_SIZE(wo->buffers); ++i)
+ wo->buffers[i].buffer.Clear();
+
+ waveOutClose(wo->handle);
+
+ CloseHandle(wo->event);
+}
+
+/**
+ * Copy data into a buffer, and prepare the wave header.
+ */
+static bool
+winmm_set_buffer(WinmmOutput *wo, WinmmBuffer *buffer,
+ const void *data, size_t size,
+ Error &error)
+{
+ void *dest = buffer->buffer.Get(size);
+ assert(dest != nullptr);
+
+ memcpy(dest, data, size);
+
+ memset(&buffer->hdr, 0, sizeof(buffer->hdr));
+ buffer->hdr.lpData = (LPSTR)dest;
+ buffer->hdr.dwBufferLength = size;
+
+ MMRESULT result = waveOutPrepareHeader(wo->handle, &buffer->hdr,
+ sizeof(buffer->hdr));
+ if (result != MMSYSERR_NOERROR) {
+ error.Set(winmm_output_domain, result,
+ "waveOutPrepareHeader() failed");
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Wait until the buffer is finished.
+ */
+static bool
+winmm_drain_buffer(WinmmOutput *wo, WinmmBuffer *buffer,
+ Error &error)
+{
+ if ((buffer->hdr.dwFlags & WHDR_DONE) == WHDR_DONE)
+ /* already finished */
+ return true;
+
+ while (true) {
+ MMRESULT result = waveOutUnprepareHeader(wo->handle,
+ &buffer->hdr,
+ sizeof(buffer->hdr));
+ if (result == MMSYSERR_NOERROR)
+ return true;
+ else if (result != WAVERR_STILLPLAYING) {
+ error.Set(winmm_output_domain, result,
+ "waveOutUnprepareHeader() failed");
+ return false;
+ }
+
+ /* wait some more */
+ WaitForSingleObject(wo->event, INFINITE);
+ }
+}
+
+static size_t
+winmm_output_play(AudioOutput *ao, const void *chunk, size_t size, Error &error)
+{
+ WinmmOutput *wo = (WinmmOutput *)ao;
+
+ /* get the next buffer from the ring and prepare it */
+ WinmmBuffer *buffer = &wo->buffers[wo->next_buffer];
+ if (!winmm_drain_buffer(wo, buffer, error) ||
+ !winmm_set_buffer(wo, buffer, chunk, size, error))
+ return 0;
+
+ /* enqueue the buffer */
+ MMRESULT result = waveOutWrite(wo->handle, &buffer->hdr,
+ sizeof(buffer->hdr));
+ if (result != MMSYSERR_NOERROR) {
+ waveOutUnprepareHeader(wo->handle, &buffer->hdr,
+ sizeof(buffer->hdr));
+ error.Set(winmm_output_domain, result,
+ "waveOutWrite() failed");
+ return 0;
+ }
+
+ /* mark our buffer as "used" */
+ wo->next_buffer = (wo->next_buffer + 1) %
+ ARRAY_SIZE(wo->buffers);
+
+ return size;
+}
+
+static bool
+winmm_drain_all_buffers(WinmmOutput *wo, Error &error)
+{
+ for (unsigned i = wo->next_buffer; i < ARRAY_SIZE(wo->buffers); ++i)
+ if (!winmm_drain_buffer(wo, &wo->buffers[i], error))
+ return false;
+
+ for (unsigned i = 0; i < wo->next_buffer; ++i)
+ if (!winmm_drain_buffer(wo, &wo->buffers[i], error))
+ return false;
+
+ return true;
+}
+
+static void
+winmm_stop(WinmmOutput *wo)
+{
+ waveOutReset(wo->handle);
+
+ for (unsigned i = 0; i < ARRAY_SIZE(wo->buffers); ++i) {
+ WinmmBuffer *buffer = &wo->buffers[i];
+ waveOutUnprepareHeader(wo->handle, &buffer->hdr,
+ sizeof(buffer->hdr));
+ }
+}
+
+static void
+winmm_output_drain(AudioOutput *ao)
+{
+ WinmmOutput *wo = (WinmmOutput *)ao;
+
+ if (!winmm_drain_all_buffers(wo, IgnoreError()))
+ winmm_stop(wo);
+}
+
+static void
+winmm_output_cancel(AudioOutput *ao)
+{
+ WinmmOutput *wo = (WinmmOutput *)ao;
+
+ winmm_stop(wo);
+}
+
+const struct AudioOutputPlugin winmm_output_plugin = {
+ "winmm",
+ winmm_output_test_default_device,
+ winmm_output_init,
+ winmm_output_finish,
+ nullptr,
+ nullptr,
+ winmm_output_open,
+ winmm_output_close,
+ nullptr,
+ nullptr,
+ winmm_output_play,
+ winmm_output_drain,
+ winmm_output_cancel,
+ nullptr,
+ &winmm_mixer_plugin,
+};
diff --git a/src/output/plugins/WinmmOutputPlugin.hxx b/src/output/plugins/WinmmOutputPlugin.hxx
new file mode 100644
index 000000000..50fae4f2f
--- /dev/null
+++ b/src/output/plugins/WinmmOutputPlugin.hxx
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_WINMM_OUTPUT_PLUGIN_HXX
+#define MPD_WINMM_OUTPUT_PLUGIN_HXX
+
+#include "check.h"
+
+#ifdef ENABLE_WINMM_OUTPUT
+
+#include "Compiler.h"
+
+#include <windows.h>
+#include <mmsystem.h>
+
+struct WinmmOutput;
+
+extern const struct AudioOutputPlugin winmm_output_plugin;
+
+gcc_pure
+HWAVEOUT
+winmm_output_get_handle(WinmmOutput &output);
+
+#endif
+
+#endif
diff --git a/src/output/plugins/httpd/HttpdClient.cxx b/src/output/plugins/httpd/HttpdClient.cxx
new file mode 100644
index 000000000..3797c3d26
--- /dev/null
+++ b/src/output/plugins/httpd/HttpdClient.cxx
@@ -0,0 +1,484 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "HttpdClient.hxx"
+#include "HttpdInternal.hxx"
+#include "util/ASCII.hxx"
+#include "Page.hxx"
+#include "IcyMetaDataServer.hxx"
+#include "system/SocketError.hxx"
+#include "Log.hxx"
+
+#include <assert.h>
+#include <string.h>
+#include <stdio.h>
+
+HttpdClient::~HttpdClient()
+{
+ if (state == RESPONSE) {
+ if (current_page != nullptr)
+ current_page->Unref();
+
+ ClearQueue();
+ }
+
+ if (metadata)
+ metadata->Unref();
+
+ if (IsDefined())
+ BufferedSocket::Close();
+}
+
+void
+HttpdClient::Close()
+{
+ httpd.RemoveClient(*this);
+}
+
+void
+HttpdClient::LockClose()
+{
+ const ScopeLock protect(httpd.mutex);
+ Close();
+}
+
+void
+HttpdClient::BeginResponse()
+{
+ assert(state != RESPONSE);
+
+ state = RESPONSE;
+ current_page = nullptr;
+
+ if (!head_method)
+ httpd.SendHeader(*this);
+}
+
+/**
+ * Handle a line of the HTTP request.
+ */
+bool
+HttpdClient::HandleLine(const char *line)
+{
+ assert(state != RESPONSE);
+
+ if (state == REQUEST) {
+ if (memcmp(line, "HEAD /", 6) == 0) {
+ line += 6;
+ head_method = true;
+ } else if (memcmp(line, "GET /", 5) == 0) {
+ line += 5;
+ } else {
+ /* only GET is supported */
+ LogWarning(httpd_output_domain,
+ "malformed request line from client");
+ return false;
+ }
+
+ line = strchr(line, ' ');
+ if (line == nullptr || memcmp(line + 1, "HTTP/", 5) != 0) {
+ /* HTTP/0.9 without request headers */
+
+ if (head_method)
+ return false;
+
+ BeginResponse();
+ return true;
+ }
+
+ /* after the request line, request headers follow */
+ state = HEADERS;
+ return true;
+ } else {
+ if (*line == 0) {
+ /* empty line: request is finished */
+
+ BeginResponse();
+ return true;
+ }
+
+ if (StringEqualsCaseASCII(line, "Icy-MetaData: 1", 15) ||
+ StringEqualsCaseASCII(line, "Icy-MetaData:1", 14)) {
+ /* Send icy metadata */
+ metadata_requested = metadata_supported;
+ return true;
+ }
+
+ if (StringEqualsCaseASCII(line, "transferMode.dlna.org: Streaming", 32)) {
+ /* Send as dlna */
+ dlna_streaming_requested = true;
+ /* metadata is not supported by dlna streaming, so disable it */
+ metadata_supported = false;
+ metadata_requested = false;
+ return true;
+ }
+
+ /* expect more request headers */
+ return true;
+ }
+}
+
+/**
+ * Sends the status line and response headers to the client.
+ */
+bool
+HttpdClient::SendResponse()
+{
+ char buffer[1024], *allocated = nullptr;
+ const char *response;
+
+ assert(state == RESPONSE);
+
+ if (dlna_streaming_requested) {
+ snprintf(buffer, sizeof(buffer),
+ "HTTP/1.1 206 OK\r\n"
+ "Content-Type: %s\r\n"
+ "Content-Length: 10000\r\n"
+ "Content-RangeX: 0-1000000/1000000\r\n"
+ "transferMode.dlna.org: Streaming\r\n"
+ "Accept-Ranges: bytes\r\n"
+ "Connection: close\r\n"
+ "realTimeInfo.dlna.org: DLNA.ORG_TLAG=*\r\n"
+ "contentFeatures.dlna.org: DLNA.ORG_OP=01;DLNA.ORG_CI=0\r\n"
+ "\r\n",
+ httpd.content_type);
+ response = buffer;
+
+ } else if (metadata_requested) {
+ response = allocated =
+ icy_server_metadata_header(httpd.name, httpd.genre,
+ httpd.website,
+ httpd.content_type,
+ metaint);
+ } else { /* revert to a normal HTTP request */
+ snprintf(buffer, sizeof(buffer),
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Type: %s\r\n"
+ "Connection: close\r\n"
+ "Pragma: no-cache\r\n"
+ "Cache-Control: no-cache, no-store\r\n"
+ "\r\n",
+ httpd.content_type);
+ response = buffer;
+ }
+
+ ssize_t nbytes = SocketMonitor::Write(response, strlen(response));
+ delete[] allocated;
+ if (gcc_unlikely(nbytes < 0)) {
+ const SocketErrorMessage msg;
+ FormatWarning(httpd_output_domain,
+ "failed to write to client: %s",
+ (const char *)msg);
+ Close();
+ return false;
+ }
+
+ return true;
+}
+
+HttpdClient::HttpdClient(HttpdOutput &_httpd, int _fd, EventLoop &_loop,
+ bool _metadata_supported)
+ :BufferedSocket(_fd, _loop),
+ httpd(_httpd),
+ state(REQUEST),
+ queue_size(0),
+ head_method(false),
+ dlna_streaming_requested(false),
+ metadata_supported(_metadata_supported),
+ metadata_requested(false), metadata_sent(true),
+ metaint(8192), /*TODO: just a std value */
+ metadata(nullptr),
+ metadata_current_position(0), metadata_fill(0)
+{
+}
+
+void
+HttpdClient::ClearQueue()
+{
+ assert(state == RESPONSE);
+
+ while (!pages.empty()) {
+ Page *page = pages.front();
+ pages.pop();
+
+#ifndef NDEBUG
+ assert(queue_size >= page->size);
+ queue_size -= page->size;
+#endif
+
+ page->Unref();
+ }
+
+ assert(queue_size == 0);
+}
+
+void
+HttpdClient::CancelQueue()
+{
+ if (state != RESPONSE)
+ return;
+
+ ClearQueue();
+
+ if (current_page == nullptr)
+ CancelWrite();
+}
+
+ssize_t
+HttpdClient::TryWritePage(const Page &page, size_t position)
+{
+ assert(position < page.size);
+
+ return Write(page.data + position, page.size - position);
+}
+
+ssize_t
+HttpdClient::TryWritePageN(const Page &page, size_t position, ssize_t n)
+{
+ return n >= 0
+ ? Write(page.data + position, n)
+ : TryWritePage(page, position);
+}
+
+ssize_t
+HttpdClient::GetBytesTillMetaData() const
+{
+ if (metadata_requested &&
+ current_page->size - current_position > metaint - metadata_fill)
+ return metaint - metadata_fill;
+
+ return -1;
+}
+
+inline bool
+HttpdClient::TryWrite()
+{
+ const ScopeLock protect(httpd.mutex);
+
+ assert(state == RESPONSE);
+
+ if (current_page == nullptr) {
+ if (pages.empty()) {
+ /* another thread has removed the event source
+ while this thread was waiting for
+ httpd.mutex */
+ CancelWrite();
+ return true;
+ }
+
+ current_page = pages.front();
+ pages.pop();
+ current_position = 0;
+
+ assert(queue_size >= current_page->size);
+ queue_size -= current_page->size;
+ }
+
+ const ssize_t bytes_to_write = GetBytesTillMetaData();
+ if (bytes_to_write == 0) {
+ if (!metadata_sent) {
+ ssize_t nbytes = TryWritePage(*metadata,
+ metadata_current_position);
+ if (nbytes < 0) {
+ auto e = GetSocketError();
+ if (IsSocketErrorAgain(e))
+ return true;
+
+ if (!IsSocketErrorClosed(e)) {
+ SocketErrorMessage msg(e);
+ FormatWarning(httpd_output_domain,
+ "failed to write to client: %s",
+ (const char *)msg);
+ }
+
+ Close();
+ return false;
+ }
+
+ metadata_current_position += nbytes;
+
+ if (metadata->size - metadata_current_position == 0) {
+ metadata_fill = 0;
+ metadata_current_position = 0;
+ metadata_sent = true;
+ }
+ } else {
+ char empty_data = 0;
+
+ ssize_t nbytes = Write(&empty_data, 1);
+ if (nbytes < 0) {
+ auto e = GetSocketError();
+ if (IsSocketErrorAgain(e))
+ return true;
+
+ if (!IsSocketErrorClosed(e)) {
+ SocketErrorMessage msg(e);
+ FormatWarning(httpd_output_domain,
+ "failed to write to client: %s",
+ (const char *)msg);
+ }
+
+ Close();
+ return false;
+ }
+
+ metadata_fill = 0;
+ metadata_current_position = 0;
+ }
+ } else {
+ ssize_t nbytes =
+ TryWritePageN(*current_page, current_position,
+ bytes_to_write);
+ if (nbytes < 0) {
+ auto e = GetSocketError();
+ if (IsSocketErrorAgain(e))
+ return true;
+
+ if (!IsSocketErrorClosed(e)) {
+ SocketErrorMessage msg(e);
+ FormatWarning(httpd_output_domain,
+ "failed to write to client: %s",
+ (const char *)msg);
+ }
+
+ Close();
+ return false;
+ }
+
+ current_position += nbytes;
+ assert(current_position <= current_page->size);
+
+ if (metadata_requested)
+ metadata_fill += nbytes;
+
+ if (current_position >= current_page->size) {
+ current_page->Unref();
+ current_page = nullptr;
+
+ if (pages.empty())
+ /* all pages are sent: remove the
+ event source */
+ CancelWrite();
+ }
+ }
+
+ return true;
+}
+
+void
+HttpdClient::PushPage(Page *page)
+{
+ if (state != RESPONSE)
+ /* the client is still writing the HTTP request */
+ return;
+
+ if (queue_size > 256 * 1024) {
+ FormatDebug(httpd_output_domain,
+ "client is too slow, flushing its queue");
+ ClearQueue();
+ }
+
+ page->Ref();
+ pages.push(page);
+ queue_size += page->size;
+
+ ScheduleWrite();
+}
+
+void
+HttpdClient::PushMetaData(Page *page)
+{
+ assert(page != nullptr);
+
+ if (metadata) {
+ metadata->Unref();
+ metadata = nullptr;
+ }
+
+ page->Ref();
+ metadata = page;
+ metadata_sent = false;
+}
+
+bool
+HttpdClient::OnSocketReady(unsigned flags)
+{
+ if (!BufferedSocket::OnSocketReady(flags))
+ return false;
+
+ if (flags & WRITE)
+ if (!TryWrite())
+ return false;
+
+ return true;
+}
+
+BufferedSocket::InputResult
+HttpdClient::OnSocketInput(void *data, size_t length)
+{
+ if (state == RESPONSE) {
+ LogWarning(httpd_output_domain,
+ "unexpected input from client");
+ LockClose();
+ return InputResult::CLOSED;
+ }
+
+ char *line = (char *)data;
+ char *newline = (char *)memchr(line, '\n', length);
+ if (newline == nullptr)
+ return InputResult::MORE;
+
+ ConsumeInput(newline + 1 - line);
+
+ if (newline > line && newline[-1] == '\r')
+ --newline;
+
+ /* terminate the string at the end of the line */
+ *newline = 0;
+
+ if (!HandleLine(line)) {
+ LockClose();
+ return InputResult::CLOSED;
+ }
+
+ if (state == RESPONSE) {
+ if (!SendResponse())
+ return InputResult::CLOSED;
+
+ if (head_method) {
+ LockClose();
+ return InputResult::CLOSED;
+ }
+ }
+
+ return InputResult::AGAIN;
+}
+
+void
+HttpdClient::OnSocketError(Error &&error)
+{
+ LogError(error);
+}
+
+void
+HttpdClient::OnSocketClosed()
+{
+ LockClose();
+}
diff --git a/src/output/plugins/httpd/HttpdClient.hxx b/src/output/plugins/httpd/HttpdClient.hxx
new file mode 100644
index 000000000..f94f05769
--- /dev/null
+++ b/src/output/plugins/httpd/HttpdClient.hxx
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_OUTPUT_HTTPD_CLIENT_HXX
+#define MPD_OUTPUT_HTTPD_CLIENT_HXX
+
+#include "event/BufferedSocket.hxx"
+#include "Compiler.h"
+
+#include <queue>
+#include <list>
+
+#include <stddef.h>
+
+class HttpdOutput;
+class Page;
+
+class HttpdClient final : BufferedSocket {
+ /**
+ * The httpd output object this client is connected to.
+ */
+ HttpdOutput &httpd;
+
+ /**
+ * The current state of the client.
+ */
+ enum {
+ /** reading the request line */
+ REQUEST,
+
+ /** reading the request headers */
+ HEADERS,
+
+ /** sending the HTTP response */
+ RESPONSE,
+ } state;
+
+ /**
+ * A queue of #Page objects to be sent to the client.
+ */
+ std::queue<Page *, std::list<Page *>> pages;
+
+ /**
+ * The sum of all page sizes in #pages.
+ */
+ size_t queue_size;
+
+ /**
+ * The #page which is currently being sent to the client.
+ */
+ Page *current_page;
+
+ /**
+ * The amount of bytes which were already sent from
+ * #current_page.
+ */
+ size_t current_position;
+
+ /**
+ * Is this a HEAD request?
+ */
+ bool head_method;
+
+ /**
+ * If DLNA streaming was an option.
+ */
+ bool dlna_streaming_requested;
+
+ /* ICY */
+
+ /**
+ * Do we support sending Icy-Metadata to the client? This is
+ * disabled if the httpd audio output uses encoder tags.
+ */
+ bool metadata_supported;
+
+ /**
+ * If we should sent icy metadata.
+ */
+ bool metadata_requested;
+
+ /**
+ * If the current metadata was already sent to the client.
+ */
+ bool metadata_sent;
+
+ /**
+ * The amount of streaming data between each metadata block
+ */
+ unsigned metaint;
+
+ /**
+ * The metadata as #Page which is currently being sent to the client.
+ */
+ Page *metadata;
+
+ /*
+ * The amount of bytes which were already sent from the metadata.
+ */
+ size_t metadata_current_position;
+
+ /**
+ * The amount of streaming data sent to the client
+ * since the last icy information was sent.
+ */
+ unsigned metadata_fill;
+
+public:
+ /**
+ * @param httpd the HTTP output device
+ * @param fd the socket file descriptor
+ */
+ HttpdClient(HttpdOutput &httpd, int _fd, EventLoop &_loop,
+ bool _metadata_supported);
+
+ /**
+ * Note: this does not remove the client from the
+ * #HttpdOutput object.
+ */
+ ~HttpdClient();
+
+ /**
+ * Frees the client and removes it from the server's client list.
+ */
+ void Close();
+
+ void LockClose();
+
+ /**
+ * Clears the page queue.
+ */
+ void CancelQueue();
+
+ /**
+ * Handle a line of the HTTP request.
+ */
+ bool HandleLine(const char *line);
+
+ /**
+ * Switch the client to the "RESPONSE" state.
+ */
+ void BeginResponse();
+
+ /**
+ * Sends the status line and response headers to the client.
+ */
+ bool SendResponse();
+
+ gcc_pure
+ ssize_t GetBytesTillMetaData() const;
+
+ ssize_t TryWritePage(const Page &page, size_t position);
+ ssize_t TryWritePageN(const Page &page, size_t position, ssize_t n);
+
+ bool TryWrite();
+
+ /**
+ * Appends a page to the client's queue.
+ */
+ void PushPage(Page *page);
+
+ /**
+ * Sends the passed metadata.
+ */
+ void PushMetaData(Page *page);
+
+private:
+ void ClearQueue();
+
+protected:
+ virtual bool OnSocketReady(unsigned flags) override;
+ virtual InputResult OnSocketInput(void *data, size_t length) override;
+ virtual void OnSocketError(Error &&error) override;
+ virtual void OnSocketClosed() override;
+};
+
+#endif
diff --git a/src/output/plugins/httpd/HttpdInternal.hxx b/src/output/plugins/httpd/HttpdInternal.hxx
new file mode 100644
index 000000000..20ff15e42
--- /dev/null
+++ b/src/output/plugins/httpd/HttpdInternal.hxx
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+/** \file
+ *
+ * Internal declarations for the "httpd" audio output plugin.
+ */
+
+#ifndef MPD_OUTPUT_HTTPD_INTERNAL_H
+#define MPD_OUTPUT_HTTPD_INTERNAL_H
+
+#include "output/Internal.hxx"
+#include "output/Timer.hxx"
+#include "thread/Mutex.hxx"
+#include "event/ServerSocket.hxx"
+#include "event/DeferredMonitor.hxx"
+#include "util/Cast.hxx"
+#include "Compiler.h"
+
+#ifdef _LIBCPP_VERSION
+/* can't use incomplete template arguments with libc++ */
+#include "HttpdClient.hxx"
+#endif
+
+#include <forward_list>
+#include <queue>
+#include <list>
+
+struct config_param;
+class Error;
+class EventLoop;
+class ServerSocket;
+class HttpdClient;
+class Page;
+struct Encoder;
+struct Tag;
+
+class HttpdOutput final : ServerSocket, DeferredMonitor {
+ AudioOutput base;
+
+ /**
+ * True if the audio output is open and accepts client
+ * connections.
+ */
+ bool open;
+
+ /**
+ * The configured encoder plugin.
+ */
+ Encoder *encoder;
+
+ /**
+ * Number of bytes which were fed into the encoder, without
+ * ever receiving new output. This is used to estimate
+ * whether MPD should manually flush the encoder, to avoid
+ * buffer underruns in the client.
+ */
+ size_t unflushed_input;
+
+public:
+ /**
+ * The MIME type produced by the #encoder.
+ */
+ const char *content_type;
+
+ /**
+ * This mutex protects the listener socket and the client
+ * list.
+ */
+ mutable Mutex mutex;
+
+ /**
+ * This condition gets signalled when an item is removed from
+ * #pages.
+ */
+ Cond cond;
+
+private:
+ /**
+ * A #Timer object to synchronize this output with the
+ * wallclock.
+ */
+ Timer *timer;
+
+ /**
+ * The header page, which is sent to every client on connect.
+ */
+ Page *header;
+
+ /**
+ * The metadata, which is sent to every client.
+ */
+ Page *metadata;
+
+ /**
+ * The page queue, i.e. pages from the encoder to be
+ * broadcasted to all clients. This container is necessary to
+ * pass pages from the OutputThread to the IOThread. It is
+ * protected by #mutex, and removing signals #cond.
+ */
+ std::queue<Page *, std::list<Page *>> pages;
+
+ public:
+ /**
+ * The configured name.
+ */
+ char const *name;
+ /**
+ * The configured genre.
+ */
+ char const *genre;
+ /**
+ * The configured website address.
+ */
+ char const *website;
+
+private:
+ /**
+ * A linked list containing all clients which are currently
+ * connected.
+ */
+ std::forward_list<HttpdClient> clients;
+
+ /**
+ * A temporary buffer for the httpd_output_read_page()
+ * function.
+ */
+ char buffer[32768];
+
+ /**
+ * The maximum and current number of clients connected
+ * at the same time.
+ */
+ unsigned clients_max, clients_cnt;
+
+public:
+ HttpdOutput(EventLoop &_loop);
+ ~HttpdOutput();
+
+#if defined(__clang__) || GCC_CHECK_VERSION(4,7)
+ constexpr
+#endif
+ static HttpdOutput *Cast(AudioOutput *ao) {
+ return &ContainerCast(*ao, &HttpdOutput::base);
+ }
+
+ using DeferredMonitor::GetEventLoop;
+
+ bool Init(const config_param &param, Error &error);
+
+ bool Configure(const config_param &param, Error &error);
+
+ AudioOutput *InitAndConfigure(const config_param &param,
+ Error &error) {
+ if (!Init(param, error))
+ return nullptr;
+
+ if (!Configure(param, error))
+ return nullptr;
+
+ return &base;
+ }
+
+ bool Bind(Error &error);
+ void Unbind();
+
+ /**
+ * Caller must lock the mutex.
+ */
+ bool OpenEncoder(AudioFormat &audio_format, Error &error);
+
+ /**
+ * Caller must lock the mutex.
+ */
+ bool Open(AudioFormat &audio_format, Error &error);
+
+ /**
+ * Caller must lock the mutex.
+ */
+ void Close();
+
+ /**
+ * Check whether there is at least one client.
+ *
+ * Caller must lock the mutex.
+ */
+ gcc_pure
+ bool HasClients() const {
+ return !clients.empty();
+ }
+
+ /**
+ * Check whether there is at least one client.
+ */
+ gcc_pure
+ bool LockHasClients() const {
+ const ScopeLock protect(mutex);
+ return HasClients();
+ }
+
+ void AddClient(int fd);
+
+ /**
+ * Removes a client from the httpd_output.clients linked list.
+ */
+ void RemoveClient(HttpdClient &client);
+
+ /**
+ * Sends the encoder header to the client. This is called
+ * right after the response headers have been sent.
+ */
+ void SendHeader(HttpdClient &client) const;
+
+ gcc_pure
+ unsigned Delay() const;
+
+ /**
+ * Reads data from the encoder (as much as available) and
+ * returns it as a new #page object.
+ */
+ Page *ReadPage();
+
+ /**
+ * Broadcasts a page struct to all clients.
+ *
+ * Mutext must not be locked.
+ */
+ void BroadcastPage(Page *page);
+
+ /**
+ * Broadcasts data from the encoder to all clients.
+ */
+ void BroadcastFromEncoder();
+
+ bool EncodeAndPlay(const void *chunk, size_t size, Error &error);
+
+ void SendTag(const Tag *tag);
+
+ size_t Play(const void *chunk, size_t size, Error &error);
+
+ void CancelAllClients();
+
+private:
+ virtual void RunDeferred() override;
+
+ virtual void OnAccept(int fd, const sockaddr &address,
+ size_t address_length, int uid) override;
+};
+
+extern const class Domain httpd_output_domain;
+
+#endif
diff --git a/src/output/plugins/httpd/HttpdOutputPlugin.cxx b/src/output/plugins/httpd/HttpdOutputPlugin.cxx
new file mode 100644
index 000000000..e3ba7727d
--- /dev/null
+++ b/src/output/plugins/httpd/HttpdOutputPlugin.cxx
@@ -0,0 +1,601 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "HttpdOutputPlugin.hxx"
+#include "HttpdInternal.hxx"
+#include "HttpdClient.hxx"
+#include "output/OutputAPI.hxx"
+#include "encoder/EncoderPlugin.hxx"
+#include "encoder/EncoderList.hxx"
+#include "system/Resolver.hxx"
+#include "Page.hxx"
+#include "IcyMetaDataServer.hxx"
+#include "system/fd_util.h"
+#include "IOThread.hxx"
+#include "event/Call.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+#include "Log.hxx"
+
+#include <assert.h>
+
+#include <sys/types.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+
+#ifdef HAVE_LIBWRAP
+#include <sys/socket.h> /* needed for AF_UNIX */
+#include <tcpd.h>
+#endif
+
+const Domain httpd_output_domain("httpd_output");
+
+inline
+HttpdOutput::HttpdOutput(EventLoop &_loop)
+ :ServerSocket(_loop), DeferredMonitor(_loop),
+ base(httpd_output_plugin),
+ encoder(nullptr), unflushed_input(0),
+ metadata(nullptr)
+{
+}
+
+HttpdOutput::~HttpdOutput()
+{
+ if (metadata != nullptr)
+ metadata->Unref();
+
+ if (encoder != nullptr)
+ encoder_finish(encoder);
+
+}
+
+inline bool
+HttpdOutput::Bind(Error &error)
+{
+ open = false;
+
+ bool result = false;
+ BlockingCall(GetEventLoop(), [this, &error, &result](){
+ result = ServerSocket::Open(error);
+ });
+ return result;
+}
+
+inline void
+HttpdOutput::Unbind()
+{
+ assert(!open);
+
+ BlockingCall(GetEventLoop(), [this](){
+ ServerSocket::Close();
+ });
+}
+
+inline bool
+HttpdOutput::Configure(const config_param &param, Error &error)
+{
+ /* read configuration */
+ name = param.GetBlockValue("name", "Set name in config");
+ genre = param.GetBlockValue("genre", "Set genre in config");
+ website = param.GetBlockValue("website", "Set website in config");
+
+ unsigned port = param.GetBlockValue("port", 8000u);
+
+ const char *encoder_name =
+ param.GetBlockValue("encoder", "vorbis");
+ const auto encoder_plugin = encoder_plugin_get(encoder_name);
+ if (encoder_plugin == nullptr) {
+ error.Format(httpd_output_domain,
+ "No such encoder: %s", encoder_name);
+ return false;
+ }
+
+ clients_max = param.GetBlockValue("max_clients", 0u);
+
+ /* set up bind_to_address */
+
+ const char *bind_to_address = param.GetBlockValue("bind_to_address");
+ bool success = bind_to_address != nullptr &&
+ strcmp(bind_to_address, "any") != 0
+ ? AddHost(bind_to_address, port, error)
+ : AddPort(port, error);
+ if (!success)
+ return false;
+
+ /* initialize encoder */
+
+ encoder = encoder_init(*encoder_plugin, param, error);
+ if (encoder == nullptr)
+ return false;
+
+ /* determine content type */
+ content_type = encoder_get_mime_type(encoder);
+ if (content_type == nullptr)
+ content_type = "application/octet-stream";
+
+ return true;
+}
+
+inline bool
+HttpdOutput::Init(const config_param &param, Error &error)
+{
+ return base.Configure(param, error);
+}
+
+static AudioOutput *
+httpd_output_init(const config_param &param, Error &error)
+{
+ HttpdOutput *httpd = new HttpdOutput(io_thread_get());
+
+ AudioOutput *result = httpd->InitAndConfigure(param, error);
+ if (result == nullptr)
+ delete httpd;
+
+ return result;
+}
+
+static void
+httpd_output_finish(AudioOutput *ao)
+{
+ HttpdOutput *httpd = HttpdOutput::Cast(ao);
+
+ delete httpd;
+}
+
+/**
+ * Creates a new #HttpdClient object and adds it into the
+ * HttpdOutput.clients linked list.
+ */
+inline void
+HttpdOutput::AddClient(int fd)
+{
+ clients.emplace_front(*this, fd, GetEventLoop(),
+ encoder->plugin.tag == nullptr);
+ ++clients_cnt;
+
+ /* pass metadata to client */
+ if (metadata != nullptr)
+ clients.front().PushMetaData(metadata);
+}
+
+void
+HttpdOutput::RunDeferred()
+{
+ /* this method runs in the IOThread; it broadcasts pages from
+ our own queue to all clients */
+
+ const ScopeLock protect(mutex);
+
+ while (!pages.empty()) {
+ Page *page = pages.front();
+ pages.pop();
+
+ for (auto &client : clients)
+ client.PushPage(page);
+
+ page->Unref();
+ }
+
+ /* wake up the client that may be waiting for the queue to be
+ flushed */
+ cond.broadcast();
+}
+
+void
+HttpdOutput::OnAccept(int fd, const sockaddr &address,
+ size_t address_length, gcc_unused int uid)
+{
+ /* the listener socket has become readable - a client has
+ connected */
+
+#ifdef HAVE_LIBWRAP
+ if (address.sa_family != AF_UNIX) {
+ const auto hostaddr = sockaddr_to_string(&address,
+ address_length);
+ // TODO: shall we obtain the program name from argv[0]?
+ const char *progname = "mpd";
+
+ struct request_info req;
+ request_init(&req, RQ_FILE, fd, RQ_DAEMON, progname, 0);
+
+ fromhost(&req);
+
+ if (!hosts_access(&req)) {
+ /* tcp wrappers says no */
+ FormatWarning(httpd_output_domain,
+ "libwrap refused connection (libwrap=%s) from %s",
+ progname, hostaddr.c_str());
+ close_socket(fd);
+ return;
+ }
+ }
+#else
+ (void)address;
+ (void)address_length;
+#endif /* HAVE_WRAP */
+
+ const ScopeLock protect(mutex);
+
+ if (fd >= 0) {
+ /* can we allow additional client */
+ if (open && (clients_max == 0 || clients_cnt < clients_max))
+ AddClient(fd);
+ else
+ close_socket(fd);
+ } else if (fd < 0 && errno != EINTR) {
+ LogErrno(httpd_output_domain, "accept() failed");
+ }
+}
+
+Page *
+HttpdOutput::ReadPage()
+{
+ if (unflushed_input >= 65536) {
+ /* we have fed a lot of input into the encoder, but it
+ didn't give anything back yet - flush now to avoid
+ buffer underruns */
+ encoder_flush(encoder, IgnoreError());
+ unflushed_input = 0;
+ }
+
+ size_t size = 0;
+ do {
+ size_t nbytes = encoder_read(encoder,
+ buffer + size,
+ sizeof(buffer) - size);
+ if (nbytes == 0)
+ break;
+
+ unflushed_input = 0;
+
+ size += nbytes;
+ } while (size < sizeof(buffer));
+
+ if (size == 0)
+ return nullptr;
+
+ return Page::Copy(buffer, size);
+}
+
+static bool
+httpd_output_enable(AudioOutput *ao, Error &error)
+{
+ HttpdOutput *httpd = HttpdOutput::Cast(ao);
+
+ return httpd->Bind(error);
+}
+
+static void
+httpd_output_disable(AudioOutput *ao)
+{
+ HttpdOutput *httpd = HttpdOutput::Cast(ao);
+
+ httpd->Unbind();
+}
+
+inline bool
+HttpdOutput::OpenEncoder(AudioFormat &audio_format, Error &error)
+{
+ if (!encoder_open(encoder, audio_format, error))
+ return false;
+
+ /* we have to remember the encoder header, i.e. the first
+ bytes of encoder output after opening it, because it has to
+ be sent to every new client */
+ header = ReadPage();
+
+ unflushed_input = 0;
+
+ return true;
+}
+
+inline bool
+HttpdOutput::Open(AudioFormat &audio_format, Error &error)
+{
+ assert(!open);
+ assert(clients.empty());
+
+ /* open the encoder */
+
+ if (!OpenEncoder(audio_format, error))
+ return false;
+
+ /* initialize other attributes */
+
+ clients_cnt = 0;
+ timer = new Timer(audio_format);
+
+ open = true;
+
+ return true;
+}
+
+static bool
+httpd_output_open(AudioOutput *ao, AudioFormat &audio_format,
+ Error &error)
+{
+ HttpdOutput *httpd = HttpdOutput::Cast(ao);
+
+ const ScopeLock protect(httpd->mutex);
+ return httpd->Open(audio_format, error);
+}
+
+inline void
+HttpdOutput::Close()
+{
+ assert(open);
+
+ open = false;
+
+ delete timer;
+
+ BlockingCall(GetEventLoop(), [this](){
+ clients.clear();
+ });
+
+ if (header != nullptr)
+ header->Unref();
+
+ encoder_close(encoder);
+}
+
+static void
+httpd_output_close(AudioOutput *ao)
+{
+ HttpdOutput *httpd = HttpdOutput::Cast(ao);
+
+ const ScopeLock protect(httpd->mutex);
+ httpd->Close();
+}
+
+void
+HttpdOutput::RemoveClient(HttpdClient &client)
+{
+ assert(clients_cnt > 0);
+
+ for (auto prev = clients.before_begin(), i = std::next(prev);;
+ prev = i, i = std::next(prev)) {
+ assert(i != clients.end());
+ if (&*i == &client) {
+ clients.erase_after(prev);
+ clients_cnt--;
+ break;
+ }
+ }
+}
+
+void
+HttpdOutput::SendHeader(HttpdClient &client) const
+{
+ if (header != nullptr)
+ client.PushPage(header);
+}
+
+inline unsigned
+HttpdOutput::Delay() const
+{
+ if (!LockHasClients() && base.pause) {
+ /* if there's no client and this output is paused,
+ then httpd_output_pause() will not do anything, it
+ will not fill the buffer and it will not update the
+ timer; therefore, we reset the timer here */
+ timer->Reset();
+
+ /* some arbitrary delay that is long enough to avoid
+ consuming too much CPU, and short enough to notice
+ new clients quickly enough */
+ return 1000;
+ }
+
+ return timer->IsStarted()
+ ? timer->GetDelay()
+ : 0;
+}
+
+static unsigned
+httpd_output_delay(AudioOutput *ao)
+{
+ HttpdOutput *httpd = HttpdOutput::Cast(ao);
+
+ return httpd->Delay();
+}
+
+void
+HttpdOutput::BroadcastPage(Page *page)
+{
+ assert(page != nullptr);
+
+ mutex.lock();
+ pages.push(page);
+ page->Ref();
+ mutex.unlock();
+
+ DeferredMonitor::Schedule();
+}
+
+void
+HttpdOutput::BroadcastFromEncoder()
+{
+ /* synchronize with the IOThread */
+ mutex.lock();
+ while (!pages.empty())
+ cond.wait(mutex);
+
+ Page *page;
+ while ((page = ReadPage()) != nullptr)
+ pages.push(page);
+
+ mutex.unlock();
+
+ DeferredMonitor::Schedule();
+}
+
+inline bool
+HttpdOutput::EncodeAndPlay(const void *chunk, size_t size, Error &error)
+{
+ if (!encoder_write(encoder, chunk, size, error))
+ return false;
+
+ unflushed_input += size;
+
+ BroadcastFromEncoder();
+ return true;
+}
+
+inline size_t
+HttpdOutput::Play(const void *chunk, size_t size, Error &error)
+{
+ if (LockHasClients()) {
+ if (!EncodeAndPlay(chunk, size, error))
+ return 0;
+ }
+
+ if (!timer->IsStarted())
+ timer->Start();
+ timer->Add(size);
+
+ return size;
+}
+
+static size_t
+httpd_output_play(AudioOutput *ao, const void *chunk, size_t size,
+ Error &error)
+{
+ HttpdOutput *httpd = HttpdOutput::Cast(ao);
+
+ return httpd->Play(chunk, size, error);
+}
+
+static bool
+httpd_output_pause(AudioOutput *ao)
+{
+ HttpdOutput *httpd = HttpdOutput::Cast(ao);
+
+ if (httpd->LockHasClients()) {
+ static const char silence[1020] = { 0 };
+ return httpd_output_play(ao, silence, sizeof(silence),
+ IgnoreError()) > 0;
+ } else {
+ return true;
+ }
+}
+
+inline void
+HttpdOutput::SendTag(const Tag *tag)
+{
+ assert(tag != nullptr);
+
+ if (encoder->plugin.tag != nullptr) {
+ /* embed encoder tags */
+
+ /* flush the current stream, and end it */
+
+ encoder_pre_tag(encoder, IgnoreError());
+ BroadcastFromEncoder();
+
+ /* send the tag to the encoder - which starts a new
+ stream now */
+
+ encoder_tag(encoder, tag, IgnoreError());
+
+ /* the first page generated by the encoder will now be
+ used as the new "header" page, which is sent to all
+ new clients */
+
+ Page *page = ReadPage();
+ if (page != nullptr) {
+ if (header != nullptr)
+ header->Unref();
+ header = page;
+ BroadcastPage(page);
+ }
+ } else {
+ /* use Icy-Metadata */
+
+ if (metadata != nullptr)
+ metadata->Unref();
+
+ static constexpr TagType types[] = {
+ TAG_ALBUM, TAG_ARTIST, TAG_TITLE,
+ TAG_NUM_OF_ITEM_TYPES
+ };
+
+ metadata = icy_server_metadata_page(*tag, &types[0]);
+ if (metadata != nullptr) {
+ const ScopeLock protect(mutex);
+ for (auto &client : clients)
+ client.PushMetaData(metadata);
+ }
+ }
+}
+
+static void
+httpd_output_tag(AudioOutput *ao, const Tag *tag)
+{
+ HttpdOutput *httpd = HttpdOutput::Cast(ao);
+
+ httpd->SendTag(tag);
+}
+
+inline void
+HttpdOutput::CancelAllClients()
+{
+ const ScopeLock protect(mutex);
+
+ while (!pages.empty()) {
+ Page *page = pages.front();
+ pages.pop();
+ page->Unref();
+ }
+
+ for (auto &client : clients)
+ client.CancelQueue();
+
+ cond.broadcast();
+}
+
+static void
+httpd_output_cancel(AudioOutput *ao)
+{
+ HttpdOutput *httpd = HttpdOutput::Cast(ao);
+
+ BlockingCall(io_thread_get(), [httpd](){
+ httpd->CancelAllClients();
+ });
+}
+
+const struct AudioOutputPlugin httpd_output_plugin = {
+ "httpd",
+ nullptr,
+ httpd_output_init,
+ httpd_output_finish,
+ httpd_output_enable,
+ httpd_output_disable,
+ httpd_output_open,
+ httpd_output_close,
+ httpd_output_delay,
+ httpd_output_tag,
+ httpd_output_play,
+ nullptr,
+ httpd_output_cancel,
+ httpd_output_pause,
+ nullptr,
+};
diff --git a/src/output/plugins/httpd/HttpdOutputPlugin.hxx b/src/output/plugins/httpd/HttpdOutputPlugin.hxx
new file mode 100644
index 000000000..df99e2b43
--- /dev/null
+++ b/src/output/plugins/httpd/HttpdOutputPlugin.hxx
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_HTTPD_OUTPUT_PLUGIN_HXX
+#define MPD_HTTPD_OUTPUT_PLUGIN_HXX
+
+extern const struct AudioOutputPlugin httpd_output_plugin;
+
+#endif
diff --git a/src/output/plugins/httpd/IcyMetaDataServer.cxx b/src/output/plugins/httpd/IcyMetaDataServer.cxx
new file mode 100644
index 000000000..146df23d1
--- /dev/null
+++ b/src/output/plugins/httpd/IcyMetaDataServer.cxx
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "IcyMetaDataServer.hxx"
+#include "Page.hxx"
+#include "tag/Tag.hxx"
+#include "util/FormatString.hxx"
+
+#include <glib.h>
+
+#include <string.h>
+
+char*
+icy_server_metadata_header(const char *name,
+ const char *genre, const char *url,
+ const char *content_type, int metaint)
+{
+ return FormatNew("ICY 200 OK\r\n"
+ "icy-notice1:<BR>This stream requires an audio player!<BR>\r\n" /* TODO */
+ "icy-notice2:MPD - The music player daemon<BR>\r\n"
+ "icy-name: %s\r\n" /* TODO */
+ "icy-genre: %s\r\n" /* TODO */
+ "icy-url: %s\r\n" /* TODO */
+ "icy-pub:1\r\n"
+ "icy-metaint:%d\r\n"
+ /* TODO "icy-br:%d\r\n" */
+ "Content-Type: %s\r\n"
+ "Connection: close\r\n"
+ "Pragma: no-cache\r\n"
+ "Cache-Control: no-cache, no-store\r\n"
+ "\r\n",
+ name,
+ genre,
+ url,
+ metaint,
+ /* bitrate, */
+ content_type);
+}
+
+static char *
+icy_server_metadata_string(const char *stream_title, const char* stream_url)
+{
+ gchar *icy_metadata;
+ guint meta_length;
+
+ // The leading n is a placeholder for the length information
+ icy_metadata = FormatNew("nStreamTitle='%s';"
+ "StreamUrl='%s';",
+ stream_title,
+ stream_url);
+
+ meta_length = strlen(icy_metadata);
+
+ meta_length--; // subtract placeholder
+
+ meta_length = ((int)meta_length / 16) + 1;
+
+ icy_metadata[0] = meta_length;
+
+ if (meta_length > 255) {
+ delete[] icy_metadata;
+ return nullptr;
+ }
+
+ return icy_metadata;
+}
+
+Page *
+icy_server_metadata_page(const Tag &tag, const TagType *types)
+{
+ const gchar *tag_items[TAG_NUM_OF_ITEM_TYPES];
+ gint last_item, item;
+ guint position;
+ gchar *icy_string;
+ gchar stream_title[(1 + 255 - 28) * 16]; // Length + Metadata -
+ // "StreamTitle='';StreamUrl='';"
+ // = 4081 - 28
+ stream_title[0] = '\0';
+
+ last_item = -1;
+
+ while (*types != TAG_NUM_OF_ITEM_TYPES) {
+ const gchar *tag_item = tag.GetValue(*types++);
+ if (tag_item)
+ tag_items[++last_item] = tag_item;
+ }
+
+ position = item = 0;
+ while (position < sizeof(stream_title) && item <= last_item) {
+ gint length = 0;
+
+ length = g_strlcpy(stream_title + position,
+ tag_items[item++],
+ sizeof(stream_title) - position);
+
+ position += length;
+
+ if (item <= last_item) {
+ length = g_strlcpy(stream_title + position,
+ " - ",
+ sizeof(stream_title) - position);
+
+ position += length;
+ }
+ }
+
+ icy_string = icy_server_metadata_string(stream_title, "");
+
+ if (icy_string == nullptr)
+ return nullptr;
+
+ Page *icy_metadata = Page::Copy(icy_string, (icy_string[0] * 16) + 1);
+
+ delete[] icy_string;
+
+ return icy_metadata;
+}
diff --git a/src/output/plugins/httpd/IcyMetaDataServer.hxx b/src/output/plugins/httpd/IcyMetaDataServer.hxx
new file mode 100644
index 000000000..773b46641
--- /dev/null
+++ b/src/output/plugins/httpd/IcyMetaDataServer.hxx
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_ICY_META_DATA_SERVER_HXX
+#define MPD_ICY_META_DATA_SERVER_HXX
+
+#include "tag/TagType.h"
+
+struct Tag;
+class Page;
+
+/**
+ * Free the return value with delete[].
+ */
+char*
+icy_server_metadata_header(const char *name,
+ const char *genre, const char *url,
+ const char *content_type, int metaint);
+
+Page *
+icy_server_metadata_page(const Tag &tag, const TagType *types);
+
+#endif
diff --git a/src/output/plugins/httpd/Page.cxx b/src/output/plugins/httpd/Page.cxx
new file mode 100644
index 000000000..e22134bbc
--- /dev/null
+++ b/src/output/plugins/httpd/Page.cxx
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "Page.hxx"
+#include "util/Alloc.hxx"
+
+#include <new>
+
+#include <assert.h>
+#include <string.h>
+#include <stdlib.h>
+
+Page *
+Page::Create(size_t size)
+{
+ void *p = xalloc(sizeof(Page) + size -
+ sizeof(Page::data));
+ return ::new(p) Page(size);
+}
+
+Page *
+Page::Copy(const void *data, size_t size)
+{
+ assert(data != nullptr);
+
+ Page *page = Create(size);
+ memcpy(page->data, data, size);
+ return page;
+}
+
+Page *
+Page::Concat(const Page &a, const Page &b)
+{
+ Page *page = Create(a.size + b.size);
+
+ memcpy(page->data, a.data, a.size);
+ memcpy(page->data + a.size, b.data, b.size);
+
+ return page;
+}
+
+bool
+Page::Unref()
+{
+ bool unused = ref.Decrement();
+
+ if (unused) {
+ this->Page::~Page();
+ free(this);
+ }
+
+ return unused;
+}
diff --git a/src/output/plugins/httpd/Page.hxx b/src/output/plugins/httpd/Page.hxx
new file mode 100644
index 000000000..95f35d06a
--- /dev/null
+++ b/src/output/plugins/httpd/Page.hxx
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+/** \file
+ *
+ * This is a library which manages reference counted buffers.
+ */
+
+#ifndef MPD_PAGE_HXX
+#define MPD_PAGE_HXX
+
+#include "util/RefCount.hxx"
+
+#include <stddef.h>
+
+/**
+ * A dynamically allocated buffer which keeps track of its reference
+ * count. This is useful for passing buffers around, when several
+ * instances hold references to one buffer.
+ */
+class Page {
+ /**
+ * The number of references to this buffer. This library uses
+ * atomic functions to access it, i.e. no locks are required.
+ * As soon as this attribute reaches zero, the buffer is
+ * freed.
+ */
+ RefCount ref;
+
+public:
+ /**
+ * The size of this buffer in bytes.
+ */
+ const size_t size;
+
+ /**
+ * Dynamic array containing the buffer data.
+ */
+ unsigned char data[sizeof(long)];
+
+protected:
+ Page(size_t _size):size(_size) {}
+ ~Page() = default;
+
+ /**
+ * Allocates a new #Page object, without filling the data
+ * element.
+ */
+ static Page *Create(size_t size);
+
+public:
+ /**
+ * Creates a new #page object, and copies data from the
+ * specified buffer. It is initialized with a reference count
+ * of 1.
+ *
+ * @param data the source buffer
+ * @param size the size of the source buffer
+ */
+ static Page *Copy(const void *data, size_t size);
+
+ /**
+ * Concatenates two pages to a new page.
+ *
+ * @param a the first page
+ * @param b the second page, which is appended
+ */
+ static Page *Concat(const Page &a, const Page &b);
+
+ /**
+ * Increases the reference counter.
+ */
+ void Ref() {
+ ref.Increment();
+ }
+
+ /**
+ * Decreases the reference counter. If it reaches zero, the #page is
+ * freed.
+ *
+ * @return true if the #page has been freed
+ */
+ bool Unref();
+};
+
+#endif
diff --git a/src/output/plugins/sles/AndroidSimpleBufferQueue.hxx b/src/output/plugins/sles/AndroidSimpleBufferQueue.hxx
new file mode 100644
index 000000000..c7dd4ccca
--- /dev/null
+++ b/src/output/plugins/sles/AndroidSimpleBufferQueue.hxx
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2011-2012 Max Kellermann <max@duempel.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef SLES_ANDROID_SIMPLE_BUFFER_QUEUE_HPP
+#define SLES_ANDROID_SIMPLE_BUFFER_QUEUE_HPP
+
+#include <SLES/OpenSLES_Android.h>
+
+namespace SLES {
+ /**
+ * OO wrapper for an OpenSL/ES SLAndroidSimpleBufferQueueItf
+ * variable.
+ */
+ class AndroidSimpleBufferQueue {
+ SLAndroidSimpleBufferQueueItf queue;
+
+ public:
+ AndroidSimpleBufferQueue() = default;
+ explicit AndroidSimpleBufferQueue(SLAndroidSimpleBufferQueueItf _queue)
+ :queue(_queue) {}
+
+ SLresult Enqueue(const void *pBuffer, SLuint32 size) {
+ return (*queue)->Enqueue(queue, pBuffer, size);
+ }
+
+ SLresult Clear() {
+ return (*queue)->Clear(queue);
+ }
+
+ SLresult GetState(SLAndroidSimpleBufferQueueState *pState) {
+ return (*queue)->GetState(queue, pState);
+ }
+
+ SLresult RegisterCallback(slAndroidSimpleBufferQueueCallback callback,
+ void *pContext) {
+ return (*queue)->RegisterCallback(queue, callback, pContext);
+ }
+ };
+}
+
+#endif
diff --git a/src/output/plugins/sles/Engine.hxx b/src/output/plugins/sles/Engine.hxx
new file mode 100644
index 000000000..7c6e3cf50
--- /dev/null
+++ b/src/output/plugins/sles/Engine.hxx
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2011-2012 Max Kellermann <max@duempel.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef SLES_ENGINE_HPP
+#define SLES_ENGINE_HPP
+
+#include <SLES/OpenSLES.h>
+
+namespace SLES {
+ /**
+ * OO wrapper for an OpenSL/ES SLEngineItf variable.
+ */
+ class Engine {
+ SLEngineItf engine;
+
+ public:
+ Engine() = default;
+ explicit Engine(SLEngineItf _engine):engine(_engine) {}
+
+ SLresult CreateAudioPlayer(SLObjectItf *pPlayer,
+ SLDataSource *pAudioSrc, SLDataSink *pAudioSnk,
+ SLuint32 numInterfaces,
+ const SLInterfaceID *pInterfaceIds,
+ const SLboolean *pInterfaceRequired) {
+ return (*engine)->CreateAudioPlayer(engine, pPlayer,
+ pAudioSrc, pAudioSnk,
+ numInterfaces, pInterfaceIds,
+ pInterfaceRequired);
+ }
+
+ SLresult CreateOutputMix(SLObjectItf *pMix,
+ SLuint32 numInterfaces,
+ const SLInterfaceID *pInterfaceIds,
+ const SLboolean *pInterfaceRequired) {
+ return (*engine)->CreateOutputMix(engine, pMix,
+ numInterfaces, pInterfaceIds,
+ pInterfaceRequired);
+ }
+ };
+}
+
+#endif
diff --git a/src/output/plugins/sles/Object.hxx b/src/output/plugins/sles/Object.hxx
new file mode 100644
index 000000000..852d62d0d
--- /dev/null
+++ b/src/output/plugins/sles/Object.hxx
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2011-2012 Max Kellermann <max@duempel.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef SLES_OBJECT_HPP
+#define SLES_OBJECT_HPP
+
+#include <SLES/OpenSLES.h>
+
+namespace SLES {
+ /**
+ * OO wrapper for an OpenSL/ES SLObjectItf variable.
+ */
+ class Object {
+ SLObjectItf object;
+
+ public:
+ Object() = default;
+ explicit Object(SLObjectItf _object):object(_object) {}
+
+ operator SLObjectItf() {
+ return object;
+ }
+
+ SLresult Realize(bool async) {
+ return (*object)->Realize(object, async);
+ }
+
+ void Destroy() {
+ (*object)->Destroy(object);
+ }
+
+ SLresult GetInterface(const SLInterfaceID iid, void *pInterface) {
+ return (*object)->GetInterface(object, iid, pInterface);
+ }
+ };
+}
+
+#endif
diff --git a/src/output/plugins/sles/Play.hxx b/src/output/plugins/sles/Play.hxx
new file mode 100644
index 000000000..c760151ef
--- /dev/null
+++ b/src/output/plugins/sles/Play.hxx
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2011-2012 Max Kellermann <max@duempel.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef SLES_PLAY_HPP
+#define SLES_PLAY_HPP
+
+#include <SLES/OpenSLES.h>
+
+namespace SLES {
+ /**
+ * OO wrapper for an OpenSL/ES SLPlayItf variable.
+ */
+ class Play {
+ SLPlayItf play;
+
+ public:
+ Play() = default;
+ explicit Play(SLPlayItf _play):play(_play) {}
+
+ SLresult SetPlayState(SLuint32 state) {
+ return (*play)->SetPlayState(play, state);
+ }
+ };
+}
+
+#endif
diff --git a/src/output/plugins/sles/SlesOutputPlugin.cxx b/src/output/plugins/sles/SlesOutputPlugin.cxx
new file mode 100644
index 000000000..85fd9f2f2
--- /dev/null
+++ b/src/output/plugins/sles/SlesOutputPlugin.cxx
@@ -0,0 +1,539 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "SlesOutputPlugin.hxx"
+#include "Object.hxx"
+#include "Engine.hxx"
+#include "Play.hxx"
+#include "AndroidSimpleBufferQueue.hxx"
+#include "../../OutputAPI.hxx"
+#include "util/Macros.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+#include "system/ByteOrder.hxx"
+#include "Log.hxx"
+
+#include <SLES/OpenSLES.h>
+#include <SLES/OpenSLES_Android.h>
+
+class SlesOutput {
+ static constexpr unsigned N_BUFFERS = 3;
+ static constexpr size_t BUFFER_SIZE = 65536;
+
+ AudioOutput base;
+
+ SLES::Object engine_object, mix_object, play_object;
+ SLES::Play play;
+ SLES::AndroidSimpleBufferQueue queue;
+
+ /**
+ * This mutex protects the attributes "next" and "filled". It
+ * is only needed while playback is launched, when the initial
+ * buffers are being enqueued in the caller thread, while
+ * another thread may invoke the registered callback.
+ */
+ Mutex mutex;
+
+ Cond cond;
+
+ bool pause, cancel;
+
+ /**
+ * The number of buffers queued to OpenSLES.
+ */
+ unsigned n_queued;
+
+ /**
+ * The index of the next buffer to be enqueued.
+ */
+ unsigned next;
+
+ /**
+ * Does the "next" buffer already contain synthesised samples?
+ * This can happen when PCMSynthesiser::Synthesise() has been
+ * called, but the OpenSL/ES buffer queue was full. The
+ * buffer will then be postponed.
+ */
+ unsigned filled;
+
+ /**
+ * An array of buffers. It's one more than being managed by
+ * OpenSL/ES, and the one not enqueued (see attribute #next)
+ * will be written to.
+ */
+ uint8_t buffers[N_BUFFERS][BUFFER_SIZE];
+
+public:
+ SlesOutput()
+ :base(sles_output_plugin) {}
+
+ operator AudioOutput *() {
+ return &base;
+ }
+
+ bool Initialize(const config_param &param, Error &error) {
+ return base.Configure(param, error);
+ }
+
+ bool Configure(const config_param &param, Error &error);
+
+ bool Open(AudioFormat &audio_format, Error &error);
+ void Close();
+
+ unsigned Delay() {
+ return pause && !cancel ? 100 : 0;
+ }
+
+ size_t Play(const void *chunk, size_t size, Error &error);
+
+ void Drain();
+ void Cancel();
+ bool Pause();
+
+private:
+ void PlayedCallback();
+
+ /**
+ * OpenSL/ES callback which gets invoked when a buffer has
+ * been consumed. It synthesises and enqueues the next
+ * buffer.
+ */
+ static void PlayedCallback(gcc_unused SLAndroidSimpleBufferQueueItf caller,
+ void *pContext)
+ {
+ SlesOutput &sles = *(SlesOutput *)pContext;
+ sles.PlayedCallback();
+ }
+};
+
+static constexpr Domain sles_domain("sles");
+
+inline bool
+SlesOutput::Configure(const config_param &, Error &)
+{
+ return true;
+}
+
+inline bool
+SlesOutput::Open(AudioFormat &audio_format, Error &error)
+{
+ SLresult result;
+ SLObjectItf _object;
+
+ result = slCreateEngine(&_object, 0, nullptr, 0,
+ nullptr, nullptr);
+ if (result != SL_RESULT_SUCCESS) {
+ error.Set(sles_domain, int(result), "slCreateEngine() failed");
+ return false;
+ }
+
+ engine_object = SLES::Object(_object);
+
+ result = engine_object.Realize(false);
+ if (result != SL_RESULT_SUCCESS) {
+ error.Set(sles_domain, int(result), "Engine.Realize() failed");
+ engine_object.Destroy();
+ return false;
+ }
+
+ SLEngineItf _engine;
+ result = engine_object.GetInterface(SL_IID_ENGINE, &_engine);
+ if (result != SL_RESULT_SUCCESS) {
+ error.Set(sles_domain, int(result),
+ "Engine.GetInterface(IID_ENGINE) failed");
+ engine_object.Destroy();
+ return false;
+ }
+
+ SLES::Engine engine(_engine);
+
+ result = engine.CreateOutputMix(&_object, 0, nullptr, nullptr);
+ if (result != SL_RESULT_SUCCESS) {
+ error.Set(sles_domain, int(result),
+ "Engine.CreateOutputMix() failed");
+ engine_object.Destroy();
+ return false;
+ }
+
+ mix_object = SLES::Object(_object);
+
+ result = mix_object.Realize(false);
+ if (result != SL_RESULT_SUCCESS) {
+ error.Set(sles_domain, int(result),
+ "Mix.Realize() failed");
+ mix_object.Destroy();
+ engine_object.Destroy();
+ return false;
+ }
+
+ SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {
+ SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,
+ N_BUFFERS,
+ };
+
+ if (audio_format.channels > 2)
+ audio_format.channels = 1;
+
+ SLDataFormat_PCM format_pcm;
+ format_pcm.formatType = SL_DATAFORMAT_PCM;
+ format_pcm.numChannels = audio_format.channels;
+ /* from the Android NDK docs: "Note that the field samplesPerSec is
+ actually in units of milliHz, despite the misleading name." */
+ format_pcm.samplesPerSec = audio_format.sample_rate * 1000u;
+ format_pcm.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16;
+ format_pcm.containerSize = SL_PCMSAMPLEFORMAT_FIXED_16;
+ format_pcm.channelMask = audio_format.channels == 1
+ ? SL_SPEAKER_FRONT_CENTER
+ : SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT;
+ format_pcm.endianness = IsLittleEndian()
+ ? SL_BYTEORDER_LITTLEENDIAN
+ : SL_BYTEORDER_BIGENDIAN;
+
+ SLDataSource audioSrc = { &loc_bufq, &format_pcm };
+
+ SLDataLocator_OutputMix loc_outmix = {
+ SL_DATALOCATOR_OUTPUTMIX,
+ mix_object,
+ };
+
+ SLDataSink audioSnk = {
+ &loc_outmix,
+ nullptr,
+ };
+
+ const SLInterfaceID ids2[] = {
+ SL_IID_PLAY,
+ SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
+ SL_IID_ANDROIDCONFIGURATION,
+ };
+
+ static constexpr SLboolean req2[] = {
+ SL_BOOLEAN_TRUE,
+ SL_BOOLEAN_TRUE,
+ SL_BOOLEAN_TRUE,
+ };
+
+ result = engine.CreateAudioPlayer(&_object, &audioSrc, &audioSnk,
+ ARRAY_SIZE(ids2), ids2, req2);
+ if (result != SL_RESULT_SUCCESS) {
+ error.Set(sles_domain, int(result),
+ "Engine.CreateAudioPlayer() failed");
+ mix_object.Destroy();
+ engine_object.Destroy();
+ return false;
+ }
+
+ play_object = SLES::Object(_object);
+
+ SLAndroidConfigurationItf android_config;
+ if (play_object.GetInterface(SL_IID_ANDROIDCONFIGURATION,
+ &android_config) == SL_RESULT_SUCCESS) {
+ SLint32 stream_type = SL_ANDROID_STREAM_MEDIA;
+ (*android_config)->SetConfiguration(android_config,
+ SL_ANDROID_KEY_STREAM_TYPE,
+ &stream_type,
+ sizeof(stream_type));
+ }
+
+ result = play_object.Realize(false);
+
+ if (result != SL_RESULT_SUCCESS) {
+ error.Set(sles_domain, int(result),
+ "Play.Realize() failed");
+ play_object.Destroy();
+ mix_object.Destroy();
+ engine_object.Destroy();
+ return false;
+ }
+
+ SLPlayItf _play;
+ result = play_object.GetInterface(SL_IID_PLAY, &_play);
+ if (result != SL_RESULT_SUCCESS) {
+ error.Set(sles_domain, int(result),
+ "Play.GetInterface(IID_PLAY) failed");
+ play_object.Destroy();
+ mix_object.Destroy();
+ engine_object.Destroy();
+ return false;
+ }
+
+ play = SLES::Play(_play);
+
+ SLAndroidSimpleBufferQueueItf _queue;
+ result = play_object.GetInterface(SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
+ &_queue);
+ if (result != SL_RESULT_SUCCESS) {
+ error.Set(sles_domain, int(result),
+ "Play.GetInterface(IID_ANDROIDSIMPLEBUFFERQUEUE) failed");
+ play_object.Destroy();
+ mix_object.Destroy();
+ engine_object.Destroy();
+ return false;
+ }
+
+ queue = SLES::AndroidSimpleBufferQueue(_queue);
+ result = queue.RegisterCallback(PlayedCallback, (void *)this);
+ if (result != SL_RESULT_SUCCESS) {
+ error.Set(sles_domain, int(result),
+ "Play.RegisterCallback() failed");
+ play_object.Destroy();
+ mix_object.Destroy();
+ engine_object.Destroy();
+ return false;
+ }
+
+ result = play.SetPlayState(SL_PLAYSTATE_PLAYING);
+ if (result != SL_RESULT_SUCCESS) {
+ error.Set(sles_domain, int(result),
+ "Play.SetPlayState(PLAYING) failed");
+ play_object.Destroy();
+ mix_object.Destroy();
+ engine_object.Destroy();
+ return false;
+ }
+
+ pause = cancel = false;
+ n_queued = 0;
+ next = 0;
+ filled = 0;
+
+ // TODO: support other sample formats
+ audio_format.format = SampleFormat::S16;
+
+ return true;
+}
+
+inline void
+SlesOutput::Close()
+{
+ play.SetPlayState(SL_PLAYSTATE_STOPPED);
+ play_object.Destroy();
+ mix_object.Destroy();
+ engine_object.Destroy();
+}
+
+inline size_t
+SlesOutput::Play(const void *chunk, size_t size, Error &error)
+{
+ cancel = false;
+
+ if (pause) {
+ SLresult result = play.SetPlayState(SL_PLAYSTATE_PLAYING);
+ if (result != SL_RESULT_SUCCESS) {
+ error.Set(sles_domain, int(result),
+ "Play.SetPlayState(PLAYING) failed");
+ return false;
+ }
+
+ pause = false;
+ }
+
+ const ScopeLock protect(mutex);
+
+ assert(filled < BUFFER_SIZE);
+
+ while (n_queued == N_BUFFERS) {
+ assert(filled == 0);
+ cond.wait(mutex);
+ }
+
+ size_t nbytes = std::min(BUFFER_SIZE - filled, size);
+ memcpy(buffers[next] + filled, chunk, nbytes);
+ filled += nbytes;
+ if (filled < BUFFER_SIZE)
+ return nbytes;
+
+ SLresult result = queue.Enqueue(buffers[next], BUFFER_SIZE);
+ if (result != SL_RESULT_SUCCESS) {
+ error.Set(sles_domain, int(result),
+ "AndroidSimpleBufferQueue.Enqueue() failed");
+ return 0;
+ }
+
+ ++n_queued;
+ next = (next + 1) % N_BUFFERS;
+ filled = 0;
+
+ return nbytes;
+}
+
+inline void
+SlesOutput::Drain()
+{
+ const ScopeLock protect(mutex);
+
+ assert(filled < BUFFER_SIZE);
+
+ while (n_queued > 0)
+ cond.wait(mutex);
+}
+
+inline void
+SlesOutput::Cancel()
+{
+ pause = true;
+ cancel = true;
+
+ SLresult result = play.SetPlayState(SL_PLAYSTATE_PAUSED);
+ if (result != SL_RESULT_SUCCESS)
+ FormatError(sles_domain, "Play.SetPlayState(PAUSED) failed");
+
+ result = queue.Clear();
+ if (result != SL_RESULT_SUCCESS)
+ FormatWarning(sles_domain,
+ "AndroidSimpleBufferQueue.Clear() failed");
+
+ const ScopeLock protect(mutex);
+ n_queued = 0;
+ filled = 0;
+}
+
+inline bool
+SlesOutput::Pause()
+{
+ cancel = false;
+
+ if (pause)
+ return true;
+
+ pause = true;
+
+ SLresult result = play.SetPlayState(SL_PLAYSTATE_PAUSED);
+ if (result != SL_RESULT_SUCCESS) {
+ FormatError(sles_domain, "Play.SetPlayState(PAUSED) failed");
+ return false;
+ }
+
+ return true;
+}
+
+inline void
+SlesOutput::PlayedCallback()
+{
+ const ScopeLock protect(mutex);
+ assert(n_queued > 0);
+ --n_queued;
+ cond.signal();
+}
+
+static bool
+sles_test_default_device()
+{
+ /* this is the default output plugin on Android, and it should
+ be available in any case */
+ return true;
+}
+
+static AudioOutput *
+sles_output_init(const config_param &param, Error &error)
+{
+ SlesOutput *sles = new SlesOutput();
+
+ if (!sles->Initialize(param, error) ||
+ !sles->Configure(param, error)) {
+ delete sles;
+ return nullptr;
+ }
+
+ return *sles;
+}
+
+static void
+sles_output_finish(AudioOutput *ao)
+{
+ SlesOutput *sles = (SlesOutput *)ao;
+
+ delete sles;
+}
+
+static bool
+sles_output_open(AudioOutput *ao, AudioFormat &audio_format, Error &error)
+{
+ SlesOutput &sles = *(SlesOutput *)ao;
+
+ return sles.Open(audio_format, error);
+}
+
+static void
+sles_output_close(AudioOutput *ao)
+{
+ SlesOutput &sles = *(SlesOutput *)ao;
+
+ sles.Close();
+}
+
+static unsigned
+sles_output_delay(AudioOutput *ao)
+{
+ SlesOutput &sles = *(SlesOutput *)ao;
+
+ return sles.Delay();
+}
+
+static size_t
+sles_output_play(AudioOutput *ao, const void *chunk, size_t size,
+ Error &error)
+{
+ SlesOutput &sles = *(SlesOutput *)ao;
+
+ return sles.Play(chunk, size, error);
+}
+
+static void
+sles_output_drain(AudioOutput *ao)
+{
+ SlesOutput &sles = *(SlesOutput *)ao;
+
+ sles.Drain();
+}
+
+static void
+sles_output_cancel(AudioOutput *ao)
+{
+ SlesOutput &sles = *(SlesOutput *)ao;
+
+ sles.Cancel();
+}
+
+static bool
+sles_output_pause(AudioOutput *ao)
+{
+ SlesOutput &sles = *(SlesOutput *)ao;
+
+ return sles.Pause();
+}
+
+const struct AudioOutputPlugin sles_output_plugin = {
+ "sles",
+ sles_test_default_device,
+ sles_output_init,
+ sles_output_finish,
+ nullptr,
+ nullptr,
+ sles_output_open,
+ sles_output_close,
+ sles_output_delay,
+ nullptr,
+ sles_output_play,
+ sles_output_drain,
+ sles_output_cancel,
+ sles_output_pause,
+ nullptr,
+};
diff --git a/src/output/plugins/sles/SlesOutputPlugin.hxx b/src/output/plugins/sles/SlesOutputPlugin.hxx
new file mode 100644
index 000000000..5424dec2e
--- /dev/null
+++ b/src/output/plugins/sles/SlesOutputPlugin.hxx
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_SLES_OUTPUT_PLUGIN_HXX
+#define MPD_SLES_OUTPUT_PLUGIN_HXX
+
+extern const struct AudioOutputPlugin sles_output_plugin;
+
+#endif