diff options
Diffstat (limited to 'src/output/plugins')
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 ¶m, 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 ¶m, 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 ¶m, 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 ¶m, Error &error) { + return base.Configure(param, error); + } + + bool Configure(const config_param ¶m, 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 ¶m, 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 ¶m, 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 ¶m, 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 ¶m, 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 ¶m, 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 ¶m, 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 ¶m, Error &error) { + return base.Configure(param, error); + } +}; + +static AudioOutput * +null_init(const config_param ¶m, 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 ¶m) +{ + 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 ¶m, 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 ¶m, 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 ¶m, 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 ¶m, 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 ¶m, 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 ¶m, Error &error) { + return base.Configure(param, error); + } + + bool Configure(const config_param ¶m, Error &error); +}; + +static constexpr Domain pipe_output_domain("pipe_output"); + +inline bool +PipeOutput::Configure(const config_param ¶m, 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 ¶m, 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 = ± + + 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 ¶m, 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 ¶m, Error &error_r) { + return base.Configure(param, error_r); + } + + bool Configure(const config_param ¶m, 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 ¶m, 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 ¶m, 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 ¶m, Error &error) { + return base.Configure(param, error); + } + + void Configure(const config_param ¶m); + + 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 ¶m) +{ + 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 ¶m, 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 ¶m, Error &error) { + return base.Configure(param, error); + } + + bool Configure(const config_param ¶m, 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 ¶m, 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 ¶m, 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 ¶m, 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 ¶m, 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 ¶m, 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 ¶m, 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 ¶m, Error &error); + + bool Configure(const config_param ¶m, Error &error); + + AudioOutput *InitAndConfigure(const config_param ¶m, + 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 ¶m, 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 ¶m, Error &error) +{ + return base.Configure(param, error); +} + +static AudioOutput * +httpd_output_init(const config_param ¶m, 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 ¶m, Error &error) { + return base.Configure(param, error); + } + + bool Configure(const config_param ¶m, 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 ¶m, 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 |