diff options
Diffstat (limited to 'src/output')
24 files changed, 3744 insertions, 1137 deletions
diff --git a/src/output/alsa_plugin.c b/src/output/alsa_plugin.c index 818c83ca2..9177fabe4 100644 --- a/src/output/alsa_plugin.c +++ b/src/output/alsa_plugin.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2009 The Music Player Daemon Project + * Copyright (C) 2003-2010 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify @@ -17,7 +17,8 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "../output_api.h" +#include "config.h" +#include "output_api.h" #include "mixer_list.h" #include <glib.h> @@ -69,6 +70,16 @@ struct alsa_data { /** the size of one audio frame */ size_t 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; }; /** @@ -172,15 +183,148 @@ alsa_test_default_device(void) } static snd_pcm_format_t -get_bitformat(const struct audio_format *af) +get_bitformat(enum sample_format sample_format) +{ + switch (sample_format) { + case SAMPLE_FORMAT_S8: + return SND_PCM_FORMAT_S8; + + case SAMPLE_FORMAT_S16: + return SND_PCM_FORMAT_S16; + + case SAMPLE_FORMAT_S24_P32: + return SND_PCM_FORMAT_S24; + + case SAMPLE_FORMAT_S24: + return G_BYTE_ORDER == G_BIG_ENDIAN + ? SND_PCM_FORMAT_S24_3BE + : SND_PCM_FORMAT_S24_3LE; + + case SAMPLE_FORMAT_S32: + return SND_PCM_FORMAT_S32; + + default: + return SND_PCM_FORMAT_UNKNOWN; + } +} + +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; + } +} + +/** + * Attempts to configure the specified sample format. + */ +static int +alsa_output_try_format(snd_pcm_t *pcm, snd_pcm_hw_params_t *hwparams, + struct audio_format *audio_format, + enum sample_format sample_format) +{ + snd_pcm_format_t alsa_format = get_bitformat(sample_format); + if (alsa_format == SND_PCM_FORMAT_UNKNOWN) + return -EINVAL; + + int err = snd_pcm_hw_params_set_format(pcm, hwparams, alsa_format); + if (err == 0) + audio_format->format = sample_format; + + return err; +} + +/** + * Attempts to configure the specified sample format with reversed + * host byte order. + */ +static int +alsa_output_try_reverse(snd_pcm_t *pcm, snd_pcm_hw_params_t *hwparams, + struct audio_format *audio_format, + enum sample_format sample_format) +{ + snd_pcm_format_t alsa_format = + byteswap_bitformat(get_bitformat(sample_format)); + if (alsa_format == SND_PCM_FORMAT_UNKNOWN) + return -EINVAL; + + int err = snd_pcm_hw_params_set_format(pcm, hwparams, alsa_format); + if (err == 0) { + audio_format->format = sample_format; + audio_format->reverse_endian = 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_both(snd_pcm_t *pcm, snd_pcm_hw_params_t *hwparams, + struct audio_format *audio_format, + enum sample_format sample_format) +{ + int err = alsa_output_try_format(pcm, hwparams, audio_format, + sample_format); + if (err == -EINVAL) + err = alsa_output_try_reverse(pcm, hwparams, audio_format, + sample_format); + + 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, + struct audio_format *audio_format) { - switch (af->bits) { - case 8: return SND_PCM_FORMAT_S8; - case 16: return SND_PCM_FORMAT_S16; - case 24: return SND_PCM_FORMAT_S24; - case 32: return SND_PCM_FORMAT_S32; + /* try the input format first */ + + int err = alsa_output_try_format_both(pcm, hwparams, audio_format, + audio_format->format); + if (err != -EINVAL) + return err; + + /* if unsupported by the hardware, try other formats */ + + static const enum sample_format probe_formats[] = { + SAMPLE_FORMAT_S24_P32, + SAMPLE_FORMAT_S32, + SAMPLE_FORMAT_S24, + SAMPLE_FORMAT_S16, + SAMPLE_FORMAT_S8, + SAMPLE_FORMAT_UNDEFINED, + }; + + for (unsigned i = 0; probe_formats[i] != SAMPLE_FORMAT_UNDEFINED; ++i) { + if (probe_formats[i] == audio_format->format) + continue; + + err = alsa_output_try_format_both(pcm, hwparams, audio_format, + probe_formats[i]); + if (err != -EINVAL) + return err; } - return SND_PCM_FORMAT_UNKNOWN; + + return -EINVAL; } /** @@ -189,7 +333,6 @@ get_bitformat(const struct audio_format *af) */ static bool alsa_setup(struct alsa_data *ad, struct audio_format *audio_format, - snd_pcm_format_t bitformat, GError **error) { snd_pcm_hw_params_t *hwparams; @@ -208,7 +351,6 @@ alsa_setup(struct alsa_data *ad, struct audio_format *audio_format, configure_hw: /* configure HW params */ snd_pcm_hw_params_alloca(&hwparams); - cmd = "snd_pcm_hw_params_any"; err = snd_pcm_hw_params_any(ad->pcm, hwparams); if (err < 0) @@ -235,31 +377,12 @@ configure_hw: ad->writei = snd_pcm_writei; } - err = snd_pcm_hw_params_set_format(ad->pcm, hwparams, bitformat); - if (err == -EINVAL && (audio_format->bits == 24 || - audio_format->bits == 16)) { - /* fall back to 32 bit, let pcm_convert.c do the conversion */ - err = snd_pcm_hw_params_set_format(ad->pcm, hwparams, - SND_PCM_FORMAT_S32); - if (err == 0) - audio_format->bits = 32; - } - - if (err == -EINVAL && audio_format->bits != 16) { - /* fall back to 16 bit, let pcm_convert.c do the conversion */ - err = snd_pcm_hw_params_set_format(ad->pcm, hwparams, - SND_PCM_FORMAT_S16); - if (err == 0) { - g_debug("ALSA device \"%s\": converting %u bit to 16 bit\n", - alsa_device(ad), audio_format->bits); - audio_format->bits = 16; - } - } - + err = alsa_output_setup_format(ad->pcm, hwparams, audio_format); if (err < 0) { g_set_error(error, alsa_output_quark(), err, - "ALSA device \"%s\" does not support %u bit audio: %s", - alsa_device(ad), audio_format->bits, + "ALSA device \"%s\" does not support format %s: %s", + alsa_device(ad), + sample_format_to_string(audio_format->format), snd_strerror(-err)); return false; } @@ -285,6 +408,26 @@ configure_hw: } 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); + g_debug("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); + g_debug("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"; @@ -365,6 +508,9 @@ configure_hw: g_debug("buffer_size=%u period_size=%u", (unsigned)alsa_buffer_size, (unsigned)alsa_period_size); + ad->period_frames = alsa_period_size; + ad->period_position = 0; + return true; error: @@ -378,19 +524,9 @@ static bool alsa_open(void *data, struct audio_format *audio_format, GError **error) { struct alsa_data *ad = data; - snd_pcm_format_t bitformat; int err; bool success; - bitformat = get_bitformat(audio_format); - if (bitformat == SND_PCM_FORMAT_UNKNOWN) { - /* sample format is not supported by this plugin - - fall back to 16 bit samples */ - - audio_format->bits = 16; - bitformat = SND_PCM_FORMAT_S16; - } - err = snd_pcm_open(&ad->pcm, alsa_device(ad), SND_PCM_STREAM_PLAYBACK, ad->mode); if (err < 0) { @@ -400,7 +536,7 @@ alsa_open(void *data, struct audio_format *audio_format, GError **error) return false; } - success = alsa_setup(ad, audio_format, bitformat, error); + success = alsa_setup(ad, audio_format, error); if (!success) { snd_pcm_close(ad->pcm); return false; @@ -431,6 +567,7 @@ alsa_recover(struct alsa_data *ad, int err) /* 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: @@ -448,11 +585,47 @@ alsa_recover(struct alsa_data *ad, int err) } static void +alsa_drain(void *data) +{ + struct alsa_data *ad = data; + + 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; + size_t nbytes = nframes * ad->frame_size; + void *buffer = g_malloc(nbytes); + snd_pcm_hw_params_t *params; + snd_pcm_format_t format; + unsigned channels; + + snd_pcm_hw_params_alloca(¶ms); + snd_pcm_hw_params_current(ad->pcm, params); + snd_pcm_hw_params_get_format(params, &format); + snd_pcm_hw_params_get_channels(params, &channels); + + snd_pcm_format_set_silence(format, buffer, nframes * channels); + ad->writei(ad->pcm, buffer, nframes); + g_free(buffer); + } + + snd_pcm_drain(ad->pcm); + + ad->period_position = 0; +} + +static void alsa_cancel(void *data) { struct alsa_data *ad = data; - alsa_recover(ad, snd_pcm_drop(ad->pcm)); + ad->period_position = 0; + + snd_pcm_drop(ad->pcm); } static void @@ -460,9 +633,6 @@ alsa_close(void *data) { struct alsa_data *ad = data; - if (snd_pcm_state(ad->pcm) == SND_PCM_STATE_RUNNING) - snd_pcm_drain(ad->pcm); - snd_pcm_close(ad->pcm); } @@ -475,8 +645,11 @@ alsa_play(void *data, const void *chunk, size_t size, GError **error) while (true) { snd_pcm_sframes_t ret = ad->writei(ad->pcm, chunk, size); - if (ret > 0) + if (ret > 0) { + ad->period_position = (ad->period_position + ret) + % ad->period_frames; return ret * ad->frame_size; + } if (ret < 0 && ret != -EAGAIN && ret != -EINTR && alsa_recover(ad, ret) < 0) { @@ -494,7 +667,9 @@ const struct audio_output_plugin alsaPlugin = { .finish = alsa_finish, .open = alsa_open, .play = alsa_play, + .drain = alsa_drain, .cancel = alsa_cancel, .close = alsa_close, - .mixer_plugin = &alsa_mixer, + + .mixer_plugin = &alsa_mixer_plugin, }; diff --git a/src/output/ao_plugin.c b/src/output/ao_plugin.c index 12d2b7552..d5c95018c 100644 --- a/src/output/ao_plugin.c +++ b/src/output/ao_plugin.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2009 The Music Player Daemon Project + * Copyright (C) 2003-2010 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify @@ -17,7 +17,8 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "../output_api.h" +#include "config.h" +#include "output_api.h" #include <ao/ao.h> #include <glib.h> @@ -169,13 +170,24 @@ ao_output_open(void *data, struct audio_format *audio_format, ao_sample_format format; struct ao_data *ad = (struct ao_data *)data; - /* support for 24 bit samples in libao is currently dubious, - and until we have sorted that out, resample everything to - 16 bit */ - if (audio_format->bits > 16) - audio_format->bits = 16; + switch (audio_format->format) { + case SAMPLE_FORMAT_S8: + format.bits = 8; + break; + + case SAMPLE_FORMAT_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 = SAMPLE_FORMAT_S16; + format.bits = 16; + break; + } - format.bits = audio_format->bits; format.rate = audio_format->sample_rate; format.byte_format = AO_FMT_NATIVE; format.channels = audio_format->channels; diff --git a/src/output/ffado_output_plugin.c b/src/output/ffado_output_plugin.c new file mode 100644 index 000000000..723698ed0 --- /dev/null +++ b/src/output/ffado_output_plugin.c @@ -0,0 +1,347 @@ +/* + * Copyright (C) 2003-2010 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. + */ + +/* + * Warning: this plugin was not tested successfully. I just couldn't + * keep libffado2 from crashing. Use at your own risk. + * + * For details, see my Debian bug reports: + * + * http://bugs.debian.org/601657 + * http://bugs.debian.org/601659 + * http://bugs.debian.org/601663 + * + */ + +#include "config.h" +#include "output_api.h" +#include "timer.h" + +#include <glib.h> +#include <assert.h> + +#include <libffado/ffado.h> + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "ffado" + +enum { + MAX_STREAMS = 8, +}; + +struct mpd_ffado_stream { + /** libffado's stream number */ + int number; + + float *buffer; +}; + +struct mpd_ffado_device { + char *device_name; + int verbose; + unsigned period_size, nb_buffers; + + ffado_device_t *dev; + + /** + * The current sample position inside the stream buffers. New + * samples get appended at this position on all streams at the + * same time. When the buffers are full + * (buffer_position==period_size), + * ffado_streaming_transfer_playback_buffers() gets called to + * hand them over to libffado. + */ + unsigned buffer_position; + + /** + * The number of streams which are really used by MPD. + */ + int num_streams; + struct mpd_ffado_stream streams[MAX_STREAMS]; +}; + +static inline GQuark +ffado_output_quark(void) +{ + return g_quark_from_static_string("ffado_output"); +} + +static void * +ffado_init(G_GNUC_UNUSED const struct audio_format *audio_format, + const struct config_param *param, + GError **error_r) +{ + g_debug("using libffado version %s, API=%d", + ffado_get_version(), ffado_get_api_version()); + + struct mpd_ffado_device *fd = g_new(struct mpd_ffado_device, 1); + fd->device_name = config_dup_block_string(param, "device", NULL); + fd->verbose = config_get_block_unsigned(param, "verbose", 0); + + fd->period_size = config_get_block_unsigned(param, "period_size", + 1024); + if (fd->period_size == 0 || fd->period_size > 1024 * 1024) { + g_set_error(error_r, ffado_output_quark(), 0, + "invalid period_size setting"); + return false; + } + + fd->nb_buffers = config_get_block_unsigned(param, "nb_buffers", 3); + if (fd->nb_buffers == 0 || fd->nb_buffers > 1024) { + g_set_error(error_r, ffado_output_quark(), 0, + "invalid nb_buffers setting"); + return false; + } + + return fd; +} + +static void +ffado_finish(void *data) +{ + struct mpd_ffado_device *fd = data; + + g_free(fd->device_name); + g_free(fd); +} + +static bool +ffado_configure_stream(ffado_device_t *dev, struct mpd_ffado_stream *stream, + GError **error_r) +{ + char *buffer = (char *)stream->buffer; + if (ffado_streaming_set_playback_stream_buffer(dev, stream->number, + buffer) != 0) { + g_set_error(error_r, ffado_output_quark(), 0, + "failed to configure stream buffer"); + return false; + } + + if (ffado_streaming_playback_stream_onoff(dev, stream->number, + 1) != 0) { + g_set_error(error_r, ffado_output_quark(), 0, + "failed to disable stream"); + return false; + } + + return true; +} + +static bool +ffado_configure(struct mpd_ffado_device *fd, struct audio_format *audio_format, + GError **error_r) +{ + assert(fd != NULL); + assert(fd->dev != NULL); + assert(audio_format->channels <= MAX_STREAMS); + + if (ffado_streaming_set_audio_datatype(fd->dev, + ffado_audio_datatype_float) != 0) { + g_set_error(error_r, ffado_output_quark(), 0, + "ffado_streaming_set_audio_datatype() failed"); + return false; + } + + int num_streams = ffado_streaming_get_nb_playback_streams(fd->dev); + if (num_streams < 0) { + g_set_error(error_r, ffado_output_quark(), 0, + "ffado_streaming_get_nb_playback_streams() failed"); + return false; + } + + g_debug("there are %d playback streams", num_streams); + + fd->num_streams = 0; + for (int i = 0; i < num_streams; ++i) { + char name[256]; + ffado_streaming_get_playback_stream_name(fd->dev, i, name, + sizeof(name) - 1); + + ffado_streaming_stream_type type = + ffado_streaming_get_playback_stream_type(fd->dev, i); + if (type != ffado_stream_type_audio) { + g_debug("stream %d name='%s': not an audio stream", + i, name); + continue; + } + + if (fd->num_streams >= audio_format->channels) { + g_debug("stream %d name='%s': ignoring", + i, name); + continue; + } + + g_debug("stream %d name='%s'", i, name); + + struct mpd_ffado_stream *stream = + &fd->streams[fd->num_streams++]; + + stream->number = i; + + /* allocated buffer is zeroed = silence */ + stream->buffer = g_new0(float, fd->period_size); + + if (!ffado_configure_stream(fd->dev, stream, error_r)) + return false; + } + + if (!audio_valid_channel_count(fd->num_streams)) { + g_set_error(error_r, ffado_output_quark(), 0, + "invalid channel count from libffado: %u", + audio_format->channels); + return false; + } + + g_debug("configured %d audio streams", fd->num_streams); + + if (ffado_streaming_prepare(fd->dev) != 0) { + g_set_error(error_r, ffado_output_quark(), 0, + "ffado_streaming_prepare() failed"); + return false; + } + + if (ffado_streaming_start(fd->dev) != 0) { + g_set_error(error_r, ffado_output_quark(), 0, + "ffado_streaming_start() failed"); + return false; + } + + audio_format->channels = fd->num_streams; + return true; +} + +static bool +ffado_open(void *data, struct audio_format *audio_format, GError **error_r) +{ + struct mpd_ffado_device *fd = data; + + /* will be converted to floating point, choose best input + format */ + audio_format->format = SAMPLE_FORMAT_S24_P32; + + ffado_device_info_t device_info; + memset(&device_info, 0, sizeof(device_info)); + if (fd->device_name != NULL) { + device_info.nb_device_spec_strings = 1; + device_info.device_spec_strings = &fd->device_name; + } + + ffado_options_t options; + memset(&options, 0, sizeof(options)); + options.sample_rate = audio_format->sample_rate; + options.period_size = fd->period_size; + options.nb_buffers = fd->nb_buffers; + options.verbose = fd->verbose; + + fd->dev = ffado_streaming_init(device_info, options); + if (fd->dev == NULL) { + g_set_error(error_r, ffado_output_quark(), 0, + "ffado_streaming_init() failed"); + return false; + } + + if (!ffado_configure(fd, audio_format, error_r)) { + ffado_streaming_finish(fd->dev); + + for (int i = 0; i < fd->num_streams; ++i) { + struct mpd_ffado_stream *stream = &fd->streams[i]; + g_free(stream->buffer); + } + + return false; + } + + fd->buffer_position = 0; + + return true; +} + +static void +ffado_close(void *data) +{ + struct mpd_ffado_device *fd = data; + + ffado_streaming_stop(fd->dev); + ffado_streaming_finish(fd->dev); + + for (int i = 0; i < fd->num_streams; ++i) { + struct mpd_ffado_stream *stream = &fd->streams[i]; + g_free(stream->buffer); + } +} + +static size_t +ffado_play(void *data, const void *chunk, size_t size, GError **error_r) +{ + struct mpd_ffado_device *fd = data; + + /* wait for prefious buffer to finish (if it was full) */ + + if (fd->buffer_position >= fd->period_size) { + switch (ffado_streaming_wait(fd->dev)) { + case ffado_wait_ok: + case ffado_wait_xrun: + break; + + default: + g_set_error(error_r, ffado_output_quark(), 0, + "ffado_streaming_wait() failed"); + return 0; + } + + fd->buffer_position = 0; + } + + /* copy samples to stream buffers, non-interleaved */ + + const int32_t *p = chunk; + unsigned num_frames = size / sizeof(*p) / fd->num_streams; + if (num_frames > fd->period_size - fd->buffer_position) + num_frames = fd->period_size - fd->buffer_position; + + for (unsigned i = num_frames; i > 0; --i) { + for (int stream = 0; stream < fd->num_streams; ++stream) + fd->streams[stream].buffer[fd->buffer_position] = + *p++ / (float)(1 << 23); + ++fd->buffer_position; + } + + /* if buffer full, transfer to device */ + + if (fd->buffer_position >= fd->period_size && + /* libffado documentation says this function returns -1 on + error, but that is a lie - it returns a boolean value, + and "false" means error */ + !ffado_streaming_transfer_playback_buffers(fd->dev)) { + g_set_error(error_r, ffado_output_quark(), 0, + "ffado_streaming_transfer_playback_buffers() failed"); + return 0; + } + + return num_frames * sizeof(*p) * fd->num_streams; +} + +const struct audio_output_plugin ffado_output_plugin = { + .name = "ffado", + .init = ffado_init, + .finish = ffado_finish, + .open = ffado_open, + .close = ffado_close, + .play = ffado_play, +}; diff --git a/src/output/fifo_plugin.c b/src/output/fifo_output_plugin.c index 76bbe8cfa..f4217ec4d 100644 --- a/src/output/fifo_plugin.c +++ b/src/output/fifo_output_plugin.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2009 The Music Player Daemon Project + * Copyright (C) 2003-2010 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify @@ -17,15 +17,17 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "../output_api.h" -#include "../utils.h" -#include "../timer.h" +#include "config.h" +#include "output_api.h" +#include "utils.h" +#include "timer.h" +#include "fd_util.h" +#include "open.h" #include <glib.h> #include <sys/types.h> #include <sys/stat.h> -#include <fcntl.h> #include <errno.h> #include <string.h> #include <unistd.h> @@ -152,7 +154,7 @@ fifo_open(struct fifo_data *fd, GError **error) if (!fifo_check(fd, error)) return false; - fd->input = open(fd->path, O_RDONLY|O_NONBLOCK); + fd->input = open_cloexec(fd->path, O_RDONLY|O_NONBLOCK|O_BINARY, 0); if (fd->input < 0) { g_set_error(error, fifo_output_quark(), errno, "Could not open FIFO \"%s\" for reading: %s", @@ -161,7 +163,7 @@ fifo_open(struct fifo_data *fd, GError **error) return false; } - fd->output = open(fd->path, O_WRONLY|O_NONBLOCK); + fd->output = open_cloexec(fd->path, O_WRONLY|O_NONBLOCK|O_BINARY, 0); if (fd->output < 0) { g_set_error(error, fifo_output_quark(), errno, "Could not open FIFO \"%s\" for writing: %s", diff --git a/src/output/httpd_client.c b/src/output/httpd_client.c index 52a398e3b..6bd095838 100644 --- a/src/output/httpd_client.c +++ b/src/output/httpd_client.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2009 The Music Player Daemon Project + * Copyright (C) 2003-2010 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify @@ -17,11 +17,13 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +#include "config.h" #include "httpd_client.h" #include "httpd_internal.h" #include "fifo_buffer.h" #include "page.h" #include "icy_server.h" +#include "glib_compat.h" #include <stdbool.h> #include <assert.h> @@ -280,11 +282,12 @@ httpd_client_send_response(struct httpd_client *client) } else { gchar *metadata_header; - metadata_header = icy_server_metadata_header("Add config information here!", /* TODO */ - "Add config information here!", /* TODO */ - "Add config information here!", /* TODO */ - client->httpd->content_type, - client->metaint); + metadata_header = icy_server_metadata_header( + client->httpd->name, + client->httpd->genre, + client->httpd->website, + client->httpd->content_type, + client->metaint); g_strlcpy(buffer, metadata_header, sizeof(buffer)); @@ -482,11 +485,6 @@ httpd_client_queue_size(const struct httpd_client *client) return size; } -/* g_queue_clear() was introduced in GLib 2.14 */ -#if !GLIB_CHECK_VERSION(2,14,0) -#define g_queue_clear(q) do { g_queue_free(q); q = g_queue_new(); } while (0) -#endif - void httpd_client_cancel(struct httpd_client *client) { diff --git a/src/output/httpd_client.h b/src/output/httpd_client.h index 4a2912f80..7ebd0bbc0 100644 --- a/src/output/httpd_client.h +++ b/src/output/httpd_client.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2009 The Music Player Daemon Project + * Copyright (C) 2003-2010 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify diff --git a/src/output/httpd_internal.h b/src/output/httpd_internal.h index 2257e27a2..fee72c07f 100644 --- a/src/output/httpd_internal.h +++ b/src/output/httpd_internal.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2009 The Music Player Daemon Project + * Copyright (C) 2003-2010 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify @@ -29,30 +29,34 @@ #include <glib.h> -#include <sys/socket.h> +#include <stdbool.h> struct httpd_client; struct httpd_output { /** - * The configured encoder plugin. + * True if the audio output is open and accepts client + * connections. */ - struct encoder *encoder; + bool open; /** - * The MIME type produced by the #encoder. + * The configured encoder plugin. */ - const char *content_type; + struct encoder *encoder; /** - * The configured address of the listener socket. + * 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. */ - struct sockaddr_storage address; + size_t unflushed_input; /** - * The size of #address. + * The MIME type produced by the #encoder. */ - socklen_t address_size; + const char *content_type; /** * This mutex protects the listener socket and the client @@ -69,12 +73,7 @@ struct httpd_output { /** * The listener socket. */ - int fd; - - /** - * A GLib main loop source id for the listener socket. - */ - guint source_id; + struct server_socket *server_socket; /** * The header page, which is sent to every client on connect. @@ -87,6 +86,19 @@ struct httpd_output { struct page *metadata; /** + * The configured name. + */ + char const *name; + /** + * The configured genre. + */ + char const *genre; + /** + * The configured website address. + */ + char const *website; + + /** * A linked list containing all clients which are currently * connected. */ @@ -97,6 +109,12 @@ struct httpd_output { * function. */ char buffer[32768]; + + /** + * The maximum and current number of clients connected + * at the same time. + */ + guint clients_max, clients_cnt; }; /** diff --git a/src/output/httpd_output_plugin.c b/src/output/httpd_output_plugin.c index 026e8d9d8..72994018a 100644 --- a/src/output/httpd_output_plugin.c +++ b/src/output/httpd_output_plugin.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2009 The Music Player Daemon Project + * Copyright (C) 2003-2010 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify @@ -17,6 +17,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +#include "config.h" #include "httpd_internal.h" #include "httpd_client.h" #include "output_api.h" @@ -25,15 +26,19 @@ #include "socket_util.h" #include "page.h" #include "icy_server.h" +#include "fd_util.h" +#include "server_socket.h" #include <assert.h> #include <sys/types.h> -#include <netinet/in.h> -#include <netdb.h> #include <unistd.h> #include <errno.h> +#ifdef HAVE_LIBWRAP +#include <tcpd.h> +#endif + #undef G_LOG_DOMAIN #define G_LOG_DOMAIN "httpd_output" @@ -46,18 +51,49 @@ httpd_output_quark(void) return g_quark_from_static_string("httpd_output"); } +static void +httpd_listen_in_event(int fd, const struct sockaddr *address, + size_t address_length, int uid, void *ctx); + +static bool +httpd_output_bind(struct httpd_output *httpd, GError **error_r) +{ + httpd->open = false; + + g_mutex_lock(httpd->mutex); + bool success = server_socket_open(httpd->server_socket, error_r); + g_mutex_unlock(httpd->mutex); + + return success; +} + +static void +httpd_output_unbind(struct httpd_output *httpd) +{ + assert(!httpd->open); + + g_mutex_lock(httpd->mutex); + server_socket_close(httpd->server_socket); + g_mutex_unlock(httpd->mutex); +} + static void * httpd_output_init(G_GNUC_UNUSED const struct audio_format *audio_format, const struct config_param *param, GError **error) { struct httpd_output *httpd = g_new(struct httpd_output, 1); - const char *encoder_name; + const char *encoder_name, *bind_to_address; const struct encoder_plugin *encoder_plugin; guint port; - struct sockaddr_in *sin; /* read configuration */ + httpd->name = + config_get_block_string(param, "name", "Set name in config"); + httpd->genre = + config_get_block_string(param, "genre", "Set genre in config"); + httpd->website = + config_get_block_string(param, "website", "Set website in config"); port = config_get_block_unsigned(param, "port", 8000); @@ -69,21 +105,21 @@ httpd_output_init(G_GNUC_UNUSED const struct audio_format *audio_format, return NULL; } - if (strcmp(encoder_name, "vorbis") == 0) - httpd->content_type = "audio/ogg"; - else if (strcmp(encoder_name, "lame") == 0) - httpd->content_type = "audio/mpeg"; - else - httpd->content_type = "application/octet-stream"; + httpd->clients_max = config_get_block_unsigned(param,"max_clients", 0); + + /* set up bind_to_address */ - /* initialize listen address */ + httpd->server_socket = server_socket_new(httpd_listen_in_event, httpd); - sin = (struct sockaddr_in *)&httpd->address; - memset(sin, 0, sizeof(sin)); - sin->sin_port = htons(port); - sin->sin_family = AF_INET; - sin->sin_addr.s_addr = INADDR_ANY; - httpd->address_size = sizeof(*sin); + bind_to_address = + config_get_block_string(param, "bind_to_address", NULL); + bool success = bind_to_address != NULL && + strcmp(bind_to_address, "any") != 0 + ? server_socket_add_host(httpd->server_socket, bind_to_address, + port, error) + : server_socket_add_port(httpd->server_socket, port, error); + if (!success) + return NULL; /* initialize metadata */ httpd->metadata = NULL; @@ -94,6 +130,12 @@ httpd_output_init(G_GNUC_UNUSED const struct audio_format *audio_format, if (httpd->encoder == NULL) return NULL; + /* determine content type */ + httpd->content_type = encoder_get_mime_type(httpd->encoder); + if (httpd->content_type == NULL) { + httpd->content_type = "application/octet-stream"; + } + httpd->mutex = g_mutex_new(); return httpd; @@ -108,6 +150,7 @@ httpd_output_finish(void *data) page_unref(httpd->metadata); encoder_finish(httpd->encoder); + server_socket_free(httpd->server_socket); g_mutex_free(httpd->mutex); g_free(httpd); } @@ -124,36 +167,64 @@ httpd_client_add(struct httpd_output *httpd, int fd) httpd->encoder->plugin->tag == NULL); httpd->clients = g_list_prepend(httpd->clients, client); + httpd->clients_cnt++; /* pass metadata to client */ if (httpd->metadata) httpd_client_send_metadata(client, httpd->metadata); } -static gboolean -httpd_listen_in_event(G_GNUC_UNUSED GIOChannel *source, - G_GNUC_UNUSED GIOCondition condition, - gpointer data) +static void +httpd_listen_in_event(int fd, const struct sockaddr *address, + size_t address_length, G_GNUC_UNUSED int uid, void *ctx) { - struct httpd_output *httpd = data; - int fd; - struct sockaddr_storage sa; - socklen_t sa_length = sizeof(sa); - - g_mutex_lock(httpd->mutex); + struct httpd_output *httpd = ctx; /* the listener socket has become readable - a client has connected */ - fd = accept(httpd->fd, (struct sockaddr*)&sa, &sa_length); - if (fd >= 0) - httpd_client_add(httpd, fd); - else if (fd < 0 && errno != EINTR) +#ifdef HAVE_LIBWRAP + if (address->sa_family != AF_UNIX) { + char *hostaddr = sockaddr_to_string(address, address_length, NULL); + const char *progname = g_get_prgname(); + + struct request_info req; + request_init(&req, RQ_FILE, fd, RQ_DAEMON, progname, 0); + + fromhost(&req); + + if (!hosts_access(&req)) { + /* tcp wrappers says no */ + g_warning("libwrap refused connection (libwrap=%s) from %s", + progname, hostaddr); + g_free(hostaddr); + close(fd); + g_mutex_unlock(httpd->mutex); + return; + } + + g_free(hostaddr); + } +#else + (void)address; + (void)address_length; +#endif /* HAVE_WRAP */ + + g_mutex_lock(httpd->mutex); + + if (fd >= 0) { + /* can we allow additional client */ + if (httpd->open && + (httpd->clients_max == 0 || + httpd->clients_cnt < httpd->clients_max)) + httpd_client_add(httpd, fd); + else + close(fd); + } else if (fd < 0 && errno != EINTR) { g_warning("accept() failed: %s", g_strerror(errno)); + } g_mutex_unlock(httpd->mutex); - - return true; } /** @@ -165,12 +236,22 @@ httpd_output_read_page(struct httpd_output *httpd) { size_t size = 0, nbytes; + if (httpd->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(httpd->encoder, NULL); + httpd->unflushed_input = 0; + } + do { nbytes = encoder_read(httpd->encoder, httpd->buffer + size, sizeof(httpd->buffer) - size); if (nbytes == 0) break; + httpd->unflushed_input = 0; + size += nbytes; } while (size < sizeof(httpd->buffer)); @@ -195,41 +276,41 @@ httpd_output_encoder_open(struct httpd_output *httpd, bytes of encoder output after opening it, because it has to be sent to every new client */ httpd->header = httpd_output_read_page(httpd); + + httpd->unflushed_input = 0; + return true; } static bool +httpd_output_enable(void *data, GError **error_r) +{ + struct httpd_output *httpd = data; + + return httpd_output_bind(httpd, error_r); +} + +static void +httpd_output_disable(void *data) +{ + struct httpd_output *httpd = data; + + httpd_output_unbind(httpd); +} + +static bool httpd_output_open(void *data, struct audio_format *audio_format, GError **error) { struct httpd_output *httpd = data; bool success; - GIOChannel *channel; g_mutex_lock(httpd->mutex); - /* create and set up listener socket */ - - httpd->fd = socket_bind_listen(PF_INET, SOCK_STREAM, 0, - (struct sockaddr *)&httpd->address, - httpd->address_size, - 16, error); - if (httpd->fd < 0) { - g_mutex_unlock(httpd->mutex); - return false; - } - - channel = g_io_channel_unix_new(httpd->fd); - httpd->source_id = g_io_add_watch(channel, G_IO_IN, - httpd_listen_in_event, httpd); - g_io_channel_unref(channel); - /* open the encoder */ success = httpd_output_encoder_open(httpd, audio_format, error); if (!success) { - g_source_remove(httpd->source_id); - close(httpd->fd); g_mutex_unlock(httpd->mutex); return false; } @@ -237,8 +318,11 @@ httpd_output_open(void *data, struct audio_format *audio_format, /* initialize other attributes */ httpd->clients = NULL; + httpd->clients_cnt = 0; httpd->timer = timer_new(audio_format); + httpd->open = true; + g_mutex_unlock(httpd->mutex); return true; } @@ -257,6 +341,8 @@ static void httpd_output_close(void *data) g_mutex_lock(httpd->mutex); + httpd->open = false; + timer_free(httpd->timer); g_list_foreach(httpd->clients, httpd_client_delete, NULL); @@ -267,9 +353,6 @@ static void httpd_output_close(void *data) encoder_close(httpd->encoder); - g_source_remove(httpd->source_id); - close(httpd->fd); - g_mutex_unlock(httpd->mutex); } @@ -281,6 +364,7 @@ httpd_output_remove_client(struct httpd_output *httpd, assert(client != NULL); httpd->clients = g_list_remove(httpd->clients, client); + httpd->clients_cnt--; } void @@ -291,6 +375,16 @@ httpd_output_send_header(struct httpd_output *httpd, httpd_client_send(client, httpd->header); } +static unsigned +httpd_output_delay(void *data) +{ + struct httpd_output *httpd = data; + + return httpd->timer->started + ? timer_delay(httpd->timer) + : 0; +} + static void httpd_client_check_queue(gpointer data, G_GNUC_UNUSED gpointer user_data) { @@ -352,6 +446,8 @@ httpd_output_encode_and_play(struct httpd_output *httpd, if (!success) return false; + httpd->unflushed_input += size; + httpd_output_encoder_to_clients(httpd); return true; @@ -378,13 +474,29 @@ httpd_output_play(void *data, const void *chunk, size_t size, GError **error) if (!httpd->timer->started) timer_start(httpd->timer); - else - timer_sync(httpd->timer); timer_add(httpd->timer, size); return size; } +static bool +httpd_output_pause(void *data) +{ + struct httpd_output *httpd = data; + + g_mutex_lock(httpd->mutex); + bool has_clients = httpd->clients != NULL; + g_mutex_unlock(httpd->mutex); + + if (has_clients) { + static const char silence[1020]; + return httpd_output_play(data, silence, sizeof(silence), NULL); + } else { + g_usleep(100000); + return true; + } +} + static void httpd_send_metadata(gpointer data, gpointer user_data) { @@ -433,9 +545,8 @@ httpd_output_tag(void *data, const struct tag *tag) page_unref (httpd->metadata); httpd->metadata = - icy_server_metadata_page(tag, TAG_ITEM_ALBUM, - TAG_ITEM_ARTIST, - TAG_ITEM_TITLE, + icy_server_metadata_page(tag, TAG_ALBUM, + TAG_ARTIST, TAG_TITLE, TAG_NUM_OF_ITEM_TYPES); if (httpd->metadata != NULL) { g_mutex_lock(httpd->mutex); @@ -468,9 +579,13 @@ const struct audio_output_plugin httpd_output_plugin = { .name = "httpd", .init = httpd_output_init, .finish = httpd_output_finish, + .enable = httpd_output_enable, + .disable = httpd_output_disable, .open = httpd_output_open, .close = httpd_output_close, + .delay = httpd_output_delay, .send_tag = httpd_output_tag, .play = httpd_output_play, + .pause = httpd_output_pause, .cancel = httpd_output_cancel, }; diff --git a/src/output/jack_output_plugin.c b/src/output/jack_output_plugin.c new file mode 100644 index 000000000..110ee5f26 --- /dev/null +++ b/src/output/jack_output_plugin.c @@ -0,0 +1,722 @@ +/* + * Copyright (C) 2003-2010 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 "output_api.h" + +#include <assert.h> + +#include <glib.h> +#include <jack/jack.h> +#include <jack/types.h> +#include <jack/ringbuffer.h> + +#include <stdlib.h> +#include <stdio.h> +#include <sys/types.h> +#include <unistd.h> +#include <errno.h> + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "jack" + +enum { + MAX_PORTS = 16, +}; + +static const size_t sample_size = sizeof(jack_default_audio_sample_t); + +struct jack_data { + /** + * 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 */ + struct audio_format 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; +}; + +/** + * The quark used for GError.domain. + */ +static inline GQuark +jack_output_quark(void) +{ + return g_quark_from_static_string("jack_output"); +} + +/** + * Determine the number of frames guaranteed to be available on all + * channels. + */ +static jack_nframes_t +mpd_jack_available(const struct jack_data *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 % sample_size == 0); + + return min / sample_size; +} + +static int +mpd_jack_process(jack_nframes_t nframes, void *arg) +{ + struct jack_data *jd = (struct jack_data *) arg; + jack_default_audio_sample_t *out; + + 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 * sample_size); + + /* generate silence while MPD is paused */ + + for (unsigned i = 0; i < jd->audio_format.channels; ++i) { + out = 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) { + out = jack_port_get_buffer(jd->ports[i], nframes); + jack_ringbuffer_read(jd->ringbuffer[i], + (char *)out, available * 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) { + out = jack_port_get_buffer(jd->ports[i], nframes); + + for (jack_nframes_t f = 0; f < nframes; ++f) + out[f] = 0.0; + } + + return 0; +} + +static void +mpd_jack_shutdown(void *arg) +{ + struct jack_data *jd = (struct jack_data *) arg; + jd->shutdown = true; +} + +static void +set_audioformat(struct jack_data *jd, struct audio_format *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 != SAMPLE_FORMAT_S16 && + audio_format->format != SAMPLE_FORMAT_S24_P32) + audio_format->format = SAMPLE_FORMAT_S24_P32; +} + +static void +mpd_jack_error(const char *msg) +{ + g_warning("%s", msg); +} + +#ifdef HAVE_JACK_SET_INFO_FUNCTION +static void +mpd_jack_info(const char *msg) +{ + g_message("%s", msg); +} +#endif + +/** + * Disconnect the JACK client. + */ +static void +mpd_jack_disconnect(struct jack_data *jd) +{ + assert(jd != NULL); + assert(jd->client != NULL); + + jack_deactivate(jd->client); + jack_client_close(jd->client); + jd->client = NULL; +} + +/** + * Connect the JACK client and performs some basic setup + * (e.g. register callbacks). + */ +static bool +mpd_jack_connect(struct jack_data *jd, GError **error_r) +{ + jack_status_t status; + + assert(jd != NULL); + + jd->shutdown = false; + + jd->client = jack_client_open(jd->name, jd->options, &status, + jd->server_name); + if (jd->client == NULL) { + g_set_error(error_r, jack_output_quark(), 0, + "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] == NULL) { + g_set_error(error_r, jack_output_quark(), 0, + "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(int line, const char *source, char **dest, GError **error_r) +{ + char **list = g_strsplit(source, ",", 0); + unsigned n = 0; + + for (n = 0; list[n] != NULL; ++n) { + if (n >= MAX_PORTS) { + g_set_error(error_r, jack_output_quark(), 0, + "too many port names in line %d", + line); + return 0; + } + + dest[n] = list[n]; + } + + g_free(list); + + if (n == 0) { + g_set_error(error_r, jack_output_quark(), 0, + "at least one port name expected in line %d", + line); + return 0; + } + + return n; +} + +static void * +mpd_jack_init(G_GNUC_UNUSED const struct audio_format *audio_format, + const struct config_param *param, GError **error_r) +{ + struct jack_data *jd; + const char *value; + + jd = g_new(struct jack_data, 1); + jd->options = JackNullOption; + + jd->name = config_get_block_string(param, "client_name", NULL); + if (jd->name != NULL) + 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 = config_get_block_string(param, "server_name", NULL); + if (jd->server_name != NULL) + jd->options |= JackServerName; + + if (!config_get_block_bool(param, "autostart", false)) + jd->options |= JackNoStartServer; + + /* configure the source ports */ + + value = config_get_block_string(param, "source_ports", "left,right"); + jd->num_source_ports = parse_port_list(param->line, value, + jd->source_ports, error_r); + if (jd->num_source_ports == 0) + return NULL; + + /* configure the destination ports */ + + value = config_get_block_string(param, "destination_ports", NULL); + if (value == NULL) { + /* compatibility with MPD < 0.16 */ + value = config_get_block_string(param, "ports", NULL); + if (value != NULL) + g_warning("deprecated option 'ports' in line %d", + param->line); + } + + if (value != NULL) { + jd->num_destination_ports = + parse_port_list(param->line, value, + jd->destination_ports, error_r); + if (jd->num_destination_ports == 0) + return NULL; + } else { + jd->num_destination_ports = 0; + } + + if (jd->num_destination_ports > 0 && + jd->num_destination_ports != jd->num_source_ports) + g_warning("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 = + config_get_block_unsigned(param, "ringbuffer_size", 32768); + + jack_set_error_function(mpd_jack_error); + +#ifdef HAVE_JACK_SET_INFO_FUNCTION + jack_set_info_function(mpd_jack_info); +#endif + + return jd; +} + +static void +mpd_jack_finish(void *data) +{ + struct jack_data *jd = data; + + 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]); + + g_free(jd); +} + +static bool +mpd_jack_enable(void *data, GError **error_r) +{ + struct jack_data *jd = (struct jack_data *)data; + + for (unsigned i = 0; i < jd->num_source_ports; ++i) + jd->ringbuffer[i] = NULL; + + return mpd_jack_connect(jd, error_r); +} + +static void +mpd_jack_disable(void *data) +{ + struct jack_data *jd = (struct jack_data *)data; + + if (jd->client != NULL) + mpd_jack_disconnect(jd); + + for (unsigned i = 0; i < jd->num_source_ports; ++i) { + if (jd->ringbuffer[i] != NULL) { + jack_ringbuffer_free(jd->ringbuffer[i]); + jd->ringbuffer[i] = NULL; + } + } +} + +/** + * Stops the playback on the JACK connection. + */ +static void +mpd_jack_stop(struct jack_data *jd) +{ + assert(jd != NULL); + + if (jd->client == NULL) + 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(struct jack_data *jd, GError **error_r) +{ + const char *destination_ports[MAX_PORTS], **jports; + const char *duplicate_port = NULL; + unsigned num_destination_ports; + + assert(jd->client != NULL); + 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] == NULL) + 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) ) { + g_set_error(error_r, jack_output_quark(), 0, + "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, NULL, NULL, + JackPortIsPhysical | JackPortIsInput); + if (jports == NULL) { + g_set_error(error_r, jack_output_quark(), 0, + "no ports found"); + mpd_jack_stop(jd); + return false; + } + + assert(*jports != NULL); + + for (num_destination_ports = 0; + num_destination_ports < MAX_PORTS && + jports[num_destination_ports] != NULL; + ++num_destination_ports) { + g_debug("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 = NULL; + } + + 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) { + g_set_error(error_r, jack_output_quark(), 0, + "Not a valid JACK port: %s", + destination_ports[i]); + + if (jports != NULL) + free(jports); + + mpd_jack_stop(jd); + return false; + } + } + + if (duplicate_port != NULL) { + /* 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) { + g_set_error(error_r, jack_output_quark(), 0, + "Not a valid JACK port: %s", + duplicate_port); + + if (jports != NULL) + free(jports); + + mpd_jack_stop(jd); + return false; + } + } + + if (jports != NULL) + free(jports); + + return true; +} + +static bool +mpd_jack_open(void *data, struct audio_format *audio_format, GError **error_r) +{ + struct jack_data *jd = data; + + assert(jd != NULL); + + jd->pause = false; + + if (jd->client == NULL && !mpd_jack_connect(jd, error_r)) + return false; + + set_audioformat(jd, audio_format); + jd->audio_format = *audio_format; + + if (!mpd_jack_start(jd, error_r)) + return false; + + return true; +} + +static void +mpd_jack_close(G_GNUC_UNUSED void *data) +{ + struct jack_data *jd = data; + + mpd_jack_stop(jd); +} + +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(struct jack_data *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], (void*)&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(struct jack_data *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], (void*)&sample, + sizeof(sample)); + } + } +} + +static void +mpd_jack_write_samples(struct jack_data *jd, const void *src, + unsigned num_samples) +{ + switch (jd->audio_format.format) { + case SAMPLE_FORMAT_S16: + mpd_jack_write_samples_16(jd, (const int16_t*)src, + num_samples); + break; + + case SAMPLE_FORMAT_S24_P32: + mpd_jack_write_samples_24(jd, (const int32_t*)src, + num_samples); + break; + + default: + assert(false); + } +} + +static size_t +mpd_jack_play(void *data, const void *chunk, size_t size, GError **error_r) +{ + struct jack_data *jd = data; + const size_t frame_size = audio_format_frame_size(&jd->audio_format); + size_t space = 0, space1; + + jd->pause = false; + + assert(size % frame_size == 0); + size /= frame_size; + + while (true) { + if (jd->shutdown) { + g_set_error(error_r, jack_output_quark(), 0, + "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 >= frame_size) + break; + + /* XXX do something more intelligent to + synchronize */ + g_usleep(1000); + } + + space /= sample_size; + if (space < size) + size = space; + + mpd_jack_write_samples(jd, chunk, size); + return size * frame_size; +} + +static bool +mpd_jack_pause(void *data) +{ + struct jack_data *jd = data; + + if (jd->shutdown) + return false; + + jd->pause = true; + + /* due to a MPD API limitation, we have to sleep a little bit + here, to avoid hogging the CPU */ + g_usleep(50000); + + return true; +} + +const struct audio_output_plugin jack_output_plugin = { + .name = "jack", + .test_default_device = mpd_jack_test_default_device, + .init = mpd_jack_init, + .finish = mpd_jack_finish, + .enable = mpd_jack_enable, + .disable = mpd_jack_disable, + .open = mpd_jack_open, + .play = mpd_jack_play, + .pause = mpd_jack_pause, + .close = mpd_jack_close, +}; diff --git a/src/output/jack_plugin.c b/src/output/jack_plugin.c deleted file mode 100644 index 5dc1eca24..000000000 --- a/src/output/jack_plugin.c +++ /dev/null @@ -1,450 +0,0 @@ -/* - * Copyright (C) 2003-2009 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 "../output_api.h" -#include "config.h" - -#include <assert.h> - -#include <glib.h> -#include <jack/jack.h> -#include <jack/types.h> -#include <jack/ringbuffer.h> - -#include <stdlib.h> -#include <stdio.h> -#include <sys/types.h> -#include <unistd.h> -#include <errno.h> - -#undef G_LOG_DOMAIN -#define G_LOG_DOMAIN "jack" - -static const size_t sample_size = sizeof(jack_default_audio_sample_t); - -static const char *const port_names[2] = { - "left", "right", -}; - -struct jack_data { - const char *name; - - /* configuration */ - char *output_ports[2]; - int ringbuffer_size; - - /* the current audio format */ - struct audio_format audio_format; - - /* jack library stuff */ - jack_port_t *ports[2]; - jack_client_t *client; - jack_ringbuffer_t *ringbuffer[2]; - - bool shutdown; -}; - -/** - * The quark used for GError.domain. - */ -static inline GQuark -jack_output_quark(void) -{ - return g_quark_from_static_string("jack_output"); -} - -static void -mpd_jack_client_free(struct jack_data *jd) -{ - assert(jd != NULL); - - if (jd->client != NULL) { - jack_deactivate(jd->client); - jack_client_close(jd->client); - jd->client = NULL; - } - - for (unsigned i = 0; i < G_N_ELEMENTS(jd->ringbuffer); ++i) { - if (jd->ringbuffer[i] != NULL) { - jack_ringbuffer_free(jd->ringbuffer[i]); - jd->ringbuffer[i] = NULL; - } - } -} - -static void -mpd_jack_free(struct jack_data *jd) -{ - assert(jd != NULL); - - for (unsigned i = 0; i < G_N_ELEMENTS(jd->output_ports); ++i) - g_free(jd->output_ports[i]); - - g_free(jd); -} - -static void -mpd_jack_finish(void *data) -{ - struct jack_data *jd = data; - mpd_jack_free(jd); -} - -static int -mpd_jack_process(jack_nframes_t nframes, void *arg) -{ - struct jack_data *jd = (struct jack_data *) arg; - jack_default_audio_sample_t *out; - size_t available; - - if (nframes <= 0) - return 0; - - for (unsigned i = 0; i < G_N_ELEMENTS(jd->ringbuffer); ++i) { - available = jack_ringbuffer_read_space(jd->ringbuffer[i]); - assert(available % sample_size == 0); - available /= sample_size; - if (available > nframes) - available = nframes; - - out = jack_port_get_buffer(jd->ports[i], nframes); - jack_ringbuffer_read(jd->ringbuffer[i], - (char *)out, available * sample_size); - - while (available < nframes) - /* ringbuffer underrun, fill with silence */ - out[available++] = 0.0; - } - - return 0; -} - -static void -mpd_jack_shutdown(void *arg) -{ - struct jack_data *jd = (struct jack_data *) arg; - jd->shutdown = true; -} - -static void -set_audioformat(struct jack_data *jd, struct audio_format *audio_format) -{ - audio_format->sample_rate = jack_get_sample_rate(jd->client); - audio_format->channels = 2; - - if (audio_format->bits != 16 && audio_format->bits != 24) - audio_format->bits = 24; -} - -static void -mpd_jack_error(const char *msg) -{ - g_warning("%s", msg); -} - -#ifdef HAVE_JACK_SET_INFO_FUNCTION -static void -mpd_jack_info(const char *msg) -{ - g_message("%s", msg); -} -#endif - -static void * -mpd_jack_init(G_GNUC_UNUSED const struct audio_format *audio_format, - const struct config_param *param, GError **error) -{ - struct jack_data *jd; - const char *value; - - jd = g_new(struct jack_data, 1); - jd->name = config_get_block_string(param, "name", "mpd_jack"); - - g_debug("mpd_jack_init (pid=%d)", getpid()); - - value = config_get_block_string(param, "ports", NULL); - if (value != NULL) { - char **ports = g_strsplit(value, ",", 0); - - if (ports[0] == NULL || ports[1] == NULL || ports[2] != NULL) { - g_set_error(error, jack_output_quark(), 0, - "two port names expected in line %d", - param->line); - return NULL; - } - - jd->output_ports[0] = ports[0]; - jd->output_ports[1] = ports[1]; - - g_free(ports); - } else { - jd->output_ports[0] = NULL; - jd->output_ports[1] = NULL; - } - - jd->ringbuffer_size = - config_get_block_unsigned(param, "ringbuffer_size", 32768); - - jack_set_error_function(mpd_jack_error); - -#ifdef HAVE_JACK_SET_INFO_FUNCTION - jack_set_info_function(mpd_jack_info); -#endif - - return jd; -} - -static bool -mpd_jack_test_default_device(void) -{ - return true; -} - -static bool -mpd_jack_connect(struct jack_data *jd, GError **error) -{ - const char *output_ports[2], **jports; - - for (unsigned i = 0; i < G_N_ELEMENTS(jd->ringbuffer); ++i) - jd->ringbuffer[i] = - jack_ringbuffer_create(jd->ringbuffer_size); - - jd->shutdown = false; - - if ((jd->client = jack_client_new(jd->name)) == NULL) { - g_set_error(error, jack_output_quark(), 0, - "Failed to connect to JACK server"); - 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 < G_N_ELEMENTS(jd->ports); ++i) { - jd->ports[i] = jack_port_register(jd->client, port_names[i], - JACK_DEFAULT_AUDIO_TYPE, - JackPortIsOutput, 0); - if (jd->ports[i] == NULL) { - g_set_error(error, jack_output_quark(), 0, - "Cannot register output port \"%s\"", - port_names[i]); - return false; - } - } - - if ( jack_activate(jd->client) ) { - g_set_error(error, jack_output_quark(), 0, - "cannot activate client"); - return false; - } - - if (jd->output_ports[1] == NULL) { - /* no output ports were configured - ask libjack for - defaults */ - jports = jack_get_ports(jd->client, NULL, NULL, - JackPortIsPhysical | JackPortIsInput); - if (jports == NULL) { - g_set_error(error, jack_output_quark(), 0, - "no ports found"); - return false; - } - - output_ports[0] = jports[0]; - output_ports[1] = jports[1] != NULL ? jports[1] : jports[0]; - - g_debug("output_ports: %s %s", jports[0], jports[1]); - } else { - /* use the configured output ports */ - - output_ports[0] = jd->output_ports[0]; - output_ports[1] = jd->output_ports[1]; - - jports = NULL; - } - - for (unsigned i = 0; i < G_N_ELEMENTS(jd->ports); ++i) { - int ret; - - ret = jack_connect(jd->client, jack_port_name(jd->ports[i]), - output_ports[i]); - if (ret != 0) { - g_set_error(error, jack_output_quark(), 0, - "Not a valid JACK port: %s", - output_ports[i]); - - if (jports != NULL) - free(jports); - - return false; - } - } - - if (jports != NULL) - free(jports); - - return true; -} - -static bool -mpd_jack_open(void *data, struct audio_format *audio_format, GError **error) -{ - struct jack_data *jd = data; - - assert(jd != NULL); - - if (!mpd_jack_connect(jd, error)) { - mpd_jack_client_free(jd); - return false; - } - - set_audioformat(jd, audio_format); - jd->audio_format = *audio_format; - - return true; -} - -static void -mpd_jack_close(G_GNUC_UNUSED void *data) -{ - struct jack_data *jd = data; - - mpd_jack_client_free(jd); -} - -static void -mpd_jack_cancel (G_GNUC_UNUSED void *data) -{ -} - -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(struct jack_data *jd, const int16_t *src, - unsigned num_samples) -{ - jack_default_audio_sample_t sample; - - while (num_samples-- > 0) { - sample = sample_16_to_jack(*src++); - jack_ringbuffer_write(jd->ringbuffer[0], (void*)&sample, - sizeof(sample)); - - sample = sample_16_to_jack(*src++); - jack_ringbuffer_write(jd->ringbuffer[1], (void*)&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(struct jack_data *jd, const int32_t *src, - unsigned num_samples) -{ - jack_default_audio_sample_t sample; - - while (num_samples-- > 0) { - sample = sample_24_to_jack(*src++); - jack_ringbuffer_write(jd->ringbuffer[0], (void*)&sample, - sizeof(sample)); - - sample = sample_24_to_jack(*src++); - jack_ringbuffer_write(jd->ringbuffer[1], (void*)&sample, - sizeof(sample)); - } -} - -static void -mpd_jack_write_samples(struct jack_data *jd, const void *src, - unsigned num_samples) -{ - switch (jd->audio_format.bits) { - case 16: - mpd_jack_write_samples_16(jd, (const int16_t*)src, - num_samples); - break; - - case 24: - mpd_jack_write_samples_24(jd, (const int32_t*)src, - num_samples); - break; - - default: - assert(false); - } -} - -static size_t -mpd_jack_play(void *data, const void *chunk, size_t size, GError **error) -{ - struct jack_data *jd = data; - const size_t frame_size = audio_format_frame_size(&jd->audio_format); - size_t space = 0, space1; - - assert(size % frame_size == 0); - size /= frame_size; - - while (true) { - if (jd->shutdown) { - g_set_error(error, jack_output_quark(), 0, - "Refusing to play, because " - "there is no client thread"); - return 0; - } - - space = jack_ringbuffer_write_space(jd->ringbuffer[0]); - space1 = jack_ringbuffer_write_space(jd->ringbuffer[1]); - if (space > space1) - /* send data symmetrically */ - space = space1; - - if (space >= frame_size) - break; - - /* XXX do something more intelligent to - synchronize */ - g_usleep(1000); - } - - space /= sample_size; - if (space < size) - size = space; - - mpd_jack_write_samples(jd, chunk, size); - return size * frame_size; -} - -const struct audio_output_plugin jackPlugin = { - .name = "jack", - .test_default_device = mpd_jack_test_default_device, - .init = mpd_jack_init, - .finish = mpd_jack_finish, - .open = mpd_jack_open, - .play = mpd_jack_play, - .cancel = mpd_jack_cancel, - .close = mpd_jack_close, -}; diff --git a/src/output/mvp_plugin.c b/src/output/mvp_plugin.c index 96f9435a8..20587f5c5 100644 --- a/src/output/mvp_plugin.c +++ b/src/output/mvp_plugin.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2009 The Music Player Daemon Project + * Copyright (C) 2003-2010 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify @@ -22,7 +22,9 @@ * http://mvpmc.sourceforge.net/ */ -#include "../output_api.h" +#include "config.h" +#include "output_api.h" +#include "fd_util.h" #include <glib.h> @@ -115,7 +117,7 @@ mvp_output_test_default_device(void) { int fd; - fd = open("/dev/adec_pcm", O_WRONLY); + fd = open_cloexec("/dev/adec_pcm", O_WRONLY, 0); if (fd >= 0) { close(fd); @@ -170,19 +172,19 @@ mvp_set_pcm_params(struct mvp_data *md, struct audio_format *audio_format, } /* 0,1=24bit(24) , 2,3=16bit */ - switch (audio_format->bits) { - case 16: + switch (audio_format->format) { + case SAMPLE_FORMAT_S16: mix[1] = 2; break; - case 24: + case SAMPLE_FORMAT_S24_P32: mix[1] = 0; break; default: - g_debug("unsupported sample format %u - falling back to stereo", - audio_format->bits); - audio_format->bits = 16; + g_debug("unsupported sample format %s - falling back to 16 bit", + sample_format_to_string(audio_format->format)); + audio_format->format = SAMPLE_FORMAT_S16; mix[1] = 2; break; } @@ -230,7 +232,8 @@ mvp_output_open(void *data, struct audio_format *audio_format, GError **error) int mix[5] = { 0, 2, 7, 1, 0 }; bool success; - if ((md->fd = open("/dev/adec_pcm", O_RDWR | O_NONBLOCK)) < 0) { + md->fd = open_cloexec("/dev/adec_pcm", O_RDWR | O_NONBLOCK, 0); + if (md->fd < 0) { g_set_error(error, mvp_output_quark(), errno, "Error opening /dev/adec_pcm: %s", strerror(errno)); diff --git a/src/output/null_plugin.c b/src/output/null_plugin.c index e9731b019..89abbd91f 100644 --- a/src/output/null_plugin.c +++ b/src/output/null_plugin.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2009 The Music Player Daemon Project + * Copyright (C) 2003-2010 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify @@ -17,8 +17,9 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "../output_api.h" -#include "../timer.h" +#include "config.h" +#include "output_api.h" +#include "timer.h" #include <glib.h> diff --git a/src/output/openal_plugin.c b/src/output/openal_plugin.c new file mode 100644 index 000000000..767b3eb17 --- /dev/null +++ b/src/output/openal_plugin.c @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2003-2010 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 "output_api.h" +#include "timer.h" + +#include <glib.h> + +#ifndef HAVE_OSX +#include <AL/al.h> +#include <AL/alc.h> +#else +#include <OpenAL/al.h> +#include <OpenAL/alc.h> +#endif + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "openal" + +/* should be enough for buffer size = 2048 */ +#define NUM_BUFFERS 16 + +struct openal_data { + const char *device_name; + ALCdevice *device; + ALCcontext *context; + Timer *timer; + ALuint buffers[NUM_BUFFERS]; + int filled; + ALuint source; + ALenum format; + ALuint frequency; +}; + +static inline GQuark +openal_output_quark(void) +{ + return g_quark_from_static_string("openal_output"); +} + +static ALenum +openal_audio_format(struct audio_format *audio_format) +{ + switch (audio_format->format) { + case SAMPLE_FORMAT_S16: + if (audio_format->channels == 2) + return AL_FORMAT_STEREO16; + if (audio_format->channels == 1) + return AL_FORMAT_MONO16; + break; + + case SAMPLE_FORMAT_S8: + if (audio_format->channels == 2) + return AL_FORMAT_STEREO8; + if (audio_format->channels == 1) + return AL_FORMAT_MONO8; + break; + + default: + /* fall back to 16 bit */ + audio_format->format = SAMPLE_FORMAT_S16; + if (audio_format->channels == 2) + return AL_FORMAT_STEREO16; + if (audio_format->channels == 1) + return AL_FORMAT_MONO16; + break; + } + + return 0; +} + +static bool +openal_setup_context(struct openal_data *od, + GError **error) +{ + od->device = alcOpenDevice(od->device_name); + + if (od->device == NULL) { + g_set_error(error, openal_output_quark(), 0, + "Error opening OpenAL device \"%s\"\n", + od->device_name); + return false; + } + + od->context = alcCreateContext(od->device, NULL); + + if (od->context == NULL) { + g_set_error(error, openal_output_quark(), 0, + "Error creating context for \"%s\"\n", + od->device_name); + alcCloseDevice(od->device); + return false; + } + + return true; +} + +static void +openal_unqueue_buffers(struct openal_data *od) +{ + ALint num; + ALuint buffer; + + alGetSourcei(od->source, AL_BUFFERS_QUEUED, &num); + + while (num--) { + alSourceUnqueueBuffers(od->source, 1, &buffer); + } +} + +static void * +openal_init(G_GNUC_UNUSED const struct audio_format *audio_format, + const struct config_param *param, + G_GNUC_UNUSED GError **error) +{ + const char *device_name = config_get_block_string(param, "device", NULL); + struct openal_data *od; + + if (device_name == NULL) { + device_name = alcGetString(NULL, ALC_DEFAULT_DEVICE_SPECIFIER); + } + + od = g_new(struct openal_data, 1); + od->device_name = device_name; + + return od; +} + +static void +openal_finish(void *data) +{ + struct openal_data *od = data; + + g_free(od); +} + +static bool +openal_open(void *data, struct audio_format *audio_format, + GError **error) +{ + struct openal_data *od = data; + + od->format = openal_audio_format(audio_format); + + if (!od->format) { + struct audio_format_string s; + g_set_error(error, openal_output_quark(), 0, + "Unsupported audio format: %s", + audio_format_to_string(audio_format, &s)); + return false; + } + + if (!openal_setup_context(od, error)) { + return false; + } + + alcMakeContextCurrent(od->context); + alGenBuffers(NUM_BUFFERS, od->buffers); + + if (alGetError() != AL_NO_ERROR) { + g_set_error(error, openal_output_quark(), 0, + "Failed to generate buffers"); + return false; + } + + alGenSources(1, &od->source); + + if (alGetError() != AL_NO_ERROR) { + g_set_error(error, openal_output_quark(), 0, + "Failed to generate source"); + alDeleteBuffers(NUM_BUFFERS, od->buffers); + return false; + } + + od->filled = 0; + od->timer = timer_new(audio_format); + od->frequency = audio_format->sample_rate; + + return true; +} + +static void +openal_close(void *data) +{ + struct openal_data *od = data; + + timer_free(od->timer); + alcMakeContextCurrent(od->context); + alDeleteSources(1, &od->source); + alDeleteBuffers(NUM_BUFFERS, od->buffers); + alcDestroyContext(od->context); + alcCloseDevice(od->device); +} + +static size_t +openal_play(void *data, const void *chunk, size_t size, + G_GNUC_UNUSED GError **error) +{ + struct openal_data *od = data; + ALuint buffer; + ALint num, state; + + if (alcGetCurrentContext() != od->context) { + alcMakeContextCurrent(od->context); + } + + alGetSourcei(od->source, AL_BUFFERS_PROCESSED, &num); + + if (od->filled < NUM_BUFFERS) { + /* fill all buffers */ + buffer = od->buffers[od->filled]; + od->filled++; + } else { + /* wait for processed buffer */ + while (num < 1) { + if (!od->timer->started) { + timer_start(od->timer); + } else { + timer_sync(od->timer); + } + + timer_add(od->timer, size); + + alGetSourcei(od->source, AL_BUFFERS_PROCESSED, &num); + } + + alSourceUnqueueBuffers(od->source, 1, &buffer); + } + + alBufferData(buffer, od->format, chunk, size, od->frequency); + alSourceQueueBuffers(od->source, 1, &buffer); + alGetSourcei(od->source, AL_SOURCE_STATE, &state); + + if (state != AL_PLAYING) { + alSourcePlay(od->source); + } + + return size; +} + +static void +openal_cancel(void *data) +{ + struct openal_data *od = data; + + od->filled = 0; + alcMakeContextCurrent(od->context); + alSourceStop(od->source); + openal_unqueue_buffers(od); +} + +const struct audio_output_plugin openal_output_plugin = { + .name = "openal", + .init = openal_init, + .finish = openal_finish, + .open = openal_open, + .close = openal_close, + .play = openal_play, + .cancel = openal_cancel, +}; diff --git a/src/output/oss_plugin.c b/src/output/oss_plugin.c index a66bc0598..bb4164990 100644 --- a/src/output/oss_plugin.c +++ b/src/output/oss_plugin.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2009 The Music Player Daemon Project + * Copyright (C) 2003-2010 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify @@ -17,8 +17,10 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "../output_api.h" +#include "config.h" +#include "output_api.h" #include "mixer_list.h" +#include "fd_util.h" #include <glib.h> @@ -28,6 +30,7 @@ #include <errno.h> #include <stdlib.h> #include <unistd.h> +#include <assert.h> #undef G_LOG_DOMAIN #define G_LOG_DOMAIN "oss" @@ -38,33 +41,15 @@ # include <sys/soundcard.h> #endif /* !(defined(__OpenBSD__) || defined(__NetBSD__) */ -#if G_BYTE_ORDER == G_BIG_ENDIAN -# define AFMT_S16_MPD AFMT_S16_BE -#else -# define AFMT_S16_MPD AFMT_S16_LE -#endif - struct oss_data { int fd; const char *device; - struct audio_format audio_format; - int bitFormat; - int *supported[3]; - unsigned num_supported[3]; - int *unsupported[3]; - unsigned num_unsupported[3]; -}; - -enum oss_support { - OSS_SUPPORTED = 1, - OSS_UNSUPPORTED = 0, - OSS_UNKNOWN = -1, -}; -enum oss_param { - OSS_RATE = 0, - OSS_CHANNELS = 1, - OSS_BITS = 2, + /** + * The current input audio format. This is needed to reopen + * the device after cancel(). + */ + struct audio_format audio_format; }; /** @@ -76,188 +61,6 @@ oss_output_quark(void) return g_quark_from_static_string("oss_output"); } -static enum oss_param -oss_param_from_ioctl(unsigned param) -{ - enum oss_param idx = OSS_RATE; - - switch (param) { - case SNDCTL_DSP_SPEED: - idx = OSS_RATE; - break; - case SNDCTL_DSP_CHANNELS: - idx = OSS_CHANNELS; - break; - case SNDCTL_DSP_SAMPLESIZE: - idx = OSS_BITS; - break; - } - - return idx; -} - -static bool -oss_find_supported_param(struct oss_data *od, unsigned param, int val) -{ - enum oss_param idx = oss_param_from_ioctl(param); - - for (unsigned i = 0; i < od->num_supported[idx]; i++) - if (od->supported[idx][i] == val) - return true; - - return false; -} - -static bool -oss_can_convert(int idx, int val) -{ - switch (idx) { - case OSS_BITS: - if (val != 16) - return false; - break; - case OSS_CHANNELS: - if (val != 2) - return false; - break; - } - - return true; -} - -static int -oss_get_supported_param(struct oss_data *od, unsigned param, int val) -{ - enum oss_param idx = oss_param_from_ioctl(param); - int ret = -1; - int least = val; - int diff; - - for (unsigned i = 0; i < od->num_supported[idx]; i++) { - diff = od->supported[idx][i] - val; - if (diff < 0) - diff = -diff; - if (diff < least) { - if (!oss_can_convert(idx, od->supported[idx][i])) - continue; - - least = diff; - ret = od->supported[idx][i]; - } - } - - return ret; -} - -static bool -oss_find_unsupported_param(struct oss_data *od, unsigned param, int val) -{ - enum oss_param idx = oss_param_from_ioctl(param); - - for (unsigned i = 0; i < od->num_unsupported[idx]; i++) { - if (od->unsupported[idx][i] == val) - return true; - } - - return false; -} - -static void -oss_add_supported_param(struct oss_data *od, unsigned param, int val) -{ - enum oss_param idx = oss_param_from_ioctl(param); - - od->num_supported[idx]++; - od->supported[idx] = g_realloc(od->supported[idx], - od->num_supported[idx] * sizeof(int)); - od->supported[idx][od->num_supported[idx] - 1] = val; -} - -static void -oss_add_unsupported_param(struct oss_data *od, unsigned param, int val) -{ - enum oss_param idx = oss_param_from_ioctl(param); - - od->num_unsupported[idx]++; - od->unsupported[idx] = g_realloc(od->unsupported[idx], - od->num_unsupported[idx] * - sizeof(int)); - od->unsupported[idx][od->num_unsupported[idx] - 1] = val; -} - -static void -oss_remove_supported_param(struct oss_data *od, unsigned param, int val) -{ - unsigned j = 0; - enum oss_param idx = oss_param_from_ioctl(param); - - for (unsigned i = 0; i < od->num_supported[idx] - 1; i++) { - if (od->supported[idx][i] == val) - j = 1; - od->supported[idx][i] = od->supported[idx][i + j]; - } - - od->num_supported[idx]--; - od->supported[idx] = g_realloc(od->supported[idx], - od->num_supported[idx] * sizeof(int)); -} - -static void -oss_remove_unsupported_param(struct oss_data *od, unsigned param, int val) -{ - unsigned j = 0; - enum oss_param idx = oss_param_from_ioctl(param); - - for (unsigned i = 0; i < od->num_unsupported[idx] - 1; i++) { - if (od->unsupported[idx][i] == val) - j = 1; - od->unsupported[idx][i] = od->unsupported[idx][i + j]; - } - - od->num_unsupported[idx]--; - od->unsupported[idx] = g_realloc(od->unsupported[idx], - od->num_unsupported[idx] * - sizeof(int)); -} - -static enum oss_support -oss_param_is_supported(struct oss_data *od, unsigned param, int val) -{ - if (oss_find_supported_param(od, param, val)) - return OSS_SUPPORTED; - if (oss_find_unsupported_param(od, param, val)) - return OSS_UNSUPPORTED; - return OSS_UNKNOWN; -} - -static void -oss_set_supported(struct oss_data *od, unsigned param, int val) -{ - enum oss_support supported = oss_param_is_supported(od, param, val); - - if (supported == OSS_SUPPORTED) - return; - - if (supported == OSS_UNSUPPORTED) - oss_remove_unsupported_param(od, param, val); - - oss_add_supported_param(od, param, val); -} - -static void -oss_set_unsupported(struct oss_data *od, unsigned param, int val) -{ - enum oss_support supported = oss_param_is_supported(od, param, val); - - if (supported == OSS_UNSUPPORTED) - return; - - if (supported == OSS_SUPPORTED) - oss_remove_supported_param(od, param, val); - - oss_add_unsupported_param(od, param, val); -} - static struct oss_data * oss_data_new(void) { @@ -266,38 +69,12 @@ oss_data_new(void) ret->device = NULL; ret->fd = -1; - ret->supported[OSS_RATE] = NULL; - ret->supported[OSS_CHANNELS] = NULL; - ret->supported[OSS_BITS] = NULL; - ret->unsupported[OSS_RATE] = NULL; - ret->unsupported[OSS_CHANNELS] = NULL; - ret->unsupported[OSS_BITS] = NULL; - - ret->num_supported[OSS_RATE] = 0; - ret->num_supported[OSS_CHANNELS] = 0; - ret->num_supported[OSS_BITS] = 0; - ret->num_unsupported[OSS_RATE] = 0; - ret->num_unsupported[OSS_CHANNELS] = 0; - ret->num_unsupported[OSS_BITS] = 0; - - oss_set_supported(ret, SNDCTL_DSP_SPEED, 48000); - oss_set_supported(ret, SNDCTL_DSP_SPEED, 44100); - oss_set_supported(ret, SNDCTL_DSP_CHANNELS, 2); - oss_set_supported(ret, SNDCTL_DSP_SAMPLESIZE, 16); - return ret; } static void oss_data_free(struct oss_data *od) { - g_free(od->supported[OSS_RATE]); - g_free(od->supported[OSS_CHANNELS]); - g_free(od->supported[OSS_BITS]); - g_free(od->unsupported[OSS_RATE]); - g_free(od->unsupported[OSS_CHANNELS]); - g_free(od->unsupported[OSS_BITS]); - g_free(od); } @@ -343,7 +120,9 @@ oss_output_test_default_device(void) int fd, i; for (i = G_N_ELEMENTS(default_devices); --i >= 0; ) { - if ((fd = open(default_devices[i], O_WRONLY)) >= 0) { + fd = open_cloexec(default_devices[i], O_WRONLY, 0); + + if (fd >= 0) { close(fd); return true; } @@ -419,113 +198,386 @@ oss_output_finish(void *data) oss_data_free(od); } -static int -oss_set_param(struct oss_data *od, unsigned param, int *value) +static void +oss_close(struct oss_data *od) { - int val = *value; - int copy; - enum oss_support supported = oss_param_is_supported(od, param, val); - - do { - if (supported == OSS_UNSUPPORTED) { - val = oss_get_supported_param(od, param, val); - if (copy < 0) - return -1; - } - copy = val; - if (ioctl(od->fd, param, ©)) { - oss_set_unsupported(od, param, val); - supported = OSS_UNSUPPORTED; - } else { - if (supported == OSS_UNKNOWN) { - oss_set_supported(od, param, val); - supported = OSS_SUPPORTED; - } - val = copy; - } - } while (supported == OSS_UNSUPPORTED); + 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 a GError. + */ +static enum oss_setup_result +oss_try_ioctl_r(int fd, unsigned long request, int *value_r, + const char *msg, GError **error_r) +{ + assert(fd >= 0); + assert(value_r != NULL); + assert(msg != NULL); + assert(error_r == NULL || *error_r == NULL); - *value = val; + int ret = ioctl(fd, request, value_r); + if (ret >= 0) + return SUCCESS; - return 0; + if (errno == EINVAL) + return UNSUPPORTED; + + g_set_error(error_r, oss_output_quark(), errno, + "%s: %s", msg, g_strerror(errno)); + return ERROR; } -static void -oss_close(struct oss_data *od) +/** + * 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 a GError. + */ +static enum oss_setup_result +oss_try_ioctl(int fd, unsigned long request, int value, + const char *msg, GError **error_r) { - if (od->fd >= 0) - while (close(od->fd) && errno == EINTR) ; - od->fd = -1; + return oss_try_ioctl_r(fd, request, &value, msg, error_r); } /** - * Sets up the OSS device which was opened before. + * Set up the channel number, and attempts to find alternatives if the + * specified number is not supported. */ static bool -oss_setup(struct oss_data *od, GError **error) +oss_setup_channels(int fd, struct audio_format *audio_format, GError **error_r) { - int tmp; - - tmp = od->audio_format.channels; - if (oss_set_param(od, SNDCTL_DSP_CHANNELS, &tmp)) { - g_set_error(error, oss_output_quark(), errno, - "OSS device \"%s\" does not support %u channels: %s", - od->device, od->audio_format.channels, - strerror(errno)); + 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_r); + switch (result) { + case SUCCESS: + if (!audio_valid_channel_count(channels)) + break; + + audio_format->channels = channels; + return true; + + case ERROR: return false; + + case UNSUPPORTED: + break; } - od->audio_format.channels = tmp; - tmp = od->audio_format.sample_rate; - if (oss_set_param(od, SNDCTL_DSP_SPEED, &tmp)) { - g_set_error(error, oss_output_quark(), errno, - "OSS device \"%s\" does not support %u Hz audio: %s", - od->device, od->audio_format.sample_rate, - strerror(errno)); - return false; + 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_r); + switch (result) { + case SUCCESS: + if (!audio_valid_channel_count(channels)) + break; + + audio_format->channels = channels; + return true; + + case ERROR: + return false; + + case UNSUPPORTED: + break; + } } - od->audio_format.sample_rate = tmp; - switch (od->audio_format.bits) { - case 8: - tmp = AFMT_S8; - break; - case 16: - tmp = AFMT_S16_MPD; + g_set_error(error_r, oss_output_quark(), EINVAL, "%s", 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, struct audio_format *audio_format, + GError **error_r) +{ + 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_r); + 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_r); + 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; + } + } + + g_set_error(error_r, oss_output_quark(), EINVAL, "%s", 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(enum sample_format format) +{ + switch (format) { + case SAMPLE_FORMAT_UNDEFINED: + return AFMT_QUERY; + + case SAMPLE_FORMAT_S8: + return AFMT_S8; + + case SAMPLE_FORMAT_S16: + return AFMT_S16_NE; + + case SAMPLE_FORMAT_S24: +#ifdef AFMT_S24_PACKED + return AFMT_S24_PACKED; +#else + return AFMT_QUERY; +#endif + + case SAMPLE_FORMAT_S24_P32: +#ifdef AFMT_S24_NE + return AFMT_S24_NE; +#else + return AFMT_QUERY; +#endif + + case SAMPLE_FORMAT_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 + * SAMPLE_FORMAT_UNDEFINED if there is no direct counterpart. + */ +static enum sample_format +sample_format_from_oss(int format) +{ + switch (format) { + case AFMT_S8: + return SAMPLE_FORMAT_S8; + + case AFMT_S16_NE: + return SAMPLE_FORMAT_S16; + +#ifdef AFMT_S24_PACKED + case AFMT_S24_PACKED: + return SAMPLE_FORMAT_S24; +#endif + +#ifdef AFMT_S24_NE + case AFMT_S24_NE: + return SAMPLE_FORMAT_S24_P32; +#endif + +#ifdef AFMT_S32_NE + case AFMT_S32_NE: + return SAMPLE_FORMAT_S32; +#endif default: - /* not supported by OSS - fall back to 16 bit */ - od->audio_format.bits = 16; - tmp = AFMT_S16_MPD; - break; + return SAMPLE_FORMAT_UNDEFINED; } +} - if (oss_set_param(od, SNDCTL_DSP_SAMPLESIZE, &tmp)) { - g_set_error(error, oss_output_quark(), errno, - "OSS device \"%s\" does not support %u bit audio: %s", - od->device, tmp, strerror(errno)); +/** + * 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, struct audio_format *audio_format, + GError **error_r) +{ + const char *const msg = "Failed to set sample format"; + int oss_format = sample_format_to_oss(audio_format->format); + enum oss_setup_result result = oss_format != AFMT_QUERY + ? oss_try_ioctl_r(fd, SNDCTL_DSP_SAMPLESIZE, + &oss_format, msg, error_r) + : UNSUPPORTED; + enum sample_format mpd_format; + switch (result) { + case SUCCESS: + mpd_format = sample_format_from_oss(oss_format); + if (mpd_format == SAMPLE_FORMAT_UNDEFINED) + break; + + audio_format->format = mpd_format; + return true; + + case ERROR: return false; + + case UNSUPPORTED: + break; } - return true; + /* the requested sample format is not available - probe for + other formats supported by MPD */ + + static const enum sample_format sample_formats[] = { + SAMPLE_FORMAT_S24_P32, + SAMPLE_FORMAT_S32, + SAMPLE_FORMAT_S24, + SAMPLE_FORMAT_S16, + SAMPLE_FORMAT_S8, + SAMPLE_FORMAT_UNDEFINED /* sentinel */ + }; + + for (unsigned i = 0; sample_formats[i] != SAMPLE_FORMAT_UNDEFINED; ++i) { + mpd_format = sample_formats[i]; + if (mpd_format == audio_format->format) + /* don't try that again */ + continue; + + oss_format = sample_format_to_oss(mpd_format); + if (oss_format == AFMT_QUERY) + /* not supported by this OSS version */ + continue; + + result = oss_try_ioctl_r(fd, SNDCTL_DSP_SAMPLESIZE, + &oss_format, msg, error_r); + switch (result) { + case SUCCESS: + mpd_format = sample_format_from_oss(oss_format); + if (mpd_format == SAMPLE_FORMAT_UNDEFINED) + break; + + audio_format->format = mpd_format; + return true; + + case ERROR: + return false; + + case UNSUPPORTED: + break; + } + } + + g_set_error(error_r, oss_output_quark(), EINVAL, "%s", msg); + return false; } +/** + * Sets up the OSS device which was opened before. + */ static bool -oss_open(struct oss_data *od, GError **error) +oss_setup(struct oss_data *od, struct audio_format *audio_format, + GError **error_r) { - bool success; + return oss_setup_channels(od->fd, audio_format, error_r) && + oss_setup_sample_rate(od->fd, audio_format, error_r) && + oss_setup_sample_format(od->fd, audio_format, error_r); +} - if ((od->fd = open(od->device, O_WRONLY)) < 0) { - g_set_error(error, oss_output_quark(), errno, +/** + * Reopen the device with the saved audio_format, without any probing. + */ +static bool +oss_reopen(struct oss_data *od, GError **error_r) +{ + assert(od->fd < 0); + + od->fd = open_cloexec(od->device, O_WRONLY, 0); + if (od->fd < 0) { + g_set_error(error_r, oss_output_quark(), errno, "Error opening OSS device \"%s\": %s", od->device, strerror(errno)); return false; } - success = oss_setup(od, error); - if (!success) { + 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_r); + if (result != SUCCESS) { + oss_close(od); + if (result == UNSUPPORTED) + g_set_error(error_r, oss_output_quark(), EINVAL, + "%s", 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_r); + if (result != SUCCESS) { + oss_close(od); + if (result == UNSUPPORTED) + g_set_error(error_r, oss_output_quark(), EINVAL, + "%s", msg2); + return false; + } + + const char *const msg3 = "Failed to set sample format"; + assert(sample_format_to_oss(od->audio_format.format) != AFMT_QUERY); + result = oss_try_ioctl(od->fd, SNDCTL_DSP_SAMPLESIZE, + sample_format_to_oss(od->audio_format.format), + msg3, error_r); + if (result != SUCCESS) { oss_close(od); + if (result == UNSUPPORTED) + g_set_error(error_r, oss_output_quark(), EINVAL, + "%s", msg3); return false; } @@ -535,18 +587,23 @@ oss_open(struct oss_data *od, GError **error) static bool oss_output_open(void *data, struct audio_format *audio_format, GError **error) { - bool ret; struct oss_data *od = data; - od->audio_format = *audio_format; - - ret = oss_open(od, error); - if (!ret) + od->fd = open_cloexec(od->device, O_WRONLY, 0); + if (od->fd < 0) { + g_set_error(error, oss_output_quark(), errno, + "Error opening OSS device \"%s\": %s", + od->device, strerror(errno)); return false; + } - *audio_format = od->audio_format; + if (!oss_setup(od, audio_format, error)) { + oss_close(od); + return false; + } - return ret; + od->audio_format = *audio_format; + return true; } static void @@ -575,7 +632,7 @@ oss_output_play(void *data, const void *chunk, size_t size, GError **error) ssize_t ret; /* reopen the device since it was closed by dropBufferedAudio */ - if (od->fd < 0 && !oss_open(od, error)) + if (od->fd < 0 && !oss_reopen(od, error)) return 0; while (true) { @@ -601,5 +658,6 @@ const struct audio_output_plugin oss_output_plugin = { .close = oss_output_close, .play = oss_output_play, .cancel = oss_output_cancel, - .mixer_plugin = &oss_mixer, + + .mixer_plugin = &oss_mixer_plugin, }; diff --git a/src/output/osx_plugin.c b/src/output/osx_plugin.c index 04173bf79..17d138d35 100644 --- a/src/output/osx_plugin.c +++ b/src/output/osx_plugin.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2009 The Music Player Daemon Project + * Copyright (C) 2003-2010 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify @@ -17,7 +17,8 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "../output_api.h" +#include "config.h" +#include "output_api.h" #include <glib.h> #include <AudioUnit/AudioUnit.h> @@ -165,9 +166,6 @@ osx_output_open(void *data, struct audio_format *audio_format, GError **error) OSStatus status; ComponentResult result; - if (audio_format->bits > 16) - audio_format->bits = 16; - desc.componentType = kAudioUnitType_Output; desc.componentSubType = kAudioUnitSubType_DefaultOutput; desc.componentManufacturer = kAudioUnitManufacturer_Apple; @@ -225,7 +223,21 @@ osx_output_open(void *data, struct audio_format *audio_format, GError **error) stream_description.mFramesPerPacket = 1; stream_description.mBytesPerFrame = stream_description.mBytesPerPacket; stream_description.mChannelsPerFrame = audio_format->channels; - stream_description.mBitsPerChannel = audio_format->bits; + + switch (audio_format->format) { + case SAMPLE_FORMAT_S8: + stream_description.mBitsPerChannel = 8; + break; + + case SAMPLE_FORMAT_S16: + stream_description.mBitsPerChannel = 16; + break; + + default: + audio_format->format = SAMPLE_FORMAT_S16; + stream_description.mBitsPerChannel = 16; + break; + } result = AudioUnitSetProperty(od->au, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, diff --git a/src/output/pipe_output_plugin.c b/src/output/pipe_output_plugin.c index 610ad9e8d..1d1aec7b1 100644 --- a/src/output/pipe_output_plugin.c +++ b/src/output/pipe_output_plugin.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2009 The Music Player Daemon Project + * Copyright (C) 2003-2010 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify @@ -17,6 +17,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +#include "config.h" #include "output_api.h" #include <stdio.h> diff --git a/src/output/pulse_output_plugin.c b/src/output/pulse_output_plugin.c new file mode 100644 index 000000000..d29fbd705 --- /dev/null +++ b/src/output/pulse_output_plugin.c @@ -0,0 +1,825 @@ +/* + * Copyright (C) 2003-2010 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 "pulse_output_plugin.h" +#include "output_api.h" +#include "mixer_list.h" +#include "mixer/pulse_mixer_plugin.h" + +#include <glib.h> + +#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 <assert.h> + +#define MPD_PULSE_NAME "Music Player Daemon" + +/** + * The quark used for GError.domain. + */ +static inline GQuark +pulse_output_quark(void) +{ + return g_quark_from_static_string("pulse_output"); +} + +void +pulse_output_set_mixer(struct pulse_output *po, struct pulse_mixer *pm) +{ + assert(po != NULL); + assert(po->mixer == NULL); + assert(pm != NULL); + + po->mixer = pm; + + if (po->mainloop == NULL) + return; + + pa_threaded_mainloop_lock(po->mainloop); + + if (po->context != NULL && + pa_context_get_state(po->context) == PA_CONTEXT_READY) { + pulse_mixer_on_connect(pm, po->context); + + if (po->stream != NULL && + 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(struct pulse_output *po, struct pulse_mixer *pm) +{ + assert(po != NULL); + assert(pm != NULL); + assert(po->mixer == pm); + + po->mixer = NULL; +} + +bool +pulse_output_set_volume(struct pulse_output *po, + const struct pa_cvolume *volume, GError **error_r) +{ + pa_operation *o; + + if (po->context == NULL || po->stream == NULL || + pa_stream_get_state(po->stream) != PA_STREAM_READY) { + g_set_error(error_r, pulse_output_quark(), 0, "disconnected"); + return false; + } + + o = pa_context_set_sink_input_volume(po->context, + pa_stream_get_index(po->stream), + volume, NULL, NULL); + if (o == NULL) { + g_set_error(error_r, pulse_output_quark(), 0, + "failed to set PulseAudio volume: %s", + pa_strerror(pa_context_errno(po->context))); + 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) +{ + pa_operation_state_t state; + + assert(mainloop != NULL); + assert(operation != NULL); + + state = pa_operation_get_state(operation); + while (state == PA_OPERATION_RUNNING) { + pa_threaded_mainloop_wait(mainloop); + state = pa_operation_get_state(operation); + } + + 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(G_GNUC_UNUSED pa_stream *s, + G_GNUC_UNUSED int success, void *userdata) +{ + struct pulse_output *po = userdata; + + pa_threaded_mainloop_signal(po->mainloop, 0); +} + +static void +pulse_output_context_state_cb(struct pa_context *context, void *userdata) +{ + struct pulse_output *po = userdata; + + switch (pa_context_get_state(context)) { + case PA_CONTEXT_READY: + if (po->mixer != NULL) + 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 != NULL) + 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) +{ + struct pulse_output *po = userdata; + pa_subscription_event_type_t facility + = t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK; + pa_subscription_event_type_t type + = t & PA_SUBSCRIPTION_EVENT_TYPE_MASK; + + if (po->mixer != NULL && + facility == PA_SUBSCRIPTION_EVENT_SINK_INPUT && + po->stream != NULL && + 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(struct pulse_output *po, GError **error_r) +{ + int error; + + error = pa_context_connect(po->context, po->server, + (pa_context_flags_t)0, NULL); + if (error < 0) { + g_set_error(error_r, pulse_output_quark(), 0, + "pa_context_connect() has failed: %s", + pa_strerror(pa_context_errno(po->context))); + return false; + } + + return true; +} + +/** + * Create, set up and connect a context. + * + * @return true on success, false on error + */ +static bool +pulse_output_setup_context(struct pulse_output *po, GError **error_r) +{ + po->context = pa_context_new(pa_threaded_mainloop_get_api(po->mainloop), + MPD_PULSE_NAME); + if (po->context == NULL) { + g_set_error(error_r, pulse_output_quark(), 0, + "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_r)) { + pa_context_unref(po->context); + po->context = NULL; + return false; + } + + return true; +} + +/** + * Frees and clears the context. + */ +static void +pulse_output_delete_context(struct pulse_output *po) +{ + pa_context_disconnect(po->context); + pa_context_unref(po->context); + po->context = NULL; +} + +static void * +pulse_output_init(G_GNUC_UNUSED const struct audio_format *audio_format, + const struct config_param *param, + G_GNUC_UNUSED GError **error_r) +{ + struct pulse_output *po; + + g_setenv("PULSE_PROP_media.role", "music", true); + + po = g_new(struct pulse_output, 1); + po->name = config_get_block_string(param, "name", "mpd_pulse"); + po->server = config_get_block_string(param, "server", NULL); + po->sink = config_get_block_string(param, "sink", NULL); + + po->mixer = NULL; + po->mainloop = NULL; + po->context = NULL; + po->stream = NULL; + + return po; +} + +static void +pulse_output_finish(void *data) +{ + struct pulse_output *po = data; + + g_free(po); +} + +static bool +pulse_output_enable(void *data, GError **error_r) +{ + struct pulse_output *po = data; + + assert(po->mainloop == NULL); + assert(po->context == NULL); + + /* create the libpulse mainloop and start the thread */ + + po->mainloop = pa_threaded_mainloop_new(); + if (po->mainloop == NULL) { + g_free(po); + + g_set_error(error_r, pulse_output_quark(), 0, + "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 = NULL; + + g_set_error(error_r, pulse_output_quark(), 0, + "pa_threaded_mainloop_start() has failed"); + return false; + } + + pa_threaded_mainloop_unlock(po->mainloop); + + /* create the libpulse context and connect it */ + + pa_threaded_mainloop_lock(po->mainloop); + + if (!pulse_output_setup_context(po, error_r)) { + pa_threaded_mainloop_unlock(po->mainloop); + pa_threaded_mainloop_stop(po->mainloop); + pa_threaded_mainloop_free(po->mainloop); + po->mainloop = NULL; + return false; + } + + pa_threaded_mainloop_unlock(po->mainloop); + + return true; +} + +static void +pulse_output_disable(void *data) +{ + struct pulse_output *po = data; + + pa_threaded_mainloop_stop(po->mainloop); + if (po->context != NULL) + pulse_output_delete_context(po); + pa_threaded_mainloop_free(po->mainloop); + po->mainloop = NULL; +} + +/** + * Check if the context is (already) connected, and waits if not. If + * the context has been disconnected, retry to connect. + * + * @return true on success, false on error + */ +static bool +pulse_output_wait_connection(struct pulse_output *po, GError **error_r) +{ + pa_context_state_t state; + + pa_threaded_mainloop_lock(po->mainloop); + + if (po->context == NULL && !pulse_output_setup_context(po, error_r)) + return false; + + while (true) { + state = pa_context_get_state(po->context); + switch (state) { + case PA_CONTEXT_READY: + /* nothing to do */ + pa_threaded_mainloop_unlock(po->mainloop); + return true; + + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_TERMINATED: + case PA_CONTEXT_FAILED: + /* failure */ + g_set_error(error_r, pulse_output_quark(), 0, + "failed to connect: %s", + pa_strerror(pa_context_errno(po->context))); + pulse_output_delete_context(po); + pa_threaded_mainloop_unlock(po->mainloop); + 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_state_cb(pa_stream *stream, void *userdata) +{ + struct pulse_output *po = userdata; + + switch (pa_stream_get_state(stream)) { + case PA_STREAM_READY: + if (po->mixer != NULL) + 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 != NULL) + 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(G_GNUC_UNUSED pa_stream *stream, size_t nbytes, + void *userdata) +{ + struct pulse_output *po = userdata; + + po->writable = nbytes; + pa_threaded_mainloop_signal(po->mainloop, 0); +} + +static bool +pulse_output_open(void *data, struct audio_format *audio_format, + GError **error_r) +{ + struct pulse_output *po = data; + pa_sample_spec ss; + int error; + + if (po->context != NULL) { + 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_r)) + 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 = SAMPLE_FORMAT_S16; + + ss.format = PA_SAMPLE_S16NE; + ss.rate = audio_format->sample_rate; + ss.channels = audio_format->channels; + + pa_threaded_mainloop_lock(po->mainloop); + + /* create a stream .. */ + + po->stream = pa_stream_new(po->context, po->name, &ss, NULL); + if (po->stream == NULL) { + g_set_error(error_r, pulse_output_quark(), 0, + "pa_stream_new() has failed: %s", + pa_strerror(pa_context_errno(po->context))); + pa_threaded_mainloop_unlock(po->mainloop); + return false; + } + + 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); + + /* .. and connect it (asynchronously) */ + + error = pa_stream_connect_playback(po->stream, po->sink, + NULL, 0, NULL, NULL); + if (error < 0) { + pa_stream_unref(po->stream); + po->stream = NULL; + + g_set_error(error_r, pulse_output_quark(), 0, + "pa_stream_connect_playback() has failed: %s", + pa_strerror(pa_context_errno(po->context))); + pa_threaded_mainloop_unlock(po->mainloop); + return false; + } + + pa_threaded_mainloop_unlock(po->mainloop); + +#if !PA_CHECK_VERSION(0,9,11) + po->pause = false; +#endif + + return true; +} + +static void +pulse_output_close(void *data) +{ + struct pulse_output *po = data; + pa_operation *o; + + 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 == NULL) { + g_warning("pa_stream_drain() has failed: %s", + pa_strerror(pa_context_errno(po->context))); + } else + pulse_wait_for_operation(po->mainloop, o); + } + + pa_stream_disconnect(po->stream); + pa_stream_unref(po->stream); + po->stream = NULL; + + if (po->context != NULL && + 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 for a signal + * if not. The mainloop must be locked before calling this function. + * + * @return the current stream state + */ +static pa_stream_state_t +pulse_output_check_stream(struct pulse_output *po) +{ + pa_stream_state_t state = pa_stream_get_state(po->stream); + + switch (state) { + case PA_STREAM_READY: + case PA_STREAM_FAILED: + case PA_STREAM_TERMINATED: + case PA_STREAM_UNCONNECTED: + break; + + case PA_STREAM_CREATING: + pa_threaded_mainloop_wait(po->mainloop); + state = pa_stream_get_state(po->stream); + break; + } + + return state; +} + +/** + * 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(struct pulse_output *po, GError **error_r) +{ + pa_stream_state_t state = pa_stream_get_state(po->stream); + + switch (state) { + case PA_STREAM_READY: + return true; + + case PA_STREAM_FAILED: + case PA_STREAM_TERMINATED: + case PA_STREAM_UNCONNECTED: + g_set_error(error_r, pulse_output_quark(), 0, + "disconnected"); + return false; + + case PA_STREAM_CREATING: + break; + } + + do { + state = pulse_output_check_stream(po); + } while (state == PA_STREAM_CREATING); + + if (state != PA_STREAM_READY) { + g_set_error(error_r, pulse_output_quark(), 0, + "failed to connect the stream: %s", + pa_strerror(pa_context_errno(po->context))); + return false; + } + + return true; +} + +/** + * Determines whether the stream is paused. On libpulse older than + * 0.9.11, it uses a custom pause flag. + */ +static bool +pulse_output_stream_is_paused(struct pulse_output *po) +{ + assert(po->stream != NULL); + +#if !defined(PA_CHECK_VERSION) || !PA_CHECK_VERSION(0,9,11) + return po->pause; +#else + return pa_stream_is_corked(po->stream); +#endif +} + +/** + * Sets cork mode on the stream. + */ +static bool +pulse_output_stream_pause(struct pulse_output *po, bool pause, + GError **error_r) +{ + pa_operation *o; + + assert(po->stream != NULL); + + o = pa_stream_cork(po->stream, pause, + pulse_output_stream_success_cb, po); + if (o == NULL) { + g_set_error(error_r, pulse_output_quark(), 0, + "pa_stream_cork() has failed: %s", + pa_strerror(pa_context_errno(po->context))); + return false; + } + + if (!pulse_wait_for_operation(po->mainloop, o)) { + g_set_error(error_r, pulse_output_quark(), 0, + "pa_stream_cork() has failed: %s", + pa_strerror(pa_context_errno(po->context))); + return false; + } + +#if !PA_CHECK_VERSION(0,9,11) + po->pause = pause; +#endif + return true; +} + +static size_t +pulse_output_play(void *data, const void *chunk, size_t size, GError **error_r) +{ + struct pulse_output *po = data; + int error; + + assert(po->stream != NULL); + + pa_threaded_mainloop_lock(po->mainloop); + + /* check if the stream is (already) connected */ + + if (!pulse_output_wait_stream(po, error_r)) { + pa_threaded_mainloop_unlock(po->mainloop); + return 0; + } + + assert(po->context != NULL); + + /* unpause if previously paused */ + + if (pulse_output_stream_is_paused(po) && + !pulse_output_stream_pause(po, false, error_r)) + return 0; + + /* wait until the server allows us to write */ + + while (po->writable == 0) { + pa_threaded_mainloop_wait(po->mainloop); + + if (pa_stream_get_state(po->stream) != PA_STREAM_READY) { + pa_threaded_mainloop_unlock(po->mainloop); + g_set_error(error_r, pulse_output_quark(), 0, + "disconnected"); + return false; + } + } + + /* now write */ + + if (size > po->writable) + /* don't send more than possible */ + size = po->writable; + + po->writable -= size; + + error = pa_stream_write(po->stream, chunk, size, NULL, + 0, PA_SEEK_RELATIVE); + pa_threaded_mainloop_unlock(po->mainloop); + if (error < 0) { + g_set_error(error_r, pulse_output_quark(), error, + "%s", pa_strerror(error)); + return 0; + } + + return size; +} + +static void +pulse_output_cancel(void *data) +{ + struct pulse_output *po = data; + pa_operation *o; + + assert(po->stream != NULL); + + 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 != NULL); + + o = pa_stream_flush(po->stream, pulse_output_stream_success_cb, po); + if (o == NULL) { + g_warning("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(void *data) +{ + struct pulse_output *po = data; + GError *error = NULL; + + assert(po->stream != NULL); + + pa_threaded_mainloop_lock(po->mainloop); + + /* check if the stream is (already/still) connected */ + + if (!pulse_output_wait_stream(po, &error)) { + pa_threaded_mainloop_unlock(po->mainloop); + g_warning("%s", error->message); + g_error_free(error); + return false; + } + + assert(po->context != NULL); + + /* cork the stream */ + + if (pulse_output_stream_is_paused(po)) { + /* already paused; due to a MPD API limitation, we + have to sleep a little bit here, to avoid hogging + the CPU */ + + g_usleep(50000); + } else if (!pulse_output_stream_pause(po, true, &error)) { + pa_threaded_mainloop_unlock(po->mainloop); + g_warning("%s", error->message); + g_error_free(error); + return false; + } + + pa_threaded_mainloop_unlock(po->mainloop); + + return true; +} + +static bool +pulse_output_test_default_device(void) +{ + struct pulse_output *po; + bool success; + + po = pulse_output_init(NULL, NULL, NULL); + if (po == NULL) + return false; + + success = pulse_output_wait_connection(po, NULL); + pulse_output_finish(po); + + return success; +} + +const struct audio_output_plugin pulse_output_plugin = { + .name = "pulse", + + .test_default_device = pulse_output_test_default_device, + .init = pulse_output_init, + .finish = pulse_output_finish, + .enable = pulse_output_enable, + .disable = pulse_output_disable, + .open = pulse_output_open, + .play = pulse_output_play, + .cancel = pulse_output_cancel, + .pause = pulse_output_pause, + .close = pulse_output_close, + + .mixer_plugin = &pulse_mixer_plugin, +}; diff --git a/src/output/pulse_output_plugin.h b/src/output/pulse_output_plugin.h new file mode 100644 index 000000000..06e3aec43 --- /dev/null +++ b/src/output/pulse_output_plugin.h @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2003-2010 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_H +#define MPD_PULSE_OUTPUT_PLUGIN_H + +#include <stdbool.h> +#include <stddef.h> + +#include <glib.h> + +#include <pulse/version.h> + +#if !defined(PA_CHECK_VERSION) +/** + * This macro was implemented in libpulse 0.9.16. + */ +#define PA_CHECK_VERSION(a,b,c) false +#endif + +struct pa_operation; +struct pa_cvolume; + +struct pulse_output { + const char *name; + const char *server; + const char *sink; + + struct pulse_mixer *mixer; + + struct pa_threaded_mainloop *mainloop; + struct pa_context *context; + struct pa_stream *stream; + + size_t writable; + +#if !PA_CHECK_VERSION(0,9,11) + /** + * We need this variable because pa_stream_is_corked() wasn't + * added before 0.9.11. + */ + bool pause; +#endif +}; + +void +pulse_output_set_mixer(struct pulse_output *po, struct pulse_mixer *pm); + +void +pulse_output_clear_mixer(struct pulse_output *po, struct pulse_mixer *pm); + +bool +pulse_output_set_volume(struct pulse_output *po, + const struct pa_cvolume *volume, GError **error_r); + +#endif diff --git a/src/output/pulse_plugin.c b/src/output/pulse_plugin.c deleted file mode 100644 index ffc7abc8b..000000000 --- a/src/output/pulse_plugin.c +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright (C) 2003-2009 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 "../output_api.h" -#include "mixer_list.h" - -#include <glib.h> -#include <pulse/simple.h> -#include <pulse/error.h> - -#define MPD_PULSE_NAME "mpd" - -struct pulse_data { - const char *name; - const char *server; - const char *sink; - - pa_simple *s; -}; - -/** - * The quark used for GError.domain. - */ -static inline GQuark -pulse_output_quark(void) -{ - return g_quark_from_static_string("pulse_output"); -} - -static struct pulse_data *pulse_new_data(void) -{ - struct pulse_data *ret; - - ret = g_new(struct pulse_data, 1); - - ret->server = NULL; - ret->sink = NULL; - - return ret; -} - -static void pulse_free_data(struct pulse_data *pd) -{ - g_free(pd); -} - -static void * -pulse_init(G_GNUC_UNUSED const struct audio_format *audio_format, - const struct config_param *param, G_GNUC_UNUSED GError **error) -{ - struct pulse_data *pd; - - pd = pulse_new_data(); - pd->name = config_get_block_string(param, "name", "mpd_pulse"); - pd->server = config_get_block_string(param, "server", NULL); - pd->sink = config_get_block_string(param, "sink", NULL); - - return pd; -} - -static void pulse_finish(void *data) -{ - struct pulse_data *pd = data; - - pulse_free_data(pd); -} - -static bool pulse_test_default_device(void) -{ - pa_simple *s; - pa_sample_spec ss; - int error; - - ss.format = PA_SAMPLE_S16NE; - ss.rate = 44100; - ss.channels = 2; - - s = pa_simple_new(NULL, MPD_PULSE_NAME, PA_STREAM_PLAYBACK, NULL, - MPD_PULSE_NAME, &ss, NULL, NULL, &error); - if (!s) { - g_message("Cannot connect to default PulseAudio server: %s\n", - pa_strerror(error)); - return false; - } - - pa_simple_free(s); - - return true; -} - -static bool -pulse_open(void *data, struct audio_format *audio_format, GError **error_r) -{ - struct pulse_data *pd = data; - pa_sample_spec ss; - int error; - - /* MPD doesn't support the other pulseaudio sample formats, so - we just force MPD to send us everything as 16 bit */ - audio_format->bits = 16; - - ss.format = PA_SAMPLE_S16NE; - ss.rate = audio_format->sample_rate; - ss.channels = audio_format->channels; - - pd->s = pa_simple_new(pd->server, MPD_PULSE_NAME, PA_STREAM_PLAYBACK, - pd->sink, pd->name, - &ss, NULL, NULL, - &error); - if (!pd->s) { - g_set_error(error_r, pulse_output_quark(), error, - "Cannot connect to PulseAudio server: %s", - pa_strerror(error)); - return false; - } - - return true; -} - -static void pulse_cancel(void *data) -{ - struct pulse_data *pd = data; - int error; - - if (pa_simple_flush(pd->s, &error) < 0) - g_warning("Flush failed in PulseAudio output \"%s\": %s\n", - pd->name, pa_strerror(error)); -} - -static void pulse_close(void *data) -{ - struct pulse_data *pd = data; - - pa_simple_drain(pd->s, NULL); - pa_simple_free(pd->s); -} - -static size_t -pulse_play(void *data, const void *chunk, size_t size, GError **error_r) -{ - struct pulse_data *pd = data; - int error; - - if (pa_simple_write(pd->s, chunk, size, &error) < 0) { - g_set_error(error_r, pulse_output_quark(), error, - "%s", pa_strerror(error)); - return 0; - } - - return size; -} - -const struct audio_output_plugin pulse_plugin = { - .name = "pulse", - .test_default_device = pulse_test_default_device, - .init = pulse_init, - .finish = pulse_finish, - .open = pulse_open, - .play = pulse_play, - .cancel = pulse_cancel, - .close = pulse_close, - .mixer_plugin = &pulse_mixer, -}; diff --git a/src/output/recorder_output_plugin.c b/src/output/recorder_output_plugin.c new file mode 100644 index 000000000..c01d927c4 --- /dev/null +++ b/src/output/recorder_output_plugin.c @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2003-2010 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 "output_api.h" +#include "encoder_plugin.h" +#include "encoder_list.h" +#include "fd_util.h" +#include "open.h" + +#include <assert.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <errno.h> + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "recorder" + +struct recorder_output { + /** + * The configured encoder plugin. + */ + struct encoder *encoder; + + /** + * The destination file name. + */ + const char *path; + + /** + * The destination file descriptor. + */ + int fd; + + /** + * The buffer for encoder_read(). + */ + char buffer[32768]; +}; + +/** + * The quark used for GError.domain. + */ +static inline GQuark +recorder_output_quark(void) +{ + return g_quark_from_static_string("recorder_output"); +} + +static void * +recorder_output_init(G_GNUC_UNUSED const struct audio_format *audio_format, + const struct config_param *param, GError **error_r) +{ + struct recorder_output *recorder = g_new(struct recorder_output, 1); + const char *encoder_name; + const struct encoder_plugin *encoder_plugin; + + /* read configuration */ + + encoder_name = config_get_block_string(param, "encoder", "vorbis"); + encoder_plugin = encoder_plugin_get(encoder_name); + if (encoder_plugin == NULL) { + g_set_error(error_r, recorder_output_quark(), 0, + "No such encoder: %s", encoder_name); + return NULL; + } + + recorder->path = config_get_block_string(param, "path", NULL); + if (recorder->path == NULL) { + g_set_error(error_r, recorder_output_quark(), 0, + "'path' not configured"); + return NULL; + } + + /* initialize encoder */ + + recorder->encoder = encoder_init(encoder_plugin, param, error_r); + if (recorder->encoder == NULL) + return NULL; + + return recorder; +} + +static void +recorder_output_finish(void *data) +{ + struct recorder_output *recorder = data; + + encoder_finish(recorder->encoder); + g_free(recorder); +} + +/** + * Writes pending data from the encoder to the output file. + */ +static bool +recorder_output_encoder_to_file(struct recorder_output *recorder, + GError **error_r) +{ + size_t size = 0, position, nbytes; + + assert(recorder->fd >= 0); + + /* read from the encoder */ + + size = encoder_read(recorder->encoder, recorder->buffer, + sizeof(recorder->buffer)); + if (size == 0) + return true; + + /* write everything into the file */ + + position = 0; + while (true) { + nbytes = write(recorder->fd, recorder->buffer + position, + size - position); + if (nbytes > 0) { + position += (size_t)nbytes; + if (position >= size) + return true; + } else if (nbytes == 0) { + /* shouldn't happen for files */ + g_set_error(error_r, recorder_output_quark(), 0, + "write() returned 0"); + return false; + } else if (errno != EINTR) { + g_set_error(error_r, recorder_output_quark(), 0, + "Failed to write to '%s': %s", + recorder->path, g_strerror(errno)); + return false; + } + } +} + +static bool +recorder_output_open(void *data, struct audio_format *audio_format, + GError **error_r) +{ + struct recorder_output *recorder = data; + bool success; + + /* create the output file */ + + recorder->fd = open_cloexec(recorder->path, + O_CREAT|O_WRONLY|O_TRUNC|O_BINARY, + 0666); + if (recorder->fd < 0) { + g_set_error(error_r, recorder_output_quark(), 0, + "Failed to create '%s': %s", + recorder->path, g_strerror(errno)); + return false; + } + + /* open the encoder */ + + success = encoder_open(recorder->encoder, audio_format, error_r); + if (!success) { + close(recorder->fd); + unlink(recorder->path); + return false; + } + + return true; +} + +static void +recorder_output_close(void *data) +{ + struct recorder_output *recorder = data; + + /* flush the encoder and write the rest to the file */ + + if (encoder_flush(recorder->encoder, NULL)) + recorder_output_encoder_to_file(recorder, NULL); + + /* now really close everything */ + + encoder_close(recorder->encoder); + + close(recorder->fd); +} + +static size_t +recorder_output_play(void *data, const void *chunk, size_t size, + GError **error_r) +{ + struct recorder_output *recorder = data; + + return encoder_write(recorder->encoder, chunk, size, error_r) && + recorder_output_encoder_to_file(recorder, error_r) + ? size : 0; +} + +const struct audio_output_plugin recorder_output_plugin = { + .name = "recorder", + .init = recorder_output_init, + .finish = recorder_output_finish, + .open = recorder_output_open, + .close = recorder_output_close, + .play = recorder_output_play, +}; diff --git a/src/output/shout_plugin.c b/src/output/shout_plugin.c index dbc56f337..baaeccf92 100644 --- a/src/output/shout_plugin.c +++ b/src/output/shout_plugin.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2009 The Music Player Daemon Project + * Copyright (C) 2003-2010 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify @@ -17,9 +17,11 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +#include "config.h" #include "output_api.h" #include "encoder_plugin.h" #include "encoder_list.h" +#include "mpd_error.h" #include <shout/shout.h> #include <glib.h> @@ -100,8 +102,8 @@ static void free_shout_data(struct shout_data *sd) #define check_block_param(name) { \ block_param = config_get_block_param(param, name); \ if (!block_param) { \ - g_error("no \"%s\" defined for shout device defined at line " \ - "%i\n", name, param->line); \ + MPD_ERROR("no \"%s\" defined for shout device defined at line " \ + "%i\n", name, param->line); \ } \ } @@ -126,6 +128,13 @@ my_shout_init_driver(const struct audio_format *audio_format, struct block_param *block_param; int public; + if (audio_format == NULL || + !audio_format_fully_defined(audio_format)) { + g_set_error(error, shout_output_quark(), 0, + "Need full audio format specification"); + return NULL; + } + sd = new_shout_data(); if (shout_init_count == 0) @@ -191,8 +200,6 @@ my_shout_init_driver(const struct audio_format *audio_format, } } - check_block_param("format"); - encoding = config_get_block_string(param, "encoding", "ogg"); encoder_plugin = shout_encoder_plugin_get(encoding); if (encoder_plugin == NULL) { @@ -335,7 +342,6 @@ write_page(struct shout_data *sd, GError **error) if (sd->buf.len == 0) return true; - shout_sync(sd->shout_conn); err = shout_send(sd->shout_conn, sd->buf.data, sd->buf.len); if (!handle_shout_error(sd, err, error)) return false; @@ -434,6 +440,18 @@ my_shout_open_device(void *data, struct audio_format *audio_format, return true; } +static unsigned +my_shout_delay(void *data) +{ + struct shout_data *sd = (struct shout_data *)data; + + int delay = shout_delay(sd->shout_conn); + if (delay < 0) + delay = 0; + + return delay; +} + static size_t my_shout_play(void *data, const void *chunk, size_t size, GError **error) { @@ -448,15 +466,8 @@ my_shout_play(void *data, const void *chunk, size_t size, GError **error) static bool my_shout_pause(void *data) { - struct shout_data *sd = (struct shout_data *)data; static const char silence[1020]; - if (shout_delay(sd->shout_conn) > 500) { - /* cap the latency for unpause */ - g_usleep(500000); - return true; - } - return my_shout_play(data, silence, sizeof(silence), NULL); } @@ -471,10 +482,10 @@ shout_tag_to_metadata(const struct tag *tag, char *dest, size_t size) for (unsigned i = 0; i < tag->num_items; i++) { switch (tag->items[i]->type) { - case TAG_ITEM_ARTIST: + case TAG_ARTIST: strncpy(artist, tag->items[i]->value, size); break; - case TAG_ITEM_TITLE: + case TAG_TITLE: strncpy(title, tag->items[i]->value, size); break; @@ -533,6 +544,7 @@ const struct audio_output_plugin shoutPlugin = { .init = my_shout_init_driver, .finish = my_shout_finish_driver, .open = my_shout_open_device, + .delay = my_shout_delay, .play = my_shout_play, .pause = my_shout_pause, .cancel = my_shout_drop_buffered_audio, diff --git a/src/output/solaris_output_plugin.c b/src/output/solaris_output_plugin.c index 5febf0afc..deb3298a5 100644 --- a/src/output/solaris_output_plugin.c +++ b/src/output/solaris_output_plugin.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2009 The Music Player Daemon Project + * Copyright (C) 2003-2010 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify @@ -17,7 +17,9 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +#include "config.h" #include "output_api.h" +#include "fd_util.h" #include <glib.h> @@ -87,11 +89,11 @@ solaris_output_open(void *data, struct audio_format *audio_format, /* support only 16 bit mono/stereo for now; nothing else has been tested */ - audio_format->bits = 16; + audio_format->format = SAMPLE_FORMAT_S16; /* open the device in non-blocking mode */ - so->fd = open(so->device, O_WRONLY|O_NONBLOCK); + so->fd = open_cloexec(so->device, O_WRONLY|O_NONBLOCK); if (so->fd < 0) { g_set_error(error, solaris_output_quark(), errno, "Failed to open %s: %s", @@ -117,7 +119,7 @@ solaris_output_open(void *data, struct audio_format *audio_format, info.play.sample_rate = audio_format->sample_rate; info.play.channels = audio_format->channels; - info.play.precision = audio_format->bits; + info.play.precision = 16; info.play.encoding = AUDIO_ENCODING_LINEAR; ret = ioctl(so->fd, AUDIO_SETINFO, &info); diff --git a/src/output/winmm_output_plugin.c b/src/output/winmm_output_plugin.c new file mode 100644 index 000000000..b9687874d --- /dev/null +++ b/src/output/winmm_output_plugin.c @@ -0,0 +1,337 @@ +/* + * Copyright (C) 2003-2010 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 "output_api.h" +#include "pcm_buffer.h" +#include "mixer_list.h" +#include "winmm_output_plugin.h" + +#include <stdlib.h> +#include <string.h> +#include <windows.h> + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "winmm_output" + +struct winmm_buffer { + struct pcm_buffer buffer; + + WAVEHDR hdr; +}; + +struct winmm_output { + UINT device_id; + HWAVEOUT handle; + + /** + * This event is triggered by Windows when a buffer is + * finished. + */ + HANDLE event; + + struct winmm_buffer buffers[8]; + unsigned next_buffer; +}; + +/** + * The quark used for GError.domain. + */ +static inline GQuark +winmm_output_quark(void) +{ + return g_quark_from_static_string("winmm_output"); +} + +HWAVEOUT +winmm_output_get_handle(struct winmm_output* output) +{ + return output->handle; +} + +static bool +winmm_output_test_default_device(void) +{ + return waveOutGetNumDevs() > 0; +} + +static UINT +get_device_id(const char *device_name) +{ + /* if device is not specified use wave mapper */ + if (device_name == NULL) + return WAVE_MAPPER; + + /* check for device id */ + char *endptr; + UINT id = strtoul(device_name, &endptr, 0); + if (endptr > device_name && *endptr == 0) + return id; + + /* check for device name */ + for (UINT i = 0; i < waveOutGetNumDevs(); 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) + return i; + } + + /* fallback to wave mapper */ + return WAVE_MAPPER; +} + +static void * +winmm_output_init(G_GNUC_UNUSED const struct audio_format *audio_format, + G_GNUC_UNUSED const struct config_param *param, + G_GNUC_UNUSED GError **error) +{ + struct winmm_output *wo = g_new(struct winmm_output, 1); + const char *device = config_get_block_string(param, "device", NULL); + wo->device_id = get_device_id(device); + return wo; +} + +static void +winmm_output_finish(void *data) +{ + struct winmm_output *wo = data; + + g_free(wo); +} + +static bool +winmm_output_open(void *data, struct audio_format *audio_format, + GError **error_r) +{ + struct winmm_output *wo = data; + + wo->event = CreateEvent(NULL, false, false, NULL); + if (wo->event == NULL) { + g_set_error(error_r, winmm_output_quark(), 0, + "CreateEvent() failed"); + return false; + } + + switch (audio_format->format) { + case SAMPLE_FORMAT_S8: + case SAMPLE_FORMAT_S16: + break; + + case SAMPLE_FORMAT_S24: + case SAMPLE_FORMAT_S24_P32: + case SAMPLE_FORMAT_S32: + case SAMPLE_FORMAT_UNDEFINED: + /* we havn't tested formats other than S16 */ + audio_format->format = SAMPLE_FORMAT_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_frame_size(audio_format); + format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign; + format.wBitsPerSample = audio_format_sample_size(audio_format) * 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); + g_set_error(error_r, winmm_output_quark(), result, + "waveOutOpen() failed"); + return false; + } + + for (unsigned i = 0; i < G_N_ELEMENTS(wo->buffers); ++i) { + pcm_buffer_init(&wo->buffers[i].buffer); + memset(&wo->buffers[i].hdr, 0, sizeof(wo->buffers[i].hdr)); + } + + wo->next_buffer = 0; + + return true; +} + +static void +winmm_output_close(void *data) +{ + struct winmm_output *wo = data; + + for (unsigned i = 0; i < G_N_ELEMENTS(wo->buffers); ++i) + pcm_buffer_deinit(&wo->buffers[i].buffer); + + waveOutClose(wo->handle); + + CloseHandle(wo->event); +} + +/** + * Copy data into a buffer, and prepare the wave header. + */ +static bool +winmm_set_buffer(struct winmm_output *wo, struct winmm_buffer *buffer, + const void *data, size_t size, + GError **error_r) +{ + void *dest = pcm_buffer_get(&buffer->buffer, size); + if (dest == NULL) { + g_set_error(error_r, winmm_output_quark(), 0, + "Out of memory"); + return false; + } + + memcpy(dest, data, size); + + memset(&buffer->hdr, 0, sizeof(buffer->hdr)); + buffer->hdr.lpData = dest; + buffer->hdr.dwBufferLength = size; + + MMRESULT result = waveOutPrepareHeader(wo->handle, &buffer->hdr, + sizeof(buffer->hdr)); + if (result != MMSYSERR_NOERROR) { + g_set_error(error_r, winmm_output_quark(), result, + "waveOutPrepareHeader() failed"); + return false; + } + + return true; +} + +/** + * Wait until the buffer is finished. + */ +static bool +winmm_drain_buffer(struct winmm_output *wo, struct winmm_buffer *buffer, + GError **error_r) +{ + 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) { + g_set_error(error_r, winmm_output_quark(), result, + "waveOutUnprepareHeader() failed"); + return false; + } + + /* wait some more */ + WaitForSingleObject(wo->event, INFINITE); + } +} + +static size_t +winmm_output_play(void *data, const void *chunk, size_t size, GError **error_r) +{ + struct winmm_output *wo = data; + + /* get the next buffer from the ring and prepare it */ + struct winmm_buffer *buffer = &wo->buffers[wo->next_buffer]; + if (!winmm_drain_buffer(wo, buffer, error_r) || + !winmm_set_buffer(wo, buffer, chunk, size, error_r)) + 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)); + g_set_error(error_r, winmm_output_quark(), result, + "waveOutWrite() failed"); + return 0; + } + + /* mark our buffer as "used" */ + wo->next_buffer = (wo->next_buffer + 1) % + G_N_ELEMENTS(wo->buffers); + + return size; +} + +static bool +winmm_drain_all_buffers(struct winmm_output *wo, GError **error_r) +{ + for (unsigned i = wo->next_buffer; i < G_N_ELEMENTS(wo->buffers); ++i) + if (!winmm_drain_buffer(wo, &wo->buffers[i], error_r)) + return false; + + for (unsigned i = 0; i < wo->next_buffer; ++i) + if (!winmm_drain_buffer(wo, &wo->buffers[i], error_r)) + return false; + + return true; +} + +static void +winmm_stop(struct winmm_output *wo) +{ + waveOutReset(wo->handle); + + for (unsigned i = 0; i < G_N_ELEMENTS(wo->buffers); ++i) { + struct winmm_buffer *buffer = &wo->buffers[i]; + waveOutUnprepareHeader(wo->handle, &buffer->hdr, + sizeof(buffer->hdr)); + } +} + +static void +winmm_output_drain(void *data) +{ + struct winmm_output *wo = data; + + if (!winmm_drain_all_buffers(wo, NULL)) + winmm_stop(wo); +} + +static void +winmm_output_cancel(void *data) +{ + struct winmm_output *wo = data; + + winmm_stop(wo); +} + +const struct audio_output_plugin winmm_output_plugin = { + .name = "winmm", + .test_default_device = winmm_output_test_default_device, + .init = winmm_output_init, + .finish = winmm_output_finish, + .open = winmm_output_open, + .close = winmm_output_close, + .play = winmm_output_play, + .drain = winmm_output_drain, + .cancel = winmm_output_cancel, + .mixer_plugin = &winmm_mixer_plugin, +}; diff --git a/src/output/winmm_output_plugin.h b/src/output/winmm_output_plugin.h new file mode 100644 index 000000000..39507275a --- /dev/null +++ b/src/output/winmm_output_plugin.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2003-2010 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_H +#define MPD_WINMM_OUTPUT_PLUGIN_H + +#include <windows.h> + +struct winmm_output; + +HWAVEOUT winmm_output_get_handle(struct winmm_output*); + +#endif |