aboutsummaryrefslogtreecommitdiffstats
path: root/src/output/OutputThread.cxx
diff options
context:
space:
mode:
authorMax Kellermann <max@duempel.org>2014-01-23 23:49:50 +0100
committerMax Kellermann <max@duempel.org>2014-01-23 23:49:50 +0100
commitea5b901bcce20949a8d1fd622a7b03ff6f56ae20 (patch)
treeab37a61260fdaafb94b5e54aca54928e00c9cf01 /src/output/OutputThread.cxx
parentf1f19841bdd291c055f59b6603f69278c66366d8 (diff)
downloadmpd-ea5b901bcce20949a8d1fd622a7b03ff6f56ae20.tar.gz
mpd-ea5b901bcce20949a8d1fd622a7b03ff6f56ae20.tar.xz
mpd-ea5b901bcce20949a8d1fd622a7b03ff6f56ae20.zip
output/*: move to output/plugins/
Diffstat (limited to 'src/output/OutputThread.cxx')
-rw-r--r--src/output/OutputThread.cxx690
1 files changed, 690 insertions, 0 deletions
diff --git a/src/output/OutputThread.cxx b/src/output/OutputThread.cxx
new file mode 100644
index 000000000..f7e28b5f4
--- /dev/null
+++ b/src/output/OutputThread.cxx
@@ -0,0 +1,690 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "OutputThread.hxx"
+#include "OutputInternal.hxx"
+#include "OutputAPI.hxx"
+#include "OutputError.hxx"
+#include "pcm/PcmMix.hxx"
+#include "notify.hxx"
+#include "FilterInternal.hxx"
+#include "filter/ConvertFilterPlugin.hxx"
+#include "filter/ReplayGainFilterPlugin.hxx"
+#include "PlayerControl.hxx"
+#include "MusicPipe.hxx"
+#include "MusicChunk.hxx"
+#include "thread/Util.hxx"
+#include "thread/Name.hxx"
+#include "system/FatalError.hxx"
+#include "util/Error.hxx"
+#include "Log.hxx"
+#include "Compiler.h"
+
+#include <assert.h>
+#include <string.h>
+
+static void ao_command_finished(struct audio_output *ao)
+{
+ assert(ao->command != AO_COMMAND_NONE);
+ ao->command = AO_COMMAND_NONE;
+
+ ao->mutex.unlock();
+ audio_output_client_notify.Signal();
+ ao->mutex.lock();
+}
+
+static bool
+ao_enable(struct audio_output *ao)
+{
+ Error error;
+ bool success;
+
+ if (ao->really_enabled)
+ return true;
+
+ ao->mutex.unlock();
+ success = ao_plugin_enable(ao, error);
+ ao->mutex.lock();
+ if (!success) {
+ FormatError(error,
+ "Failed to enable \"%s\" [%s]",
+ ao->name, ao->plugin->name);
+ return false;
+ }
+
+ ao->really_enabled = true;
+ return true;
+}
+
+static void
+ao_close(struct audio_output *ao, bool drain);
+
+static void
+ao_disable(struct audio_output *ao)
+{
+ if (ao->open)
+ ao_close(ao, false);
+
+ if (ao->really_enabled) {
+ ao->really_enabled = false;
+
+ ao->mutex.unlock();
+ ao_plugin_disable(ao);
+ ao->mutex.lock();
+ }
+}
+
+static AudioFormat
+ao_filter_open(struct audio_output *ao, AudioFormat &format,
+ Error &error_r)
+{
+ assert(format.IsValid());
+
+ /* the replay_gain filter cannot fail here */
+ if (ao->replay_gain_filter != nullptr &&
+ !ao->replay_gain_filter->Open(format, error_r).IsDefined())
+ return AudioFormat::Undefined();
+
+ if (ao->other_replay_gain_filter != nullptr &&
+ !ao->other_replay_gain_filter->Open(format, error_r).IsDefined()) {
+ if (ao->replay_gain_filter != nullptr)
+ ao->replay_gain_filter->Close();
+ return AudioFormat::Undefined();
+ }
+
+ const AudioFormat af = ao->filter->Open(format, error_r);
+ if (!af.IsDefined()) {
+ if (ao->replay_gain_filter != nullptr)
+ ao->replay_gain_filter->Close();
+ if (ao->other_replay_gain_filter != nullptr)
+ ao->other_replay_gain_filter->Close();
+ }
+
+ return af;
+}
+
+static void
+ao_filter_close(struct audio_output *ao)
+{
+ if (ao->replay_gain_filter != nullptr)
+ ao->replay_gain_filter->Close();
+ if (ao->other_replay_gain_filter != nullptr)
+ ao->other_replay_gain_filter->Close();
+
+ ao->filter->Close();
+}
+
+static void
+ao_open(struct audio_output *ao)
+{
+ bool success;
+ Error error;
+ struct audio_format_string af_string;
+
+ assert(!ao->open);
+ assert(ao->pipe != nullptr);
+ assert(ao->chunk == nullptr);
+ assert(ao->in_audio_format.IsValid());
+
+ ao->fail_timer.Reset();
+
+ /* enable the device (just in case the last enable has failed) */
+
+ if (!ao_enable(ao))
+ /* still no luck */
+ return;
+
+ /* open the filter */
+
+ const AudioFormat filter_audio_format =
+ ao_filter_open(ao, ao->in_audio_format, error);
+ if (!filter_audio_format.IsDefined()) {
+ FormatError(error, "Failed to open filter for \"%s\" [%s]",
+ ao->name, ao->plugin->name);
+
+ ao->fail_timer.Update();
+ return;
+ }
+
+ assert(filter_audio_format.IsValid());
+
+ ao->out_audio_format = filter_audio_format;
+ ao->out_audio_format.ApplyMask(ao->config_audio_format);
+
+ ao->mutex.unlock();
+ success = ao_plugin_open(ao, ao->out_audio_format, error);
+ ao->mutex.lock();
+
+ assert(!ao->open);
+
+ if (!success) {
+ FormatError(error, "Failed to open \"%s\" [%s]",
+ ao->name, ao->plugin->name);
+
+ ao_filter_close(ao);
+ ao->fail_timer.Update();
+ return;
+ }
+
+ if (!convert_filter_set(ao->convert_filter, ao->out_audio_format,
+ error)) {
+ FormatError(error, "Failed to convert for \"%s\" [%s]",
+ ao->name, ao->plugin->name);
+
+ ao_filter_close(ao);
+ ao->fail_timer.Update();
+ return;
+ }
+
+ ao->open = true;
+
+ FormatDebug(output_domain,
+ "opened plugin=%s name=\"%s\" audio_format=%s",
+ ao->plugin->name, ao->name,
+ audio_format_to_string(ao->out_audio_format, &af_string));
+
+ if (ao->in_audio_format != ao->out_audio_format)
+ FormatDebug(output_domain, "converting from %s",
+ audio_format_to_string(ao->in_audio_format,
+ &af_string));
+}
+
+static void
+ao_close(struct audio_output *ao, bool drain)
+{
+ assert(ao->open);
+
+ ao->pipe = nullptr;
+
+ ao->chunk = nullptr;
+ ao->open = false;
+
+ ao->mutex.unlock();
+
+ if (drain)
+ ao_plugin_drain(ao);
+ else
+ ao_plugin_cancel(ao);
+
+ ao_plugin_close(ao);
+ ao_filter_close(ao);
+
+ ao->mutex.lock();
+
+ FormatDebug(output_domain, "closed plugin=%s name=\"%s\"",
+ ao->plugin->name, ao->name);
+}
+
+static void
+ao_reopen_filter(struct audio_output *ao)
+{
+ Error error;
+
+ ao_filter_close(ao);
+ const AudioFormat filter_audio_format =
+ ao_filter_open(ao, ao->in_audio_format, error);
+ if (!filter_audio_format.IsDefined() ||
+ !convert_filter_set(ao->convert_filter, ao->out_audio_format,
+ error)) {
+ FormatError(error,
+ "Failed to open filter for \"%s\" [%s]",
+ ao->name, ao->plugin->name);
+
+ /* this is a little code duplication fro ao_close(),
+ but we cannot call this function because we must
+ not call filter_close(ao->filter) again */
+
+ ao->pipe = nullptr;
+
+ ao->chunk = nullptr;
+ ao->open = false;
+ ao->fail_timer.Update();
+
+ ao->mutex.unlock();
+ ao_plugin_close(ao);
+ ao->mutex.lock();
+
+ return;
+ }
+}
+
+static void
+ao_reopen(struct audio_output *ao)
+{
+ if (!ao->config_audio_format.IsFullyDefined()) {
+ if (ao->open) {
+ const MusicPipe *mp = ao->pipe;
+ ao_close(ao, true);
+ ao->pipe = mp;
+ }
+
+ /* no audio format is configured: copy in->out, let
+ the output's open() method determine the effective
+ out_audio_format */
+ ao->out_audio_format = ao->in_audio_format;
+ ao->out_audio_format.ApplyMask(ao->config_audio_format);
+ }
+
+ if (ao->open)
+ /* the audio format has changed, and all filters have
+ to be reconfigured */
+ ao_reopen_filter(ao);
+ else
+ ao_open(ao);
+}
+
+/**
+ * Wait until the output's delay reaches zero.
+ *
+ * @return true if playback should be continued, false if a command
+ * was issued
+ */
+static bool
+ao_wait(struct audio_output *ao)
+{
+ while (true) {
+ unsigned delay = ao_plugin_delay(ao);
+ if (delay == 0)
+ return true;
+
+ (void)ao->cond.timed_wait(ao->mutex, delay);
+
+ if (ao->command != AO_COMMAND_NONE)
+ return false;
+ }
+}
+
+static const void *
+ao_chunk_data(struct audio_output *ao, const struct music_chunk *chunk,
+ Filter *replay_gain_filter,
+ unsigned *replay_gain_serial_p,
+ size_t *length_r)
+{
+ assert(chunk != nullptr);
+ assert(!chunk->IsEmpty());
+ assert(chunk->CheckFormat(ao->in_audio_format));
+
+ const void *data = chunk->data;
+ size_t length = chunk->length;
+
+ (void)ao;
+
+ assert(length % ao->in_audio_format.GetFrameSize() == 0);
+
+ if (length > 0 && replay_gain_filter != nullptr) {
+ if (chunk->replay_gain_serial != *replay_gain_serial_p) {
+ replay_gain_filter_set_info(replay_gain_filter,
+ chunk->replay_gain_serial != 0
+ ? &chunk->replay_gain_info
+ : nullptr);
+ *replay_gain_serial_p = chunk->replay_gain_serial;
+ }
+
+ Error error;
+ data = replay_gain_filter->FilterPCM(data, length,
+ &length, error);
+ if (data == nullptr) {
+ FormatError(error, "\"%s\" [%s] failed to filter",
+ ao->name, ao->plugin->name);
+ return nullptr;
+ }
+ }
+
+ *length_r = length;
+ return data;
+}
+
+static const void *
+ao_filter_chunk(struct audio_output *ao, const struct music_chunk *chunk,
+ size_t *length_r)
+{
+ size_t length;
+ const void *data = ao_chunk_data(ao, chunk, ao->replay_gain_filter,
+ &ao->replay_gain_serial, &length);
+ if (data == nullptr)
+ return nullptr;
+
+ if (length == 0) {
+ /* empty chunk, nothing to do */
+ *length_r = 0;
+ return data;
+ }
+
+ /* cross-fade */
+
+ if (chunk->other != nullptr) {
+ size_t other_length;
+ const void *other_data =
+ ao_chunk_data(ao, chunk->other,
+ ao->other_replay_gain_filter,
+ &ao->other_replay_gain_serial,
+ &other_length);
+ if (other_data == nullptr)
+ return nullptr;
+
+ if (other_length == 0) {
+ *length_r = 0;
+ return data;
+ }
+
+ /* if the "other" chunk is longer, then that trailer
+ is used as-is, without mixing; it is part of the
+ "next" song being faded in, and if there's a rest,
+ it means cross-fading ends here */
+
+ if (length > other_length)
+ length = other_length;
+
+ void *dest = ao->cross_fade_buffer.Get(other_length);
+ memcpy(dest, other_data, other_length);
+ if (!pcm_mix(ao->cross_fade_dither, dest, data, length,
+ ao->in_audio_format.format,
+ 1.0 - chunk->mix_ratio)) {
+ FormatError(output_domain,
+ "Cannot cross-fade format %s",
+ sample_format_to_string(ao->in_audio_format.format));
+ return nullptr;
+ }
+
+ data = dest;
+ length = other_length;
+ }
+
+ /* apply filter chain */
+
+ Error error;
+ data = ao->filter->FilterPCM(data, length, &length, error);
+ if (data == nullptr) {
+ FormatError(error, "\"%s\" [%s] failed to filter",
+ ao->name, ao->plugin->name);
+ return nullptr;
+ }
+
+ *length_r = length;
+ return data;
+}
+
+static bool
+ao_play_chunk(struct audio_output *ao, const struct music_chunk *chunk)
+{
+ assert(ao != nullptr);
+ assert(ao->filter != nullptr);
+
+ if (ao->tags && gcc_unlikely(chunk->tag != nullptr)) {
+ ao->mutex.unlock();
+ ao_plugin_send_tag(ao, chunk->tag);
+ ao->mutex.lock();
+ }
+
+ size_t size;
+#if GCC_CHECK_VERSION(4,7)
+ /* workaround -Wmaybe-uninitialized false positive */
+ size = 0;
+#endif
+ const char *data = (const char *)ao_filter_chunk(ao, chunk, &size);
+ if (data == nullptr) {
+ ao_close(ao, false);
+
+ /* don't automatically reopen this device for 10
+ seconds */
+ ao->fail_timer.Update();
+ return false;
+ }
+
+ Error error;
+
+ while (size > 0 && ao->command == AO_COMMAND_NONE) {
+ size_t nbytes;
+
+ if (!ao_wait(ao))
+ break;
+
+ ao->mutex.unlock();
+ nbytes = ao_plugin_play(ao, data, size, error);
+ ao->mutex.lock();
+ if (nbytes == 0) {
+ /* play()==0 means failure */
+ FormatError(error, "\"%s\" [%s] failed to play",
+ ao->name, ao->plugin->name);
+
+ ao_close(ao, false);
+
+ /* don't automatically reopen this device for
+ 10 seconds */
+ assert(!ao->fail_timer.IsDefined());
+ ao->fail_timer.Update();
+
+ return false;
+ }
+
+ assert(nbytes <= size);
+ assert(nbytes % ao->out_audio_format.GetFrameSize() == 0);
+
+ data += nbytes;
+ size -= nbytes;
+ }
+
+ return true;
+}
+
+static const struct music_chunk *
+ao_next_chunk(struct audio_output *ao)
+{
+ return ao->chunk != nullptr
+ /* continue the previous play() call */
+ ? ao->chunk->next
+ /* get the first chunk from the pipe */
+ : ao->pipe->Peek();
+}
+
+/**
+ * Plays all remaining chunks, until the tail of the pipe has been
+ * reached (and no more chunks are queued), or until a command is
+ * received.
+ *
+ * @return true if at least one chunk has been available, false if the
+ * tail of the pipe was already reached
+ */
+static bool
+ao_play(struct audio_output *ao)
+{
+ bool success;
+ const struct music_chunk *chunk;
+
+ assert(ao->pipe != nullptr);
+
+ chunk = ao_next_chunk(ao);
+ if (chunk == nullptr)
+ /* no chunk available */
+ return false;
+
+ ao->chunk_finished = false;
+
+ assert(!ao->in_playback_loop);
+ ao->in_playback_loop = true;
+
+ while (chunk != nullptr && ao->command == AO_COMMAND_NONE) {
+ assert(!ao->chunk_finished);
+
+ ao->chunk = chunk;
+
+ success = ao_play_chunk(ao, chunk);
+ if (!success) {
+ assert(ao->chunk == nullptr);
+ break;
+ }
+
+ assert(ao->chunk == chunk);
+ chunk = chunk->next;
+ }
+
+ assert(ao->in_playback_loop);
+ ao->in_playback_loop = false;
+
+ ao->chunk_finished = true;
+
+ ao->mutex.unlock();
+ ao->player_control->LockSignal();
+ ao->mutex.lock();
+
+ return true;
+}
+
+static void ao_pause(struct audio_output *ao)
+{
+ bool ret;
+
+ ao->mutex.unlock();
+ ao_plugin_cancel(ao);
+ ao->mutex.lock();
+
+ ao->pause = true;
+ ao_command_finished(ao);
+
+ do {
+ if (!ao_wait(ao))
+ break;
+
+ ao->mutex.unlock();
+ ret = ao_plugin_pause(ao);
+ ao->mutex.lock();
+
+ if (!ret) {
+ ao_close(ao, false);
+ break;
+ }
+ } while (ao->command == AO_COMMAND_NONE);
+
+ ao->pause = false;
+}
+
+static void
+audio_output_task(void *arg)
+{
+ struct audio_output *ao = (struct audio_output *)arg;
+
+ FormatThreadName("output:%s", ao->name);
+
+ SetThreadRealtime();
+
+ ao->mutex.lock();
+
+ while (1) {
+ switch (ao->command) {
+ case AO_COMMAND_NONE:
+ break;
+
+ case AO_COMMAND_ENABLE:
+ ao_enable(ao);
+ ao_command_finished(ao);
+ break;
+
+ case AO_COMMAND_DISABLE:
+ ao_disable(ao);
+ ao_command_finished(ao);
+ break;
+
+ case AO_COMMAND_OPEN:
+ ao_open(ao);
+ ao_command_finished(ao);
+ break;
+
+ case AO_COMMAND_REOPEN:
+ ao_reopen(ao);
+ ao_command_finished(ao);
+ break;
+
+ case AO_COMMAND_CLOSE:
+ assert(ao->open);
+ assert(ao->pipe != nullptr);
+
+ ao_close(ao, false);
+ ao_command_finished(ao);
+ break;
+
+ case AO_COMMAND_PAUSE:
+ if (!ao->open) {
+ /* the output has failed after
+ audio_output_all_pause() has
+ submitted the PAUSE command; bail
+ out */
+ ao_command_finished(ao);
+ break;
+ }
+
+ ao_pause(ao);
+ /* don't "break" here: this might cause
+ ao_play() to be called when command==CLOSE
+ ends the paused state - "continue" checks
+ the new command first */
+ continue;
+
+ case AO_COMMAND_DRAIN:
+ if (ao->open) {
+ assert(ao->chunk == nullptr);
+ assert(ao->pipe->Peek() == nullptr);
+
+ ao->mutex.unlock();
+ ao_plugin_drain(ao);
+ ao->mutex.lock();
+ }
+
+ ao_command_finished(ao);
+ continue;
+
+ case AO_COMMAND_CANCEL:
+ ao->chunk = nullptr;
+
+ if (ao->open) {
+ ao->mutex.unlock();
+ ao_plugin_cancel(ao);
+ ao->mutex.lock();
+ }
+
+ ao_command_finished(ao);
+ continue;
+
+ case AO_COMMAND_KILL:
+ ao->chunk = nullptr;
+ ao_command_finished(ao);
+ ao->mutex.unlock();
+ return;
+ }
+
+ if (ao->open && ao->allow_play && ao_play(ao))
+ /* don't wait for an event if there are more
+ chunks in the pipe */
+ continue;
+
+ if (ao->command == AO_COMMAND_NONE) {
+ ao->woken_for_play = false;
+ ao->cond.wait(ao->mutex);
+ }
+ }
+}
+
+void audio_output_thread_start(struct audio_output *ao)
+{
+ assert(ao->command == AO_COMMAND_NONE);
+
+ Error error;
+ if (!ao->thread.Start(audio_output_task, ao, error))
+ FatalError(error);
+}