From ea5b901bcce20949a8d1fd622a7b03ff6f56ae20 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Thu, 23 Jan 2014 23:49:50 +0100 Subject: output/*: move to output/plugins/ --- Makefile.am | 88 +-- src/CommandLine.cxx | 4 +- src/Main.cxx | 2 +- src/MixerAll.cxx | 4 +- src/OutputAPI.hxx | 33 -- src/OutputAll.cxx | 589 ------------------ src/OutputAll.hxx | 174 ------ src/OutputCommand.cxx | 112 ---- src/OutputCommand.hxx | 51 -- src/OutputControl.cxx | 325 ---------- src/OutputControl.hxx | 94 --- src/OutputError.cxx | 23 - src/OutputError.hxx | 25 - src/OutputFinish.cxx | 51 -- src/OutputInit.cxx | 329 ---------- src/OutputInternal.hxx | 301 ---------- src/OutputList.cxx | 100 ---- src/OutputList.hxx | 33 -- src/OutputPlugin.cxx | 109 ---- src/OutputPlugin.hxx | 202 ------- src/OutputPrint.cxx | 45 -- src/OutputPrint.hxx | 34 -- src/OutputState.cxx | 92 --- src/OutputState.hxx | 44 -- src/OutputThread.cxx | 690 --------------------- src/OutputThread.hxx | 28 - src/PlayerThread.cxx | 2 +- src/StateFile.cxx | 2 +- src/command/OutputCommands.cxx | 4 +- src/command/PlayerCommands.cxx | 2 +- src/mixer/AlsaMixerPlugin.cxx | 2 +- src/mixer/PulseMixerPlugin.cxx | 2 +- src/mixer/RoarMixerPlugin.cxx | 2 +- src/mixer/WinmmMixerPlugin.cxx | 4 +- src/output/AlsaOutputPlugin.cxx | 868 --------------------------- src/output/AlsaOutputPlugin.hxx | 25 - src/output/AoOutputPlugin.cxx | 286 --------- src/output/AoOutputPlugin.hxx | 25 - src/output/FifoOutputPlugin.cxx | 313 ---------- src/output/FifoOutputPlugin.hxx | 25 - src/output/HttpdClient.cxx | 485 --------------- src/output/HttpdClient.hxx | 193 ------ src/output/HttpdInternal.hxx | 279 --------- src/output/HttpdOutputPlugin.cxx | 601 ------------------- src/output/HttpdOutputPlugin.hxx | 25 - src/output/JackOutputPlugin.cxx | 765 ------------------------ src/output/JackOutputPlugin.hxx | 25 - src/output/NullOutputPlugin.cxx | 141 ----- src/output/NullOutputPlugin.hxx | 25 - src/output/OSXOutputPlugin.cxx | 428 ------------- src/output/OSXOutputPlugin.hxx | 25 - src/output/OpenALOutputPlugin.cxx | 285 --------- src/output/OpenALOutputPlugin.hxx | 25 - src/output/OssOutputPlugin.cxx | 776 ------------------------ src/output/OssOutputPlugin.hxx | 25 - src/output/OutputAPI.hxx | 33 ++ src/output/OutputAll.cxx | 589 ++++++++++++++++++ src/output/OutputAll.hxx | 174 ++++++ src/output/OutputCommand.cxx | 112 ++++ src/output/OutputCommand.hxx | 51 ++ src/output/OutputControl.cxx | 325 ++++++++++ src/output/OutputControl.hxx | 94 +++ src/output/OutputError.cxx | 23 + src/output/OutputError.hxx | 25 + src/output/OutputFinish.cxx | 51 ++ src/output/OutputInit.cxx | 329 ++++++++++ src/output/OutputInternal.hxx | 301 ++++++++++ src/output/OutputList.cxx | 100 ++++ src/output/OutputList.hxx | 33 ++ src/output/OutputPlugin.cxx | 109 ++++ src/output/OutputPlugin.hxx | 202 +++++++ src/output/OutputPrint.cxx | 45 ++ src/output/OutputPrint.hxx | 34 ++ src/output/OutputState.cxx | 92 +++ src/output/OutputState.hxx | 44 ++ src/output/OutputThread.cxx | 690 +++++++++++++++++++++ src/output/OutputThread.hxx | 28 + src/output/PipeOutputPlugin.cxx | 147 ----- src/output/PipeOutputPlugin.hxx | 25 - src/output/PulseOutputPlugin.cxx | 889 ---------------------------- src/output/PulseOutputPlugin.hxx | 46 -- src/output/RecorderOutputPlugin.cxx | 262 -------- src/output/RecorderOutputPlugin.hxx | 25 - src/output/RoarOutputPlugin.cxx | 428 ------------- src/output/RoarOutputPlugin.hxx | 33 -- src/output/ShoutOutputPlugin.cxx | 544 ----------------- src/output/ShoutOutputPlugin.hxx | 25 - src/output/SolarisOutputPlugin.cxx | 201 ------- src/output/SolarisOutputPlugin.hxx | 25 - src/output/WinmmOutputPlugin.cxx | 353 ----------- src/output/WinmmOutputPlugin.hxx | 42 -- src/output/plugins/AlsaOutputPlugin.cxx | 868 +++++++++++++++++++++++++++ src/output/plugins/AlsaOutputPlugin.hxx | 25 + src/output/plugins/AoOutputPlugin.cxx | 286 +++++++++ src/output/plugins/AoOutputPlugin.hxx | 25 + src/output/plugins/FifoOutputPlugin.cxx | 313 ++++++++++ src/output/plugins/FifoOutputPlugin.hxx | 25 + src/output/plugins/HttpdClient.cxx | 485 +++++++++++++++ src/output/plugins/HttpdClient.hxx | 193 ++++++ src/output/plugins/HttpdInternal.hxx | 279 +++++++++ src/output/plugins/HttpdOutputPlugin.cxx | 601 +++++++++++++++++++ src/output/plugins/HttpdOutputPlugin.hxx | 25 + src/output/plugins/JackOutputPlugin.cxx | 765 ++++++++++++++++++++++++ src/output/plugins/JackOutputPlugin.hxx | 25 + src/output/plugins/NullOutputPlugin.cxx | 141 +++++ src/output/plugins/NullOutputPlugin.hxx | 25 + src/output/plugins/OSXOutputPlugin.cxx | 428 +++++++++++++ src/output/plugins/OSXOutputPlugin.hxx | 25 + src/output/plugins/OpenALOutputPlugin.cxx | 285 +++++++++ src/output/plugins/OpenALOutputPlugin.hxx | 25 + src/output/plugins/OssOutputPlugin.cxx | 776 ++++++++++++++++++++++++ src/output/plugins/OssOutputPlugin.hxx | 25 + src/output/plugins/PipeOutputPlugin.cxx | 147 +++++ src/output/plugins/PipeOutputPlugin.hxx | 25 + src/output/plugins/PulseOutputPlugin.cxx | 889 ++++++++++++++++++++++++++++ src/output/plugins/PulseOutputPlugin.hxx | 46 ++ src/output/plugins/RecorderOutputPlugin.cxx | 262 ++++++++ src/output/plugins/RecorderOutputPlugin.hxx | 25 + src/output/plugins/RoarOutputPlugin.cxx | 428 +++++++++++++ src/output/plugins/RoarOutputPlugin.hxx | 33 ++ src/output/plugins/ShoutOutputPlugin.cxx | 544 +++++++++++++++++ src/output/plugins/ShoutOutputPlugin.hxx | 25 + src/output/plugins/SolarisOutputPlugin.cxx | 201 +++++++ src/output/plugins/SolarisOutputPlugin.hxx | 25 + src/output/plugins/WinmmOutputPlugin.cxx | 353 +++++++++++ src/output/plugins/WinmmOutputPlugin.hxx | 42 ++ test/read_mixer.cxx | 4 +- test/run_output.cxx | 6 +- 128 files changed, 12244 insertions(+), 12232 deletions(-) delete mode 100644 src/OutputAPI.hxx delete mode 100644 src/OutputAll.cxx delete mode 100644 src/OutputAll.hxx delete mode 100644 src/OutputCommand.cxx delete mode 100644 src/OutputCommand.hxx delete mode 100644 src/OutputControl.cxx delete mode 100644 src/OutputControl.hxx delete mode 100644 src/OutputError.cxx delete mode 100644 src/OutputError.hxx delete mode 100644 src/OutputFinish.cxx delete mode 100644 src/OutputInit.cxx delete mode 100644 src/OutputInternal.hxx delete mode 100644 src/OutputList.cxx delete mode 100644 src/OutputList.hxx delete mode 100644 src/OutputPlugin.cxx delete mode 100644 src/OutputPlugin.hxx delete mode 100644 src/OutputPrint.cxx delete mode 100644 src/OutputPrint.hxx delete mode 100644 src/OutputState.cxx delete mode 100644 src/OutputState.hxx delete mode 100644 src/OutputThread.cxx delete mode 100644 src/OutputThread.hxx delete mode 100644 src/output/AlsaOutputPlugin.cxx delete mode 100644 src/output/AlsaOutputPlugin.hxx delete mode 100644 src/output/AoOutputPlugin.cxx delete mode 100644 src/output/AoOutputPlugin.hxx delete mode 100644 src/output/FifoOutputPlugin.cxx delete mode 100644 src/output/FifoOutputPlugin.hxx delete mode 100644 src/output/HttpdClient.cxx delete mode 100644 src/output/HttpdClient.hxx delete mode 100644 src/output/HttpdInternal.hxx delete mode 100644 src/output/HttpdOutputPlugin.cxx delete mode 100644 src/output/HttpdOutputPlugin.hxx delete mode 100644 src/output/JackOutputPlugin.cxx delete mode 100644 src/output/JackOutputPlugin.hxx delete mode 100644 src/output/NullOutputPlugin.cxx delete mode 100644 src/output/NullOutputPlugin.hxx delete mode 100644 src/output/OSXOutputPlugin.cxx delete mode 100644 src/output/OSXOutputPlugin.hxx delete mode 100644 src/output/OpenALOutputPlugin.cxx delete mode 100644 src/output/OpenALOutputPlugin.hxx delete mode 100644 src/output/OssOutputPlugin.cxx delete mode 100644 src/output/OssOutputPlugin.hxx create mode 100644 src/output/OutputAPI.hxx create mode 100644 src/output/OutputAll.cxx create mode 100644 src/output/OutputAll.hxx create mode 100644 src/output/OutputCommand.cxx create mode 100644 src/output/OutputCommand.hxx create mode 100644 src/output/OutputControl.cxx create mode 100644 src/output/OutputControl.hxx create mode 100644 src/output/OutputError.cxx create mode 100644 src/output/OutputError.hxx create mode 100644 src/output/OutputFinish.cxx create mode 100644 src/output/OutputInit.cxx create mode 100644 src/output/OutputInternal.hxx create mode 100644 src/output/OutputList.cxx create mode 100644 src/output/OutputList.hxx create mode 100644 src/output/OutputPlugin.cxx create mode 100644 src/output/OutputPlugin.hxx create mode 100644 src/output/OutputPrint.cxx create mode 100644 src/output/OutputPrint.hxx create mode 100644 src/output/OutputState.cxx create mode 100644 src/output/OutputState.hxx create mode 100644 src/output/OutputThread.cxx create mode 100644 src/output/OutputThread.hxx delete mode 100644 src/output/PipeOutputPlugin.cxx delete mode 100644 src/output/PipeOutputPlugin.hxx delete mode 100644 src/output/PulseOutputPlugin.cxx delete mode 100644 src/output/PulseOutputPlugin.hxx delete mode 100644 src/output/RecorderOutputPlugin.cxx delete mode 100644 src/output/RecorderOutputPlugin.hxx delete mode 100644 src/output/RoarOutputPlugin.cxx delete mode 100644 src/output/RoarOutputPlugin.hxx delete mode 100644 src/output/ShoutOutputPlugin.cxx delete mode 100644 src/output/ShoutOutputPlugin.hxx delete mode 100644 src/output/SolarisOutputPlugin.cxx delete mode 100644 src/output/SolarisOutputPlugin.hxx delete mode 100644 src/output/WinmmOutputPlugin.cxx delete mode 100644 src/output/WinmmOutputPlugin.hxx create mode 100644 src/output/plugins/AlsaOutputPlugin.cxx create mode 100644 src/output/plugins/AlsaOutputPlugin.hxx create mode 100644 src/output/plugins/AoOutputPlugin.cxx create mode 100644 src/output/plugins/AoOutputPlugin.hxx create mode 100644 src/output/plugins/FifoOutputPlugin.cxx create mode 100644 src/output/plugins/FifoOutputPlugin.hxx create mode 100644 src/output/plugins/HttpdClient.cxx create mode 100644 src/output/plugins/HttpdClient.hxx create mode 100644 src/output/plugins/HttpdInternal.hxx create mode 100644 src/output/plugins/HttpdOutputPlugin.cxx create mode 100644 src/output/plugins/HttpdOutputPlugin.hxx create mode 100644 src/output/plugins/JackOutputPlugin.cxx create mode 100644 src/output/plugins/JackOutputPlugin.hxx create mode 100644 src/output/plugins/NullOutputPlugin.cxx create mode 100644 src/output/plugins/NullOutputPlugin.hxx create mode 100644 src/output/plugins/OSXOutputPlugin.cxx create mode 100644 src/output/plugins/OSXOutputPlugin.hxx create mode 100644 src/output/plugins/OpenALOutputPlugin.cxx create mode 100644 src/output/plugins/OpenALOutputPlugin.hxx create mode 100644 src/output/plugins/OssOutputPlugin.cxx create mode 100644 src/output/plugins/OssOutputPlugin.hxx create mode 100644 src/output/plugins/PipeOutputPlugin.cxx create mode 100644 src/output/plugins/PipeOutputPlugin.hxx create mode 100644 src/output/plugins/PulseOutputPlugin.cxx create mode 100644 src/output/plugins/PulseOutputPlugin.hxx create mode 100644 src/output/plugins/RecorderOutputPlugin.cxx create mode 100644 src/output/plugins/RecorderOutputPlugin.hxx create mode 100644 src/output/plugins/RoarOutputPlugin.cxx create mode 100644 src/output/plugins/RoarOutputPlugin.hxx create mode 100644 src/output/plugins/ShoutOutputPlugin.cxx create mode 100644 src/output/plugins/ShoutOutputPlugin.hxx create mode 100644 src/output/plugins/SolarisOutputPlugin.cxx create mode 100644 src/output/plugins/SolarisOutputPlugin.hxx create mode 100644 src/output/plugins/WinmmOutputPlugin.cxx create mode 100644 src/output/plugins/WinmmOutputPlugin.hxx diff --git a/Makefile.am b/Makefile.am index 371d46a1b..541494c21 100644 --- a/Makefile.am +++ b/Makefile.am @@ -913,23 +913,23 @@ OUTPUT_LIBS = \ $(SHOUT_LIBS) OUTPUT_API_SRC = \ - src/OutputAPI.hxx \ - src/OutputInternal.hxx \ - src/OutputList.cxx src/OutputList.hxx \ - src/OutputAll.cxx src/OutputAll.hxx \ - src/OutputThread.cxx src/OutputThread.hxx \ - src/OutputError.cxx src/OutputError.hxx \ - src/OutputControl.cxx src/OutputControl.hxx \ - src/OutputState.cxx src/OutputState.hxx \ - src/OutputPrint.cxx src/OutputPrint.hxx \ - src/OutputCommand.cxx src/OutputCommand.hxx \ - src/OutputPlugin.cxx src/OutputPlugin.hxx \ - src/OutputFinish.cxx \ - src/OutputInit.cxx + src/output/OutputAPI.hxx \ + src/output/OutputInternal.hxx \ + src/output/OutputList.cxx src/output/OutputList.hxx \ + src/output/OutputAll.cxx src/output/OutputAll.hxx \ + src/output/OutputThread.cxx src/output/OutputThread.hxx \ + src/output/OutputError.cxx src/output/OutputError.hxx \ + src/output/OutputControl.cxx src/output/OutputControl.hxx \ + src/output/OutputState.cxx src/output/OutputState.hxx \ + src/output/OutputPrint.cxx src/output/OutputPrint.hxx \ + src/output/OutputCommand.cxx src/output/OutputCommand.hxx \ + src/output/OutputPlugin.cxx src/output/OutputPlugin.hxx \ + src/output/OutputFinish.cxx \ + src/output/OutputInit.cxx liboutput_plugins_a_SOURCES = \ - src/output/NullOutputPlugin.cxx \ - src/output/NullOutputPlugin.hxx + src/output/plugins/NullOutputPlugin.cxx \ + src/output/plugins/NullOutputPlugin.hxx MIXER_LIBS = \ libmixer_plugins.a \ @@ -952,88 +952,100 @@ libmixer_plugins_a_CPPFLAGS = $(AM_CPPFLAGS) \ if HAVE_ALSA liboutput_plugins_a_SOURCES += \ - src/output/AlsaOutputPlugin.cxx \ - src/output/AlsaOutputPlugin.hxx + src/output/plugins/AlsaOutputPlugin.cxx \ + src/output/plugins/AlsaOutputPlugin.hxx libmixer_plugins_a_SOURCES += src/mixer/AlsaMixerPlugin.cxx endif if HAVE_ROAR liboutput_plugins_a_SOURCES += \ - src/output/RoarOutputPlugin.cxx src/output/RoarOutputPlugin.hxx + src/output/plugins/RoarOutputPlugin.cxx \ + src/output/plugins/RoarOutputPlugin.hxx libmixer_plugins_a_SOURCES += src/mixer/RoarMixerPlugin.cxx endif if HAVE_AO liboutput_plugins_a_SOURCES += \ - src/output/AoOutputPlugin.cxx src/output/AoOutputPlugin.hxx + src/output/plugins/AoOutputPlugin.cxx \ + src/output/plugins/AoOutputPlugin.hxx endif if HAVE_FIFO liboutput_plugins_a_SOURCES += \ - src/output/FifoOutputPlugin.cxx src/output/FifoOutputPlugin.hxx + src/output/plugins/FifoOutputPlugin.cxx \ + src/output/plugins/FifoOutputPlugin.hxx endif if ENABLE_PIPE_OUTPUT liboutput_plugins_a_SOURCES += \ - src/output/PipeOutputPlugin.cxx src/output/PipeOutputPlugin.hxx + src/output/plugins/PipeOutputPlugin.cxx \ + src/output/plugins/PipeOutputPlugin.hxx endif if HAVE_JACK liboutput_plugins_a_SOURCES += \ - src/output/JackOutputPlugin.cxx src/output/JackOutputPlugin.hxx + src/output/plugins/JackOutputPlugin.cxx \ + src/output/plugins/JackOutputPlugin.hxx endif if HAVE_OSS liboutput_plugins_a_SOURCES += \ - src/output/OssOutputPlugin.cxx \ - src/output/OssOutputPlugin.hxx + src/output/plugins/OssOutputPlugin.cxx \ + src/output/plugins/OssOutputPlugin.hxx libmixer_plugins_a_SOURCES += src/mixer/OssMixerPlugin.cxx endif if HAVE_OPENAL liboutput_plugins_a_SOURCES += \ - src/output/OpenALOutputPlugin.cxx src/output/OpenALOutputPlugin.hxx + src/output/plugins/OpenALOutputPlugin.cxx \ + src/output/plugins/OpenALOutputPlugin.hxx endif if HAVE_OSX liboutput_plugins_a_SOURCES += \ - src/output/OSXOutputPlugin.cxx \ - src/output/OSXOutputPlugin.hxx + src/output/plugins/OSXOutputPlugin.cxx \ + src/output/plugins/OSXOutputPlugin.hxx endif if HAVE_PULSE liboutput_plugins_a_SOURCES += \ - src/output/PulseOutputPlugin.cxx src/output/PulseOutputPlugin.hxx + src/output/plugins/PulseOutputPlugin.cxx \ + src/output/plugins/PulseOutputPlugin.hxx libmixer_plugins_a_SOURCES += \ src/mixer/PulseMixerPlugin.cxx src/mixer/PulseMixerPlugin.hxx endif if HAVE_SHOUT liboutput_plugins_a_SOURCES += \ - src/output/ShoutOutputPlugin.cxx src/output/ShoutOutputPlugin.hxx + src/output/plugins/ShoutOutputPlugin.cxx \ + src/output/plugins/ShoutOutputPlugin.hxx endif if ENABLE_RECORDER_OUTPUT liboutput_plugins_a_SOURCES += \ - src/output/RecorderOutputPlugin.cxx src/output/RecorderOutputPlugin.hxx + src/output/plugins/RecorderOutputPlugin.cxx \ + src/output/plugins/RecorderOutputPlugin.hxx endif if ENABLE_HTTPD_OUTPUT liboutput_plugins_a_SOURCES += \ src/IcyMetaDataServer.cxx src/IcyMetaDataServer.hxx \ - src/output/HttpdInternal.hxx \ - src/output/HttpdClient.cxx src/output/HttpdClient.hxx \ - src/output/HttpdOutputPlugin.cxx src/output/HttpdOutputPlugin.hxx + src/output/plugins/HttpdInternal.hxx \ + src/output/plugins/HttpdClient.cxx \ + src/output/plugins/HttpdClient.hxx \ + src/output/plugins/HttpdOutputPlugin.cxx \ + src/output/plugins/HttpdOutputPlugin.hxx endif if ENABLE_SOLARIS_OUTPUT liboutput_plugins_a_SOURCES += \ - src/output/SolarisOutputPlugin.cxx src/output/SolarisOutputPlugin.hxx + src/output/plugins/SolarisOutputPlugin.cxx src/output/plugins/SolarisOutputPlugin.hxx endif if ENABLE_WINMM_OUTPUT liboutput_plugins_a_SOURCES += \ - src/output/WinmmOutputPlugin.cxx src/output/WinmmOutputPlugin.hxx + src/output/plugins/WinmmOutputPlugin.cxx \ + src/output/plugins/WinmmOutputPlugin.hxx libmixer_plugins_a_SOURCES += src/mixer/WinmmMixerPlugin.cxx endif @@ -1528,9 +1540,9 @@ test_run_output_SOURCES = test/run_output.cxx \ src/AudioParser.cxx \ src/Timer.cxx \ src/Page.cxx \ - src/OutputError.cxx \ - src/OutputInit.cxx src/OutputFinish.cxx src/OutputList.cxx \ - src/OutputPlugin.cxx \ + src/output/OutputError.cxx \ + src/output/OutputInit.cxx src/output/OutputFinish.cxx src/output/OutputList.cxx \ + src/output/OutputPlugin.cxx \ src/MixerControl.cxx \ src/MixerType.cxx \ src/FilterPlugin.cxx \ diff --git a/src/CommandLine.cxx b/src/CommandLine.cxx index 0667844f6..97cc2b18e 100644 --- a/src/CommandLine.cxx +++ b/src/CommandLine.cxx @@ -27,8 +27,8 @@ #include "DatabasePlugin.hxx" #include "DecoderList.hxx" #include "DecoderPlugin.hxx" -#include "OutputList.hxx" -#include "OutputPlugin.hxx" +#include "output/OutputList.hxx" +#include "output/OutputPlugin.hxx" #include "InputRegistry.hxx" #include "InputPlugin.hxx" #include "playlist/PlaylistRegistry.hxx" diff --git a/src/Main.cxx b/src/Main.cxx index 8808a78d9..9eb39a7e2 100644 --- a/src/Main.cxx +++ b/src/Main.cxx @@ -37,7 +37,7 @@ #include "command/AllCommands.hxx" #include "Partition.hxx" #include "Volume.hxx" -#include "OutputAll.hxx" +#include "output/OutputAll.hxx" #include "tag/TagConfig.hxx" #include "ReplayGainConfig.hxx" #include "Idle.hxx" diff --git a/src/MixerAll.cxx b/src/MixerAll.cxx index 2310048c4..3cc92baee 100644 --- a/src/MixerAll.cxx +++ b/src/MixerAll.cxx @@ -22,9 +22,9 @@ #include "MixerControl.hxx" #include "MixerInternal.hxx" #include "MixerList.hxx" -#include "OutputAll.hxx" +#include "output/OutputAll.hxx" +#include "output/OutputInternal.hxx" #include "pcm/Volume.hxx" -#include "OutputInternal.hxx" #include "util/Error.hxx" #include "util/Domain.hxx" #include "Log.hxx" diff --git a/src/OutputAPI.hxx b/src/OutputAPI.hxx deleted file mode 100644 index 322ed3971..000000000 --- a/src/OutputAPI.hxx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_OUTPUT_API_HXX -#define MPD_OUTPUT_API_HXX - -// IWYU pragma: begin_exports - -#include "OutputPlugin.hxx" -#include "OutputInternal.hxx" -#include "AudioFormat.hxx" -#include "tag/Tag.hxx" -#include "ConfigData.hxx" - -// IWYU pragma: end_exports - -#endif diff --git a/src/OutputAll.cxx b/src/OutputAll.cxx deleted file mode 100644 index b3623f1af..000000000 --- a/src/OutputAll.cxx +++ /dev/null @@ -1,589 +0,0 @@ -/* - * 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 "OutputAll.hxx" -#include "PlayerControl.hxx" -#include "OutputInternal.hxx" -#include "OutputControl.hxx" -#include "OutputError.hxx" -#include "MusicBuffer.hxx" -#include "MusicPipe.hxx" -#include "MusicChunk.hxx" -#include "system/FatalError.hxx" -#include "util/Error.hxx" -#include "ConfigData.hxx" -#include "ConfigGlobal.hxx" -#include "ConfigOption.hxx" -#include "notify.hxx" - -#include -#include - -static AudioFormat input_audio_format; - -static struct audio_output **audio_outputs; -static unsigned int num_audio_outputs; - -/** - * The #MusicBuffer object where consumed chunks are returned. - */ -static MusicBuffer *g_music_buffer; - -/** - * The #MusicPipe object which feeds all audio outputs. It is filled - * by audio_output_all_play(). - */ -static MusicPipe *g_mp; - -/** - * The "elapsed_time" stamp of the most recently finished chunk. - */ -static float audio_output_all_elapsed_time = -1.0; - -unsigned int audio_output_count(void) -{ - return num_audio_outputs; -} - -struct audio_output * -audio_output_get(unsigned i) -{ - assert(i < num_audio_outputs); - - assert(audio_outputs[i] != nullptr); - - return audio_outputs[i]; -} - -struct audio_output * -audio_output_find(const char *name) -{ - for (unsigned i = 0; i < num_audio_outputs; ++i) { - struct audio_output *ao = audio_output_get(i); - - if (strcmp(ao->name, name) == 0) - return ao; - } - - /* name not found */ - return nullptr; -} - -gcc_const -static unsigned -audio_output_config_count(void) -{ - unsigned int nr = 0; - const struct config_param *param = nullptr; - - while ((param = config_get_next_param(CONF_AUDIO_OUTPUT, param))) - nr++; - if (!nr) - nr = 1; /* we'll always have at least one device */ - return nr; -} - -void -audio_output_all_init(PlayerControl &pc) -{ - const struct config_param *param = nullptr; - unsigned int i; - Error error; - - num_audio_outputs = audio_output_config_count(); - audio_outputs = new audio_output *[num_audio_outputs]; - - const config_param empty; - - for (i = 0; i < num_audio_outputs; i++) - { - unsigned int j; - - param = config_get_next_param(CONF_AUDIO_OUTPUT, param); - if (param == nullptr) { - /* only allow param to be nullptr if there - just one audio output */ - assert(i == 0); - assert(num_audio_outputs == 1); - - param = ∅ - } - - audio_output *output = audio_output_new(*param, pc, error); - if (output == nullptr) { - if (param != nullptr) - FormatFatalError("line %i: %s", - param->line, - error.GetMessage()); - else - FatalError(error); - } - - audio_outputs[i] = output; - - /* require output names to be unique: */ - for (j = 0; j < i; j++) { - if (!strcmp(output->name, audio_outputs[j]->name)) { - FormatFatalError("output devices with identical " - "names: %s", output->name); - } - } - } -} - -void -audio_output_all_finish(void) -{ - unsigned int i; - - for (i = 0; i < num_audio_outputs; i++) { - audio_output_disable(audio_outputs[i]); - audio_output_finish(audio_outputs[i]); - } - - delete[] audio_outputs; - audio_outputs = nullptr; - num_audio_outputs = 0; -} - -void -audio_output_all_enable_disable(void) -{ - for (unsigned i = 0; i < num_audio_outputs; i++) { - struct audio_output *ao = audio_outputs[i]; - bool enabled; - - ao->mutex.lock(); - enabled = ao->really_enabled; - ao->mutex.unlock(); - - if (ao->enabled != enabled) { - if (ao->enabled) - audio_output_enable(ao); - else - audio_output_disable(ao); - } - } -} - -/** - * Determine if all (active) outputs have finished the current - * command. - */ -static bool -audio_output_all_finished(void) -{ - for (unsigned i = 0; i < num_audio_outputs; ++i) { - struct audio_output *ao = audio_outputs[i]; - - const ScopeLock protect(ao->mutex); - if (audio_output_is_open(ao) && - !audio_output_command_is_finished(ao)) - return false; - } - - return true; -} - -static void audio_output_wait_all(void) -{ - while (!audio_output_all_finished()) - audio_output_client_notify.Wait(); -} - -/** - * Signals all audio outputs which are open. - */ -static void -audio_output_allow_play_all(void) -{ - for (unsigned i = 0; i < num_audio_outputs; ++i) - audio_output_allow_play(audio_outputs[i]); -} - -static void -audio_output_reset_reopen(struct audio_output *ao) -{ - const ScopeLock protect(ao->mutex); - - ao->fail_timer.Reset(); -} - -/** - * Resets the "reopen" flag on all audio devices. MPD should - * immediately retry to open the device instead of waiting for the - * timeout when the user wants to start playback. - */ -static void -audio_output_all_reset_reopen(void) -{ - for (unsigned i = 0; i < num_audio_outputs; ++i) { - struct audio_output *ao = audio_outputs[i]; - - audio_output_reset_reopen(ao); - } -} - -/** - * Opens all output devices which are enabled, but closed. - * - * @return true if there is at least open output device which is open - */ -static bool -audio_output_all_update(void) -{ - unsigned int i; - bool ret = false; - - if (!input_audio_format.IsDefined()) - return false; - - for (i = 0; i < num_audio_outputs; ++i) - ret = audio_output_update(audio_outputs[i], - input_audio_format, *g_mp) || ret; - - return ret; -} - -void -audio_output_all_set_replay_gain_mode(ReplayGainMode mode) -{ - for (unsigned i = 0; i < num_audio_outputs; ++i) - audio_output_set_replay_gain_mode(audio_outputs[i], mode); -} - -bool -audio_output_all_play(struct music_chunk *chunk, Error &error) -{ - bool ret; - unsigned int i; - - assert(g_music_buffer != nullptr); - assert(g_mp != nullptr); - assert(chunk != nullptr); - assert(chunk->CheckFormat(input_audio_format)); - - ret = audio_output_all_update(); - if (!ret) { - /* TODO: obtain real error */ - error.Set(output_domain, "Failed to open audio output"); - return false; - } - - g_mp->Push(chunk); - - for (i = 0; i < num_audio_outputs; ++i) - audio_output_play(audio_outputs[i]); - - return true; -} - -bool -audio_output_all_open(const AudioFormat audio_format, - MusicBuffer &buffer, - Error &error) -{ - bool ret = false, enabled = false; - unsigned int i; - - assert(g_music_buffer == nullptr || g_music_buffer == &buffer); - assert((g_mp == nullptr) == (g_music_buffer == nullptr)); - - g_music_buffer = &buffer; - - /* the audio format must be the same as existing chunks in the - pipe */ - assert(g_mp == nullptr || g_mp->CheckFormat(audio_format)); - - if (g_mp == nullptr) - g_mp = new MusicPipe(); - else - /* if the pipe hasn't been cleared, the the audio - format must not have changed */ - assert(g_mp->IsEmpty() || audio_format == input_audio_format); - - input_audio_format = audio_format; - - audio_output_all_reset_reopen(); - audio_output_all_enable_disable(); - audio_output_all_update(); - - for (i = 0; i < num_audio_outputs; ++i) { - if (audio_outputs[i]->enabled) - enabled = true; - - if (audio_outputs[i]->open) - ret = true; - } - - if (!enabled) - error.Set(output_domain, "All audio outputs are disabled"); - else if (!ret) - /* TODO: obtain real error */ - error.Set(output_domain, "Failed to open audio output"); - - if (!ret) - /* close all devices if there was an error */ - audio_output_all_close(); - - return ret; -} - -/** - * Has the specified audio output already consumed this chunk? - */ -static bool -chunk_is_consumed_in(const struct audio_output *ao, - const struct music_chunk *chunk) -{ - if (!ao->open) - return true; - - if (ao->chunk == nullptr) - return false; - - assert(chunk == ao->chunk || g_mp->Contains(ao->chunk)); - - if (chunk != ao->chunk) { - assert(chunk->next != nullptr); - return true; - } - - return ao->chunk_finished && chunk->next == nullptr; -} - -/** - * Has this chunk been consumed by all audio outputs? - */ -static bool -chunk_is_consumed(const struct music_chunk *chunk) -{ - for (unsigned i = 0; i < num_audio_outputs; ++i) { - struct audio_output *ao = audio_outputs[i]; - - const ScopeLock protect(ao->mutex); - if (!chunk_is_consumed_in(ao, chunk)) - return false; - } - - return true; -} - -/** - * There's only one chunk left in the pipe (#g_mp), and all audio - * outputs have consumed it already. Clear the reference. - */ -static void -clear_tail_chunk(gcc_unused const struct music_chunk *chunk, bool *locked) -{ - assert(chunk->next == nullptr); - assert(g_mp->Contains(chunk)); - - for (unsigned i = 0; i < num_audio_outputs; ++i) { - struct audio_output *ao = audio_outputs[i]; - - /* this mutex will be unlocked by the caller when it's - ready */ - ao->mutex.lock(); - locked[i] = ao->open; - - if (!locked[i]) { - ao->mutex.unlock(); - continue; - } - - assert(ao->chunk == chunk); - assert(ao->chunk_finished); - ao->chunk = nullptr; - } -} - -unsigned -audio_output_all_check(void) -{ - const struct music_chunk *chunk; - bool is_tail; - struct music_chunk *shifted; - bool locked[num_audio_outputs]; - - assert(g_music_buffer != nullptr); - assert(g_mp != nullptr); - - while ((chunk = g_mp->Peek()) != nullptr) { - assert(!g_mp->IsEmpty()); - - if (!chunk_is_consumed(chunk)) - /* at least one output is not finished playing - this chunk */ - return g_mp->GetSize(); - - if (chunk->length > 0 && chunk->times >= 0.0) - /* only update elapsed_time if the chunk - provides a defined value */ - audio_output_all_elapsed_time = chunk->times; - - is_tail = chunk->next == nullptr; - if (is_tail) - /* this is the tail of the pipe - clear the - chunk reference in all outputs */ - clear_tail_chunk(chunk, locked); - - /* remove the chunk from the pipe */ - shifted = g_mp->Shift(); - assert(shifted == chunk); - - if (is_tail) - /* unlock all audio outputs which were locked - by clear_tail_chunk() */ - for (unsigned i = 0; i < num_audio_outputs; ++i) - if (locked[i]) - audio_outputs[i]->mutex.unlock(); - - /* return the chunk to the buffer */ - g_music_buffer->Return(shifted); - } - - return 0; -} - -bool -audio_output_all_wait(PlayerControl &pc, unsigned threshold) -{ - pc.Lock(); - - if (audio_output_all_check() < threshold) { - pc.Unlock(); - return true; - } - - pc.Wait(); - pc.Unlock(); - - return audio_output_all_check() < threshold; -} - -void -audio_output_all_pause(void) -{ - unsigned int i; - - audio_output_all_update(); - - for (i = 0; i < num_audio_outputs; ++i) - audio_output_pause(audio_outputs[i]); - - audio_output_wait_all(); -} - -void -audio_output_all_drain(void) -{ - for (unsigned i = 0; i < num_audio_outputs; ++i) - audio_output_drain_async(audio_outputs[i]); - - audio_output_wait_all(); -} - -void -audio_output_all_cancel(void) -{ - unsigned int i; - - /* send the cancel() command to all audio outputs */ - - for (i = 0; i < num_audio_outputs; ++i) - audio_output_cancel(audio_outputs[i]); - - audio_output_wait_all(); - - /* clear the music pipe and return all chunks to the buffer */ - - if (g_mp != nullptr) - g_mp->Clear(*g_music_buffer); - - /* the audio outputs are now waiting for a signal, to - synchronize the cleared music pipe */ - - audio_output_allow_play_all(); - - /* invalidate elapsed_time */ - - audio_output_all_elapsed_time = -1.0; -} - -void -audio_output_all_close(void) -{ - unsigned int i; - - for (i = 0; i < num_audio_outputs; ++i) - audio_output_close(audio_outputs[i]); - - if (g_mp != nullptr) { - assert(g_music_buffer != nullptr); - - g_mp->Clear(*g_music_buffer); - delete g_mp; - g_mp = nullptr; - } - - g_music_buffer = nullptr; - - input_audio_format.Clear(); - - audio_output_all_elapsed_time = -1.0; -} - -void -audio_output_all_release(void) -{ - unsigned int i; - - for (i = 0; i < num_audio_outputs; ++i) - audio_output_release(audio_outputs[i]); - - if (g_mp != nullptr) { - assert(g_music_buffer != nullptr); - - g_mp->Clear(*g_music_buffer); - delete g_mp; - g_mp = nullptr; - } - - g_music_buffer = nullptr; - - input_audio_format.Clear(); - - audio_output_all_elapsed_time = -1.0; -} - -void -audio_output_all_song_border(void) -{ - /* clear the elapsed_time pointer at the beginning of a new - song */ - audio_output_all_elapsed_time = 0.0; -} - -float -audio_output_all_get_elapsed_time(void) -{ - return audio_output_all_elapsed_time; -} diff --git a/src/OutputAll.hxx b/src/OutputAll.hxx deleted file mode 100644 index b6166eb48..000000000 --- a/src/OutputAll.hxx +++ /dev/null @@ -1,174 +0,0 @@ -/* - * 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. - */ - -/* - * Functions for dealing with all configured (enabled) audion outputs - * at once. - * - */ - -#ifndef OUTPUT_ALL_H -#define OUTPUT_ALL_H - -#include "ReplayGainInfo.hxx" -#include "Compiler.h" - -struct AudioFormat; -class MusicBuffer; -struct music_chunk; -struct PlayerControl; -class Error; - -/** - * Global initialization: load audio outputs from the configuration - * file and initialize them. - */ -void -audio_output_all_init(PlayerControl &pc); - -/** - * Global finalization: free memory occupied by audio outputs. All - */ -void -audio_output_all_finish(void); - -/** - * Returns the total number of audio output devices, including those - * who are disabled right now. - */ -gcc_const -unsigned int audio_output_count(void); - -/** - * Returns the "i"th audio output device. - */ -gcc_const -struct audio_output * -audio_output_get(unsigned i); - -/** - * Returns the audio output device with the specified name. Returns - * NULL if the name does not exist. - */ -gcc_pure -struct audio_output * -audio_output_find(const char *name); - -/** - * Checks the "enabled" flag of all audio outputs, and if one has - * changed, commit the change. - */ -void -audio_output_all_enable_disable(void); - -/** - * Opens all audio outputs which are not disabled. - * - * @param audio_format the preferred audio format - * @param buffer the #music_buffer where consumed #music_chunk objects - * should be returned - * @return true on success, false on failure - */ -bool -audio_output_all_open(AudioFormat audio_format, - MusicBuffer &buffer, - Error &error); - -/** - * Closes all audio outputs. - */ -void -audio_output_all_close(void); - -/** - * Closes all audio outputs. Outputs with the "always_on" flag are - * put into pause mode. - */ -void -audio_output_all_release(void); - -void -audio_output_all_set_replay_gain_mode(ReplayGainMode mode); - -/** - * Enqueue a #music_chunk object for playing, i.e. pushes it to a - * #MusicPipe. - * - * @param chunk the #music_chunk object to be played - * @return true on success, false if no audio output was able to play - * (all closed then) - */ -bool -audio_output_all_play(music_chunk *chunk, Error &error); - -/** - * Checks if the output devices have drained their music pipe, and - * returns the consumed music chunks to the #music_buffer. - * - * @return the number of chunks to play left in the #MusicPipe - */ -unsigned -audio_output_all_check(void); - -/** - * Checks if the size of the #MusicPipe is below the #threshold. If - * not, it attempts to synchronize with all output threads, and waits - * until another #music_chunk is finished. - * - * @param threshold the maximum number of chunks in the pipe - * @return true if there are less than #threshold chunks in the pipe - */ -bool -audio_output_all_wait(PlayerControl &pc, unsigned threshold); - -/** - * Puts all audio outputs into pause mode. Most implementations will - * simply close it then. - */ -void -audio_output_all_pause(void); - -/** - * Drain all audio outputs. - */ -void -audio_output_all_drain(void); - -/** - * Try to cancel data which may still be in the device's buffers. - */ -void -audio_output_all_cancel(void); - -/** - * Indicate that a new song will begin now. - */ -void -audio_output_all_song_border(void); - -/** - * Returns the "elapsed_time" stamp of the most recently finished - * chunk. A negative value is returned when no chunk has been - * finished yet. - */ -gcc_pure -float -audio_output_all_get_elapsed_time(void); - -#endif diff --git a/src/OutputCommand.cxx b/src/OutputCommand.cxx deleted file mode 100644 index 839e9bd88..000000000 --- a/src/OutputCommand.cxx +++ /dev/null @@ -1,112 +0,0 @@ -/* - * 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. - */ - -/* - * Glue functions for controlling the audio outputs over the MPD - * protocol. These functions perform extra validation on all - * parameters, because they might be from an untrusted source. - * - */ - -#include "config.h" -#include "OutputCommand.hxx" -#include "OutputAll.hxx" -#include "OutputInternal.hxx" -#include "PlayerControl.hxx" -#include "MixerControl.hxx" -#include "Idle.hxx" - -extern unsigned audio_output_state_version; - -bool -audio_output_enable_index(unsigned idx) -{ - struct audio_output *ao; - - if (idx >= audio_output_count()) - return false; - - ao = audio_output_get(idx); - if (ao->enabled) - return true; - - ao->enabled = true; - idle_add(IDLE_OUTPUT); - - ao->player_control->UpdateAudio(); - - ++audio_output_state_version; - - return true; -} - -bool -audio_output_disable_index(unsigned idx) -{ - struct audio_output *ao; - - if (idx >= audio_output_count()) - return false; - - ao = audio_output_get(idx); - if (!ao->enabled) - return true; - - ao->enabled = false; - idle_add(IDLE_OUTPUT); - - Mixer *mixer = ao->mixer; - if (mixer != nullptr) { - mixer_close(mixer); - idle_add(IDLE_MIXER); - } - - ao->player_control->UpdateAudio(); - - ++audio_output_state_version; - - return true; -} - -bool -audio_output_toggle_index(unsigned idx) -{ - struct audio_output *ao; - - if (idx >= audio_output_count()) - return false; - - ao = audio_output_get(idx); - const bool enabled = ao->enabled = !ao->enabled; - idle_add(IDLE_OUTPUT); - - if (!enabled) { - Mixer *mixer = ao->mixer; - if (mixer != nullptr) { - mixer_close(mixer); - idle_add(IDLE_MIXER); - } - } - - ao->player_control->UpdateAudio(); - - ++audio_output_state_version; - - return true; -} diff --git a/src/OutputCommand.hxx b/src/OutputCommand.hxx deleted file mode 100644 index 4c44dff53..000000000 --- a/src/OutputCommand.hxx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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. - */ - -/* - * Glue functions for controlling the audio outputs over the MPD - * protocol. These functions perform extra validation on all - * parameters, because they might be from an untrusted source. - * - */ - -#ifndef MPD_OUTPUT_COMMAND_HXX -#define MPD_OUTPUT_COMMAND_HXX - -/** - * Enables an audio output. Returns false if the specified output - * does not exist. - */ -bool -audio_output_enable_index(unsigned idx); - -/** - * Disables an audio output. Returns false if the specified output - * does not exist. - */ -bool -audio_output_disable_index(unsigned idx); - -/** - * Toggles an audio output. Returns false if the specified output - * does not exist. - */ -bool -audio_output_toggle_index(unsigned idx); - -#endif diff --git a/src/OutputControl.cxx b/src/OutputControl.cxx deleted file mode 100644 index b938754fd..000000000 --- a/src/OutputControl.cxx +++ /dev/null @@ -1,325 +0,0 @@ - -/* - * 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 "OutputControl.hxx" -#include "OutputThread.hxx" -#include "OutputInternal.hxx" -#include "OutputPlugin.hxx" -#include "OutputError.hxx" -#include "MixerControl.hxx" -#include "notify.hxx" -#include "filter/ReplayGainFilterPlugin.hxx" -#include "util/Error.hxx" -#include "Log.hxx" - -#include - -/** after a failure, wait this number of seconds before - automatically reopening the device */ -static constexpr unsigned REOPEN_AFTER = 10; - -struct notify audio_output_client_notify; - -/** - * Waits for command completion. - * - * @param ao the #audio_output instance; must be locked - */ -static void ao_command_wait(struct audio_output *ao) -{ - while (ao->command != AO_COMMAND_NONE) { - ao->mutex.unlock(); - audio_output_client_notify.Wait(); - ao->mutex.lock(); - } -} - -/** - * Sends a command to the #audio_output object, but does not wait for - * completion. - * - * @param ao the #audio_output instance; must be locked - */ -static void ao_command_async(struct audio_output *ao, - enum audio_output_command cmd) -{ - assert(ao->command == AO_COMMAND_NONE); - ao->command = cmd; - ao->cond.signal(); -} - -/** - * Sends a command to the #audio_output object and waits for - * completion. - * - * @param ao the #audio_output instance; must be locked - */ -static void -ao_command(struct audio_output *ao, enum audio_output_command cmd) -{ - ao_command_async(ao, cmd); - ao_command_wait(ao); -} - -/** - * Lock the #audio_output object and execute the command - * synchronously. - */ -static void -ao_lock_command(struct audio_output *ao, enum audio_output_command cmd) -{ - const ScopeLock protect(ao->mutex); - ao_command(ao, cmd); -} - -void -audio_output_set_replay_gain_mode(struct audio_output *ao, - ReplayGainMode mode) -{ - if (ao->replay_gain_filter != nullptr) - replay_gain_filter_set_mode(ao->replay_gain_filter, mode); - if (ao->other_replay_gain_filter != nullptr) - replay_gain_filter_set_mode(ao->other_replay_gain_filter, mode); -} - -void -audio_output_enable(struct audio_output *ao) -{ - if (!ao->thread.IsDefined()) { - if (ao->plugin->enable == nullptr) { - /* don't bother to start the thread now if the - device doesn't even have a enable() method; - just assign the variable and we're done */ - ao->really_enabled = true; - return; - } - - audio_output_thread_start(ao); - } - - ao_lock_command(ao, AO_COMMAND_ENABLE); -} - -void -audio_output_disable(struct audio_output *ao) -{ - if (!ao->thread.IsDefined()) { - if (ao->plugin->disable == nullptr) - ao->really_enabled = false; - else - /* if there's no thread yet, the device cannot - be enabled */ - assert(!ao->really_enabled); - - return; - } - - ao_lock_command(ao, AO_COMMAND_DISABLE); -} - -/** - * Object must be locked (and unlocked) by the caller. - */ -static bool -audio_output_open(struct audio_output *ao, - const AudioFormat audio_format, - const MusicPipe &mp) -{ - bool open; - - assert(ao != nullptr); - assert(ao->allow_play); - assert(audio_format.IsValid()); - - ao->fail_timer.Reset(); - - if (ao->open && audio_format == ao->in_audio_format) { - assert(ao->pipe == &mp || - (ao->always_on && ao->pause)); - - if (ao->pause) { - ao->chunk = nullptr; - ao->pipe = ∓ - - /* unpause with the CANCEL command; this is a - hack, but suits well for forcing the thread - to leave the ao_pause() thread, and we need - to flush the device buffer anyway */ - - /* we're not using audio_output_cancel() here, - because that function is asynchronous */ - ao_command(ao, AO_COMMAND_CANCEL); - } - - return true; - } - - ao->in_audio_format = audio_format; - ao->chunk = nullptr; - - ao->pipe = ∓ - - if (!ao->thread.IsDefined()) - audio_output_thread_start(ao); - - ao_command(ao, ao->open ? AO_COMMAND_REOPEN : AO_COMMAND_OPEN); - open = ao->open; - - if (open && ao->mixer != nullptr) { - Error error; - if (!mixer_open(ao->mixer, error)) - FormatWarning(output_domain, - "Failed to open mixer for '%s'", - ao->name); - } - - return open; -} - -/** - * Same as audio_output_close(), but expects the lock to be held by - * the caller. - */ -static void -audio_output_close_locked(struct audio_output *ao) -{ - assert(ao != nullptr); - assert(ao->allow_play); - - if (ao->mixer != nullptr) - mixer_auto_close(ao->mixer); - - assert(!ao->open || !ao->fail_timer.IsDefined()); - - if (ao->open) - ao_command(ao, AO_COMMAND_CLOSE); - else - ao->fail_timer.Reset(); -} - -bool -audio_output_update(struct audio_output *ao, - const AudioFormat audio_format, - const MusicPipe &mp) -{ - const ScopeLock protect(ao->mutex); - - if (ao->enabled && ao->really_enabled) { - if (ao->fail_timer.Check(REOPEN_AFTER * 1000)) { - return audio_output_open(ao, audio_format, mp); - } - } else if (audio_output_is_open(ao)) - audio_output_close_locked(ao); - - return false; -} - -void -audio_output_play(struct audio_output *ao) -{ - const ScopeLock protect(ao->mutex); - - assert(ao->allow_play); - - if (audio_output_is_open(ao) && !ao->in_playback_loop && - !ao->woken_for_play) { - ao->woken_for_play = true; - ao->cond.signal(); - } -} - -void audio_output_pause(struct audio_output *ao) -{ - if (ao->mixer != nullptr && ao->plugin->pause == nullptr) - /* the device has no pause mode: close the mixer, - unless its "global" flag is set (checked by - mixer_auto_close()) */ - mixer_auto_close(ao->mixer); - - const ScopeLock protect(ao->mutex); - - assert(ao->allow_play); - if (audio_output_is_open(ao)) - ao_command_async(ao, AO_COMMAND_PAUSE); -} - -void -audio_output_drain_async(struct audio_output *ao) -{ - const ScopeLock protect(ao->mutex); - - assert(ao->allow_play); - if (audio_output_is_open(ao)) - ao_command_async(ao, AO_COMMAND_DRAIN); -} - -void audio_output_cancel(struct audio_output *ao) -{ - const ScopeLock protect(ao->mutex); - - if (audio_output_is_open(ao)) { - ao->allow_play = false; - ao_command_async(ao, AO_COMMAND_CANCEL); - } -} - -void -audio_output_allow_play(struct audio_output *ao) -{ - const ScopeLock protect(ao->mutex); - - ao->allow_play = true; - if (audio_output_is_open(ao)) - ao->cond.signal(); -} - -void -audio_output_release(struct audio_output *ao) -{ - if (ao->always_on) - audio_output_pause(ao); - else - audio_output_close(ao); -} - -void audio_output_close(struct audio_output *ao) -{ - assert(ao != nullptr); - assert(!ao->open || !ao->fail_timer.IsDefined()); - - const ScopeLock protect(ao->mutex); - audio_output_close_locked(ao); -} - -void audio_output_finish(struct audio_output *ao) -{ - audio_output_close(ao); - - assert(!ao->fail_timer.IsDefined()); - - if (ao->thread.IsDefined()) { - assert(ao->allow_play); - ao_lock_command(ao, AO_COMMAND_KILL); - ao->thread.Join(); - } - - audio_output_free(ao); -} diff --git a/src/OutputControl.hxx b/src/OutputControl.hxx deleted file mode 100644 index 7195412ef..000000000 --- a/src/OutputControl.hxx +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_OUTPUT_CONTROL_HXX -#define MPD_OUTPUT_CONTROL_HXX - -#include "ReplayGainInfo.hxx" - -#include - -struct audio_output; -struct AudioFormat; -struct config_param; -class MusicPipe; - -void -audio_output_set_replay_gain_mode(audio_output *ao, - ReplayGainMode mode); - -/** - * Enables the device. - */ -void -audio_output_enable(audio_output *ao); - -/** - * Disables the device. - */ -void -audio_output_disable(audio_output *ao); - -/** - * Opens or closes the device, depending on the "enabled" flag. - * - * @return true if the device is open - */ -bool -audio_output_update(audio_output *ao, - AudioFormat audio_format, - const MusicPipe &mp); - -void -audio_output_play(audio_output *ao); - -void -audio_output_pause(audio_output *ao); - -void -audio_output_drain_async(audio_output *ao); - -/** - * Clear the "allow_play" flag and send the "CANCEL" command - * asynchronously. To finish the operation, the caller has to call - * audio_output_allow_play(). - */ -void -audio_output_cancel(audio_output *ao); - -/** - * Set the "allow_play" and signal the thread. - */ -void -audio_output_allow_play(audio_output *ao); - -void -audio_output_close(audio_output *ao); - -/** - * Closes the audio output, but if the "always_on" flag is set, put it - * into pause mode instead. - */ -void -audio_output_release(audio_output *ao); - -void -audio_output_finish(audio_output *ao); - -#endif diff --git a/src/OutputError.cxx b/src/OutputError.cxx deleted file mode 100644 index 9d4128912..000000000 --- a/src/OutputError.cxx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 "OutputError.hxx" -#include "util/Domain.hxx" - -const Domain output_domain("output"); diff --git a/src/OutputError.hxx b/src/OutputError.hxx deleted file mode 100644 index e3a20142f..000000000 --- a/src/OutputError.hxx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_OUTPUT_ERROR_HXX -#define MPD_OUTPUT_ERROR_HXX - -extern const class Domain output_domain; - -#endif diff --git a/src/OutputFinish.cxx b/src/OutputFinish.cxx deleted file mode 100644 index 43f0dd1ec..000000000 --- a/src/OutputFinish.cxx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 "OutputInternal.hxx" -#include "OutputPlugin.hxx" -#include "MixerControl.hxx" -#include "FilterInternal.hxx" - -#include - -void -ao_base_finish(struct audio_output *ao) -{ - assert(!ao->open); - assert(!ao->fail_timer.IsDefined()); - assert(!ao->thread.IsDefined()); - - if (ao->mixer != nullptr) - mixer_free(ao->mixer); - - delete ao->replay_gain_filter; - delete ao->other_replay_gain_filter; - delete ao->filter; -} - -void -audio_output_free(struct audio_output *ao) -{ - assert(!ao->open); - assert(!ao->fail_timer.IsDefined()); - assert(!ao->thread.IsDefined()); - - ao_plugin_finish(ao); -} diff --git a/src/OutputInit.cxx b/src/OutputInit.cxx deleted file mode 100644 index f5b1bdc81..000000000 --- a/src/OutputInit.cxx +++ /dev/null @@ -1,329 +0,0 @@ -/* - * 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 "OutputInternal.hxx" -#include "OutputList.hxx" -#include "OutputError.hxx" -#include "OutputAPI.hxx" -#include "FilterConfig.hxx" -#include "AudioParser.hxx" -#include "MixerList.hxx" -#include "MixerType.hxx" -#include "MixerControl.hxx" -#include "mixer/SoftwareMixerPlugin.hxx" -#include "FilterPlugin.hxx" -#include "FilterRegistry.hxx" -#include "filter/AutoConvertFilterPlugin.hxx" -#include "filter/ReplayGainFilterPlugin.hxx" -#include "filter/ChainFilterPlugin.hxx" -#include "ConfigError.hxx" -#include "ConfigGlobal.hxx" -#include "util/Error.hxx" -#include "Log.hxx" - -#include -#include - -#define AUDIO_OUTPUT_TYPE "type" -#define AUDIO_OUTPUT_NAME "name" -#define AUDIO_OUTPUT_FORMAT "format" -#define AUDIO_FILTERS "filters" - -static const struct audio_output_plugin * -audio_output_detect(Error &error) -{ - LogDefault(output_domain, "Attempt to detect audio output device"); - - audio_output_plugins_for_each(plugin) { - if (plugin->test_default_device == nullptr) - continue; - - FormatDefault(output_domain, - "Attempting to detect a %s audio device", - plugin->name); - if (ao_plugin_test_default_device(plugin)) - return plugin; - } - - error.Set(output_domain, "Unable to detect an audio device"); - return nullptr; -} - -/** - * Determines the mixer type which should be used for the specified - * configuration block. - * - * This handles the deprecated options mixer_type (global) and - * mixer_enabled, if the mixer_type setting is not configured. - */ -gcc_pure -static enum mixer_type -audio_output_mixer_type(const config_param ¶m) -{ - /* read the local "mixer_type" setting */ - const char *p = param.GetBlockValue("mixer_type"); - if (p != nullptr) - return mixer_type_parse(p); - - /* try the local "mixer_enabled" setting next (deprecated) */ - if (!param.GetBlockValue("mixer_enabled", true)) - return MIXER_TYPE_NONE; - - /* fall back to the global "mixer_type" setting (also - deprecated) */ - return mixer_type_parse(config_get_string(CONF_MIXER_TYPE, - "hardware")); -} - -static Mixer * -audio_output_load_mixer(struct audio_output *ao, - const config_param ¶m, - const struct mixer_plugin *plugin, - Filter &filter_chain, - Error &error) -{ - Mixer *mixer; - - switch (audio_output_mixer_type(param)) { - case MIXER_TYPE_NONE: - case MIXER_TYPE_UNKNOWN: - return nullptr; - - case MIXER_TYPE_HARDWARE: - if (plugin == nullptr) - return nullptr; - - return mixer_new(plugin, ao, param, error); - - case MIXER_TYPE_SOFTWARE: - mixer = mixer_new(&software_mixer_plugin, nullptr, - config_param(), - IgnoreError()); - assert(mixer != nullptr); - - filter_chain_append(filter_chain, "software_mixer", - software_mixer_get_filter(mixer)); - return mixer; - } - - assert(false); - gcc_unreachable(); -} - -bool -ao_base_init(struct audio_output *ao, - const struct audio_output_plugin *plugin, - const config_param ¶m, Error &error) -{ - assert(ao != nullptr); - assert(plugin != nullptr); - assert(plugin->finish != nullptr); - assert(plugin->open != nullptr); - assert(plugin->close != nullptr); - assert(plugin->play != nullptr); - - if (!param.IsNull()) { - ao->name = param.GetBlockValue(AUDIO_OUTPUT_NAME); - if (ao->name == nullptr) { - error.Set(config_domain, - "Missing \"name\" configuration"); - return false; - } - - const char *p = param.GetBlockValue(AUDIO_OUTPUT_FORMAT); - if (p != nullptr) { - bool success = - audio_format_parse(ao->config_audio_format, - p, true, error); - if (!success) - return false; - } else - ao->config_audio_format.Clear(); - } else { - ao->name = "default detected output"; - - ao->config_audio_format.Clear(); - } - - ao->plugin = plugin; - ao->tags = param.GetBlockValue("tags", true); - ao->always_on = param.GetBlockValue("always_on", false); - ao->enabled = param.GetBlockValue("enabled", true); - ao->really_enabled = false; - ao->open = false; - ao->pause = false; - ao->allow_play = true; - ao->in_playback_loop = false; - ao->woken_for_play = false; - - /* set up the filter chain */ - - ao->filter = filter_chain_new(); - assert(ao->filter != nullptr); - - /* create the normalization filter (if configured) */ - - if (config_get_bool(CONF_VOLUME_NORMALIZATION, false)) { - Filter *normalize_filter = - filter_new(&normalize_filter_plugin, config_param(), - IgnoreError()); - assert(normalize_filter != nullptr); - - filter_chain_append(*ao->filter, "normalize", - autoconvert_filter_new(normalize_filter)); - } - - Error filter_error; - filter_chain_parse(*ao->filter, - param.GetBlockValue(AUDIO_FILTERS, ""), - filter_error); - - // It's not really fatal - Part of the filter chain has been set up already - // and even an empty one will work (if only with unexpected behaviour) - if (filter_error.IsDefined()) - FormatError(filter_error, - "Failed to initialize filter chain for '%s'", - ao->name); - - ao->command = AO_COMMAND_NONE; - - ao->mixer = nullptr; - ao->replay_gain_filter = nullptr; - ao->other_replay_gain_filter = nullptr; - - /* done */ - - return true; -} - -static bool -audio_output_setup(struct audio_output *ao, const config_param ¶m, - Error &error) -{ - - /* create the replay_gain filter */ - - const char *replay_gain_handler = - param.GetBlockValue("replay_gain_handler", "software"); - - if (strcmp(replay_gain_handler, "none") != 0) { - ao->replay_gain_filter = filter_new(&replay_gain_filter_plugin, - param, IgnoreError()); - assert(ao->replay_gain_filter != nullptr); - - ao->replay_gain_serial = 0; - - ao->other_replay_gain_filter = filter_new(&replay_gain_filter_plugin, - param, - IgnoreError()); - assert(ao->other_replay_gain_filter != nullptr); - - ao->other_replay_gain_serial = 0; - } else { - ao->replay_gain_filter = nullptr; - ao->other_replay_gain_filter = nullptr; - } - - /* set up the mixer */ - - Error mixer_error; - ao->mixer = audio_output_load_mixer(ao, param, - ao->plugin->mixer_plugin, - *ao->filter, mixer_error); - if (ao->mixer == nullptr && mixer_error.IsDefined()) - FormatError(mixer_error, - "Failed to initialize hardware mixer for '%s'", - ao->name); - - /* use the hardware mixer for replay gain? */ - - if (strcmp(replay_gain_handler, "mixer") == 0) { - if (ao->mixer != nullptr) - replay_gain_filter_set_mixer(ao->replay_gain_filter, - ao->mixer, 100); - else - FormatError(output_domain, - "No such mixer for output '%s'", ao->name); - } else if (strcmp(replay_gain_handler, "software") != 0 && - ao->replay_gain_filter != nullptr) { - error.Set(config_domain, - "Invalid \"replay_gain_handler\" value"); - return false; - } - - /* the "convert" filter must be the last one in the chain */ - - ao->convert_filter = filter_new(&convert_filter_plugin, config_param(), - IgnoreError()); - assert(ao->convert_filter != nullptr); - - filter_chain_append(*ao->filter, "convert", ao->convert_filter); - - return true; -} - -struct audio_output * -audio_output_new(const config_param ¶m, - PlayerControl &pc, - Error &error) -{ - const struct audio_output_plugin *plugin; - - if (!param.IsNull()) { - const char *p; - - p = param.GetBlockValue(AUDIO_OUTPUT_TYPE); - if (p == nullptr) { - error.Set(config_domain, - "Missing \"type\" configuration"); - return nullptr; - } - - plugin = audio_output_plugin_get(p); - if (plugin == nullptr) { - error.Format(config_domain, - "No such audio output plugin: %s", p); - return nullptr; - } - } else { - LogWarning(output_domain, - "No 'audio_output' defined in config file"); - - plugin = audio_output_detect(error); - if (plugin == nullptr) - return nullptr; - - FormatDefault(output_domain, - "Successfully detected a %s audio device", - plugin->name); - } - - struct audio_output *ao = ao_plugin_init(plugin, param, error); - if (ao == nullptr) - return nullptr; - - if (!audio_output_setup(ao, param, error)) { - ao_plugin_finish(ao); - return nullptr; - } - - ao->player_control = &pc; - return ao; -} diff --git a/src/OutputInternal.hxx b/src/OutputInternal.hxx deleted file mode 100644 index 18404dc01..000000000 --- a/src/OutputInternal.hxx +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_OUTPUT_INTERNAL_HXX -#define MPD_OUTPUT_INTERNAL_HXX - -#include "AudioFormat.hxx" -#include "pcm/PcmBuffer.hxx" -#include "pcm/PcmDither.hxx" -#include "thread/Mutex.hxx" -#include "thread/Cond.hxx" -#include "thread/Thread.hxx" -#include "system/PeriodClock.hxx" - -class Error; -class Filter; -class MusicPipe; -struct config_param; -struct PlayerControl; - -enum audio_output_command { - AO_COMMAND_NONE = 0, - AO_COMMAND_ENABLE, - AO_COMMAND_DISABLE, - AO_COMMAND_OPEN, - - /** - * This command is invoked when the input audio format - * changes. - */ - AO_COMMAND_REOPEN, - - AO_COMMAND_CLOSE, - AO_COMMAND_PAUSE, - - /** - * Drains the internal (hardware) buffers of the device. This - * operation may take a while to complete. - */ - AO_COMMAND_DRAIN, - - AO_COMMAND_CANCEL, - AO_COMMAND_KILL -}; - -struct audio_output { - /** - * The device's configured display name. - */ - const char *name; - - /** - * The plugin which implements this output device. - */ - const struct audio_output_plugin *plugin; - - /** - * The #mixer object associated with this audio output device. - * May be nullptr if none is available, or if software volume is - * configured. - */ - class Mixer *mixer; - - /** - * Will this output receive tags from the decoder? The - * default is true, but it may be configured to false to - * suppress sending tags to the output. - */ - bool tags; - - /** - * Shall this output always play something (i.e. silence), - * even when playback is stopped? - */ - bool always_on; - - /** - * Has the user enabled this device? - */ - bool enabled; - - /** - * Is this device actually enabled, i.e. the "enable" method - * has succeeded? - */ - bool really_enabled; - - /** - * Is the device (already) open and functional? - * - * This attribute may only be modified by the output thread. - * It is protected with #mutex: write accesses inside the - * output thread and read accesses outside of it may only be - * performed while the lock is held. - */ - bool open; - - /** - * Is the device paused? i.e. the output thread is in the - * ao_pause() loop. - */ - bool pause; - - /** - * When this flag is set, the output thread will not do any - * playback. It will wait until the flag is cleared. - * - * This is used to synchronize the "clear" operation on the - * shared music pipe during the CANCEL command. - */ - bool allow_play; - - /** - * True while the OutputThread is inside ao_play(). This - * means the PlayerThread does not need to wake up the - * OutputThread when new chunks are added to the MusicPipe, - * because the OutputThread is already watching that. - */ - bool in_playback_loop; - - /** - * Has the OutputThread been woken up to play more chunks? - * This is set by audio_output_play() and reset by ao_play() - * to reduce the number of duplicate wakeups. - */ - bool woken_for_play; - - /** - * If not nullptr, the device has failed, and this timer is used - * to estimate how long it should stay disabled (unless - * explicitly reopened with "play"). - */ - PeriodClock fail_timer; - - /** - * The configured audio format. - */ - AudioFormat config_audio_format; - - /** - * The audio_format in which audio data is received from the - * player thread (which in turn receives it from the decoder). - */ - AudioFormat in_audio_format; - - /** - * The audio_format which is really sent to the device. This - * is basically config_audio_format (if configured) or - * in_audio_format, but may have been modified by - * plugin->open(). - */ - AudioFormat out_audio_format; - - /** - * The buffer used to allocate the cross-fading result. - */ - PcmBuffer cross_fade_buffer; - - /** - * The dithering state for cross-fading two streams. - */ - PcmDither cross_fade_dither; - - /** - * The filter object of this audio output. This is an - * instance of chain_filter_plugin. - */ - Filter *filter; - - /** - * The replay_gain_filter_plugin instance of this audio - * output. - */ - Filter *replay_gain_filter; - - /** - * The serial number of the last replay gain info. 0 means no - * replay gain info was available. - */ - unsigned replay_gain_serial; - - /** - * The replay_gain_filter_plugin instance of this audio - * output, to be applied to the second chunk during - * cross-fading. - */ - Filter *other_replay_gain_filter; - - /** - * The serial number of the last replay gain info by the - * "other" chunk during cross-fading. - */ - unsigned other_replay_gain_serial; - - /** - * The convert_filter_plugin instance of this audio output. - * It is the last item in the filter chain, and is responsible - * for converting the input data into the appropriate format - * for this audio output. - */ - Filter *convert_filter; - - /** - * The thread handle, or nullptr if the output thread isn't - * running. - */ - Thread thread; - - /** - * The next command to be performed by the output thread. - */ - enum audio_output_command command; - - /** - * The music pipe which provides music chunks to be played. - */ - const MusicPipe *pipe; - - /** - * This mutex protects #open, #fail_timer, #chunk and - * #chunk_finished. - */ - Mutex mutex; - - /** - * This condition object wakes up the output thread after - * #command has been set. - */ - Cond cond; - - /** - * The PlayerControl object which "owns" this output. This - * object is needed to signal command completion. - */ - PlayerControl *player_control; - - /** - * The #music_chunk which is currently being played. All - * chunks before this one may be returned to the - * #music_buffer, because they are not going to be used by - * this output anymore. - */ - const struct music_chunk *chunk; - - /** - * Has the output finished playing #chunk? - */ - bool chunk_finished; -}; - -/** - * Notify object used by the thread's client, i.e. we will send a - * notify signal to this object, expecting the caller to wait on it. - */ -extern struct notify audio_output_client_notify; - -static inline bool -audio_output_is_open(const struct audio_output *ao) -{ - return ao->open; -} - -static inline bool -audio_output_command_is_finished(const struct audio_output *ao) -{ - return ao->command == AO_COMMAND_NONE; -} - -struct audio_output * -audio_output_new(const config_param ¶m, - PlayerControl &pc, - Error &error); - -bool -ao_base_init(struct audio_output *ao, - const struct audio_output_plugin *plugin, - const config_param ¶m, Error &error); - -void -ao_base_finish(struct audio_output *ao); - -void -audio_output_free(struct audio_output *ao); - -#endif diff --git a/src/OutputList.cxx b/src/OutputList.cxx deleted file mode 100644 index a190da543..000000000 --- a/src/OutputList.cxx +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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 "OutputList.hxx" -#include "OutputAPI.hxx" -#include "output/AlsaOutputPlugin.hxx" -#include "output/AoOutputPlugin.hxx" -#include "output/FifoOutputPlugin.hxx" -#include "output/HttpdOutputPlugin.hxx" -#include "output/JackOutputPlugin.hxx" -#include "output/NullOutputPlugin.hxx" -#include "output/OpenALOutputPlugin.hxx" -#include "output/OssOutputPlugin.hxx" -#include "output/OSXOutputPlugin.hxx" -#include "output/PipeOutputPlugin.hxx" -#include "output/PulseOutputPlugin.hxx" -#include "output/RecorderOutputPlugin.hxx" -#include "output/RoarOutputPlugin.hxx" -#include "output/ShoutOutputPlugin.hxx" -#include "output/SolarisOutputPlugin.hxx" -#include "output/WinmmOutputPlugin.hxx" - -#include - -const struct audio_output_plugin *const audio_output_plugins[] = { -#ifdef HAVE_SHOUT - &shout_output_plugin, -#endif - &null_output_plugin, -#ifdef HAVE_FIFO - &fifo_output_plugin, -#endif -#ifdef ENABLE_PIPE_OUTPUT - &pipe_output_plugin, -#endif -#ifdef HAVE_ALSA - &alsa_output_plugin, -#endif -#ifdef HAVE_ROAR - &roar_output_plugin, -#endif -#ifdef HAVE_AO - &ao_output_plugin, -#endif -#ifdef HAVE_OSS - &oss_output_plugin, -#endif -#ifdef HAVE_OPENAL - &openal_output_plugin, -#endif -#ifdef HAVE_OSX - &osx_output_plugin, -#endif -#ifdef ENABLE_SOLARIS_OUTPUT - &solaris_output_plugin, -#endif -#ifdef HAVE_PULSE - &pulse_output_plugin, -#endif -#ifdef HAVE_JACK - &jack_output_plugin, -#endif -#ifdef ENABLE_HTTPD_OUTPUT - &httpd_output_plugin, -#endif -#ifdef ENABLE_RECORDER_OUTPUT - &recorder_output_plugin, -#endif -#ifdef ENABLE_WINMM_OUTPUT - &winmm_output_plugin, -#endif - nullptr -}; - -const struct audio_output_plugin * -audio_output_plugin_get(const char *name) -{ - audio_output_plugins_for_each(plugin) - if (strcmp(plugin->name, name) == 0) - return plugin; - - return nullptr; -} diff --git a/src/OutputList.hxx b/src/OutputList.hxx deleted file mode 100644 index dfcf7487c..000000000 --- a/src/OutputList.hxx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_OUTPUT_LIST_HXX -#define MPD_OUTPUT_LIST_HXX - -extern const struct audio_output_plugin *const audio_output_plugins[]; - -const struct audio_output_plugin * -audio_output_plugin_get(const char *name); - -#define audio_output_plugins_for_each(plugin) \ - for (const struct audio_output_plugin *plugin, \ - *const*output_plugin_iterator = &audio_output_plugins[0]; \ - (plugin = *output_plugin_iterator) != nullptr; ++output_plugin_iterator) - -#endif diff --git a/src/OutputPlugin.cxx b/src/OutputPlugin.cxx deleted file mode 100644 index 29fd6455a..000000000 --- a/src/OutputPlugin.cxx +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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 "OutputPlugin.hxx" -#include "OutputInternal.hxx" - -struct audio_output * -ao_plugin_init(const struct audio_output_plugin *plugin, - const config_param ¶m, - Error &error) -{ - assert(plugin != nullptr); - assert(plugin->init != nullptr); - - return plugin->init(param, error); -} - -void -ao_plugin_finish(struct audio_output *ao) -{ - ao->plugin->finish(ao); -} - -bool -ao_plugin_enable(struct audio_output *ao, Error &error_r) -{ - return ao->plugin->enable != nullptr - ? ao->plugin->enable(ao, error_r) - : true; -} - -void -ao_plugin_disable(struct audio_output *ao) -{ - if (ao->plugin->disable != nullptr) - ao->plugin->disable(ao); -} - -bool -ao_plugin_open(struct audio_output *ao, AudioFormat &audio_format, - Error &error) -{ - return ao->plugin->open(ao, audio_format, error); -} - -void -ao_plugin_close(struct audio_output *ao) -{ - ao->plugin->close(ao); -} - -unsigned -ao_plugin_delay(struct audio_output *ao) -{ - return ao->plugin->delay != nullptr - ? ao->plugin->delay(ao) - : 0; -} - -void -ao_plugin_send_tag(struct audio_output *ao, const Tag *tag) -{ - if (ao->plugin->send_tag != nullptr) - ao->plugin->send_tag(ao, tag); -} - -size_t -ao_plugin_play(struct audio_output *ao, const void *chunk, size_t size, - Error &error) -{ - return ao->plugin->play(ao, chunk, size, error); -} - -void -ao_plugin_drain(struct audio_output *ao) -{ - if (ao->plugin->drain != nullptr) - ao->plugin->drain(ao); -} - -void -ao_plugin_cancel(struct audio_output *ao) -{ - if (ao->plugin->cancel != nullptr) - ao->plugin->cancel(ao); -} - -bool -ao_plugin_pause(struct audio_output *ao) -{ - return ao->plugin->pause != nullptr && ao->plugin->pause(ao); -} diff --git a/src/OutputPlugin.hxx b/src/OutputPlugin.hxx deleted file mode 100644 index 7d77c92b2..000000000 --- a/src/OutputPlugin.hxx +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_OUTPUT_PLUGIN_HXX -#define MPD_OUTPUT_PLUGIN_HXX - -#include "Compiler.h" - -#include - -struct config_param; -struct AudioFormat; -struct Tag; -class Error; - -/** - * A plugin which controls an audio output device. - */ -struct audio_output_plugin { - /** - * the plugin's name - */ - const char *name; - - /** - * Test if this plugin can provide a default output, in case - * none has been configured. This method is optional. - */ - bool (*test_default_device)(void); - - /** - * Configure and initialize the device, but do not open it - * yet. - * - * @param param the configuration section, or nullptr if there is - * no configuration - * @return nullptr on error, or an opaque pointer to the plugin's - * data - */ - struct audio_output *(*init)(const config_param ¶m, - Error &error); - - /** - * Free resources allocated by this device. - */ - void (*finish)(struct audio_output *data); - - /** - * Enable the device. This may allocate resources, preparing - * for the device to be opened. Enabling a device cannot - * fail: if an error occurs during that, it should be reported - * by the open() method. - * - * @return true on success, false on error - */ - bool (*enable)(struct audio_output *data, Error &error); - - /** - * Disables the device. It is closed before this method is - * called. - */ - void (*disable)(struct audio_output *data); - - /** - * Really open the device. - * - * @param audio_format the audio format in which data is going - * to be delivered; may be modified by the plugin - */ - bool (*open)(struct audio_output *data, AudioFormat &audio_format, - Error &error); - - /** - * Close the device. - */ - void (*close)(struct audio_output *data); - - /** - * Returns a positive number if the output thread shall delay - * the next call to play() or pause(). This should be - * implemented instead of doing a sleep inside the plugin, - * because this allows MPD to listen to commands meanwhile. - * - * @return the number of milliseconds to wait - */ - unsigned (*delay)(struct audio_output *data); - - /** - * Display metadata for the next chunk. Optional method, - * because not all devices can display metadata. - */ - void (*send_tag)(struct audio_output *data, const Tag *tag); - - /** - * Play a chunk of audio data. - * - * @return the number of bytes played, or 0 on error - */ - size_t (*play)(struct audio_output *data, - const void *chunk, size_t size, - Error &error); - - /** - * Wait until the device has finished playing. - */ - void (*drain)(struct audio_output *data); - - /** - * Try to cancel data which may still be in the device's - * buffers. - */ - void (*cancel)(struct audio_output *data); - - /** - * Pause the device. If supported, it may perform a special - * action, which keeps the device open, but does not play - * anything. Output plugins like "shout" might want to play - * silence during pause, so their clients won't be - * disconnected. Plugins which do not support pausing will - * simply be closed, and have to be reopened when unpaused. - * - * @return false on error (output will be closed then), true - * for continue to pause - */ - bool (*pause)(struct audio_output *data); - - /** - * The mixer plugin associated with this output plugin. This - * may be nullptr if no mixer plugin is implemented. When - * created, this mixer plugin gets the same #config_param as - * this audio output device. - */ - const struct mixer_plugin *mixer_plugin; -}; - -static inline bool -ao_plugin_test_default_device(const struct audio_output_plugin *plugin) -{ - return plugin->test_default_device != nullptr - ? plugin->test_default_device() - : false; -} - -gcc_malloc -struct audio_output * -ao_plugin_init(const struct audio_output_plugin *plugin, - const config_param ¶m, - Error &error); - -void -ao_plugin_finish(struct audio_output *ao); - -bool -ao_plugin_enable(struct audio_output *ao, Error &error); - -void -ao_plugin_disable(struct audio_output *ao); - -bool -ao_plugin_open(struct audio_output *ao, AudioFormat &audio_format, - Error &error); - -void -ao_plugin_close(struct audio_output *ao); - -gcc_pure -unsigned -ao_plugin_delay(struct audio_output *ao); - -void -ao_plugin_send_tag(struct audio_output *ao, const Tag *tag); - -size_t -ao_plugin_play(struct audio_output *ao, const void *chunk, size_t size, - Error &error); - -void -ao_plugin_drain(struct audio_output *ao); - -void -ao_plugin_cancel(struct audio_output *ao); - -bool -ao_plugin_pause(struct audio_output *ao); - -#endif diff --git a/src/OutputPrint.cxx b/src/OutputPrint.cxx deleted file mode 100644 index ee4424df2..000000000 --- a/src/OutputPrint.cxx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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. - */ - -/* - * Protocol specific code for the audio output library. - * - */ - -#include "config.h" -#include "OutputPrint.hxx" -#include "OutputAll.hxx" -#include "OutputInternal.hxx" -#include "Client.hxx" - -void -printAudioDevices(Client &client) -{ - const unsigned n = audio_output_count(); - - for (unsigned i = 0; i < n; ++i) { - const struct audio_output *ao = audio_output_get(i); - - client_printf(client, - "outputid: %i\n" - "outputname: %s\n" - "outputenabled: %i\n", - i, ao->name, ao->enabled); - } -} diff --git a/src/OutputPrint.hxx b/src/OutputPrint.hxx deleted file mode 100644 index 2d94226fd..000000000 --- a/src/OutputPrint.hxx +++ /dev/null @@ -1,34 +0,0 @@ - -/* - * 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. - */ - -/* - * Protocol specific code for the audio output library. - * - */ - -#ifndef MPD_OUTPUT_PRINT_HXX -#define MPD_OUTPUT_PRINT_HXX - -class Client; - -void -printAudioDevices(Client &client); - -#endif diff --git a/src/OutputState.cxx b/src/OutputState.cxx deleted file mode 100644 index 1141abeda..000000000 --- a/src/OutputState.cxx +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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. - */ - -/* - * Saving and loading the audio output states to/from the state file. - * - */ - -#include "config.h" -#include "OutputState.hxx" -#include "OutputAll.hxx" -#include "OutputInternal.hxx" -#include "OutputError.hxx" -#include "Log.hxx" -#include "util/StringUtil.hxx" - -#include -#include - -#define AUDIO_DEVICE_STATE "audio_device_state:" - -unsigned audio_output_state_version; - -void -audio_output_state_save(FILE *fp) -{ - unsigned n = audio_output_count(); - - assert(n > 0); - - for (unsigned i = 0; i < n; ++i) { - const struct audio_output *ao = audio_output_get(i); - - fprintf(fp, AUDIO_DEVICE_STATE "%d:%s\n", - ao->enabled, ao->name); - } -} - -bool -audio_output_state_read(const char *line) -{ - long value; - char *endptr; - const char *name; - struct audio_output *ao; - - if (!StringStartsWith(line, AUDIO_DEVICE_STATE)) - return false; - - line += sizeof(AUDIO_DEVICE_STATE) - 1; - - value = strtol(line, &endptr, 10); - if (*endptr != ':' || (value != 0 && value != 1)) - return false; - - if (value != 0) - /* state is "enabled": no-op */ - return true; - - name = endptr + 1; - ao = audio_output_find(name); - if (ao == NULL) { - FormatDebug(output_domain, - "Ignoring device state for '%s'", name); - return true; - } - - ao->enabled = false; - return true; -} - -unsigned -audio_output_state_get_version(void) -{ - return audio_output_state_version; -} diff --git a/src/OutputState.hxx b/src/OutputState.hxx deleted file mode 100644 index a68180ae4..000000000 --- a/src/OutputState.hxx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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. - */ - -/* - * Saving and loading the audio output states to/from the state file. - * - */ - -#ifndef MPD_OUTPUT_STATE_HXX -#define MPD_OUTPUT_STATE_HXX - -#include - -bool -audio_output_state_read(const char *line); - -void -audio_output_state_save(FILE *fp); - -/** - * Generates a version number for the current state of the audio - * outputs. This is used by timer_save_state_file() to determine - * whether the state has changed and the state file should be saved. - */ -unsigned -audio_output_state_get_version(void); - -#endif diff --git a/src/OutputThread.cxx b/src/OutputThread.cxx deleted file mode 100644 index f7e28b5f4..000000000 --- a/src/OutputThread.cxx +++ /dev/null @@ -1,690 +0,0 @@ -/* - * 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 -#include - -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); -} diff --git a/src/OutputThread.hxx b/src/OutputThread.hxx deleted file mode 100644 index 1cdbd65f2..000000000 --- a/src/OutputThread.hxx +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_OUTPUT_THREAD_HXX -#define MPD_OUTPUT_THREAD_HXX - -struct audio_output; - -void -audio_output_thread_start(audio_output *ao); - -#endif diff --git a/src/PlayerThread.cxx b/src/PlayerThread.cxx index 750a1732a..bde3c1372 100644 --- a/src/PlayerThread.cxx +++ b/src/PlayerThread.cxx @@ -28,7 +28,7 @@ #include "system/FatalError.hxx" #include "CrossFade.hxx" #include "PlayerControl.hxx" -#include "OutputAll.hxx" +#include "output/OutputAll.hxx" #include "tag/Tag.hxx" #include "Idle.hxx" #include "GlobalEvents.hxx" diff --git a/src/StateFile.cxx b/src/StateFile.cxx index 392b3454b..06b7f0779 100644 --- a/src/StateFile.cxx +++ b/src/StateFile.cxx @@ -19,7 +19,7 @@ #include "config.h" #include "StateFile.hxx" -#include "OutputState.hxx" +#include "output/OutputState.hxx" #include "PlaylistState.hxx" #include "fs/TextFile.hxx" #include "Partition.hxx" diff --git a/src/command/OutputCommands.cxx b/src/command/OutputCommands.cxx index 5f708bf0d..4ed3b89f1 100644 --- a/src/command/OutputCommands.cxx +++ b/src/command/OutputCommands.cxx @@ -19,8 +19,8 @@ #include "config.h" #include "OutputCommands.hxx" -#include "OutputPrint.hxx" -#include "OutputCommand.hxx" +#include "output/OutputPrint.hxx" +#include "output/OutputCommand.hxx" #include "protocol/Result.hxx" #include "protocol/ArgParser.hxx" diff --git a/src/command/PlayerCommands.cxx b/src/command/PlayerCommands.cxx index 196fcc0bd..ae7a7ba89 100644 --- a/src/command/PlayerCommands.cxx +++ b/src/command/PlayerCommands.cxx @@ -25,7 +25,7 @@ #include "UpdateGlue.hxx" #include "Client.hxx" #include "Volume.hxx" -#include "OutputAll.hxx" +#include "output/OutputAll.hxx" #include "Partition.hxx" #include "protocol/Result.hxx" #include "protocol/ArgParser.hxx" diff --git a/src/mixer/AlsaMixerPlugin.cxx b/src/mixer/AlsaMixerPlugin.cxx index 86a949a8f..9d04ad98b 100644 --- a/src/mixer/AlsaMixerPlugin.cxx +++ b/src/mixer/AlsaMixerPlugin.cxx @@ -19,7 +19,7 @@ #include "config.h" #include "MixerInternal.hxx" -#include "OutputAPI.hxx" +#include "output/OutputAPI.hxx" #include "GlobalEvents.hxx" #include "Main.hxx" #include "event/MultiSocketMonitor.hxx" diff --git a/src/mixer/PulseMixerPlugin.cxx b/src/mixer/PulseMixerPlugin.cxx index 4d07a0b68..a379cebae 100644 --- a/src/mixer/PulseMixerPlugin.cxx +++ b/src/mixer/PulseMixerPlugin.cxx @@ -20,7 +20,7 @@ #include "config.h" #include "PulseMixerPlugin.hxx" #include "MixerInternal.hxx" -#include "output/PulseOutputPlugin.hxx" +#include "output/plugins/PulseOutputPlugin.hxx" #include "GlobalEvents.hxx" #include "util/Error.hxx" #include "util/Domain.hxx" diff --git a/src/mixer/RoarMixerPlugin.cxx b/src/mixer/RoarMixerPlugin.cxx index 839b35a6b..18f8c14f8 100644 --- a/src/mixer/RoarMixerPlugin.cxx +++ b/src/mixer/RoarMixerPlugin.cxx @@ -21,7 +21,7 @@ #include "config.h" #include "MixerInternal.hxx" -#include "output/RoarOutputPlugin.hxx" +#include "output/plugins/RoarOutputPlugin.hxx" #include "Compiler.h" struct RoarMixer final : public Mixer { diff --git a/src/mixer/WinmmMixerPlugin.cxx b/src/mixer/WinmmMixerPlugin.cxx index bd0ec4b09..6f10fd71b 100644 --- a/src/mixer/WinmmMixerPlugin.cxx +++ b/src/mixer/WinmmMixerPlugin.cxx @@ -19,8 +19,8 @@ #include "config.h" #include "MixerInternal.hxx" -#include "OutputAPI.hxx" -#include "output/WinmmOutputPlugin.hxx" +#include "output/OutputAPI.hxx" +#include "output/plugins/WinmmOutputPlugin.hxx" #include "util/Error.hxx" #include "util/Domain.hxx" diff --git a/src/output/AlsaOutputPlugin.cxx b/src/output/AlsaOutputPlugin.cxx deleted file mode 100644 index 87370ba99..000000000 --- a/src/output/AlsaOutputPlugin.cxx +++ /dev/null @@ -1,868 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "config.h" -#include "AlsaOutputPlugin.hxx" -#include "OutputAPI.hxx" -#include "MixerList.hxx" -#include "pcm/PcmExport.hxx" -#include "util/Manual.hxx" -#include "util/Error.hxx" -#include "util/Domain.hxx" -#include "Log.hxx" - -#include - -#include - -#define ALSA_PCM_NEW_HW_PARAMS_API -#define ALSA_PCM_NEW_SW_PARAMS_API - -static const char default_device[] = "default"; - -static constexpr unsigned MPD_ALSA_BUFFER_TIME_US = 500000; - -#define MPD_ALSA_RETRY_NR 5 - -typedef snd_pcm_sframes_t alsa_writei_t(snd_pcm_t * pcm, const void *buffer, - snd_pcm_uframes_t size); - -struct AlsaOutput { - struct audio_output base; - - Manual pcm_export; - - /** - * The configured name of the ALSA device; empty for the - * default device - */ - std::string device; - - /** use memory mapped I/O? */ - bool use_mmap; - - /** - * Enable DSD over USB according to the dCS suggested - * standard? - * - * @see http://www.dcsltd.co.uk/page/assets/DSDoverUSB.pdf - */ - bool dsd_usb; - - /** libasound's buffer_time setting (in microseconds) */ - unsigned int buffer_time; - - /** libasound's period_time setting (in microseconds) */ - unsigned int period_time; - - /** the mode flags passed to snd_pcm_open */ - int mode; - - /** the libasound PCM device handle */ - snd_pcm_t *pcm; - - /** - * a pointer to the libasound writei() function, which is - * snd_pcm_writei() or snd_pcm_mmap_writei(), depending on the - * use_mmap configuration - */ - alsa_writei_t *writei; - - /** - * The size of one audio frame passed to method play(). - */ - size_t in_frame_size; - - /** - * The size of one audio frame passed to libasound. - */ - size_t out_frame_size; - - /** - * The size of one period, in number of frames. - */ - snd_pcm_uframes_t period_frames; - - /** - * The number of frames written in the current period. - */ - snd_pcm_uframes_t period_position; - - /** - * Set to non-zero when the Raspberry Pi workaround has been - * activated in alsa_recover(); decremented by each write. - * This will avoid activating it again, leading to an endless - * loop. This problem was observed with a "RME Digi9636/52". - */ - unsigned pi_workaround; - - /** - * This buffer gets allocated after opening the ALSA device. - * It contains silence samples, enough to fill one period (see - * #period_frames). - */ - uint8_t *silence; - - AlsaOutput():mode(0), writei(snd_pcm_writei) { - } - - bool Init(const config_param ¶m, Error &error) { - return ao_base_init(&base, &alsa_output_plugin, - param, error); - } - - void Deinit() { - ao_base_finish(&base); - } -}; - -static constexpr Domain alsa_output_domain("alsa_output"); - -static const char * -alsa_device(const AlsaOutput *ad) -{ - return ad->device.empty() ? default_device : ad->device.c_str(); -} - -static void -alsa_configure(AlsaOutput *ad, const config_param ¶m) -{ - ad->device = param.GetBlockValue("device", ""); - - ad->use_mmap = param.GetBlockValue("use_mmap", false); - - ad->dsd_usb = param.GetBlockValue("dsd_usb", false); - - ad->buffer_time = param.GetBlockValue("buffer_time", - MPD_ALSA_BUFFER_TIME_US); - ad->period_time = param.GetBlockValue("period_time", 0u); - -#ifdef SND_PCM_NO_AUTO_RESAMPLE - if (!param.GetBlockValue("auto_resample", true)) - ad->mode |= SND_PCM_NO_AUTO_RESAMPLE; -#endif - -#ifdef SND_PCM_NO_AUTO_CHANNELS - if (!param.GetBlockValue("auto_channels", true)) - ad->mode |= SND_PCM_NO_AUTO_CHANNELS; -#endif - -#ifdef SND_PCM_NO_AUTO_FORMAT - if (!param.GetBlockValue("auto_format", true)) - ad->mode |= SND_PCM_NO_AUTO_FORMAT; -#endif -} - -static struct audio_output * -alsa_init(const config_param ¶m, Error &error) -{ - AlsaOutput *ad = new AlsaOutput(); - - if (!ad->Init(param, error)) { - delete ad; - return nullptr; - } - - alsa_configure(ad, param); - - return &ad->base; -} - -static void -alsa_finish(struct audio_output *ao) -{ - AlsaOutput *ad = (AlsaOutput *)ao; - - ad->Deinit(); - delete ad; - - /* free libasound's config cache */ - snd_config_update_free_global(); -} - -static bool -alsa_output_enable(struct audio_output *ao, gcc_unused Error &error) -{ - AlsaOutput *ad = (AlsaOutput *)ao; - - ad->pcm_export.Construct(); - return true; -} - -static void -alsa_output_disable(struct audio_output *ao) -{ - AlsaOutput *ad = (AlsaOutput *)ao; - - ad->pcm_export.Destruct(); -} - -static bool -alsa_test_default_device(void) -{ - snd_pcm_t *handle; - - int ret = snd_pcm_open(&handle, default_device, - SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); - if (ret) { - FormatError(alsa_output_domain, - "Error opening default ALSA device: %s", - snd_strerror(-ret)); - return false; - } else - snd_pcm_close(handle); - - return true; -} - -static snd_pcm_format_t -get_bitformat(SampleFormat sample_format) -{ - switch (sample_format) { - case SampleFormat::UNDEFINED: - case SampleFormat::DSD: - return SND_PCM_FORMAT_UNKNOWN; - - case SampleFormat::S8: - return SND_PCM_FORMAT_S8; - - case SampleFormat::S16: - return SND_PCM_FORMAT_S16; - - case SampleFormat::S24_P32: - return SND_PCM_FORMAT_S24; - - case SampleFormat::S32: - return SND_PCM_FORMAT_S32; - - case SampleFormat::FLOAT: - return SND_PCM_FORMAT_FLOAT; - } - - assert(false); - gcc_unreachable(); -} - -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; - } -} - -static snd_pcm_format_t -alsa_to_packed_format(snd_pcm_format_t fmt) -{ - switch (fmt) { - case SND_PCM_FORMAT_S24_LE: - return SND_PCM_FORMAT_S24_3LE; - - case SND_PCM_FORMAT_S24_BE: - return SND_PCM_FORMAT_S24_3BE; - - default: - return SND_PCM_FORMAT_UNKNOWN; - } -} - -static int -alsa_try_format_or_packed(snd_pcm_t *pcm, snd_pcm_hw_params_t *hwparams, - snd_pcm_format_t fmt, bool *packed_r) -{ - int err = snd_pcm_hw_params_set_format(pcm, hwparams, fmt); - if (err == 0) - *packed_r = false; - - if (err != -EINVAL) - return err; - - fmt = alsa_to_packed_format(fmt); - if (fmt == SND_PCM_FORMAT_UNKNOWN) - return -EINVAL; - - err = snd_pcm_hw_params_set_format(pcm, hwparams, fmt); - if (err == 0) - *packed_r = true; - - return err; -} - -/** - * Attempts to configure the specified sample format, and tries the - * reversed host byte order if was not supported. - */ -static int -alsa_output_try_format(snd_pcm_t *pcm, snd_pcm_hw_params_t *hwparams, - SampleFormat sample_format, - bool *packed_r, bool *reverse_endian_r) -{ - snd_pcm_format_t alsa_format = get_bitformat(sample_format); - if (alsa_format == SND_PCM_FORMAT_UNKNOWN) - return -EINVAL; - - int err = alsa_try_format_or_packed(pcm, hwparams, alsa_format, - packed_r); - if (err == 0) - *reverse_endian_r = false; - - if (err != -EINVAL) - return err; - - alsa_format = byteswap_bitformat(alsa_format); - if (alsa_format == SND_PCM_FORMAT_UNKNOWN) - return -EINVAL; - - err = alsa_try_format_or_packed(pcm, hwparams, alsa_format, packed_r); - if (err == 0) - *reverse_endian_r = true; - - return err; -} - -/** - * Configure a sample format, and probe other formats if that fails. - */ -static int -alsa_output_setup_format(snd_pcm_t *pcm, snd_pcm_hw_params_t *hwparams, - AudioFormat &audio_format, - bool *packed_r, bool *reverse_endian_r) -{ - /* try the input format first */ - - int err = alsa_output_try_format(pcm, hwparams, - audio_format.format, - packed_r, reverse_endian_r); - - /* if unsupported by the hardware, try other formats */ - - static const SampleFormat probe_formats[] = { - SampleFormat::S24_P32, - SampleFormat::S32, - SampleFormat::S16, - SampleFormat::S8, - SampleFormat::UNDEFINED, - }; - - for (unsigned i = 0; - err == -EINVAL && probe_formats[i] != SampleFormat::UNDEFINED; - ++i) { - const SampleFormat mpd_format = probe_formats[i]; - if (mpd_format == audio_format.format) - continue; - - err = alsa_output_try_format(pcm, hwparams, mpd_format, - packed_r, reverse_endian_r); - if (err == 0) - audio_format.format = mpd_format; - } - - return err; -} - -/** - * Set up the snd_pcm_t object which was opened by the caller. Set up - * the configured settings and the audio format. - */ -static bool -alsa_setup(AlsaOutput *ad, AudioFormat &audio_format, - bool *packed_r, bool *reverse_endian_r, Error &error) -{ - unsigned int sample_rate = audio_format.sample_rate; - unsigned int channels = audio_format.channels; - int err; - const char *cmd = nullptr; - int retry = MPD_ALSA_RETRY_NR; - unsigned int period_time, period_time_ro; - unsigned int buffer_time; - - period_time_ro = period_time = ad->period_time; -configure_hw: - /* configure HW params */ - snd_pcm_hw_params_t *hwparams; - snd_pcm_hw_params_alloca(&hwparams); - cmd = "snd_pcm_hw_params_any"; - err = snd_pcm_hw_params_any(ad->pcm, hwparams); - if (err < 0) - goto error; - - if (ad->use_mmap) { - err = snd_pcm_hw_params_set_access(ad->pcm, hwparams, - SND_PCM_ACCESS_MMAP_INTERLEAVED); - if (err < 0) { - FormatWarning(alsa_output_domain, - "Cannot set mmap'ed mode on ALSA device \"%s\": %s", - alsa_device(ad), snd_strerror(-err)); - LogWarning(alsa_output_domain, - "Falling back to direct write mode"); - ad->use_mmap = false; - } else - ad->writei = snd_pcm_mmap_writei; - } - - if (!ad->use_mmap) { - cmd = "snd_pcm_hw_params_set_access"; - err = snd_pcm_hw_params_set_access(ad->pcm, hwparams, - SND_PCM_ACCESS_RW_INTERLEAVED); - if (err < 0) - goto error; - ad->writei = snd_pcm_writei; - } - - err = alsa_output_setup_format(ad->pcm, hwparams, audio_format, - packed_r, reverse_endian_r); - if (err < 0) { - error.Format(alsa_output_domain, err, - "ALSA device \"%s\" does not support format %s: %s", - alsa_device(ad), - sample_format_to_string(audio_format.format), - snd_strerror(-err)); - return false; - } - - snd_pcm_format_t format; - if (snd_pcm_hw_params_get_format(hwparams, &format) == 0) - FormatDebug(alsa_output_domain, - "format=%s (%s)", snd_pcm_format_name(format), - snd_pcm_format_description(format)); - - err = snd_pcm_hw_params_set_channels_near(ad->pcm, hwparams, - &channels); - if (err < 0) { - error.Format(alsa_output_domain, err, - "ALSA device \"%s\" does not support %i channels: %s", - alsa_device(ad), (int)audio_format.channels, - snd_strerror(-err)); - return false; - } - audio_format.channels = (int8_t)channels; - - err = snd_pcm_hw_params_set_rate_near(ad->pcm, hwparams, - &sample_rate, nullptr); - if (err < 0 || sample_rate == 0) { - error.Format(alsa_output_domain, err, - "ALSA device \"%s\" does not support %u Hz audio", - alsa_device(ad), audio_format.sample_rate); - return false; - } - audio_format.sample_rate = sample_rate; - - snd_pcm_uframes_t buffer_size_min, buffer_size_max; - snd_pcm_hw_params_get_buffer_size_min(hwparams, &buffer_size_min); - snd_pcm_hw_params_get_buffer_size_max(hwparams, &buffer_size_max); - unsigned buffer_time_min, buffer_time_max; - snd_pcm_hw_params_get_buffer_time_min(hwparams, &buffer_time_min, 0); - snd_pcm_hw_params_get_buffer_time_max(hwparams, &buffer_time_max, 0); - FormatDebug(alsa_output_domain, "buffer: size=%u..%u time=%u..%u", - (unsigned)buffer_size_min, (unsigned)buffer_size_max, - buffer_time_min, buffer_time_max); - - snd_pcm_uframes_t period_size_min, period_size_max; - snd_pcm_hw_params_get_period_size_min(hwparams, &period_size_min, 0); - snd_pcm_hw_params_get_period_size_max(hwparams, &period_size_max, 0); - unsigned period_time_min, period_time_max; - snd_pcm_hw_params_get_period_time_min(hwparams, &period_time_min, 0); - snd_pcm_hw_params_get_period_time_max(hwparams, &period_time_max, 0); - FormatDebug(alsa_output_domain, "period: size=%u..%u time=%u..%u", - (unsigned)period_size_min, (unsigned)period_size_max, - period_time_min, period_time_max); - - if (ad->buffer_time > 0) { - buffer_time = ad->buffer_time; - cmd = "snd_pcm_hw_params_set_buffer_time_near"; - err = snd_pcm_hw_params_set_buffer_time_near(ad->pcm, hwparams, - &buffer_time, nullptr); - if (err < 0) - goto error; - } else { - err = snd_pcm_hw_params_get_buffer_time(hwparams, &buffer_time, - nullptr); - if (err < 0) - buffer_time = 0; - } - - if (period_time_ro == 0 && buffer_time >= 10000) { - period_time_ro = period_time = buffer_time / 4; - - FormatDebug(alsa_output_domain, - "default period_time = buffer_time/4 = %u/4 = %u", - buffer_time, period_time); - } - - if (period_time_ro > 0) { - period_time = period_time_ro; - cmd = "snd_pcm_hw_params_set_period_time_near"; - err = snd_pcm_hw_params_set_period_time_near(ad->pcm, hwparams, - &period_time, nullptr); - if (err < 0) - goto error; - } - - cmd = "snd_pcm_hw_params"; - err = snd_pcm_hw_params(ad->pcm, hwparams); - if (err == -EPIPE && --retry > 0 && period_time_ro > 0) { - period_time_ro = period_time_ro >> 1; - goto configure_hw; - } else if (err < 0) - goto error; - if (retry != MPD_ALSA_RETRY_NR) - FormatDebug(alsa_output_domain, - "ALSA period_time set to %d", period_time); - - snd_pcm_uframes_t alsa_buffer_size; - cmd = "snd_pcm_hw_params_get_buffer_size"; - err = snd_pcm_hw_params_get_buffer_size(hwparams, &alsa_buffer_size); - if (err < 0) - goto error; - - snd_pcm_uframes_t alsa_period_size; - cmd = "snd_pcm_hw_params_get_period_size"; - err = snd_pcm_hw_params_get_period_size(hwparams, &alsa_period_size, - nullptr); - if (err < 0) - goto error; - - /* configure SW params */ - snd_pcm_sw_params_t *swparams; - snd_pcm_sw_params_alloca(&swparams); - - cmd = "snd_pcm_sw_params_current"; - err = snd_pcm_sw_params_current(ad->pcm, swparams); - if (err < 0) - goto error; - - cmd = "snd_pcm_sw_params_set_start_threshold"; - err = snd_pcm_sw_params_set_start_threshold(ad->pcm, swparams, - alsa_buffer_size - - alsa_period_size); - if (err < 0) - goto error; - - cmd = "snd_pcm_sw_params_set_avail_min"; - err = snd_pcm_sw_params_set_avail_min(ad->pcm, swparams, - alsa_period_size); - if (err < 0) - goto error; - - cmd = "snd_pcm_sw_params"; - err = snd_pcm_sw_params(ad->pcm, swparams); - if (err < 0) - goto error; - - FormatDebug(alsa_output_domain, "buffer_size=%u period_size=%u", - (unsigned)alsa_buffer_size, (unsigned)alsa_period_size); - - if (alsa_period_size == 0) - /* this works around a SIGFPE bug that occurred when - an ALSA driver indicated period_size==0; this - caused a division by zero in alsa_play(). By using - the fallback "1", we make sure that this won't - happen again. */ - alsa_period_size = 1; - - ad->period_frames = alsa_period_size; - ad->period_position = 0; - - ad->silence = new uint8_t[snd_pcm_frames_to_bytes(ad->pcm, - alsa_period_size)]; - snd_pcm_format_set_silence(format, ad->silence, - alsa_period_size * channels); - - return true; - -error: - error.Format(alsa_output_domain, err, - "Error opening ALSA device \"%s\" (%s): %s", - alsa_device(ad), cmd, snd_strerror(-err)); - return false; -} - -static bool -alsa_setup_dsd(AlsaOutput *ad, const AudioFormat audio_format, - bool *shift8_r, bool *packed_r, bool *reverse_endian_r, - Error &error) -{ - assert(ad->dsd_usb); - assert(audio_format.format == SampleFormat::DSD); - - /* pass 24 bit to alsa_setup() */ - - AudioFormat usb_format = audio_format; - usb_format.format = SampleFormat::S24_P32; - usb_format.sample_rate /= 2; - - const AudioFormat check = usb_format; - - if (!alsa_setup(ad, usb_format, packed_r, reverse_endian_r, error)) - return false; - - /* if the device allows only 32 bit, shift all DSD-over-USB - samples left by 8 bit and leave the lower 8 bit cleared; - the DSD-over-USB documentation does not specify whether - this is legal, but there is anecdotical evidence that this - is possible (and the only option for some devices) */ - *shift8_r = usb_format.format == SampleFormat::S32; - if (usb_format.format == SampleFormat::S32) - usb_format.format = SampleFormat::S24_P32; - - if (usb_format != check) { - /* no bit-perfect playback, which is required - for DSD over USB */ - error.Format(alsa_output_domain, - "Failed to configure DSD-over-USB on ALSA device \"%s\"", - alsa_device(ad)); - delete[] ad->silence; - return false; - } - - return true; -} - -static bool -alsa_setup_or_dsd(AlsaOutput *ad, AudioFormat &audio_format, - Error &error) -{ - bool shift8 = false, packed, reverse_endian; - - const bool dsd_usb = ad->dsd_usb && - audio_format.format == SampleFormat::DSD; - const bool success = dsd_usb - ? alsa_setup_dsd(ad, audio_format, - &shift8, &packed, &reverse_endian, - error) - : alsa_setup(ad, audio_format, &packed, &reverse_endian, - error); - if (!success) - return false; - - ad->pcm_export->Open(audio_format.format, - audio_format.channels, - dsd_usb, shift8, packed, reverse_endian); - return true; -} - -static bool -alsa_open(struct audio_output *ao, AudioFormat &audio_format, Error &error) -{ - AlsaOutput *ad = (AlsaOutput *)ao; - - ad->pi_workaround = 0; - - int err = snd_pcm_open(&ad->pcm, alsa_device(ad), - SND_PCM_STREAM_PLAYBACK, ad->mode); - if (err < 0) { - error.Format(alsa_output_domain, err, - "Failed to open ALSA device \"%s\": %s", - alsa_device(ad), snd_strerror(err)); - return false; - } - - FormatDebug(alsa_output_domain, "opened %s type=%s", - snd_pcm_name(ad->pcm), - snd_pcm_type_name(snd_pcm_type(ad->pcm))); - - if (!alsa_setup_or_dsd(ad, audio_format, error)) { - snd_pcm_close(ad->pcm); - return false; - } - - ad->in_frame_size = audio_format.GetFrameSize(); - ad->out_frame_size = ad->pcm_export->GetFrameSize(audio_format); - - return true; -} - -/** - * Write silence to the ALSA device. - */ -static void -alsa_write_silence(AlsaOutput *ad, snd_pcm_uframes_t nframes) -{ - ad->writei(ad->pcm, ad->silence, nframes); -} - -static int -alsa_recover(AlsaOutput *ad, int err) -{ - if (err == -EPIPE) { - FormatDebug(alsa_output_domain, - "Underrun on ALSA device \"%s\"", alsa_device(ad)); - } else if (err == -ESTRPIPE) { - FormatDebug(alsa_output_domain, - "ALSA device \"%s\" was suspended", - alsa_device(ad)); - } - - switch (snd_pcm_state(ad->pcm)) { - case SND_PCM_STATE_PAUSED: - err = snd_pcm_pause(ad->pcm, /* disable */ 0); - break; - case SND_PCM_STATE_SUSPENDED: - err = snd_pcm_resume(ad->pcm); - if (err == -EAGAIN) - return 0; - /* fall-through to snd_pcm_prepare: */ - case SND_PCM_STATE_SETUP: - case SND_PCM_STATE_XRUN: - ad->period_position = 0; - err = snd_pcm_prepare(ad->pcm); - - if (err == 0 && ad->pi_workaround == 0) { - /* this works around a driver bug observed on - the Raspberry Pi: after snd_pcm_drop(), the - whole ring buffer must be invalidated, but - the snd_pcm_prepare() call above makes the - driver play random data that just happens - to be still in the buffer; by adding and - cancelling some silence, this bug does not - occur */ - alsa_write_silence(ad, ad->period_frames); - - /* cancel the silence data right away to avoid - increasing latency; even though this - function call invalidates the portion of - silence, the driver seems to avoid the - bug */ - snd_pcm_reset(ad->pcm); - - /* disable the workaround for some time */ - ad->pi_workaround = 8; - } - - break; - case SND_PCM_STATE_DISCONNECTED: - break; - /* this is no error, so just keep running */ - case SND_PCM_STATE_RUNNING: - err = 0; - break; - default: - /* unknown state, do nothing */ - break; - } - - return err; -} - -static void -alsa_drain(struct audio_output *ao) -{ - AlsaOutput *ad = (AlsaOutput *)ao; - - if (snd_pcm_state(ad->pcm) != SND_PCM_STATE_RUNNING) - return; - - if (ad->period_position > 0) { - /* generate some silence to finish the partial - period */ - snd_pcm_uframes_t nframes = - ad->period_frames - ad->period_position; - alsa_write_silence(ad, nframes); - } - - snd_pcm_drain(ad->pcm); - - ad->period_position = 0; -} - -static void -alsa_cancel(struct audio_output *ao) -{ - AlsaOutput *ad = (AlsaOutput *)ao; - - ad->period_position = 0; - - snd_pcm_drop(ad->pcm); -} - -static void -alsa_close(struct audio_output *ao) -{ - AlsaOutput *ad = (AlsaOutput *)ao; - - snd_pcm_close(ad->pcm); - delete[] ad->silence; -} - -static size_t -alsa_play(struct audio_output *ao, const void *chunk, size_t size, - Error &error) -{ - AlsaOutput *ad = (AlsaOutput *)ao; - - assert(size % ad->in_frame_size == 0); - - chunk = ad->pcm_export->Export(chunk, size, size); - - assert(size % ad->out_frame_size == 0); - - size /= ad->out_frame_size; - - while (true) { - snd_pcm_sframes_t ret = ad->writei(ad->pcm, chunk, size); - if (ret > 0) { - ad->period_position = (ad->period_position + ret) - % ad->period_frames; - - if (ad->pi_workaround > 0) - --ad->pi_workaround; - - size_t bytes_written = ret * ad->out_frame_size; - return ad->pcm_export->CalcSourceSize(bytes_written); - } - - if (ret < 0 && ret != -EAGAIN && ret != -EINTR && - alsa_recover(ad, ret) < 0) { - error.Set(alsa_output_domain, ret, snd_strerror(-ret)); - return 0; - } - } -} - -const struct audio_output_plugin alsa_output_plugin = { - "alsa", - alsa_test_default_device, - alsa_init, - alsa_finish, - alsa_output_enable, - alsa_output_disable, - alsa_open, - alsa_close, - nullptr, - nullptr, - alsa_play, - alsa_drain, - alsa_cancel, - nullptr, - - &alsa_mixer_plugin, -}; diff --git a/src/output/AlsaOutputPlugin.hxx b/src/output/AlsaOutputPlugin.hxx deleted file mode 100644 index 63508e041..000000000 --- a/src/output/AlsaOutputPlugin.hxx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_ALSA_OUTPUT_PLUGIN_HXX -#define MPD_ALSA_OUTPUT_PLUGIN_HXX - -extern const struct audio_output_plugin alsa_output_plugin; - -#endif diff --git a/src/output/AoOutputPlugin.cxx b/src/output/AoOutputPlugin.cxx deleted file mode 100644 index e6cd0b916..000000000 --- a/src/output/AoOutputPlugin.cxx +++ /dev/null @@ -1,286 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "config.h" -#include "AoOutputPlugin.hxx" -#include "OutputAPI.hxx" -#include "util/Error.hxx" -#include "util/Domain.hxx" -#include "Log.hxx" - -#include -#include - -#include - -/* An ao_sample_format, with all fields set to zero: */ -static ao_sample_format OUR_AO_FORMAT_INITIALIZER; - -static unsigned ao_output_ref; - -struct AoOutput { - struct audio_output base; - - size_t write_size; - int driver; - ao_option *options; - ao_device *device; - - bool Initialize(const config_param ¶m, Error &error) { - return ao_base_init(&base, &ao_output_plugin, param, - error); - } - - void Deinitialize() { - ao_base_finish(&base); - } - - bool Configure(const config_param ¶m, Error &error); -}; - -static constexpr Domain ao_output_domain("ao_output"); - -static void -ao_output_error(Error &error_r) -{ - const char *error; - - switch (errno) { - case AO_ENODRIVER: - error = "No such libao driver"; - break; - - case AO_ENOTLIVE: - error = "This driver is not a libao live device"; - break; - - case AO_EBADOPTION: - error = "Invalid libao option"; - break; - - case AO_EOPENDEVICE: - error = "Cannot open the libao device"; - break; - - case AO_EFAIL: - error = "Generic libao failure"; - break; - - default: - error_r.SetErrno(); - return; - } - - error_r.Set(ao_output_domain, errno, error); -} - -inline bool -AoOutput::Configure(const config_param ¶m, Error &error) -{ - const char *value; - - options = nullptr; - - write_size = param.GetBlockValue("write_size", 1024u); - - if (ao_output_ref == 0) { - ao_initialize(); - } - ao_output_ref++; - - value = param.GetBlockValue("driver", "default"); - if (0 == strcmp(value, "default")) - driver = ao_default_driver_id(); - else - driver = ao_driver_id(value); - - if (driver < 0) { - error.Format(ao_output_domain, - "\"%s\" is not a valid ao driver", - value); - return false; - } - - ao_info *ai = ao_driver_info(driver); - if (ai == nullptr) { - error.Set(ao_output_domain, "problems getting driver info"); - return false; - } - - FormatDebug(ao_output_domain, "using ao driver \"%s\" for \"%s\"\n", - ai->short_name, param.GetBlockValue("name", nullptr)); - - value = param.GetBlockValue("options", nullptr); - if (value != nullptr) { - gchar **_options = g_strsplit(value, ";", 0); - - for (unsigned i = 0; _options[i] != nullptr; ++i) { - gchar **key_value = g_strsplit(_options[i], "=", 2); - - if (key_value[0] == nullptr || key_value[1] == nullptr) { - error.Format(ao_output_domain, - "problems parsing options \"%s\"", - _options[i]); - return false; - } - - ao_append_option(&options, key_value[0], - key_value[1]); - - g_strfreev(key_value); - } - - g_strfreev(_options); - } - - return true; -} - -static struct audio_output * -ao_output_init(const config_param ¶m, Error &error) -{ - AoOutput *ad = new AoOutput(); - - if (!ad->Initialize(param, error)) { - delete ad; - return nullptr; - } - - if (!ad->Configure(param, error)) { - ad->Deinitialize(); - delete ad; - return nullptr; - } - - return &ad->base; -} - -static void -ao_output_finish(struct audio_output *ao) -{ - AoOutput *ad = (AoOutput *)ao; - - ao_free_options(ad->options); - ad->Deinitialize(); - delete ad; - - ao_output_ref--; - - if (ao_output_ref == 0) - ao_shutdown(); -} - -static void -ao_output_close(struct audio_output *ao) -{ - AoOutput *ad = (AoOutput *)ao; - - ao_close(ad->device); -} - -static bool -ao_output_open(struct audio_output *ao, AudioFormat &audio_format, - Error &error) -{ - ao_sample_format format = OUR_AO_FORMAT_INITIALIZER; - AoOutput *ad = (AoOutput *)ao; - - switch (audio_format.format) { - case SampleFormat::S8: - format.bits = 8; - break; - - case SampleFormat::S16: - format.bits = 16; - break; - - default: - /* support for 24 bit samples in libao is currently - dubious, and until we have sorted that out, - convert everything to 16 bit */ - audio_format.format = SampleFormat::S16; - format.bits = 16; - break; - } - - format.rate = audio_format.sample_rate; - format.byte_format = AO_FMT_NATIVE; - format.channels = audio_format.channels; - - ad->device = ao_open_live(ad->driver, &format, ad->options); - - if (ad->device == nullptr) { - ao_output_error(error); - return false; - } - - return true; -} - -/** - * For whatever reason, libao wants a non-const pointer. Let's hope - * it does not write to the buffer, and use the union deconst hack to - * work around this API misdesign. - */ -static int ao_play_deconst(ao_device *device, const void *output_samples, - uint_32 num_bytes) -{ - union { - const void *in; - char *out; - } u; - - u.in = output_samples; - return ao_play(device, u.out, num_bytes); -} - -static size_t -ao_output_play(struct audio_output *ao, const void *chunk, size_t size, - Error &error) -{ - AoOutput *ad = (AoOutput *)ao; - - if (size > ad->write_size) - size = ad->write_size; - - if (ao_play_deconst(ad->device, chunk, size) == 0) { - ao_output_error(error); - return 0; - } - - return size; -} - -const struct audio_output_plugin ao_output_plugin = { - "ao", - nullptr, - ao_output_init, - ao_output_finish, - nullptr, - nullptr, - ao_output_open, - ao_output_close, - nullptr, - nullptr, - ao_output_play, - nullptr, - nullptr, - nullptr, - nullptr, -}; diff --git a/src/output/AoOutputPlugin.hxx b/src/output/AoOutputPlugin.hxx deleted file mode 100644 index cbf2fd589..000000000 --- a/src/output/AoOutputPlugin.hxx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_AO_OUTPUT_PLUGIN_HXX -#define MPD_AO_OUTPUT_PLUGIN_HXX - -extern const struct audio_output_plugin ao_output_plugin; - -#endif diff --git a/src/output/FifoOutputPlugin.cxx b/src/output/FifoOutputPlugin.cxx deleted file mode 100644 index 9e5a1d5d2..000000000 --- a/src/output/FifoOutputPlugin.cxx +++ /dev/null @@ -1,313 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "config.h" -#include "FifoOutputPlugin.hxx" -#include "ConfigError.hxx" -#include "OutputAPI.hxx" -#include "Timer.hxx" -#include "fs/AllocatedPath.hxx" -#include "fs/FileSystem.hxx" -#include "util/Error.hxx" -#include "util/Domain.hxx" -#include "Log.hxx" -#include "open.h" - -#include -#include -#include - -#define FIFO_BUFFER_SIZE 65536 /* pipe capacity on Linux >= 2.6.11 */ - -struct FifoOutput { - struct audio_output base; - - AllocatedPath path; - std::string path_utf8; - - int input; - int output; - bool created; - Timer *timer; - - FifoOutput() - :path(AllocatedPath::Null()), input(-1), output(-1), - created(false) {} - - bool Initialize(const config_param ¶m, Error &error) { - return ao_base_init(&base, &fifo_output_plugin, param, - error); - } - - void Deinitialize() { - ao_base_finish(&base); - } - - bool Create(Error &error); - bool Check(Error &error); - void Delete(); - - bool Open(Error &error); - void Close(); -}; - -static constexpr Domain fifo_output_domain("fifo_output"); - -inline void -FifoOutput::Delete() -{ - FormatDebug(fifo_output_domain, - "Removing FIFO \"%s\"", path_utf8.c_str()); - - if (!RemoveFile(path)) { - FormatErrno(fifo_output_domain, - "Could not remove FIFO \"%s\"", - path_utf8.c_str()); - return; - } - - created = false; -} - -void -FifoOutput::Close() -{ - if (input >= 0) { - close(input); - input = -1; - } - - if (output >= 0) { - close(output); - output = -1; - } - - struct stat st; - if (created && StatFile(path, st)) - Delete(); -} - -inline bool -FifoOutput::Create(Error &error) -{ - if (!MakeFifo(path, 0666)) { - error.FormatErrno("Couldn't create FIFO \"%s\"", - path_utf8.c_str()); - return false; - } - - created = true; - return true; -} - -inline bool -FifoOutput::Check(Error &error) -{ - struct stat st; - if (!StatFile(path, st)) { - if (errno == ENOENT) { - /* Path doesn't exist */ - return Create(error); - } - - error.FormatErrno("Failed to stat FIFO \"%s\"", - path_utf8.c_str()); - return false; - } - - if (!S_ISFIFO(st.st_mode)) { - error.Format(fifo_output_domain, - "\"%s\" already exists, but is not a FIFO", - path_utf8.c_str()); - return false; - } - - return true; -} - -inline bool -FifoOutput::Open(Error &error) -{ - if (!Check(error)) - return false; - - input = OpenFile(path, O_RDONLY|O_NONBLOCK|O_BINARY, 0); - if (input < 0) { - error.FormatErrno("Could not open FIFO \"%s\" for reading", - path_utf8.c_str()); - Close(); - return false; - } - - output = OpenFile(path, O_WRONLY|O_NONBLOCK|O_BINARY, 0); - if (output < 0) { - error.FormatErrno("Could not open FIFO \"%s\" for writing", - path_utf8.c_str()); - Close(); - return false; - } - - return true; -} - -static bool -fifo_open(FifoOutput *fd, Error &error) -{ - return fd->Open(error); -} - -static struct audio_output * -fifo_output_init(const config_param ¶m, Error &error) -{ - FifoOutput *fd = new FifoOutput(); - - fd->path = param.GetBlockPath("path", error); - if (fd->path.IsNull()) { - delete fd; - - if (!error.IsDefined()) - error.Set(config_domain, - "No \"path\" parameter specified"); - return nullptr; - } - - fd->path_utf8 = fd->path.ToUTF8(); - - if (!fd->Initialize(param, error)) { - delete fd; - return nullptr; - } - - if (!fifo_open(fd, error)) { - fd->Deinitialize(); - delete fd; - return nullptr; - } - - return &fd->base; -} - -static void -fifo_output_finish(struct audio_output *ao) -{ - FifoOutput *fd = (FifoOutput *)ao; - - fd->Close(); - fd->Deinitialize(); - delete fd; -} - -static bool -fifo_output_open(struct audio_output *ao, AudioFormat &audio_format, - gcc_unused Error &error) -{ - FifoOutput *fd = (FifoOutput *)ao; - - fd->timer = new Timer(audio_format); - - return true; -} - -static void -fifo_output_close(struct audio_output *ao) -{ - FifoOutput *fd = (FifoOutput *)ao; - - delete fd->timer; -} - -static void -fifo_output_cancel(struct audio_output *ao) -{ - FifoOutput *fd = (FifoOutput *)ao; - char buf[FIFO_BUFFER_SIZE]; - int bytes = 1; - - fd->timer->Reset(); - - while (bytes > 0 && errno != EINTR) - bytes = read(fd->input, buf, FIFO_BUFFER_SIZE); - - if (bytes < 0 && errno != EAGAIN) { - FormatErrno(fifo_output_domain, - "Flush of FIFO \"%s\" failed", - fd->path_utf8.c_str()); - } -} - -static unsigned -fifo_output_delay(struct audio_output *ao) -{ - FifoOutput *fd = (FifoOutput *)ao; - - return fd->timer->IsStarted() - ? fd->timer->GetDelay() - : 0; -} - -static size_t -fifo_output_play(struct audio_output *ao, const void *chunk, size_t size, - Error &error) -{ - FifoOutput *fd = (FifoOutput *)ao; - ssize_t bytes; - - if (!fd->timer->IsStarted()) - fd->timer->Start(); - fd->timer->Add(size); - - while (true) { - bytes = write(fd->output, chunk, size); - if (bytes > 0) - return (size_t)bytes; - - if (bytes < 0) { - switch (errno) { - case EAGAIN: - /* The pipe is full, so empty it */ - fifo_output_cancel(&fd->base); - continue; - case EINTR: - continue; - } - - error.FormatErrno("Failed to write to FIFO %s", - fd->path_utf8.c_str()); - return 0; - } - } -} - -const struct audio_output_plugin fifo_output_plugin = { - "fifo", - nullptr, - fifo_output_init, - fifo_output_finish, - nullptr, - nullptr, - fifo_output_open, - fifo_output_close, - fifo_output_delay, - nullptr, - fifo_output_play, - nullptr, - fifo_output_cancel, - nullptr, - nullptr, -}; diff --git a/src/output/FifoOutputPlugin.hxx b/src/output/FifoOutputPlugin.hxx deleted file mode 100644 index 394ec3ae9..000000000 --- a/src/output/FifoOutputPlugin.hxx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_FIFO_OUTPUT_PLUGIN_HXX -#define MPD_FIFO_OUTPUT_PLUGIN_HXX - -extern const struct audio_output_plugin fifo_output_plugin; - -#endif diff --git a/src/output/HttpdClient.cxx b/src/output/HttpdClient.cxx deleted file mode 100644 index d761bdf57..000000000 --- a/src/output/HttpdClient.cxx +++ /dev/null @@ -1,485 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "config.h" -#include "HttpdClient.hxx" -#include "HttpdInternal.hxx" -#include "util/ASCII.hxx" -#include "Page.hxx" -#include "IcyMetaDataServer.hxx" -#include "system/SocketError.hxx" -#include "Log.hxx" - -#include - -#include -#include - -HttpdClient::~HttpdClient() -{ - if (state == RESPONSE) { - if (current_page != nullptr) - current_page->Unref(); - - ClearQueue(); - } - - if (metadata) - metadata->Unref(); - - if (IsDefined()) - BufferedSocket::Close(); -} - -void -HttpdClient::Close() -{ - httpd.RemoveClient(*this); -} - -void -HttpdClient::LockClose() -{ - const ScopeLock protect(httpd.mutex); - Close(); -} - -void -HttpdClient::BeginResponse() -{ - assert(state != RESPONSE); - - state = RESPONSE; - current_page = nullptr; - - if (!head_method) - httpd.SendHeader(*this); -} - -/** - * Handle a line of the HTTP request. - */ -bool -HttpdClient::HandleLine(const char *line) -{ - assert(state != RESPONSE); - - if (state == REQUEST) { - if (memcmp(line, "HEAD /", 6) == 0) { - line += 6; - head_method = true; - } else if (memcmp(line, "GET /", 5) == 0) { - line += 5; - } else { - /* only GET is supported */ - LogWarning(httpd_output_domain, - "malformed request line from client"); - return false; - } - - line = strchr(line, ' '); - if (line == nullptr || memcmp(line + 1, "HTTP/", 5) != 0) { - /* HTTP/0.9 without request headers */ - - if (head_method) - return false; - - BeginResponse(); - return true; - } - - /* after the request line, request headers follow */ - state = HEADERS; - return true; - } else { - if (*line == 0) { - /* empty line: request is finished */ - - BeginResponse(); - return true; - } - - if (StringEqualsCaseASCII(line, "Icy-MetaData: 1", 15) || - StringEqualsCaseASCII(line, "Icy-MetaData:1", 14)) { - /* Send icy metadata */ - metadata_requested = metadata_supported; - return true; - } - - if (StringEqualsCaseASCII(line, "transferMode.dlna.org: Streaming", 32)) { - /* Send as dlna */ - dlna_streaming_requested = true; - /* metadata is not supported by dlna streaming, so disable it */ - metadata_supported = false; - metadata_requested = false; - return true; - } - - /* expect more request headers */ - return true; - } -} - -/** - * Sends the status line and response headers to the client. - */ -bool -HttpdClient::SendResponse() -{ - char buffer[1024]; - assert(state == RESPONSE); - - if (dlna_streaming_requested) { - snprintf(buffer, sizeof(buffer), - "HTTP/1.1 206 OK\r\n" - "Content-Type: %s\r\n" - "Content-Length: 10000\r\n" - "Content-RangeX: 0-1000000/1000000\r\n" - "transferMode.dlna.org: Streaming\r\n" - "Accept-Ranges: bytes\r\n" - "Connection: close\r\n" - "realTimeInfo.dlna.org: DLNA.ORG_TLAG=*\r\n" - "contentFeatures.dlna.org: DLNA.ORG_OP=01;DLNA.ORG_CI=0\r\n" - "\r\n", - httpd.content_type); - - } else if (metadata_requested) { - char *metadata_header = - icy_server_metadata_header(httpd.name, httpd.genre, - httpd.website, - httpd.content_type, - metaint); - - g_strlcpy(buffer, metadata_header, sizeof(buffer)); - - delete[] metadata_header; - - } else { /* revert to a normal HTTP request */ - snprintf(buffer, sizeof(buffer), - "HTTP/1.1 200 OK\r\n" - "Content-Type: %s\r\n" - "Connection: close\r\n" - "Pragma: no-cache\r\n" - "Cache-Control: no-cache, no-store\r\n" - "\r\n", - httpd.content_type); - } - - ssize_t nbytes = SocketMonitor::Write(buffer, strlen(buffer)); - if (gcc_unlikely(nbytes < 0)) { - const SocketErrorMessage msg; - FormatWarning(httpd_output_domain, - "failed to write to client: %s", - (const char *)msg); - Close(); - return false; - } - - return true; -} - -HttpdClient::HttpdClient(HttpdOutput &_httpd, int _fd, EventLoop &_loop, - bool _metadata_supported) - :BufferedSocket(_fd, _loop), - httpd(_httpd), - state(REQUEST), - queue_size(0), - head_method(false), - dlna_streaming_requested(false), - metadata_supported(_metadata_supported), - metadata_requested(false), metadata_sent(true), - metaint(8192), /*TODO: just a std value */ - metadata(nullptr), - metadata_current_position(0), metadata_fill(0) -{ -} - -void -HttpdClient::ClearQueue() -{ - assert(state == RESPONSE); - - while (!pages.empty()) { - Page *page = pages.front(); - pages.pop(); - -#ifndef NDEBUG - assert(queue_size >= page->size); - queue_size -= page->size; -#endif - - page->Unref(); - } - - assert(queue_size == 0); -} - -void -HttpdClient::CancelQueue() -{ - if (state != RESPONSE) - return; - - ClearQueue(); - - if (current_page == nullptr) - CancelWrite(); -} - -ssize_t -HttpdClient::TryWritePage(const Page &page, size_t position) -{ - assert(position < page.size); - - return Write(page.data + position, page.size - position); -} - -ssize_t -HttpdClient::TryWritePageN(const Page &page, size_t position, ssize_t n) -{ - return n >= 0 - ? Write(page.data + position, n) - : TryWritePage(page, position); -} - -ssize_t -HttpdClient::GetBytesTillMetaData() const -{ - if (metadata_requested && - current_page->size - current_position > metaint - metadata_fill) - return metaint - metadata_fill; - - return -1; -} - -inline bool -HttpdClient::TryWrite() -{ - const ScopeLock protect(httpd.mutex); - - assert(state == RESPONSE); - - if (current_page == nullptr) { - if (pages.empty()) { - /* another thread has removed the event source - while this thread was waiting for - httpd.mutex */ - CancelWrite(); - return true; - } - - current_page = pages.front(); - pages.pop(); - current_position = 0; - - assert(queue_size >= current_page->size); - queue_size -= current_page->size; - } - - const ssize_t bytes_to_write = GetBytesTillMetaData(); - if (bytes_to_write == 0) { - if (!metadata_sent) { - ssize_t nbytes = TryWritePage(*metadata, - metadata_current_position); - if (nbytes < 0) { - auto e = GetSocketError(); - if (IsSocketErrorAgain(e)) - return true; - - if (!IsSocketErrorClosed(e)) { - SocketErrorMessage msg(e); - FormatWarning(httpd_output_domain, - "failed to write to client: %s", - (const char *)msg); - } - - Close(); - return false; - } - - metadata_current_position += nbytes; - - if (metadata->size - metadata_current_position == 0) { - metadata_fill = 0; - metadata_current_position = 0; - metadata_sent = true; - } - } else { - guchar empty_data = 0; - - ssize_t nbytes = Write(&empty_data, 1); - if (nbytes < 0) { - auto e = GetSocketError(); - if (IsSocketErrorAgain(e)) - return true; - - if (!IsSocketErrorClosed(e)) { - SocketErrorMessage msg(e); - FormatWarning(httpd_output_domain, - "failed to write to client: %s", - (const char *)msg); - } - - Close(); - return false; - } - - metadata_fill = 0; - metadata_current_position = 0; - } - } else { - ssize_t nbytes = - TryWritePageN(*current_page, current_position, - bytes_to_write); - if (nbytes < 0) { - auto e = GetSocketError(); - if (IsSocketErrorAgain(e)) - return true; - - if (!IsSocketErrorClosed(e)) { - SocketErrorMessage msg(e); - FormatWarning(httpd_output_domain, - "failed to write to client: %s", - (const char *)msg); - } - - Close(); - return false; - } - - current_position += nbytes; - assert(current_position <= current_page->size); - - if (metadata_requested) - metadata_fill += nbytes; - - if (current_position >= current_page->size) { - current_page->Unref(); - current_page = nullptr; - - if (pages.empty()) - /* all pages are sent: remove the - event source */ - CancelWrite(); - } - } - - return true; -} - -void -HttpdClient::PushPage(Page *page) -{ - if (state != RESPONSE) - /* the client is still writing the HTTP request */ - return; - - if (queue_size > 256 * 1024) { - FormatDebug(httpd_output_domain, - "client is too slow, flushing its queue"); - ClearQueue(); - } - - page->Ref(); - pages.push(page); - queue_size += page->size; - - ScheduleWrite(); -} - -void -HttpdClient::PushMetaData(Page *page) -{ - if (metadata) { - metadata->Unref(); - metadata = nullptr; - } - - g_return_if_fail (page); - - page->Ref(); - metadata = page; - metadata_sent = false; -} - -bool -HttpdClient::OnSocketReady(unsigned flags) -{ - if (!BufferedSocket::OnSocketReady(flags)) - return false; - - if (flags & WRITE) - if (!TryWrite()) - return false; - - return true; -} - -BufferedSocket::InputResult -HttpdClient::OnSocketInput(void *data, size_t length) -{ - if (state == RESPONSE) { - LogWarning(httpd_output_domain, - "unexpected input from client"); - LockClose(); - return InputResult::CLOSED; - } - - char *line = (char *)data; - char *newline = (char *)memchr(line, '\n', length); - if (newline == nullptr) - return InputResult::MORE; - - ConsumeInput(newline + 1 - line); - - if (newline > line && newline[-1] == '\r') - --newline; - - /* terminate the string at the end of the line */ - *newline = 0; - - if (!HandleLine(line)) { - LockClose(); - return InputResult::CLOSED; - } - - if (state == RESPONSE) { - if (!SendResponse()) - return InputResult::CLOSED; - - if (head_method) { - LockClose(); - return InputResult::CLOSED; - } - } - - return InputResult::AGAIN; -} - -void -HttpdClient::OnSocketError(Error &&error) -{ - LogError(error); -} - -void -HttpdClient::OnSocketClosed() -{ - LockClose(); -} diff --git a/src/output/HttpdClient.hxx b/src/output/HttpdClient.hxx deleted file mode 100644 index f94f05769..000000000 --- a/src/output/HttpdClient.hxx +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_OUTPUT_HTTPD_CLIENT_HXX -#define MPD_OUTPUT_HTTPD_CLIENT_HXX - -#include "event/BufferedSocket.hxx" -#include "Compiler.h" - -#include -#include - -#include - -class HttpdOutput; -class Page; - -class HttpdClient final : BufferedSocket { - /** - * The httpd output object this client is connected to. - */ - HttpdOutput &httpd; - - /** - * The current state of the client. - */ - enum { - /** reading the request line */ - REQUEST, - - /** reading the request headers */ - HEADERS, - - /** sending the HTTP response */ - RESPONSE, - } state; - - /** - * A queue of #Page objects to be sent to the client. - */ - std::queue> pages; - - /** - * The sum of all page sizes in #pages. - */ - size_t queue_size; - - /** - * The #page which is currently being sent to the client. - */ - Page *current_page; - - /** - * The amount of bytes which were already sent from - * #current_page. - */ - size_t current_position; - - /** - * Is this a HEAD request? - */ - bool head_method; - - /** - * If DLNA streaming was an option. - */ - bool dlna_streaming_requested; - - /* ICY */ - - /** - * Do we support sending Icy-Metadata to the client? This is - * disabled if the httpd audio output uses encoder tags. - */ - bool metadata_supported; - - /** - * If we should sent icy metadata. - */ - bool metadata_requested; - - /** - * If the current metadata was already sent to the client. - */ - bool metadata_sent; - - /** - * The amount of streaming data between each metadata block - */ - unsigned metaint; - - /** - * The metadata as #Page which is currently being sent to the client. - */ - Page *metadata; - - /* - * The amount of bytes which were already sent from the metadata. - */ - size_t metadata_current_position; - - /** - * The amount of streaming data sent to the client - * since the last icy information was sent. - */ - unsigned metadata_fill; - -public: - /** - * @param httpd the HTTP output device - * @param fd the socket file descriptor - */ - HttpdClient(HttpdOutput &httpd, int _fd, EventLoop &_loop, - bool _metadata_supported); - - /** - * Note: this does not remove the client from the - * #HttpdOutput object. - */ - ~HttpdClient(); - - /** - * Frees the client and removes it from the server's client list. - */ - void Close(); - - void LockClose(); - - /** - * Clears the page queue. - */ - void CancelQueue(); - - /** - * Handle a line of the HTTP request. - */ - bool HandleLine(const char *line); - - /** - * Switch the client to the "RESPONSE" state. - */ - void BeginResponse(); - - /** - * Sends the status line and response headers to the client. - */ - bool SendResponse(); - - gcc_pure - ssize_t GetBytesTillMetaData() const; - - ssize_t TryWritePage(const Page &page, size_t position); - ssize_t TryWritePageN(const Page &page, size_t position, ssize_t n); - - bool TryWrite(); - - /** - * Appends a page to the client's queue. - */ - void PushPage(Page *page); - - /** - * Sends the passed metadata. - */ - void PushMetaData(Page *page); - -private: - void ClearQueue(); - -protected: - virtual bool OnSocketReady(unsigned flags) override; - virtual InputResult OnSocketInput(void *data, size_t length) override; - virtual void OnSocketError(Error &&error) override; - virtual void OnSocketClosed() override; -}; - -#endif diff --git a/src/output/HttpdInternal.hxx b/src/output/HttpdInternal.hxx deleted file mode 100644 index 2ef0831ba..000000000 --- a/src/output/HttpdInternal.hxx +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -/** \file - * - * Internal declarations for the "httpd" audio output plugin. - */ - -#ifndef MPD_OUTPUT_HTTPD_INTERNAL_H -#define MPD_OUTPUT_HTTPD_INTERNAL_H - -#include "OutputInternal.hxx" -#include "Timer.hxx" -#include "thread/Mutex.hxx" -#include "event/ServerSocket.hxx" -#include "event/DeferredMonitor.hxx" -#include "util/Cast.hxx" - -#ifdef _LIBCPP_VERSION -/* can't use incomplete template arguments with libc++ */ -#include "HttpdClient.hxx" -#endif - -#include -#include -#include - -struct config_param; -class Error; -class EventLoop; -class ServerSocket; -class HttpdClient; -class Page; -struct Encoder; -struct Tag; - -class HttpdOutput final : ServerSocket, DeferredMonitor { - struct audio_output base; - - /** - * True if the audio output is open and accepts client - * connections. - */ - bool open; - - /** - * The configured encoder plugin. - */ - Encoder *encoder; - - /** - * Number of bytes which were fed into the encoder, without - * ever receiving new output. This is used to estimate - * whether MPD should manually flush the encoder, to avoid - * buffer underruns in the client. - */ - size_t unflushed_input; - -public: - /** - * The MIME type produced by the #encoder. - */ - const char *content_type; - - /** - * This mutex protects the listener socket and the client - * list. - */ - mutable Mutex mutex; - - /** - * This condition gets signalled when an item is removed from - * #pages. - */ - Cond cond; - -private: - /** - * A #Timer object to synchronize this output with the - * wallclock. - */ - Timer *timer; - - /** - * The header page, which is sent to every client on connect. - */ - Page *header; - - /** - * The metadata, which is sent to every client. - */ - Page *metadata; - - /** - * The page queue, i.e. pages from the encoder to be - * broadcasted to all clients. This container is necessary to - * pass pages from the OutputThread to the IOThread. It is - * protected by #mutex, and removing signals #cond. - */ - std::queue> pages; - - public: - /** - * The configured name. - */ - char const *name; - /** - * The configured genre. - */ - char const *genre; - /** - * The configured website address. - */ - char const *website; - -private: - /** - * A linked list containing all clients which are currently - * connected. - */ - std::forward_list clients; - - /** - * A temporary buffer for the httpd_output_read_page() - * function. - */ - char buffer[32768]; - - /** - * The maximum and current number of clients connected - * at the same time. - */ - unsigned clients_max, clients_cnt; - -public: - HttpdOutput(EventLoop &_loop); - ~HttpdOutput(); - -#if GCC_CHECK_VERSION(4,6) || defined(__clang__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Winvalid-offsetof" -#endif - - static constexpr HttpdOutput *Cast(audio_output *ao) { - return ContainerCast(ao, HttpdOutput, base); - } - -#if GCC_CHECK_VERSION(4,6) || defined(__clang__) -#pragma GCC diagnostic pop -#endif - - using DeferredMonitor::GetEventLoop; - - bool Init(const config_param ¶m, Error &error); - - void Finish() { - ao_base_finish(&base); - } - - bool Configure(const config_param ¶m, Error &error); - - audio_output *InitAndConfigure(const config_param ¶m, - Error &error) { - if (!Init(param, error)) - return nullptr; - - if (!Configure(param, error)) { - Finish(); - return nullptr; - } - - return &base; - } - - bool Bind(Error &error); - void Unbind(); - - /** - * Caller must lock the mutex. - */ - bool OpenEncoder(AudioFormat &audio_format, Error &error); - - /** - * Caller must lock the mutex. - */ - bool Open(AudioFormat &audio_format, Error &error); - - /** - * Caller must lock the mutex. - */ - void Close(); - - /** - * Check whether there is at least one client. - * - * Caller must lock the mutex. - */ - gcc_pure - bool HasClients() const { - return !clients.empty(); - } - - /** - * Check whether there is at least one client. - */ - gcc_pure - bool LockHasClients() const { - const ScopeLock protect(mutex); - return HasClients(); - } - - void AddClient(int fd); - - /** - * Removes a client from the httpd_output.clients linked list. - */ - void RemoveClient(HttpdClient &client); - - /** - * Sends the encoder header to the client. This is called - * right after the response headers have been sent. - */ - void SendHeader(HttpdClient &client) const; - - gcc_pure - unsigned Delay() const; - - /** - * Reads data from the encoder (as much as available) and - * returns it as a new #page object. - */ - Page *ReadPage(); - - /** - * Broadcasts a page struct to all clients. - * - * Mutext must not be locked. - */ - void BroadcastPage(Page *page); - - /** - * Broadcasts data from the encoder to all clients. - */ - void BroadcastFromEncoder(); - - bool EncodeAndPlay(const void *chunk, size_t size, Error &error); - - void SendTag(const Tag *tag); - - size_t Play(const void *chunk, size_t size, Error &error); - - void CancelAllClients(); - -private: - virtual void RunDeferred() override; - - virtual void OnAccept(int fd, const sockaddr &address, - size_t address_length, int uid) override; -}; - -extern const class Domain httpd_output_domain; - -#endif diff --git a/src/output/HttpdOutputPlugin.cxx b/src/output/HttpdOutputPlugin.cxx deleted file mode 100644 index 322c9a61e..000000000 --- a/src/output/HttpdOutputPlugin.cxx +++ /dev/null @@ -1,601 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "config.h" -#include "HttpdOutputPlugin.hxx" -#include "HttpdInternal.hxx" -#include "HttpdClient.hxx" -#include "OutputAPI.hxx" -#include "encoder/EncoderPlugin.hxx" -#include "encoder/EncoderList.hxx" -#include "system/Resolver.hxx" -#include "Page.hxx" -#include "IcyMetaDataServer.hxx" -#include "system/fd_util.h" -#include "IOThread.hxx" -#include "event/Call.hxx" -#include "util/Error.hxx" -#include "util/Domain.hxx" -#include "Log.hxx" - -#include - -#include -#include -#include -#include - -#ifdef HAVE_LIBWRAP -#include /* needed for AF_UNIX */ -#include -#endif - -const Domain httpd_output_domain("httpd_output"); - -inline -HttpdOutput::HttpdOutput(EventLoop &_loop) - :ServerSocket(_loop), DeferredMonitor(_loop), - encoder(nullptr), unflushed_input(0), - metadata(nullptr) -{ -} - -HttpdOutput::~HttpdOutput() -{ - if (metadata != nullptr) - metadata->Unref(); - - if (encoder != nullptr) - encoder_finish(encoder); - -} - -inline bool -HttpdOutput::Bind(Error &error) -{ - open = false; - - bool result = false; - BlockingCall(GetEventLoop(), [this, &error, &result](){ - result = ServerSocket::Open(error); - }); - return result; -} - -inline void -HttpdOutput::Unbind() -{ - assert(!open); - - BlockingCall(GetEventLoop(), [this](){ - ServerSocket::Close(); - }); -} - -inline bool -HttpdOutput::Configure(const config_param ¶m, Error &error) -{ - /* read configuration */ - name = param.GetBlockValue("name", "Set name in config"); - genre = param.GetBlockValue("genre", "Set genre in config"); - website = param.GetBlockValue("website", "Set website in config"); - - unsigned port = param.GetBlockValue("port", 8000u); - - const char *encoder_name = - param.GetBlockValue("encoder", "vorbis"); - const auto encoder_plugin = encoder_plugin_get(encoder_name); - if (encoder_plugin == nullptr) { - error.Format(httpd_output_domain, - "No such encoder: %s", encoder_name); - return false; - } - - clients_max = param.GetBlockValue("max_clients", 0u); - - /* set up bind_to_address */ - - const char *bind_to_address = param.GetBlockValue("bind_to_address"); - bool success = bind_to_address != nullptr && - strcmp(bind_to_address, "any") != 0 - ? AddHost(bind_to_address, port, error) - : AddPort(port, error); - if (!success) - return false; - - /* initialize encoder */ - - encoder = encoder_init(*encoder_plugin, param, error); - if (encoder == nullptr) - return false; - - /* determine content type */ - content_type = encoder_get_mime_type(encoder); - if (content_type == nullptr) - content_type = "application/octet-stream"; - - return true; -} - -inline bool -HttpdOutput::Init(const config_param ¶m, Error &error) -{ - return ao_base_init(&base, &httpd_output_plugin, param, error); -} - -static struct audio_output * -httpd_output_init(const config_param ¶m, Error &error) -{ - HttpdOutput *httpd = new HttpdOutput(io_thread_get()); - - audio_output *result = httpd->InitAndConfigure(param, error); - if (result == nullptr) - delete httpd; - - return result; -} - -static void -httpd_output_finish(struct audio_output *ao) -{ - HttpdOutput *httpd = HttpdOutput::Cast(ao); - - httpd->Finish(); - delete httpd; -} - -/** - * Creates a new #HttpdClient object and adds it into the - * HttpdOutput.clients linked list. - */ -inline void -HttpdOutput::AddClient(int fd) -{ - clients.emplace_front(*this, fd, GetEventLoop(), - encoder->plugin.tag == nullptr); - ++clients_cnt; - - /* pass metadata to client */ - if (metadata != nullptr) - clients.front().PushMetaData(metadata); -} - -void -HttpdOutput::RunDeferred() -{ - /* this method runs in the IOThread; it broadcasts pages from - our own queue to all clients */ - - const ScopeLock protect(mutex); - - while (!pages.empty()) { - Page *page = pages.front(); - pages.pop(); - - for (auto &client : clients) - client.PushPage(page); - - page->Unref(); - } - - /* wake up the client that may be waiting for the queue to be - flushed */ - cond.broadcast(); -} - -void -HttpdOutput::OnAccept(int fd, const sockaddr &address, - size_t address_length, gcc_unused int uid) -{ - /* the listener socket has become readable - a client has - connected */ - -#ifdef HAVE_LIBWRAP - if (address.sa_family != AF_UNIX) { - const auto hostaddr = sockaddr_to_string(&address, - address_length); - // TODO: shall we obtain the program name from argv[0]? - const char *progname = "mpd"; - - struct request_info req; - request_init(&req, RQ_FILE, fd, RQ_DAEMON, progname, 0); - - fromhost(&req); - - if (!hosts_access(&req)) { - /* tcp wrappers says no */ - FormatWarning(httpd_output_domain, - "libwrap refused connection (libwrap=%s) from %s", - progname, hostaddr.c_str()); - close_socket(fd); - return; - } - } -#else - (void)address; - (void)address_length; -#endif /* HAVE_WRAP */ - - const ScopeLock protect(mutex); - - if (fd >= 0) { - /* can we allow additional client */ - if (open && (clients_max == 0 || clients_cnt < clients_max)) - AddClient(fd); - else - close_socket(fd); - } else if (fd < 0 && errno != EINTR) { - LogErrno(httpd_output_domain, "accept() failed"); - } -} - -Page * -HttpdOutput::ReadPage() -{ - if (unflushed_input >= 65536) { - /* we have fed a lot of input into the encoder, but it - didn't give anything back yet - flush now to avoid - buffer underruns */ - encoder_flush(encoder, IgnoreError()); - unflushed_input = 0; - } - - size_t size = 0; - do { - size_t nbytes = encoder_read(encoder, - buffer + size, - sizeof(buffer) - size); - if (nbytes == 0) - break; - - unflushed_input = 0; - - size += nbytes; - } while (size < sizeof(buffer)); - - if (size == 0) - return nullptr; - - return Page::Copy(buffer, size); -} - -static bool -httpd_output_enable(struct audio_output *ao, Error &error) -{ - HttpdOutput *httpd = HttpdOutput::Cast(ao); - - return httpd->Bind(error); -} - -static void -httpd_output_disable(struct audio_output *ao) -{ - HttpdOutput *httpd = HttpdOutput::Cast(ao); - - httpd->Unbind(); -} - -inline bool -HttpdOutput::OpenEncoder(AudioFormat &audio_format, Error &error) -{ - if (!encoder_open(encoder, audio_format, error)) - return false; - - /* we have to remember the encoder header, i.e. the first - bytes of encoder output after opening it, because it has to - be sent to every new client */ - header = ReadPage(); - - unflushed_input = 0; - - return true; -} - -inline bool -HttpdOutput::Open(AudioFormat &audio_format, Error &error) -{ - assert(!open); - assert(clients.empty()); - - /* open the encoder */ - - if (!OpenEncoder(audio_format, error)) - return false; - - /* initialize other attributes */ - - clients_cnt = 0; - timer = new Timer(audio_format); - - open = true; - - return true; -} - -static bool -httpd_output_open(struct audio_output *ao, AudioFormat &audio_format, - Error &error) -{ - HttpdOutput *httpd = HttpdOutput::Cast(ao); - - const ScopeLock protect(httpd->mutex); - return httpd->Open(audio_format, error); -} - -inline void -HttpdOutput::Close() -{ - assert(open); - - open = false; - - delete timer; - - BlockingCall(GetEventLoop(), [this](){ - clients.clear(); - }); - - if (header != nullptr) - header->Unref(); - - encoder_close(encoder); -} - -static void -httpd_output_close(struct audio_output *ao) -{ - HttpdOutput *httpd = HttpdOutput::Cast(ao); - - const ScopeLock protect(httpd->mutex); - httpd->Close(); -} - -void -HttpdOutput::RemoveClient(HttpdClient &client) -{ - assert(clients_cnt > 0); - - for (auto prev = clients.before_begin(), i = std::next(prev);; - prev = i, i = std::next(prev)) { - assert(i != clients.end()); - if (&*i == &client) { - clients.erase_after(prev); - clients_cnt--; - break; - } - } -} - -void -HttpdOutput::SendHeader(HttpdClient &client) const -{ - if (header != nullptr) - client.PushPage(header); -} - -inline unsigned -HttpdOutput::Delay() const -{ - if (!LockHasClients() && base.pause) { - /* if there's no client and this output is paused, - then httpd_output_pause() will not do anything, it - will not fill the buffer and it will not update the - timer; therefore, we reset the timer here */ - timer->Reset(); - - /* some arbitrary delay that is long enough to avoid - consuming too much CPU, and short enough to notice - new clients quickly enough */ - return 1000; - } - - return timer->IsStarted() - ? timer->GetDelay() - : 0; -} - -static unsigned -httpd_output_delay(struct audio_output *ao) -{ - HttpdOutput *httpd = HttpdOutput::Cast(ao); - - return httpd->Delay(); -} - -void -HttpdOutput::BroadcastPage(Page *page) -{ - assert(page != nullptr); - - mutex.lock(); - pages.push(page); - page->Ref(); - mutex.unlock(); - - DeferredMonitor::Schedule(); -} - -void -HttpdOutput::BroadcastFromEncoder() -{ - /* synchronize with the IOThread */ - mutex.lock(); - while (!pages.empty()) - cond.wait(mutex); - - Page *page; - while ((page = ReadPage()) != nullptr) - pages.push(page); - - mutex.unlock(); - - DeferredMonitor::Schedule(); -} - -inline bool -HttpdOutput::EncodeAndPlay(const void *chunk, size_t size, Error &error) -{ - if (!encoder_write(encoder, chunk, size, error)) - return false; - - unflushed_input += size; - - BroadcastFromEncoder(); - return true; -} - -inline size_t -HttpdOutput::Play(const void *chunk, size_t size, Error &error) -{ - if (LockHasClients()) { - if (!EncodeAndPlay(chunk, size, error)) - return 0; - } - - if (!timer->IsStarted()) - timer->Start(); - timer->Add(size); - - return size; -} - -static size_t -httpd_output_play(struct audio_output *ao, const void *chunk, size_t size, - Error &error) -{ - HttpdOutput *httpd = HttpdOutput::Cast(ao); - - return httpd->Play(chunk, size, error); -} - -static bool -httpd_output_pause(struct audio_output *ao) -{ - HttpdOutput *httpd = HttpdOutput::Cast(ao); - - if (httpd->LockHasClients()) { - static const char silence[1020] = { 0 }; - return httpd_output_play(ao, silence, sizeof(silence), - IgnoreError()) > 0; - } else { - return true; - } -} - -inline void -HttpdOutput::SendTag(const Tag *tag) -{ - assert(tag != nullptr); - - if (encoder->plugin.tag != nullptr) { - /* embed encoder tags */ - - /* flush the current stream, and end it */ - - encoder_pre_tag(encoder, IgnoreError()); - BroadcastFromEncoder(); - - /* send the tag to the encoder - which starts a new - stream now */ - - encoder_tag(encoder, tag, IgnoreError()); - - /* the first page generated by the encoder will now be - used as the new "header" page, which is sent to all - new clients */ - - Page *page = ReadPage(); - if (page != nullptr) { - if (header != nullptr) - header->Unref(); - header = page; - BroadcastPage(page); - } - } else { - /* use Icy-Metadata */ - - if (metadata != nullptr) - metadata->Unref(); - - static constexpr TagType types[] = { - TAG_ALBUM, TAG_ARTIST, TAG_TITLE, - TAG_NUM_OF_ITEM_TYPES - }; - - metadata = icy_server_metadata_page(*tag, &types[0]); - if (metadata != nullptr) { - const ScopeLock protect(mutex); - for (auto &client : clients) - client.PushMetaData(metadata); - } - } -} - -static void -httpd_output_tag(struct audio_output *ao, const Tag *tag) -{ - HttpdOutput *httpd = HttpdOutput::Cast(ao); - - httpd->SendTag(tag); -} - -inline void -HttpdOutput::CancelAllClients() -{ - const ScopeLock protect(mutex); - - while (!pages.empty()) { - Page *page = pages.front(); - pages.pop(); - page->Unref(); - } - - for (auto &client : clients) - client.CancelQueue(); - - cond.broadcast(); -} - -static void -httpd_output_cancel(struct audio_output *ao) -{ - HttpdOutput *httpd = HttpdOutput::Cast(ao); - - BlockingCall(io_thread_get(), [httpd](){ - httpd->CancelAllClients(); - }); -} - -const struct audio_output_plugin httpd_output_plugin = { - "httpd", - nullptr, - httpd_output_init, - httpd_output_finish, - httpd_output_enable, - httpd_output_disable, - httpd_output_open, - httpd_output_close, - httpd_output_delay, - httpd_output_tag, - httpd_output_play, - nullptr, - httpd_output_cancel, - httpd_output_pause, - nullptr, -}; diff --git a/src/output/HttpdOutputPlugin.hxx b/src/output/HttpdOutputPlugin.hxx deleted file mode 100644 index 78218e5f0..000000000 --- a/src/output/HttpdOutputPlugin.hxx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_HTTPD_OUTPUT_PLUGIN_HXX -#define MPD_HTTPD_OUTPUT_PLUGIN_HXX - -extern const struct audio_output_plugin httpd_output_plugin; - -#endif diff --git a/src/output/JackOutputPlugin.cxx b/src/output/JackOutputPlugin.cxx deleted file mode 100644 index c65ae5225..000000000 --- a/src/output/JackOutputPlugin.cxx +++ /dev/null @@ -1,765 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "config.h" -#include "JackOutputPlugin.hxx" -#include "OutputAPI.hxx" -#include "ConfigError.hxx" -#include "util/Error.hxx" -#include "util/Domain.hxx" -#include "Log.hxx" - -#include - -#include -#include -#include -#include - -#include -#include - -enum { - MAX_PORTS = 16, -}; - -static const size_t jack_sample_size = sizeof(jack_default_audio_sample_t); - -struct JackOutput { - struct audio_output base; - - /** - * libjack options passed to jack_client_open(). - */ - jack_options_t options; - - const char *name; - - const char *server_name; - - /* configuration */ - - char *source_ports[MAX_PORTS]; - unsigned num_source_ports; - - char *destination_ports[MAX_PORTS]; - unsigned num_destination_ports; - - size_t ringbuffer_size; - - /* the current audio format */ - AudioFormat audio_format; - - /* jack library stuff */ - jack_port_t *ports[MAX_PORTS]; - jack_client_t *client; - jack_ringbuffer_t *ringbuffer[MAX_PORTS]; - - bool shutdown; - - /** - * While this flag is set, the "process" callback generates - * silence. - */ - bool pause; - - bool Initialize(const config_param ¶m, Error &error_r) { - return ao_base_init(&base, &jack_output_plugin, param, - error_r); - } - - void Deinitialize() { - ao_base_finish(&base); - } -}; - -static constexpr Domain jack_output_domain("jack_output"); - -/** - * Determine the number of frames guaranteed to be available on all - * channels. - */ -static jack_nframes_t -mpd_jack_available(const JackOutput *jd) -{ - size_t min = jack_ringbuffer_read_space(jd->ringbuffer[0]); - - for (unsigned i = 1; i < jd->audio_format.channels; ++i) { - size_t current = jack_ringbuffer_read_space(jd->ringbuffer[i]); - if (current < min) - min = current; - } - - assert(min % jack_sample_size == 0); - - return min / jack_sample_size; -} - -static int -mpd_jack_process(jack_nframes_t nframes, void *arg) -{ - JackOutput *jd = (JackOutput *) arg; - - if (nframes <= 0) - return 0; - - if (jd->pause) { - /* empty the ring buffers */ - - const jack_nframes_t available = mpd_jack_available(jd); - for (unsigned i = 0; i < jd->audio_format.channels; ++i) - jack_ringbuffer_read_advance(jd->ringbuffer[i], - available * jack_sample_size); - - /* generate silence while MPD is paused */ - - for (unsigned i = 0; i < jd->audio_format.channels; ++i) { - jack_default_audio_sample_t *out = - (jack_default_audio_sample_t *) - jack_port_get_buffer(jd->ports[i], nframes); - - for (jack_nframes_t f = 0; f < nframes; ++f) - out[f] = 0.0; - } - - return 0; - } - - jack_nframes_t available = mpd_jack_available(jd); - if (available > nframes) - available = nframes; - - for (unsigned i = 0; i < jd->audio_format.channels; ++i) { - jack_default_audio_sample_t *out = - (jack_default_audio_sample_t *) - jack_port_get_buffer(jd->ports[i], nframes); - if (out == nullptr) - /* workaround for libjack1 bug: if the server - connection fails, the process callback is - invoked anyway, but unable to get a - buffer */ - continue; - - jack_ringbuffer_read(jd->ringbuffer[i], - (char *)out, available * jack_sample_size); - - for (jack_nframes_t f = available; f < nframes; ++f) - /* ringbuffer underrun, fill with silence */ - out[f] = 0.0; - } - - /* generate silence for the unused source ports */ - - for (unsigned i = jd->audio_format.channels; - i < jd->num_source_ports; ++i) { - jack_default_audio_sample_t *out = - (jack_default_audio_sample_t *) - jack_port_get_buffer(jd->ports[i], nframes); - if (out == nullptr) - /* workaround for libjack1 bug: if the server - connection fails, the process callback is - invoked anyway, but unable to get a - buffer */ - continue; - - for (jack_nframes_t f = 0; f < nframes; ++f) - out[f] = 0.0; - } - - return 0; -} - -static void -mpd_jack_shutdown(void *arg) -{ - JackOutput *jd = (JackOutput *) arg; - jd->shutdown = true; -} - -static void -set_audioformat(JackOutput *jd, AudioFormat &audio_format) -{ - audio_format.sample_rate = jack_get_sample_rate(jd->client); - - if (jd->num_source_ports == 1) - audio_format.channels = 1; - else if (audio_format.channels > jd->num_source_ports) - audio_format.channels = 2; - - if (audio_format.format != SampleFormat::S16 && - audio_format.format != SampleFormat::S24_P32) - audio_format.format = SampleFormat::S24_P32; -} - -static void -mpd_jack_error(const char *msg) -{ - LogError(jack_output_domain, msg); -} - -#ifdef HAVE_JACK_SET_INFO_FUNCTION -static void -mpd_jack_info(const char *msg) -{ - LogDefault(jack_output_domain, msg); -} -#endif - -/** - * Disconnect the JACK client. - */ -static void -mpd_jack_disconnect(JackOutput *jd) -{ - assert(jd != nullptr); - assert(jd->client != nullptr); - - jack_deactivate(jd->client); - jack_client_close(jd->client); - jd->client = nullptr; -} - -/** - * Connect the JACK client and performs some basic setup - * (e.g. register callbacks). - */ -static bool -mpd_jack_connect(JackOutput *jd, Error &error) -{ - jack_status_t status; - - assert(jd != nullptr); - - jd->shutdown = false; - - jd->client = jack_client_open(jd->name, jd->options, &status, - jd->server_name); - if (jd->client == nullptr) { - error.Format(jack_output_domain, status, - "Failed to connect to JACK server, status=%d", - status); - return false; - } - - jack_set_process_callback(jd->client, mpd_jack_process, jd); - jack_on_shutdown(jd->client, mpd_jack_shutdown, jd); - - for (unsigned i = 0; i < jd->num_source_ports; ++i) { - jd->ports[i] = jack_port_register(jd->client, - jd->source_ports[i], - JACK_DEFAULT_AUDIO_TYPE, - JackPortIsOutput, 0); - if (jd->ports[i] == nullptr) { - error.Format(jack_output_domain, - "Cannot register output port \"%s\"", - jd->source_ports[i]); - mpd_jack_disconnect(jd); - return false; - } - } - - return true; -} - -static bool -mpd_jack_test_default_device(void) -{ - return true; -} - -static unsigned -parse_port_list(const char *source, char **dest, Error &error) -{ - char **list = g_strsplit(source, ",", 0); - unsigned n = 0; - - for (n = 0; list[n] != nullptr; ++n) { - if (n >= MAX_PORTS) { - error.Set(config_domain, - "too many port names"); - return 0; - } - - dest[n] = list[n]; - } - - g_free(list); - - if (n == 0) { - error.Format(config_domain, - "at least one port name expected"); - return 0; - } - - return n; -} - -static struct audio_output * -mpd_jack_init(const config_param ¶m, Error &error) -{ - JackOutput *jd = new JackOutput(); - - if (!jd->Initialize(param, error)) { - delete jd; - return nullptr; - } - - const char *value; - - jd->options = JackNullOption; - - jd->name = param.GetBlockValue("client_name", nullptr); - if (jd->name != nullptr) - jd->options = jack_options_t(jd->options | JackUseExactName); - else - /* if there's a no configured client name, we don't - care about the JackUseExactName option */ - jd->name = "Music Player Daemon"; - - jd->server_name = param.GetBlockValue("server_name", nullptr); - if (jd->server_name != nullptr) - jd->options = jack_options_t(jd->options | JackServerName); - - if (!param.GetBlockValue("autostart", false)) - jd->options = jack_options_t(jd->options | JackNoStartServer); - - /* configure the source ports */ - - value = param.GetBlockValue("source_ports", "left,right"); - jd->num_source_ports = parse_port_list(value, - jd->source_ports, error); - if (jd->num_source_ports == 0) - return nullptr; - - /* configure the destination ports */ - - value = param.GetBlockValue("destination_ports", nullptr); - if (value == nullptr) { - /* compatibility with MPD < 0.16 */ - value = param.GetBlockValue("ports", nullptr); - if (value != nullptr) - FormatWarning(jack_output_domain, - "deprecated option 'ports' in line %d", - param.line); - } - - if (value != nullptr) { - jd->num_destination_ports = - parse_port_list(value, - jd->destination_ports, error); - if (jd->num_destination_ports == 0) - return nullptr; - } else { - jd->num_destination_ports = 0; - } - - if (jd->num_destination_ports > 0 && - jd->num_destination_ports != jd->num_source_ports) - FormatWarning(jack_output_domain, - "number of source ports (%u) mismatches the " - "number of destination ports (%u) in line %d", - jd->num_source_ports, jd->num_destination_ports, - param.line); - - jd->ringbuffer_size = param.GetBlockValue("ringbuffer_size", 32768u); - - jack_set_error_function(mpd_jack_error); - -#ifdef HAVE_JACK_SET_INFO_FUNCTION - jack_set_info_function(mpd_jack_info); -#endif - - return &jd->base; -} - -static void -mpd_jack_finish(struct audio_output *ao) -{ - JackOutput *jd = (JackOutput *)ao; - - for (unsigned i = 0; i < jd->num_source_ports; ++i) - g_free(jd->source_ports[i]); - - for (unsigned i = 0; i < jd->num_destination_ports; ++i) - g_free(jd->destination_ports[i]); - - jd->Deinitialize(); - delete jd; -} - -static bool -mpd_jack_enable(struct audio_output *ao, Error &error) -{ - JackOutput *jd = (JackOutput *)ao; - - for (unsigned i = 0; i < jd->num_source_ports; ++i) - jd->ringbuffer[i] = nullptr; - - return mpd_jack_connect(jd, error); -} - -static void -mpd_jack_disable(struct audio_output *ao) -{ - JackOutput *jd = (JackOutput *)ao; - - if (jd->client != nullptr) - mpd_jack_disconnect(jd); - - for (unsigned i = 0; i < jd->num_source_ports; ++i) { - if (jd->ringbuffer[i] != nullptr) { - jack_ringbuffer_free(jd->ringbuffer[i]); - jd->ringbuffer[i] = nullptr; - } - } -} - -/** - * Stops the playback on the JACK connection. - */ -static void -mpd_jack_stop(JackOutput *jd) -{ - assert(jd != nullptr); - - if (jd->client == nullptr) - return; - - if (jd->shutdown) - /* the connection has failed; close it */ - mpd_jack_disconnect(jd); - else - /* the connection is alive: just stop playback */ - jack_deactivate(jd->client); -} - -static bool -mpd_jack_start(JackOutput *jd, Error &error) -{ - const char *destination_ports[MAX_PORTS], **jports; - const char *duplicate_port = nullptr; - unsigned num_destination_ports; - - assert(jd->client != nullptr); - assert(jd->audio_format.channels <= jd->num_source_ports); - - /* allocate the ring buffers on the first open(); these - persist until MPD exits. It's too unsafe to delete them - because we can never know when mpd_jack_process() gets - called */ - for (unsigned i = 0; i < jd->num_source_ports; ++i) { - if (jd->ringbuffer[i] == nullptr) - jd->ringbuffer[i] = - jack_ringbuffer_create(jd->ringbuffer_size); - - /* clear the ring buffer to be sure that data from - previous playbacks are gone */ - jack_ringbuffer_reset(jd->ringbuffer[i]); - } - - if ( jack_activate(jd->client) ) { - error.Set(jack_output_domain, "cannot activate client"); - mpd_jack_stop(jd); - return false; - } - - if (jd->num_destination_ports == 0) { - /* no output ports were configured - ask libjack for - defaults */ - jports = jack_get_ports(jd->client, nullptr, nullptr, - JackPortIsPhysical | JackPortIsInput); - if (jports == nullptr) { - error.Set(jack_output_domain, "no ports found"); - mpd_jack_stop(jd); - return false; - } - - assert(*jports != nullptr); - - for (num_destination_ports = 0; - num_destination_ports < MAX_PORTS && - jports[num_destination_ports] != nullptr; - ++num_destination_ports) { - FormatDebug(jack_output_domain, - "destination_port[%u] = '%s'\n", - num_destination_ports, - jports[num_destination_ports]); - destination_ports[num_destination_ports] = - jports[num_destination_ports]; - } - } else { - /* use the configured output ports */ - - num_destination_ports = jd->num_destination_ports; - memcpy(destination_ports, jd->destination_ports, - num_destination_ports * sizeof(*destination_ports)); - - jports = nullptr; - } - - assert(num_destination_ports > 0); - - if (jd->audio_format.channels >= 2 && num_destination_ports == 1) { - /* mix stereo signal on one speaker */ - - while (num_destination_ports < jd->audio_format.channels) - destination_ports[num_destination_ports++] = - destination_ports[0]; - } else if (num_destination_ports > jd->audio_format.channels) { - if (jd->audio_format.channels == 1 && num_destination_ports > 2) { - /* mono input file: connect the one source - channel to the both destination channels */ - duplicate_port = destination_ports[1]; - num_destination_ports = 1; - } else - /* connect only as many ports as we need */ - num_destination_ports = jd->audio_format.channels; - } - - assert(num_destination_ports <= jd->num_source_ports); - - for (unsigned i = 0; i < num_destination_ports; ++i) { - int ret; - - ret = jack_connect(jd->client, jack_port_name(jd->ports[i]), - destination_ports[i]); - if (ret != 0) { - error.Format(jack_output_domain, - "Not a valid JACK port: %s", - destination_ports[i]); - - if (jports != nullptr) - free(jports); - - mpd_jack_stop(jd); - return false; - } - } - - if (duplicate_port != nullptr) { - /* mono input file: connect the one source channel to - the both destination channels */ - int ret; - - ret = jack_connect(jd->client, jack_port_name(jd->ports[0]), - duplicate_port); - if (ret != 0) { - error.Format(jack_output_domain, - "Not a valid JACK port: %s", - duplicate_port); - - if (jports != nullptr) - free(jports); - - mpd_jack_stop(jd); - return false; - } - } - - if (jports != nullptr) - free(jports); - - return true; -} - -static bool -mpd_jack_open(struct audio_output *ao, AudioFormat &audio_format, - Error &error) -{ - JackOutput *jd = (JackOutput *)ao; - - assert(jd != nullptr); - - jd->pause = false; - - if (jd->client != nullptr && jd->shutdown) - mpd_jack_disconnect(jd); - - if (jd->client == nullptr && !mpd_jack_connect(jd, error)) - return false; - - set_audioformat(jd, audio_format); - jd->audio_format = audio_format; - - if (!mpd_jack_start(jd, error)) - return false; - - return true; -} - -static void -mpd_jack_close(gcc_unused struct audio_output *ao) -{ - JackOutput *jd = (JackOutput *)ao; - - mpd_jack_stop(jd); -} - -static unsigned -mpd_jack_delay(struct audio_output *ao) -{ - JackOutput *jd = (JackOutput *)ao; - - return jd->base.pause && jd->pause && !jd->shutdown - ? 1000 - : 0; -} - -static inline jack_default_audio_sample_t -sample_16_to_jack(int16_t sample) -{ - return sample / (jack_default_audio_sample_t)(1 << (16 - 1)); -} - -static void -mpd_jack_write_samples_16(JackOutput *jd, const int16_t *src, - unsigned num_samples) -{ - jack_default_audio_sample_t sample; - unsigned i; - - while (num_samples-- > 0) { - for (i = 0; i < jd->audio_format.channels; ++i) { - sample = sample_16_to_jack(*src++); - jack_ringbuffer_write(jd->ringbuffer[i], - (const char *)&sample, - sizeof(sample)); - } - } -} - -static inline jack_default_audio_sample_t -sample_24_to_jack(int32_t sample) -{ - return sample / (jack_default_audio_sample_t)(1 << (24 - 1)); -} - -static void -mpd_jack_write_samples_24(JackOutput *jd, const int32_t *src, - unsigned num_samples) -{ - jack_default_audio_sample_t sample; - unsigned i; - - while (num_samples-- > 0) { - for (i = 0; i < jd->audio_format.channels; ++i) { - sample = sample_24_to_jack(*src++); - jack_ringbuffer_write(jd->ringbuffer[i], - (const char *)&sample, - sizeof(sample)); - } - } -} - -static void -mpd_jack_write_samples(JackOutput *jd, const void *src, - unsigned num_samples) -{ - switch (jd->audio_format.format) { - case SampleFormat::S16: - mpd_jack_write_samples_16(jd, (const int16_t*)src, - num_samples); - break; - - case SampleFormat::S24_P32: - mpd_jack_write_samples_24(jd, (const int32_t*)src, - num_samples); - break; - - default: - assert(false); - gcc_unreachable(); - } -} - -static size_t -mpd_jack_play(struct audio_output *ao, const void *chunk, size_t size, - Error &error) -{ - JackOutput *jd = (JackOutput *)ao; - const size_t frame_size = jd->audio_format.GetFrameSize(); - size_t space = 0, space1; - - jd->pause = false; - - assert(size % frame_size == 0); - size /= frame_size; - - while (true) { - if (jd->shutdown) { - error.Set(jack_output_domain, - "Refusing to play, because " - "there is no client thread"); - return 0; - } - - space = jack_ringbuffer_write_space(jd->ringbuffer[0]); - for (unsigned i = 1; i < jd->audio_format.channels; ++i) { - space1 = jack_ringbuffer_write_space(jd->ringbuffer[i]); - if (space > space1) - /* send data symmetrically */ - space = space1; - } - - if (space >= jack_sample_size) - break; - - /* XXX do something more intelligent to - synchronize */ - g_usleep(1000); - } - - space /= jack_sample_size; - if (space < size) - size = space; - - mpd_jack_write_samples(jd, chunk, size); - return size * frame_size; -} - -static bool -mpd_jack_pause(struct audio_output *ao) -{ - JackOutput *jd = (JackOutput *)ao; - - if (jd->shutdown) - return false; - - jd->pause = true; - - return true; -} - -const struct audio_output_plugin jack_output_plugin = { - "jack", - mpd_jack_test_default_device, - mpd_jack_init, - mpd_jack_finish, - mpd_jack_enable, - mpd_jack_disable, - mpd_jack_open, - mpd_jack_close, - mpd_jack_delay, - nullptr, - mpd_jack_play, - nullptr, - nullptr, - mpd_jack_pause, - nullptr, -}; diff --git a/src/output/JackOutputPlugin.hxx b/src/output/JackOutputPlugin.hxx deleted file mode 100644 index ee3fe9238..000000000 --- a/src/output/JackOutputPlugin.hxx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_JACK_OUTPUT_PLUGIN_HXX -#define MPD_JACK_OUTPUT_PLUGIN_HXX - -extern const struct audio_output_plugin jack_output_plugin; - -#endif diff --git a/src/output/NullOutputPlugin.cxx b/src/output/NullOutputPlugin.cxx deleted file mode 100644 index 0b6476239..000000000 --- a/src/output/NullOutputPlugin.cxx +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "config.h" -#include "NullOutputPlugin.hxx" -#include "OutputAPI.hxx" -#include "Timer.hxx" - -struct NullOutput { - struct audio_output base; - - bool sync; - - Timer *timer; - - bool Initialize(const config_param ¶m, Error &error) { - return ao_base_init(&base, &null_output_plugin, param, - error); - } - - void Deinitialize() { - ao_base_finish(&base); - } -}; - -static struct audio_output * -null_init(const config_param ¶m, Error &error) -{ - NullOutput *nd = new NullOutput(); - - if (!nd->Initialize(param, error)) { - delete nd; - return nullptr; - } - - nd->sync = param.GetBlockValue("sync", true); - - return &nd->base; -} - -static void -null_finish(struct audio_output *ao) -{ - NullOutput *nd = (NullOutput *)ao; - - nd->Deinitialize(); - delete nd; -} - -static bool -null_open(struct audio_output *ao, AudioFormat &audio_format, - gcc_unused Error &error) -{ - NullOutput *nd = (NullOutput *)ao; - - if (nd->sync) - nd->timer = new Timer(audio_format); - - return true; -} - -static void -null_close(struct audio_output *ao) -{ - NullOutput *nd = (NullOutput *)ao; - - if (nd->sync) - delete nd->timer; -} - -static unsigned -null_delay(struct audio_output *ao) -{ - NullOutput *nd = (NullOutput *)ao; - - return nd->sync && nd->timer->IsStarted() - ? nd->timer->GetDelay() - : 0; -} - -static size_t -null_play(struct audio_output *ao, gcc_unused const void *chunk, size_t size, - gcc_unused Error &error) -{ - NullOutput *nd = (NullOutput *)ao; - Timer *timer = nd->timer; - - if (!nd->sync) - return size; - - if (!timer->IsStarted()) - timer->Start(); - timer->Add(size); - - return size; -} - -static void -null_cancel(struct audio_output *ao) -{ - NullOutput *nd = (NullOutput *)ao; - - if (!nd->sync) - return; - - nd->timer->Reset(); -} - -const struct audio_output_plugin null_output_plugin = { - "null", - nullptr, - null_init, - null_finish, - nullptr, - nullptr, - null_open, - null_close, - null_delay, - nullptr, - null_play, - nullptr, - null_cancel, - nullptr, - nullptr, -}; diff --git a/src/output/NullOutputPlugin.hxx b/src/output/NullOutputPlugin.hxx deleted file mode 100644 index 05b8ef3d8..000000000 --- a/src/output/NullOutputPlugin.hxx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_NULL_OUTPUT_PLUGIN_HXX -#define MPD_NULL_OUTPUT_PLUGIN_HXX - -extern const struct audio_output_plugin null_output_plugin; - -#endif diff --git a/src/output/OSXOutputPlugin.cxx b/src/output/OSXOutputPlugin.cxx deleted file mode 100644 index 6c8467752..000000000 --- a/src/output/OSXOutputPlugin.cxx +++ /dev/null @@ -1,428 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "config.h" -#include "OSXOutputPlugin.hxx" -#include "OutputAPI.hxx" -#include "util/DynamicFifoBuffer.hxx" -#include "util/Error.hxx" -#include "util/Domain.hxx" -#include "thread/Mutex.hxx" -#include "thread/Cond.hxx" -#include "system/ByteOrder.hxx" -#include "Log.hxx" - -#include -#include -#include - -struct OSXOutput { - struct audio_output base; - - /* configuration settings */ - OSType component_subtype; - /* only applicable with kAudioUnitSubType_HALOutput */ - const char *device_name; - - AudioUnit au; - Mutex mutex; - Cond condition; - - DynamicFifoBuffer *buffer; -}; - -static constexpr Domain osx_output_domain("osx_output"); - -static bool -osx_output_test_default_device(void) -{ - /* on a Mac, this is always the default plugin, if nothing - else is configured */ - return true; -} - -static void -osx_output_configure(OSXOutput *oo, const config_param ¶m) -{ - const char *device = param.GetBlockValue("device"); - - if (device == NULL || 0 == strcmp(device, "default")) { - oo->component_subtype = kAudioUnitSubType_DefaultOutput; - oo->device_name = NULL; - } - else if (0 == strcmp(device, "system")) { - oo->component_subtype = kAudioUnitSubType_SystemOutput; - oo->device_name = NULL; - } - else { - oo->component_subtype = kAudioUnitSubType_HALOutput; - /* XXX am I supposed to strdup() this? */ - oo->device_name = device; - } -} - -static struct audio_output * -osx_output_init(const config_param ¶m, Error &error) -{ - OSXOutput *oo = new OSXOutput(); - if (!ao_base_init(&oo->base, &osx_output_plugin, param, error)) { - delete oo; - return NULL; - } - - osx_output_configure(oo, param); - - return &oo->base; -} - -static void -osx_output_finish(struct audio_output *ao) -{ - OSXOutput *oo = (OSXOutput *)ao; - - delete oo; -} - -static bool -osx_output_set_device(OSXOutput *oo, Error &error) -{ - bool ret = true; - OSStatus status; - UInt32 size, numdevices; - AudioDeviceID *deviceids = NULL; - char name[256]; - unsigned int i; - - if (oo->component_subtype != kAudioUnitSubType_HALOutput) - goto done; - - /* how many audio devices are there? */ - status = AudioHardwareGetPropertyInfo(kAudioHardwarePropertyDevices, - &size, - NULL); - if (status != noErr) { - error.Format(osx_output_domain, status, - "Unable to determine number of OS X audio devices: %s", - GetMacOSStatusCommentString(status)); - ret = false; - goto done; - } - - /* what are the available audio device IDs? */ - numdevices = size / sizeof(AudioDeviceID); - deviceids = new AudioDeviceID[numdevices]; - status = AudioHardwareGetProperty(kAudioHardwarePropertyDevices, - &size, - deviceids); - if (status != noErr) { - error.Format(osx_output_domain, status, - "Unable to determine OS X audio device IDs: %s", - GetMacOSStatusCommentString(status)); - ret = false; - goto done; - } - - /* which audio device matches oo->device_name? */ - for (i = 0; i < numdevices; i++) { - size = sizeof(name); - status = AudioDeviceGetProperty(deviceids[i], 0, false, - kAudioDevicePropertyDeviceName, - &size, name); - if (status != noErr) { - error.Format(osx_output_domain, status, - "Unable to determine OS X device name " - "(device %u): %s", - (unsigned int) deviceids[i], - GetMacOSStatusCommentString(status)); - ret = false; - goto done; - } - if (strcmp(oo->device_name, name) == 0) { - FormatDebug(osx_output_domain, - "found matching device: ID=%u, name=%s", - (unsigned)deviceids[i], name); - break; - } - } - if (i == numdevices) { - FormatWarning(osx_output_domain, - "Found no audio device with name '%s' " - "(will use default audio device)", - oo->device_name); - goto done; - } - - status = AudioUnitSetProperty(oo->au, - kAudioOutputUnitProperty_CurrentDevice, - kAudioUnitScope_Global, - 0, - &(deviceids[i]), - sizeof(AudioDeviceID)); - if (status != noErr) { - error.Format(osx_output_domain, status, - "Unable to set OS X audio output device: %s", - GetMacOSStatusCommentString(status)); - ret = false; - goto done; - } - - FormatDebug(osx_output_domain, - "set OS X audio output device ID=%u, name=%s", - (unsigned)deviceids[i], name); - -done: - delete[] deviceids; - return ret; -} - -static OSStatus -osx_render(void *vdata, - gcc_unused AudioUnitRenderActionFlags *io_action_flags, - gcc_unused const AudioTimeStamp *in_timestamp, - gcc_unused UInt32 in_bus_number, - gcc_unused UInt32 in_number_frames, - AudioBufferList *buffer_list) -{ - OSXOutput *od = (OSXOutput *) vdata; - AudioBuffer *buffer = &buffer_list->mBuffers[0]; - size_t buffer_size = buffer->mDataByteSize; - - assert(od->buffer != NULL); - - od->mutex.lock(); - - auto src = od->buffer->Read(); - if (!src.IsEmpty()) { - if (src.size > buffer_size) - src.size = buffer_size; - - memcpy(buffer->mData, src.data, src.size); - od->buffer->Consume(src.size); - } - - od->condition.signal(); - od->mutex.unlock(); - - buffer->mDataByteSize = src.size; - - unsigned i; - for (i = 1; i < buffer_list->mNumberBuffers; ++i) { - buffer = &buffer_list->mBuffers[i]; - buffer->mDataByteSize = 0; - } - - return 0; -} - -static bool -osx_output_enable(struct audio_output *ao, Error &error) -{ - OSXOutput *oo = (OSXOutput *)ao; - - ComponentDescription desc; - desc.componentType = kAudioUnitType_Output; - desc.componentSubType = oo->component_subtype; - desc.componentManufacturer = kAudioUnitManufacturer_Apple; - desc.componentFlags = 0; - desc.componentFlagsMask = 0; - - Component comp = FindNextComponent(NULL, &desc); - if (comp == 0) { - error.Set(osx_output_domain, - "Error finding OS X component"); - return false; - } - - OSStatus status = OpenAComponent(comp, &oo->au); - if (status != noErr) { - error.Format(osx_output_domain, status, - "Unable to open OS X component: %s", - GetMacOSStatusCommentString(status)); - return false; - } - - if (!osx_output_set_device(oo, error)) { - CloseComponent(oo->au); - return false; - } - - AURenderCallbackStruct callback; - callback.inputProc = osx_render; - callback.inputProcRefCon = oo; - - ComponentResult result = - AudioUnitSetProperty(oo->au, - kAudioUnitProperty_SetRenderCallback, - kAudioUnitScope_Input, 0, - &callback, sizeof(callback)); - if (result != noErr) { - CloseComponent(oo->au); - error.Set(osx_output_domain, result, - "unable to set callback for OS X audio unit"); - return false; - } - - return true; -} - -static void -osx_output_disable(struct audio_output *ao) -{ - OSXOutput *oo = (OSXOutput *)ao; - - CloseComponent(oo->au); -} - -static void -osx_output_cancel(struct audio_output *ao) -{ - OSXOutput *od = (OSXOutput *)ao; - - const ScopeLock protect(od->mutex); - od->buffer->Clear(); -} - -static void -osx_output_close(struct audio_output *ao) -{ - OSXOutput *od = (OSXOutput *)ao; - - AudioOutputUnitStop(od->au); - AudioUnitUninitialize(od->au); - - delete od->buffer; -} - -static bool -osx_output_open(struct audio_output *ao, AudioFormat &audio_format, - Error &error) -{ - OSXOutput *od = (OSXOutput *)ao; - - AudioStreamBasicDescription stream_description; - stream_description.mSampleRate = audio_format.sample_rate; - stream_description.mFormatID = kAudioFormatLinearPCM; - stream_description.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger; - - switch (audio_format.format) { - case SampleFormat::S8: - stream_description.mBitsPerChannel = 8; - break; - - case SampleFormat::S16: - stream_description.mBitsPerChannel = 16; - break; - - case SampleFormat::S32: - stream_description.mBitsPerChannel = 32; - break; - - default: - audio_format.format = SampleFormat::S32; - stream_description.mBitsPerChannel = 32; - break; - } - - if (IsBigEndian()) - stream_description.mFormatFlags |= kLinearPCMFormatFlagIsBigEndian; - - stream_description.mBytesPerPacket = audio_format.GetFrameSize(); - stream_description.mFramesPerPacket = 1; - stream_description.mBytesPerFrame = stream_description.mBytesPerPacket; - stream_description.mChannelsPerFrame = audio_format.channels; - - ComponentResult result = - AudioUnitSetProperty(od->au, kAudioUnitProperty_StreamFormat, - kAudioUnitScope_Input, 0, - &stream_description, - sizeof(stream_description)); - if (result != noErr) { - error.Set(osx_output_domain, result, - "Unable to set format on OS X device"); - return false; - } - - OSStatus status = AudioUnitInitialize(od->au); - if (status != noErr) { - error.Format(osx_output_domain, status, - "Unable to initialize OS X audio unit: %s", - GetMacOSStatusCommentString(status)); - return false; - } - - /* create a buffer of 1s */ - od->buffer = new DynamicFifoBuffer(audio_format.sample_rate * - audio_format.GetFrameSize()); - - status = AudioOutputUnitStart(od->au); - if (status != 0) { - AudioUnitUninitialize(od->au); - error.Format(osx_output_domain, status, - "unable to start audio output: %s", - GetMacOSStatusCommentString(status)); - return false; - } - - return true; -} - -static size_t -osx_output_play(struct audio_output *ao, const void *chunk, size_t size, - gcc_unused Error &error) -{ - OSXOutput *od = (OSXOutput *)ao; - - const ScopeLock protect(od->mutex); - - DynamicFifoBuffer::Range dest; - while (true) { - dest = od->buffer->Write(); - if (!dest.IsEmpty()) - break; - - /* wait for some free space in the buffer */ - od->condition.wait(od->mutex); - } - - if (size > dest.size) - size = dest.size; - - memcpy(dest.data, chunk, size); - od->buffer->Append(size); - - return size; -} - -const struct audio_output_plugin osx_output_plugin = { - "osx", - osx_output_test_default_device, - osx_output_init, - osx_output_finish, - osx_output_enable, - osx_output_disable, - osx_output_open, - osx_output_close, - nullptr, - nullptr, - osx_output_play, - nullptr, - osx_output_cancel, - nullptr, - nullptr, -}; diff --git a/src/output/OSXOutputPlugin.hxx b/src/output/OSXOutputPlugin.hxx deleted file mode 100644 index 0de10f83e..000000000 --- a/src/output/OSXOutputPlugin.hxx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_OSX_OUTPUT_PLUGIN_HXX -#define MPD_OSX_OUTPUT_PLUGIN_HXX - -extern const struct audio_output_plugin osx_output_plugin; - -#endif diff --git a/src/output/OpenALOutputPlugin.cxx b/src/output/OpenALOutputPlugin.cxx deleted file mode 100644 index 52a2c9070..000000000 --- a/src/output/OpenALOutputPlugin.cxx +++ /dev/null @@ -1,285 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "config.h" -#include "OpenALOutputPlugin.hxx" -#include "OutputAPI.hxx" -#include "util/Error.hxx" -#include "util/Domain.hxx" - -#include - -#ifndef __APPLE__ -#include -#include -#else -#include -#include -#endif - -/* should be enough for buffer size = 2048 */ -#define NUM_BUFFERS 16 - -struct OpenALOutput { - struct audio_output base; - - const char *device_name; - ALCdevice *device; - ALCcontext *context; - ALuint buffers[NUM_BUFFERS]; - unsigned filled; - ALuint source; - ALenum format; - ALuint frequency; - - bool Initialize(const config_param ¶m, Error &error_r) { - return ao_base_init(&base, &openal_output_plugin, param, - error_r); - } - - void Deinitialize() { - ao_base_finish(&base); - } -}; - -static constexpr Domain openal_output_domain("openal_output"); - -static ALenum -openal_audio_format(AudioFormat &audio_format) -{ - /* note: cannot map SampleFormat::S8 to AL_FORMAT_STEREO8 or - AL_FORMAT_MONO8 since OpenAL expects unsigned 8 bit - samples, while MPD uses signed samples */ - - switch (audio_format.format) { - case SampleFormat::S16: - if (audio_format.channels == 2) - return AL_FORMAT_STEREO16; - if (audio_format.channels == 1) - return AL_FORMAT_MONO16; - - /* fall back to mono */ - audio_format.channels = 1; - return openal_audio_format(audio_format); - - default: - /* fall back to 16 bit */ - audio_format.format = SampleFormat::S16; - return openal_audio_format(audio_format); - } -} - -gcc_pure -static inline ALint -openal_get_source_i(const OpenALOutput *od, ALenum param) -{ - ALint value; - alGetSourcei(od->source, param, &value); - return value; -} - -gcc_pure -static inline bool -openal_has_processed(const OpenALOutput *od) -{ - return openal_get_source_i(od, AL_BUFFERS_PROCESSED) > 0; -} - -gcc_pure -static inline ALint -openal_is_playing(const OpenALOutput *od) -{ - return openal_get_source_i(od, AL_SOURCE_STATE) == AL_PLAYING; -} - -static bool -openal_setup_context(OpenALOutput *od, Error &error) -{ - od->device = alcOpenDevice(od->device_name); - - if (od->device == nullptr) { - error.Format(openal_output_domain, - "Error opening OpenAL device \"%s\"", - od->device_name); - return false; - } - - od->context = alcCreateContext(od->device, nullptr); - - if (od->context == nullptr) { - error.Format(openal_output_domain, - "Error creating context for \"%s\"", - od->device_name); - alcCloseDevice(od->device); - return false; - } - - return true; -} - -static struct audio_output * -openal_init(const config_param ¶m, Error &error) -{ - const char *device_name = param.GetBlockValue("device"); - if (device_name == nullptr) { - device_name = alcGetString(nullptr, ALC_DEFAULT_DEVICE_SPECIFIER); - } - - OpenALOutput *od = new OpenALOutput(); - if (!od->Initialize(param, error)) { - delete od; - return nullptr; - } - - od->device_name = device_name; - - return &od->base; -} - -static void -openal_finish(struct audio_output *ao) -{ - OpenALOutput *od = (OpenALOutput *)ao; - - od->Deinitialize(); - delete od; -} - -static bool -openal_open(struct audio_output *ao, AudioFormat &audio_format, - Error &error) -{ - OpenALOutput *od = (OpenALOutput *)ao; - - od->format = openal_audio_format(audio_format); - - if (!openal_setup_context(od, error)) { - return false; - } - - alcMakeContextCurrent(od->context); - alGenBuffers(NUM_BUFFERS, od->buffers); - - if (alGetError() != AL_NO_ERROR) { - error.Set(openal_output_domain, "Failed to generate buffers"); - return false; - } - - alGenSources(1, &od->source); - - if (alGetError() != AL_NO_ERROR) { - error.Set(openal_output_domain, "Failed to generate source"); - alDeleteBuffers(NUM_BUFFERS, od->buffers); - return false; - } - - od->filled = 0; - od->frequency = audio_format.sample_rate; - - return true; -} - -static void -openal_close(struct audio_output *ao) -{ - OpenALOutput *od = (OpenALOutput *)ao; - - alcMakeContextCurrent(od->context); - alDeleteSources(1, &od->source); - alDeleteBuffers(NUM_BUFFERS, od->buffers); - alcDestroyContext(od->context); - alcCloseDevice(od->device); -} - -static unsigned -openal_delay(struct audio_output *ao) -{ - OpenALOutput *od = (OpenALOutput *)ao; - - return od->filled < NUM_BUFFERS || openal_has_processed(od) - ? 0 - /* we don't know exactly how long we must wait for the - next buffer to finish, so this is a random - guess: */ - : 50; -} - -static size_t -openal_play(struct audio_output *ao, const void *chunk, size_t size, - gcc_unused Error &error) -{ - OpenALOutput *od = (OpenALOutput *)ao; - ALuint buffer; - - if (alcGetCurrentContext() != od->context) { - alcMakeContextCurrent(od->context); - } - - if (od->filled < NUM_BUFFERS) { - /* fill all buffers */ - buffer = od->buffers[od->filled]; - od->filled++; - } else { - /* wait for processed buffer */ - while (!openal_has_processed(od)) - g_usleep(10); - - alSourceUnqueueBuffers(od->source, 1, &buffer); - } - - alBufferData(buffer, od->format, chunk, size, od->frequency); - alSourceQueueBuffers(od->source, 1, &buffer); - - if (!openal_is_playing(od)) - alSourcePlay(od->source); - - return size; -} - -static void -openal_cancel(struct audio_output *ao) -{ - OpenALOutput *od = (OpenALOutput *)ao; - - od->filled = 0; - alcMakeContextCurrent(od->context); - alSourceStop(od->source); - - /* force-unqueue all buffers */ - alSourcei(od->source, AL_BUFFER, 0); - od->filled = 0; -} - -const struct audio_output_plugin openal_output_plugin = { - "openal", - nullptr, - openal_init, - openal_finish, - nullptr, - nullptr, - openal_open, - openal_close, - openal_delay, - nullptr, - openal_play, - nullptr, - openal_cancel, - nullptr, - nullptr, -}; diff --git a/src/output/OpenALOutputPlugin.hxx b/src/output/OpenALOutputPlugin.hxx deleted file mode 100644 index eb43d1aa5..000000000 --- a/src/output/OpenALOutputPlugin.hxx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_OPENAL_OUTPUT_PLUGIN_HXX -#define MPD_OPENAL_OUTPUT_PLUGIN_HXX - -extern const struct audio_output_plugin openal_output_plugin; - -#endif diff --git a/src/output/OssOutputPlugin.cxx b/src/output/OssOutputPlugin.cxx deleted file mode 100644 index 24f3f48b5..000000000 --- a/src/output/OssOutputPlugin.cxx +++ /dev/null @@ -1,776 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "config.h" -#include "OssOutputPlugin.hxx" -#include "OutputAPI.hxx" -#include "MixerList.hxx" -#include "system/fd_util.h" -#include "util/Error.hxx" -#include "util/Domain.hxx" -#include "util/Macros.hxx" -#include "system/ByteOrder.hxx" -#include "Log.hxx" - -#include -#include -#include -#include -#include -#include -#include - -#if defined(__OpenBSD__) || defined(__NetBSD__) -# include -#else /* !(defined(__OpenBSD__) || defined(__NetBSD__) */ -# include -#endif /* !(defined(__OpenBSD__) || defined(__NetBSD__) */ - -/* We got bug reports from FreeBSD users who said that the two 24 bit - formats generate white noise on FreeBSD, but 32 bit works. This is - a workaround until we know what exactly is expected by the kernel - audio drivers. */ -#ifndef __linux__ -#undef AFMT_S24_PACKED -#undef AFMT_S24_NE -#endif - -#ifdef AFMT_S24_PACKED -#include "pcm/PcmExport.hxx" -#include "util/Manual.hxx" -#endif - -struct OssOutput { - struct audio_output base; - -#ifdef AFMT_S24_PACKED - Manual pcm_export; -#endif - - int fd; - const char *device; - - /** - * The current input audio format. This is needed to reopen - * the device after cancel(). - */ - AudioFormat audio_format; - - /** - * The current OSS audio format. This is needed to reopen the - * device after cancel(). - */ - int oss_format; - - OssOutput():fd(-1), device(nullptr) {} - - bool Initialize(const config_param ¶m, Error &error_r) { - return ao_base_init(&base, &oss_output_plugin, param, - error_r); - } - - void Deinitialize() { - ao_base_finish(&base); - } -}; - -static constexpr Domain oss_output_domain("oss_output"); - -enum oss_stat { - OSS_STAT_NO_ERROR = 0, - OSS_STAT_NOT_CHAR_DEV = -1, - OSS_STAT_NO_PERMS = -2, - OSS_STAT_DOESN_T_EXIST = -3, - OSS_STAT_OTHER = -4, -}; - -static enum oss_stat -oss_stat_device(const char *device, int *errno_r) -{ - struct stat st; - - if (0 == stat(device, &st)) { - if (!S_ISCHR(st.st_mode)) { - return OSS_STAT_NOT_CHAR_DEV; - } - } else { - *errno_r = errno; - - switch (errno) { - case ENOENT: - case ENOTDIR: - return OSS_STAT_DOESN_T_EXIST; - case EACCES: - return OSS_STAT_NO_PERMS; - default: - return OSS_STAT_OTHER; - } - } - - return OSS_STAT_NO_ERROR; -} - -static const char *default_devices[] = { "/dev/sound/dsp", "/dev/dsp" }; - -static bool -oss_output_test_default_device(void) -{ - int fd, i; - - for (i = ARRAY_SIZE(default_devices); --i >= 0; ) { - fd = open_cloexec(default_devices[i], O_WRONLY, 0); - - if (fd >= 0) { - close(fd); - return true; - } - - FormatErrno(oss_output_domain, - "Error opening OSS device \"%s\"", - default_devices[i]); - } - - return false; -} - -static struct audio_output * -oss_open_default(Error &error) -{ - int err[ARRAY_SIZE(default_devices)]; - enum oss_stat ret[ARRAY_SIZE(default_devices)]; - - const config_param empty; - for (int i = ARRAY_SIZE(default_devices); --i >= 0; ) { - ret[i] = oss_stat_device(default_devices[i], &err[i]); - if (ret[i] == OSS_STAT_NO_ERROR) { - OssOutput *od = new OssOutput(); - if (!od->Initialize(empty, error)) { - delete od; - return NULL; - } - - od->device = default_devices[i]; - return &od->base; - } - } - - for (int i = ARRAY_SIZE(default_devices); --i >= 0; ) { - const char *dev = default_devices[i]; - switch(ret[i]) { - case OSS_STAT_NO_ERROR: - /* never reached */ - break; - case OSS_STAT_DOESN_T_EXIST: - FormatWarning(oss_output_domain, - "%s not found", dev); - break; - case OSS_STAT_NOT_CHAR_DEV: - FormatWarning(oss_output_domain, - "%s is not a character device", dev); - break; - case OSS_STAT_NO_PERMS: - FormatWarning(oss_output_domain, - "%s: permission denied", dev); - break; - case OSS_STAT_OTHER: - FormatErrno(oss_output_domain, err[i], - "Error accessing %s", dev); - } - } - - error.Set(oss_output_domain, - "error trying to open default OSS device"); - return NULL; -} - -static struct audio_output * -oss_output_init(const config_param ¶m, Error &error) -{ - const char *device = param.GetBlockValue("device"); - if (device != NULL) { - OssOutput *od = new OssOutput(); - if (!od->Initialize(param, error)) { - delete od; - return NULL; - } - - od->device = device; - return &od->base; - } - - return oss_open_default(error); -} - -static void -oss_output_finish(struct audio_output *ao) -{ - OssOutput *od = (OssOutput *)ao; - - ao_base_finish(&od->base); - delete od; -} - -#ifdef AFMT_S24_PACKED - -static bool -oss_output_enable(struct audio_output *ao, gcc_unused Error &error) -{ - OssOutput *od = (OssOutput *)ao; - - od->pcm_export.Construct(); - return true; -} - -static void -oss_output_disable(struct audio_output *ao) -{ - OssOutput *od = (OssOutput *)ao; - - od->pcm_export.Destruct(); -} - -#endif - -static void -oss_close(OssOutput *od) -{ - if (od->fd >= 0) - close(od->fd); - od->fd = -1; -} - -/** - * A tri-state type for oss_try_ioctl(). - */ -enum oss_setup_result { - SUCCESS, - ERROR, - UNSUPPORTED, -}; - -/** - * Invoke an ioctl on the OSS file descriptor. On success, SUCCESS is - * returned. If the parameter is not supported, UNSUPPORTED is - * returned. Any other failure returns ERROR and allocates an #Error. - */ -static enum oss_setup_result -oss_try_ioctl_r(int fd, unsigned long request, int *value_r, - const char *msg, Error &error) -{ - assert(fd >= 0); - assert(value_r != NULL); - assert(msg != NULL); - assert(!error.IsDefined()); - - int ret = ioctl(fd, request, value_r); - if (ret >= 0) - return SUCCESS; - - if (errno == EINVAL) - return UNSUPPORTED; - - error.SetErrno(msg); - return ERROR; -} - -/** - * Invoke an ioctl on the OSS file descriptor. On success, SUCCESS is - * returned. If the parameter is not supported, UNSUPPORTED is - * returned. Any other failure returns ERROR and allocates an #Error. - */ -static enum oss_setup_result -oss_try_ioctl(int fd, unsigned long request, int value, - const char *msg, Error &error_r) -{ - return oss_try_ioctl_r(fd, request, &value, msg, error_r); -} - -/** - * Set up the channel number, and attempts to find alternatives if the - * specified number is not supported. - */ -static bool -oss_setup_channels(int fd, AudioFormat &audio_format, Error &error) -{ - const char *const msg = "Failed to set channel count"; - int channels = audio_format.channels; - enum oss_setup_result result = - oss_try_ioctl_r(fd, SNDCTL_DSP_CHANNELS, &channels, msg, error); - switch (result) { - case SUCCESS: - if (!audio_valid_channel_count(channels)) - break; - - audio_format.channels = channels; - return true; - - case ERROR: - return false; - - case UNSUPPORTED: - break; - } - - for (unsigned i = 1; i < 2; ++i) { - if (i == audio_format.channels) - /* don't try that again */ - continue; - - channels = i; - result = oss_try_ioctl_r(fd, SNDCTL_DSP_CHANNELS, &channels, - msg, error); - switch (result) { - case SUCCESS: - if (!audio_valid_channel_count(channels)) - break; - - audio_format.channels = channels; - return true; - - case ERROR: - return false; - - case UNSUPPORTED: - break; - } - } - - error.Set(oss_output_domain, msg); - return false; -} - -/** - * Set up the sample rate, and attempts to find alternatives if the - * specified sample rate is not supported. - */ -static bool -oss_setup_sample_rate(int fd, AudioFormat &audio_format, - Error &error) -{ - const char *const msg = "Failed to set sample rate"; - int sample_rate = audio_format.sample_rate; - enum oss_setup_result result = - oss_try_ioctl_r(fd, SNDCTL_DSP_SPEED, &sample_rate, - msg, error); - switch (result) { - case SUCCESS: - if (!audio_valid_sample_rate(sample_rate)) - break; - - audio_format.sample_rate = sample_rate; - return true; - - case ERROR: - return false; - - case UNSUPPORTED: - break; - } - - static const int sample_rates[] = { 48000, 44100, 0 }; - for (unsigned i = 0; sample_rates[i] != 0; ++i) { - sample_rate = sample_rates[i]; - if (sample_rate == (int)audio_format.sample_rate) - continue; - - result = oss_try_ioctl_r(fd, SNDCTL_DSP_SPEED, &sample_rate, - msg, error); - switch (result) { - case SUCCESS: - if (!audio_valid_sample_rate(sample_rate)) - break; - - audio_format.sample_rate = sample_rate; - return true; - - case ERROR: - return false; - - case UNSUPPORTED: - break; - } - } - - error.Set(oss_output_domain, msg); - return false; -} - -/** - * Convert a MPD sample format to its OSS counterpart. Returns - * AFMT_QUERY if there is no direct counterpart. - */ -static int -sample_format_to_oss(SampleFormat format) -{ - switch (format) { - case SampleFormat::UNDEFINED: - case SampleFormat::FLOAT: - case SampleFormat::DSD: - return AFMT_QUERY; - - case SampleFormat::S8: - return AFMT_S8; - - case SampleFormat::S16: - return AFMT_S16_NE; - - case SampleFormat::S24_P32: -#ifdef AFMT_S24_NE - return AFMT_S24_NE; -#else - return AFMT_QUERY; -#endif - - case SampleFormat::S32: -#ifdef AFMT_S32_NE - return AFMT_S32_NE; -#else - return AFMT_QUERY; -#endif - } - - return AFMT_QUERY; -} - -/** - * Convert an OSS sample format to its MPD counterpart. Returns - * SampleFormat::UNDEFINED if there is no direct counterpart. - */ -static SampleFormat -sample_format_from_oss(int format) -{ - switch (format) { - case AFMT_S8: - return SampleFormat::S8; - - case AFMT_S16_NE: - return SampleFormat::S16; - -#ifdef AFMT_S24_PACKED - case AFMT_S24_PACKED: - return SampleFormat::S24_P32; -#endif - -#ifdef AFMT_S24_NE - case AFMT_S24_NE: - return SampleFormat::S24_P32; -#endif - -#ifdef AFMT_S32_NE - case AFMT_S32_NE: - return SampleFormat::S32; -#endif - - default: - return SampleFormat::UNDEFINED; - } -} - -/** - * Probe one sample format. - * - * @return the selected sample format or SampleFormat::UNDEFINED on - * error - */ -static enum oss_setup_result -oss_probe_sample_format(int fd, SampleFormat sample_format, - SampleFormat *sample_format_r, - int *oss_format_r, -#ifdef AFMT_S24_PACKED - PcmExport &pcm_export, -#endif - Error &error) -{ - int oss_format = sample_format_to_oss(sample_format); - if (oss_format == AFMT_QUERY) - return UNSUPPORTED; - - enum oss_setup_result result = - oss_try_ioctl_r(fd, SNDCTL_DSP_SAMPLESIZE, - &oss_format, - "Failed to set sample format", error); - -#ifdef AFMT_S24_PACKED - if (result == UNSUPPORTED && sample_format == SampleFormat::S24_P32) { - /* if the driver doesn't support padded 24 bit, try - packed 24 bit */ - oss_format = AFMT_S24_PACKED; - result = oss_try_ioctl_r(fd, SNDCTL_DSP_SAMPLESIZE, - &oss_format, - "Failed to set sample format", error); - } -#endif - - if (result != SUCCESS) - return result; - - sample_format = sample_format_from_oss(oss_format); - if (sample_format == SampleFormat::UNDEFINED) - return UNSUPPORTED; - - *sample_format_r = sample_format; - *oss_format_r = oss_format; - -#ifdef AFMT_S24_PACKED - pcm_export.Open(sample_format, 0, false, false, - oss_format == AFMT_S24_PACKED, - oss_format == AFMT_S24_PACKED && - !IsLittleEndian()); -#endif - - return SUCCESS; -} - -/** - * Set up the sample format, and attempts to find alternatives if the - * specified format is not supported. - */ -static bool -oss_setup_sample_format(int fd, AudioFormat &audio_format, - int *oss_format_r, -#ifdef AFMT_S24_PACKED - PcmExport &pcm_export, -#endif - Error &error) -{ - SampleFormat mpd_format; - enum oss_setup_result result = - oss_probe_sample_format(fd, audio_format.format, - &mpd_format, oss_format_r, -#ifdef AFMT_S24_PACKED - pcm_export, -#endif - error); - switch (result) { - case SUCCESS: - audio_format.format = mpd_format; - return true; - - case ERROR: - return false; - - case UNSUPPORTED: - break; - } - - if (result != UNSUPPORTED) - return result == SUCCESS; - - /* the requested sample format is not available - probe for - other formats supported by MPD */ - - static const SampleFormat sample_formats[] = { - SampleFormat::S24_P32, - SampleFormat::S32, - SampleFormat::S16, - SampleFormat::S8, - SampleFormat::UNDEFINED /* sentinel */ - }; - - for (unsigned i = 0; sample_formats[i] != SampleFormat::UNDEFINED; ++i) { - mpd_format = sample_formats[i]; - if (mpd_format == audio_format.format) - /* don't try that again */ - continue; - - result = oss_probe_sample_format(fd, mpd_format, - &mpd_format, oss_format_r, -#ifdef AFMT_S24_PACKED - pcm_export, -#endif - error); - switch (result) { - case SUCCESS: - audio_format.format = mpd_format; - return true; - - case ERROR: - return false; - - case UNSUPPORTED: - break; - } - } - - error.Set(oss_output_domain, "Failed to set sample format"); - return false; -} - -/** - * Sets up the OSS device which was opened before. - */ -static bool -oss_setup(OssOutput *od, AudioFormat &audio_format, - Error &error) -{ - return oss_setup_channels(od->fd, audio_format, error) && - oss_setup_sample_rate(od->fd, audio_format, error) && - oss_setup_sample_format(od->fd, audio_format, &od->oss_format, -#ifdef AFMT_S24_PACKED - od->pcm_export, -#endif - error); -} - -/** - * Reopen the device with the saved audio_format, without any probing. - */ -static bool -oss_reopen(OssOutput *od, Error &error) -{ - assert(od->fd < 0); - - od->fd = open_cloexec(od->device, O_WRONLY, 0); - if (od->fd < 0) { - error.FormatErrno("Error opening OSS device \"%s\"", - od->device); - return false; - } - - enum oss_setup_result result; - - const char *const msg1 = "Failed to set channel count"; - result = oss_try_ioctl(od->fd, SNDCTL_DSP_CHANNELS, - od->audio_format.channels, msg1, error); - if (result != SUCCESS) { - oss_close(od); - if (result == UNSUPPORTED) - error.Set(oss_output_domain, msg1); - return false; - } - - const char *const msg2 = "Failed to set sample rate"; - result = oss_try_ioctl(od->fd, SNDCTL_DSP_SPEED, - od->audio_format.sample_rate, msg2, error); - if (result != SUCCESS) { - oss_close(od); - if (result == UNSUPPORTED) - error.Set(oss_output_domain, msg2); - return false; - } - - const char *const msg3 = "Failed to set sample format"; - result = oss_try_ioctl(od->fd, SNDCTL_DSP_SAMPLESIZE, - od->oss_format, - msg3, error); - if (result != SUCCESS) { - oss_close(od); - if (result == UNSUPPORTED) - error.Set(oss_output_domain, msg3); - return false; - } - - return true; -} - -static bool -oss_output_open(struct audio_output *ao, AudioFormat &audio_format, - Error &error) -{ - OssOutput *od = (OssOutput *)ao; - - od->fd = open_cloexec(od->device, O_WRONLY, 0); - if (od->fd < 0) { - error.FormatErrno("Error opening OSS device \"%s\"", - od->device); - return false; - } - - if (!oss_setup(od, audio_format, error)) { - oss_close(od); - return false; - } - - od->audio_format = audio_format; - return true; -} - -static void -oss_output_close(struct audio_output *ao) -{ - OssOutput *od = (OssOutput *)ao; - - oss_close(od); -} - -static void -oss_output_cancel(struct audio_output *ao) -{ - OssOutput *od = (OssOutput *)ao; - - if (od->fd >= 0) { - ioctl(od->fd, SNDCTL_DSP_RESET, 0); - oss_close(od); - } -} - -static size_t -oss_output_play(struct audio_output *ao, const void *chunk, size_t size, - Error &error) -{ - OssOutput *od = (OssOutput *)ao; - ssize_t ret; - - /* reopen the device since it was closed by dropBufferedAudio */ - if (od->fd < 0 && !oss_reopen(od, error)) - return 0; - -#ifdef AFMT_S24_PACKED - chunk = od->pcm_export->Export(chunk, size, size); -#endif - - while (true) { - ret = write(od->fd, chunk, size); - if (ret > 0) { -#ifdef AFMT_S24_PACKED - ret = od->pcm_export->CalcSourceSize(ret); -#endif - return ret; - } - - if (ret < 0 && errno != EINTR) { - error.FormatErrno("Write error on %s", od->device); - return 0; - } - } -} - -const struct audio_output_plugin oss_output_plugin = { - "oss", - oss_output_test_default_device, - oss_output_init, - oss_output_finish, -#ifdef AFMT_S24_PACKED - oss_output_enable, - oss_output_disable, -#else - nullptr, - nullptr, -#endif - oss_output_open, - oss_output_close, - nullptr, - nullptr, - oss_output_play, - nullptr, - oss_output_cancel, - nullptr, - - &oss_mixer_plugin, -}; diff --git a/src/output/OssOutputPlugin.hxx b/src/output/OssOutputPlugin.hxx deleted file mode 100644 index 4762fa652..000000000 --- a/src/output/OssOutputPlugin.hxx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_OSS_OUTPUT_PLUGIN_HXX -#define MPD_OSS_OUTPUT_PLUGIN_HXX - -extern const struct audio_output_plugin oss_output_plugin; - -#endif diff --git a/src/output/OutputAPI.hxx b/src/output/OutputAPI.hxx new file mode 100644 index 000000000..322ed3971 --- /dev/null +++ b/src/output/OutputAPI.hxx @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OUTPUT_API_HXX +#define MPD_OUTPUT_API_HXX + +// IWYU pragma: begin_exports + +#include "OutputPlugin.hxx" +#include "OutputInternal.hxx" +#include "AudioFormat.hxx" +#include "tag/Tag.hxx" +#include "ConfigData.hxx" + +// IWYU pragma: end_exports + +#endif diff --git a/src/output/OutputAll.cxx b/src/output/OutputAll.cxx new file mode 100644 index 000000000..b3623f1af --- /dev/null +++ b/src/output/OutputAll.cxx @@ -0,0 +1,589 @@ +/* + * 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 "OutputAll.hxx" +#include "PlayerControl.hxx" +#include "OutputInternal.hxx" +#include "OutputControl.hxx" +#include "OutputError.hxx" +#include "MusicBuffer.hxx" +#include "MusicPipe.hxx" +#include "MusicChunk.hxx" +#include "system/FatalError.hxx" +#include "util/Error.hxx" +#include "ConfigData.hxx" +#include "ConfigGlobal.hxx" +#include "ConfigOption.hxx" +#include "notify.hxx" + +#include +#include + +static AudioFormat input_audio_format; + +static struct audio_output **audio_outputs; +static unsigned int num_audio_outputs; + +/** + * The #MusicBuffer object where consumed chunks are returned. + */ +static MusicBuffer *g_music_buffer; + +/** + * The #MusicPipe object which feeds all audio outputs. It is filled + * by audio_output_all_play(). + */ +static MusicPipe *g_mp; + +/** + * The "elapsed_time" stamp of the most recently finished chunk. + */ +static float audio_output_all_elapsed_time = -1.0; + +unsigned int audio_output_count(void) +{ + return num_audio_outputs; +} + +struct audio_output * +audio_output_get(unsigned i) +{ + assert(i < num_audio_outputs); + + assert(audio_outputs[i] != nullptr); + + return audio_outputs[i]; +} + +struct audio_output * +audio_output_find(const char *name) +{ + for (unsigned i = 0; i < num_audio_outputs; ++i) { + struct audio_output *ao = audio_output_get(i); + + if (strcmp(ao->name, name) == 0) + return ao; + } + + /* name not found */ + return nullptr; +} + +gcc_const +static unsigned +audio_output_config_count(void) +{ + unsigned int nr = 0; + const struct config_param *param = nullptr; + + while ((param = config_get_next_param(CONF_AUDIO_OUTPUT, param))) + nr++; + if (!nr) + nr = 1; /* we'll always have at least one device */ + return nr; +} + +void +audio_output_all_init(PlayerControl &pc) +{ + const struct config_param *param = nullptr; + unsigned int i; + Error error; + + num_audio_outputs = audio_output_config_count(); + audio_outputs = new audio_output *[num_audio_outputs]; + + const config_param empty; + + for (i = 0; i < num_audio_outputs; i++) + { + unsigned int j; + + param = config_get_next_param(CONF_AUDIO_OUTPUT, param); + if (param == nullptr) { + /* only allow param to be nullptr if there + just one audio output */ + assert(i == 0); + assert(num_audio_outputs == 1); + + param = ∅ + } + + audio_output *output = audio_output_new(*param, pc, error); + if (output == nullptr) { + if (param != nullptr) + FormatFatalError("line %i: %s", + param->line, + error.GetMessage()); + else + FatalError(error); + } + + audio_outputs[i] = output; + + /* require output names to be unique: */ + for (j = 0; j < i; j++) { + if (!strcmp(output->name, audio_outputs[j]->name)) { + FormatFatalError("output devices with identical " + "names: %s", output->name); + } + } + } +} + +void +audio_output_all_finish(void) +{ + unsigned int i; + + for (i = 0; i < num_audio_outputs; i++) { + audio_output_disable(audio_outputs[i]); + audio_output_finish(audio_outputs[i]); + } + + delete[] audio_outputs; + audio_outputs = nullptr; + num_audio_outputs = 0; +} + +void +audio_output_all_enable_disable(void) +{ + for (unsigned i = 0; i < num_audio_outputs; i++) { + struct audio_output *ao = audio_outputs[i]; + bool enabled; + + ao->mutex.lock(); + enabled = ao->really_enabled; + ao->mutex.unlock(); + + if (ao->enabled != enabled) { + if (ao->enabled) + audio_output_enable(ao); + else + audio_output_disable(ao); + } + } +} + +/** + * Determine if all (active) outputs have finished the current + * command. + */ +static bool +audio_output_all_finished(void) +{ + for (unsigned i = 0; i < num_audio_outputs; ++i) { + struct audio_output *ao = audio_outputs[i]; + + const ScopeLock protect(ao->mutex); + if (audio_output_is_open(ao) && + !audio_output_command_is_finished(ao)) + return false; + } + + return true; +} + +static void audio_output_wait_all(void) +{ + while (!audio_output_all_finished()) + audio_output_client_notify.Wait(); +} + +/** + * Signals all audio outputs which are open. + */ +static void +audio_output_allow_play_all(void) +{ + for (unsigned i = 0; i < num_audio_outputs; ++i) + audio_output_allow_play(audio_outputs[i]); +} + +static void +audio_output_reset_reopen(struct audio_output *ao) +{ + const ScopeLock protect(ao->mutex); + + ao->fail_timer.Reset(); +} + +/** + * Resets the "reopen" flag on all audio devices. MPD should + * immediately retry to open the device instead of waiting for the + * timeout when the user wants to start playback. + */ +static void +audio_output_all_reset_reopen(void) +{ + for (unsigned i = 0; i < num_audio_outputs; ++i) { + struct audio_output *ao = audio_outputs[i]; + + audio_output_reset_reopen(ao); + } +} + +/** + * Opens all output devices which are enabled, but closed. + * + * @return true if there is at least open output device which is open + */ +static bool +audio_output_all_update(void) +{ + unsigned int i; + bool ret = false; + + if (!input_audio_format.IsDefined()) + return false; + + for (i = 0; i < num_audio_outputs; ++i) + ret = audio_output_update(audio_outputs[i], + input_audio_format, *g_mp) || ret; + + return ret; +} + +void +audio_output_all_set_replay_gain_mode(ReplayGainMode mode) +{ + for (unsigned i = 0; i < num_audio_outputs; ++i) + audio_output_set_replay_gain_mode(audio_outputs[i], mode); +} + +bool +audio_output_all_play(struct music_chunk *chunk, Error &error) +{ + bool ret; + unsigned int i; + + assert(g_music_buffer != nullptr); + assert(g_mp != nullptr); + assert(chunk != nullptr); + assert(chunk->CheckFormat(input_audio_format)); + + ret = audio_output_all_update(); + if (!ret) { + /* TODO: obtain real error */ + error.Set(output_domain, "Failed to open audio output"); + return false; + } + + g_mp->Push(chunk); + + for (i = 0; i < num_audio_outputs; ++i) + audio_output_play(audio_outputs[i]); + + return true; +} + +bool +audio_output_all_open(const AudioFormat audio_format, + MusicBuffer &buffer, + Error &error) +{ + bool ret = false, enabled = false; + unsigned int i; + + assert(g_music_buffer == nullptr || g_music_buffer == &buffer); + assert((g_mp == nullptr) == (g_music_buffer == nullptr)); + + g_music_buffer = &buffer; + + /* the audio format must be the same as existing chunks in the + pipe */ + assert(g_mp == nullptr || g_mp->CheckFormat(audio_format)); + + if (g_mp == nullptr) + g_mp = new MusicPipe(); + else + /* if the pipe hasn't been cleared, the the audio + format must not have changed */ + assert(g_mp->IsEmpty() || audio_format == input_audio_format); + + input_audio_format = audio_format; + + audio_output_all_reset_reopen(); + audio_output_all_enable_disable(); + audio_output_all_update(); + + for (i = 0; i < num_audio_outputs; ++i) { + if (audio_outputs[i]->enabled) + enabled = true; + + if (audio_outputs[i]->open) + ret = true; + } + + if (!enabled) + error.Set(output_domain, "All audio outputs are disabled"); + else if (!ret) + /* TODO: obtain real error */ + error.Set(output_domain, "Failed to open audio output"); + + if (!ret) + /* close all devices if there was an error */ + audio_output_all_close(); + + return ret; +} + +/** + * Has the specified audio output already consumed this chunk? + */ +static bool +chunk_is_consumed_in(const struct audio_output *ao, + const struct music_chunk *chunk) +{ + if (!ao->open) + return true; + + if (ao->chunk == nullptr) + return false; + + assert(chunk == ao->chunk || g_mp->Contains(ao->chunk)); + + if (chunk != ao->chunk) { + assert(chunk->next != nullptr); + return true; + } + + return ao->chunk_finished && chunk->next == nullptr; +} + +/** + * Has this chunk been consumed by all audio outputs? + */ +static bool +chunk_is_consumed(const struct music_chunk *chunk) +{ + for (unsigned i = 0; i < num_audio_outputs; ++i) { + struct audio_output *ao = audio_outputs[i]; + + const ScopeLock protect(ao->mutex); + if (!chunk_is_consumed_in(ao, chunk)) + return false; + } + + return true; +} + +/** + * There's only one chunk left in the pipe (#g_mp), and all audio + * outputs have consumed it already. Clear the reference. + */ +static void +clear_tail_chunk(gcc_unused const struct music_chunk *chunk, bool *locked) +{ + assert(chunk->next == nullptr); + assert(g_mp->Contains(chunk)); + + for (unsigned i = 0; i < num_audio_outputs; ++i) { + struct audio_output *ao = audio_outputs[i]; + + /* this mutex will be unlocked by the caller when it's + ready */ + ao->mutex.lock(); + locked[i] = ao->open; + + if (!locked[i]) { + ao->mutex.unlock(); + continue; + } + + assert(ao->chunk == chunk); + assert(ao->chunk_finished); + ao->chunk = nullptr; + } +} + +unsigned +audio_output_all_check(void) +{ + const struct music_chunk *chunk; + bool is_tail; + struct music_chunk *shifted; + bool locked[num_audio_outputs]; + + assert(g_music_buffer != nullptr); + assert(g_mp != nullptr); + + while ((chunk = g_mp->Peek()) != nullptr) { + assert(!g_mp->IsEmpty()); + + if (!chunk_is_consumed(chunk)) + /* at least one output is not finished playing + this chunk */ + return g_mp->GetSize(); + + if (chunk->length > 0 && chunk->times >= 0.0) + /* only update elapsed_time if the chunk + provides a defined value */ + audio_output_all_elapsed_time = chunk->times; + + is_tail = chunk->next == nullptr; + if (is_tail) + /* this is the tail of the pipe - clear the + chunk reference in all outputs */ + clear_tail_chunk(chunk, locked); + + /* remove the chunk from the pipe */ + shifted = g_mp->Shift(); + assert(shifted == chunk); + + if (is_tail) + /* unlock all audio outputs which were locked + by clear_tail_chunk() */ + for (unsigned i = 0; i < num_audio_outputs; ++i) + if (locked[i]) + audio_outputs[i]->mutex.unlock(); + + /* return the chunk to the buffer */ + g_music_buffer->Return(shifted); + } + + return 0; +} + +bool +audio_output_all_wait(PlayerControl &pc, unsigned threshold) +{ + pc.Lock(); + + if (audio_output_all_check() < threshold) { + pc.Unlock(); + return true; + } + + pc.Wait(); + pc.Unlock(); + + return audio_output_all_check() < threshold; +} + +void +audio_output_all_pause(void) +{ + unsigned int i; + + audio_output_all_update(); + + for (i = 0; i < num_audio_outputs; ++i) + audio_output_pause(audio_outputs[i]); + + audio_output_wait_all(); +} + +void +audio_output_all_drain(void) +{ + for (unsigned i = 0; i < num_audio_outputs; ++i) + audio_output_drain_async(audio_outputs[i]); + + audio_output_wait_all(); +} + +void +audio_output_all_cancel(void) +{ + unsigned int i; + + /* send the cancel() command to all audio outputs */ + + for (i = 0; i < num_audio_outputs; ++i) + audio_output_cancel(audio_outputs[i]); + + audio_output_wait_all(); + + /* clear the music pipe and return all chunks to the buffer */ + + if (g_mp != nullptr) + g_mp->Clear(*g_music_buffer); + + /* the audio outputs are now waiting for a signal, to + synchronize the cleared music pipe */ + + audio_output_allow_play_all(); + + /* invalidate elapsed_time */ + + audio_output_all_elapsed_time = -1.0; +} + +void +audio_output_all_close(void) +{ + unsigned int i; + + for (i = 0; i < num_audio_outputs; ++i) + audio_output_close(audio_outputs[i]); + + if (g_mp != nullptr) { + assert(g_music_buffer != nullptr); + + g_mp->Clear(*g_music_buffer); + delete g_mp; + g_mp = nullptr; + } + + g_music_buffer = nullptr; + + input_audio_format.Clear(); + + audio_output_all_elapsed_time = -1.0; +} + +void +audio_output_all_release(void) +{ + unsigned int i; + + for (i = 0; i < num_audio_outputs; ++i) + audio_output_release(audio_outputs[i]); + + if (g_mp != nullptr) { + assert(g_music_buffer != nullptr); + + g_mp->Clear(*g_music_buffer); + delete g_mp; + g_mp = nullptr; + } + + g_music_buffer = nullptr; + + input_audio_format.Clear(); + + audio_output_all_elapsed_time = -1.0; +} + +void +audio_output_all_song_border(void) +{ + /* clear the elapsed_time pointer at the beginning of a new + song */ + audio_output_all_elapsed_time = 0.0; +} + +float +audio_output_all_get_elapsed_time(void) +{ + return audio_output_all_elapsed_time; +} diff --git a/src/output/OutputAll.hxx b/src/output/OutputAll.hxx new file mode 100644 index 000000000..b6166eb48 --- /dev/null +++ b/src/output/OutputAll.hxx @@ -0,0 +1,174 @@ +/* + * 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. + */ + +/* + * Functions for dealing with all configured (enabled) audion outputs + * at once. + * + */ + +#ifndef OUTPUT_ALL_H +#define OUTPUT_ALL_H + +#include "ReplayGainInfo.hxx" +#include "Compiler.h" + +struct AudioFormat; +class MusicBuffer; +struct music_chunk; +struct PlayerControl; +class Error; + +/** + * Global initialization: load audio outputs from the configuration + * file and initialize them. + */ +void +audio_output_all_init(PlayerControl &pc); + +/** + * Global finalization: free memory occupied by audio outputs. All + */ +void +audio_output_all_finish(void); + +/** + * Returns the total number of audio output devices, including those + * who are disabled right now. + */ +gcc_const +unsigned int audio_output_count(void); + +/** + * Returns the "i"th audio output device. + */ +gcc_const +struct audio_output * +audio_output_get(unsigned i); + +/** + * Returns the audio output device with the specified name. Returns + * NULL if the name does not exist. + */ +gcc_pure +struct audio_output * +audio_output_find(const char *name); + +/** + * Checks the "enabled" flag of all audio outputs, and if one has + * changed, commit the change. + */ +void +audio_output_all_enable_disable(void); + +/** + * Opens all audio outputs which are not disabled. + * + * @param audio_format the preferred audio format + * @param buffer the #music_buffer where consumed #music_chunk objects + * should be returned + * @return true on success, false on failure + */ +bool +audio_output_all_open(AudioFormat audio_format, + MusicBuffer &buffer, + Error &error); + +/** + * Closes all audio outputs. + */ +void +audio_output_all_close(void); + +/** + * Closes all audio outputs. Outputs with the "always_on" flag are + * put into pause mode. + */ +void +audio_output_all_release(void); + +void +audio_output_all_set_replay_gain_mode(ReplayGainMode mode); + +/** + * Enqueue a #music_chunk object for playing, i.e. pushes it to a + * #MusicPipe. + * + * @param chunk the #music_chunk object to be played + * @return true on success, false if no audio output was able to play + * (all closed then) + */ +bool +audio_output_all_play(music_chunk *chunk, Error &error); + +/** + * Checks if the output devices have drained their music pipe, and + * returns the consumed music chunks to the #music_buffer. + * + * @return the number of chunks to play left in the #MusicPipe + */ +unsigned +audio_output_all_check(void); + +/** + * Checks if the size of the #MusicPipe is below the #threshold. If + * not, it attempts to synchronize with all output threads, and waits + * until another #music_chunk is finished. + * + * @param threshold the maximum number of chunks in the pipe + * @return true if there are less than #threshold chunks in the pipe + */ +bool +audio_output_all_wait(PlayerControl &pc, unsigned threshold); + +/** + * Puts all audio outputs into pause mode. Most implementations will + * simply close it then. + */ +void +audio_output_all_pause(void); + +/** + * Drain all audio outputs. + */ +void +audio_output_all_drain(void); + +/** + * Try to cancel data which may still be in the device's buffers. + */ +void +audio_output_all_cancel(void); + +/** + * Indicate that a new song will begin now. + */ +void +audio_output_all_song_border(void); + +/** + * Returns the "elapsed_time" stamp of the most recently finished + * chunk. A negative value is returned when no chunk has been + * finished yet. + */ +gcc_pure +float +audio_output_all_get_elapsed_time(void); + +#endif diff --git a/src/output/OutputCommand.cxx b/src/output/OutputCommand.cxx new file mode 100644 index 000000000..839e9bd88 --- /dev/null +++ b/src/output/OutputCommand.cxx @@ -0,0 +1,112 @@ +/* + * 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. + */ + +/* + * Glue functions for controlling the audio outputs over the MPD + * protocol. These functions perform extra validation on all + * parameters, because they might be from an untrusted source. + * + */ + +#include "config.h" +#include "OutputCommand.hxx" +#include "OutputAll.hxx" +#include "OutputInternal.hxx" +#include "PlayerControl.hxx" +#include "MixerControl.hxx" +#include "Idle.hxx" + +extern unsigned audio_output_state_version; + +bool +audio_output_enable_index(unsigned idx) +{ + struct audio_output *ao; + + if (idx >= audio_output_count()) + return false; + + ao = audio_output_get(idx); + if (ao->enabled) + return true; + + ao->enabled = true; + idle_add(IDLE_OUTPUT); + + ao->player_control->UpdateAudio(); + + ++audio_output_state_version; + + return true; +} + +bool +audio_output_disable_index(unsigned idx) +{ + struct audio_output *ao; + + if (idx >= audio_output_count()) + return false; + + ao = audio_output_get(idx); + if (!ao->enabled) + return true; + + ao->enabled = false; + idle_add(IDLE_OUTPUT); + + Mixer *mixer = ao->mixer; + if (mixer != nullptr) { + mixer_close(mixer); + idle_add(IDLE_MIXER); + } + + ao->player_control->UpdateAudio(); + + ++audio_output_state_version; + + return true; +} + +bool +audio_output_toggle_index(unsigned idx) +{ + struct audio_output *ao; + + if (idx >= audio_output_count()) + return false; + + ao = audio_output_get(idx); + const bool enabled = ao->enabled = !ao->enabled; + idle_add(IDLE_OUTPUT); + + if (!enabled) { + Mixer *mixer = ao->mixer; + if (mixer != nullptr) { + mixer_close(mixer); + idle_add(IDLE_MIXER); + } + } + + ao->player_control->UpdateAudio(); + + ++audio_output_state_version; + + return true; +} diff --git a/src/output/OutputCommand.hxx b/src/output/OutputCommand.hxx new file mode 100644 index 000000000..4c44dff53 --- /dev/null +++ b/src/output/OutputCommand.hxx @@ -0,0 +1,51 @@ +/* + * 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. + */ + +/* + * Glue functions for controlling the audio outputs over the MPD + * protocol. These functions perform extra validation on all + * parameters, because they might be from an untrusted source. + * + */ + +#ifndef MPD_OUTPUT_COMMAND_HXX +#define MPD_OUTPUT_COMMAND_HXX + +/** + * Enables an audio output. Returns false if the specified output + * does not exist. + */ +bool +audio_output_enable_index(unsigned idx); + +/** + * Disables an audio output. Returns false if the specified output + * does not exist. + */ +bool +audio_output_disable_index(unsigned idx); + +/** + * Toggles an audio output. Returns false if the specified output + * does not exist. + */ +bool +audio_output_toggle_index(unsigned idx); + +#endif diff --git a/src/output/OutputControl.cxx b/src/output/OutputControl.cxx new file mode 100644 index 000000000..b938754fd --- /dev/null +++ b/src/output/OutputControl.cxx @@ -0,0 +1,325 @@ + +/* + * 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 "OutputControl.hxx" +#include "OutputThread.hxx" +#include "OutputInternal.hxx" +#include "OutputPlugin.hxx" +#include "OutputError.hxx" +#include "MixerControl.hxx" +#include "notify.hxx" +#include "filter/ReplayGainFilterPlugin.hxx" +#include "util/Error.hxx" +#include "Log.hxx" + +#include + +/** after a failure, wait this number of seconds before + automatically reopening the device */ +static constexpr unsigned REOPEN_AFTER = 10; + +struct notify audio_output_client_notify; + +/** + * Waits for command completion. + * + * @param ao the #audio_output instance; must be locked + */ +static void ao_command_wait(struct audio_output *ao) +{ + while (ao->command != AO_COMMAND_NONE) { + ao->mutex.unlock(); + audio_output_client_notify.Wait(); + ao->mutex.lock(); + } +} + +/** + * Sends a command to the #audio_output object, but does not wait for + * completion. + * + * @param ao the #audio_output instance; must be locked + */ +static void ao_command_async(struct audio_output *ao, + enum audio_output_command cmd) +{ + assert(ao->command == AO_COMMAND_NONE); + ao->command = cmd; + ao->cond.signal(); +} + +/** + * Sends a command to the #audio_output object and waits for + * completion. + * + * @param ao the #audio_output instance; must be locked + */ +static void +ao_command(struct audio_output *ao, enum audio_output_command cmd) +{ + ao_command_async(ao, cmd); + ao_command_wait(ao); +} + +/** + * Lock the #audio_output object and execute the command + * synchronously. + */ +static void +ao_lock_command(struct audio_output *ao, enum audio_output_command cmd) +{ + const ScopeLock protect(ao->mutex); + ao_command(ao, cmd); +} + +void +audio_output_set_replay_gain_mode(struct audio_output *ao, + ReplayGainMode mode) +{ + if (ao->replay_gain_filter != nullptr) + replay_gain_filter_set_mode(ao->replay_gain_filter, mode); + if (ao->other_replay_gain_filter != nullptr) + replay_gain_filter_set_mode(ao->other_replay_gain_filter, mode); +} + +void +audio_output_enable(struct audio_output *ao) +{ + if (!ao->thread.IsDefined()) { + if (ao->plugin->enable == nullptr) { + /* don't bother to start the thread now if the + device doesn't even have a enable() method; + just assign the variable and we're done */ + ao->really_enabled = true; + return; + } + + audio_output_thread_start(ao); + } + + ao_lock_command(ao, AO_COMMAND_ENABLE); +} + +void +audio_output_disable(struct audio_output *ao) +{ + if (!ao->thread.IsDefined()) { + if (ao->plugin->disable == nullptr) + ao->really_enabled = false; + else + /* if there's no thread yet, the device cannot + be enabled */ + assert(!ao->really_enabled); + + return; + } + + ao_lock_command(ao, AO_COMMAND_DISABLE); +} + +/** + * Object must be locked (and unlocked) by the caller. + */ +static bool +audio_output_open(struct audio_output *ao, + const AudioFormat audio_format, + const MusicPipe &mp) +{ + bool open; + + assert(ao != nullptr); + assert(ao->allow_play); + assert(audio_format.IsValid()); + + ao->fail_timer.Reset(); + + if (ao->open && audio_format == ao->in_audio_format) { + assert(ao->pipe == &mp || + (ao->always_on && ao->pause)); + + if (ao->pause) { + ao->chunk = nullptr; + ao->pipe = ∓ + + /* unpause with the CANCEL command; this is a + hack, but suits well for forcing the thread + to leave the ao_pause() thread, and we need + to flush the device buffer anyway */ + + /* we're not using audio_output_cancel() here, + because that function is asynchronous */ + ao_command(ao, AO_COMMAND_CANCEL); + } + + return true; + } + + ao->in_audio_format = audio_format; + ao->chunk = nullptr; + + ao->pipe = ∓ + + if (!ao->thread.IsDefined()) + audio_output_thread_start(ao); + + ao_command(ao, ao->open ? AO_COMMAND_REOPEN : AO_COMMAND_OPEN); + open = ao->open; + + if (open && ao->mixer != nullptr) { + Error error; + if (!mixer_open(ao->mixer, error)) + FormatWarning(output_domain, + "Failed to open mixer for '%s'", + ao->name); + } + + return open; +} + +/** + * Same as audio_output_close(), but expects the lock to be held by + * the caller. + */ +static void +audio_output_close_locked(struct audio_output *ao) +{ + assert(ao != nullptr); + assert(ao->allow_play); + + if (ao->mixer != nullptr) + mixer_auto_close(ao->mixer); + + assert(!ao->open || !ao->fail_timer.IsDefined()); + + if (ao->open) + ao_command(ao, AO_COMMAND_CLOSE); + else + ao->fail_timer.Reset(); +} + +bool +audio_output_update(struct audio_output *ao, + const AudioFormat audio_format, + const MusicPipe &mp) +{ + const ScopeLock protect(ao->mutex); + + if (ao->enabled && ao->really_enabled) { + if (ao->fail_timer.Check(REOPEN_AFTER * 1000)) { + return audio_output_open(ao, audio_format, mp); + } + } else if (audio_output_is_open(ao)) + audio_output_close_locked(ao); + + return false; +} + +void +audio_output_play(struct audio_output *ao) +{ + const ScopeLock protect(ao->mutex); + + assert(ao->allow_play); + + if (audio_output_is_open(ao) && !ao->in_playback_loop && + !ao->woken_for_play) { + ao->woken_for_play = true; + ao->cond.signal(); + } +} + +void audio_output_pause(struct audio_output *ao) +{ + if (ao->mixer != nullptr && ao->plugin->pause == nullptr) + /* the device has no pause mode: close the mixer, + unless its "global" flag is set (checked by + mixer_auto_close()) */ + mixer_auto_close(ao->mixer); + + const ScopeLock protect(ao->mutex); + + assert(ao->allow_play); + if (audio_output_is_open(ao)) + ao_command_async(ao, AO_COMMAND_PAUSE); +} + +void +audio_output_drain_async(struct audio_output *ao) +{ + const ScopeLock protect(ao->mutex); + + assert(ao->allow_play); + if (audio_output_is_open(ao)) + ao_command_async(ao, AO_COMMAND_DRAIN); +} + +void audio_output_cancel(struct audio_output *ao) +{ + const ScopeLock protect(ao->mutex); + + if (audio_output_is_open(ao)) { + ao->allow_play = false; + ao_command_async(ao, AO_COMMAND_CANCEL); + } +} + +void +audio_output_allow_play(struct audio_output *ao) +{ + const ScopeLock protect(ao->mutex); + + ao->allow_play = true; + if (audio_output_is_open(ao)) + ao->cond.signal(); +} + +void +audio_output_release(struct audio_output *ao) +{ + if (ao->always_on) + audio_output_pause(ao); + else + audio_output_close(ao); +} + +void audio_output_close(struct audio_output *ao) +{ + assert(ao != nullptr); + assert(!ao->open || !ao->fail_timer.IsDefined()); + + const ScopeLock protect(ao->mutex); + audio_output_close_locked(ao); +} + +void audio_output_finish(struct audio_output *ao) +{ + audio_output_close(ao); + + assert(!ao->fail_timer.IsDefined()); + + if (ao->thread.IsDefined()) { + assert(ao->allow_play); + ao_lock_command(ao, AO_COMMAND_KILL); + ao->thread.Join(); + } + + audio_output_free(ao); +} diff --git a/src/output/OutputControl.hxx b/src/output/OutputControl.hxx new file mode 100644 index 000000000..7195412ef --- /dev/null +++ b/src/output/OutputControl.hxx @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OUTPUT_CONTROL_HXX +#define MPD_OUTPUT_CONTROL_HXX + +#include "ReplayGainInfo.hxx" + +#include + +struct audio_output; +struct AudioFormat; +struct config_param; +class MusicPipe; + +void +audio_output_set_replay_gain_mode(audio_output *ao, + ReplayGainMode mode); + +/** + * Enables the device. + */ +void +audio_output_enable(audio_output *ao); + +/** + * Disables the device. + */ +void +audio_output_disable(audio_output *ao); + +/** + * Opens or closes the device, depending on the "enabled" flag. + * + * @return true if the device is open + */ +bool +audio_output_update(audio_output *ao, + AudioFormat audio_format, + const MusicPipe &mp); + +void +audio_output_play(audio_output *ao); + +void +audio_output_pause(audio_output *ao); + +void +audio_output_drain_async(audio_output *ao); + +/** + * Clear the "allow_play" flag and send the "CANCEL" command + * asynchronously. To finish the operation, the caller has to call + * audio_output_allow_play(). + */ +void +audio_output_cancel(audio_output *ao); + +/** + * Set the "allow_play" and signal the thread. + */ +void +audio_output_allow_play(audio_output *ao); + +void +audio_output_close(audio_output *ao); + +/** + * Closes the audio output, but if the "always_on" flag is set, put it + * into pause mode instead. + */ +void +audio_output_release(audio_output *ao); + +void +audio_output_finish(audio_output *ao); + +#endif diff --git a/src/output/OutputError.cxx b/src/output/OutputError.cxx new file mode 100644 index 000000000..9d4128912 --- /dev/null +++ b/src/output/OutputError.cxx @@ -0,0 +1,23 @@ +/* + * 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 "OutputError.hxx" +#include "util/Domain.hxx" + +const Domain output_domain("output"); diff --git a/src/output/OutputError.hxx b/src/output/OutputError.hxx new file mode 100644 index 000000000..e3a20142f --- /dev/null +++ b/src/output/OutputError.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OUTPUT_ERROR_HXX +#define MPD_OUTPUT_ERROR_HXX + +extern const class Domain output_domain; + +#endif diff --git a/src/output/OutputFinish.cxx b/src/output/OutputFinish.cxx new file mode 100644 index 000000000..43f0dd1ec --- /dev/null +++ b/src/output/OutputFinish.cxx @@ -0,0 +1,51 @@ +/* + * 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 "OutputInternal.hxx" +#include "OutputPlugin.hxx" +#include "MixerControl.hxx" +#include "FilterInternal.hxx" + +#include + +void +ao_base_finish(struct audio_output *ao) +{ + assert(!ao->open); + assert(!ao->fail_timer.IsDefined()); + assert(!ao->thread.IsDefined()); + + if (ao->mixer != nullptr) + mixer_free(ao->mixer); + + delete ao->replay_gain_filter; + delete ao->other_replay_gain_filter; + delete ao->filter; +} + +void +audio_output_free(struct audio_output *ao) +{ + assert(!ao->open); + assert(!ao->fail_timer.IsDefined()); + assert(!ao->thread.IsDefined()); + + ao_plugin_finish(ao); +} diff --git a/src/output/OutputInit.cxx b/src/output/OutputInit.cxx new file mode 100644 index 000000000..f5b1bdc81 --- /dev/null +++ b/src/output/OutputInit.cxx @@ -0,0 +1,329 @@ +/* + * 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 "OutputInternal.hxx" +#include "OutputList.hxx" +#include "OutputError.hxx" +#include "OutputAPI.hxx" +#include "FilterConfig.hxx" +#include "AudioParser.hxx" +#include "MixerList.hxx" +#include "MixerType.hxx" +#include "MixerControl.hxx" +#include "mixer/SoftwareMixerPlugin.hxx" +#include "FilterPlugin.hxx" +#include "FilterRegistry.hxx" +#include "filter/AutoConvertFilterPlugin.hxx" +#include "filter/ReplayGainFilterPlugin.hxx" +#include "filter/ChainFilterPlugin.hxx" +#include "ConfigError.hxx" +#include "ConfigGlobal.hxx" +#include "util/Error.hxx" +#include "Log.hxx" + +#include +#include + +#define AUDIO_OUTPUT_TYPE "type" +#define AUDIO_OUTPUT_NAME "name" +#define AUDIO_OUTPUT_FORMAT "format" +#define AUDIO_FILTERS "filters" + +static const struct audio_output_plugin * +audio_output_detect(Error &error) +{ + LogDefault(output_domain, "Attempt to detect audio output device"); + + audio_output_plugins_for_each(plugin) { + if (plugin->test_default_device == nullptr) + continue; + + FormatDefault(output_domain, + "Attempting to detect a %s audio device", + plugin->name); + if (ao_plugin_test_default_device(plugin)) + return plugin; + } + + error.Set(output_domain, "Unable to detect an audio device"); + return nullptr; +} + +/** + * Determines the mixer type which should be used for the specified + * configuration block. + * + * This handles the deprecated options mixer_type (global) and + * mixer_enabled, if the mixer_type setting is not configured. + */ +gcc_pure +static enum mixer_type +audio_output_mixer_type(const config_param ¶m) +{ + /* read the local "mixer_type" setting */ + const char *p = param.GetBlockValue("mixer_type"); + if (p != nullptr) + return mixer_type_parse(p); + + /* try the local "mixer_enabled" setting next (deprecated) */ + if (!param.GetBlockValue("mixer_enabled", true)) + return MIXER_TYPE_NONE; + + /* fall back to the global "mixer_type" setting (also + deprecated) */ + return mixer_type_parse(config_get_string(CONF_MIXER_TYPE, + "hardware")); +} + +static Mixer * +audio_output_load_mixer(struct audio_output *ao, + const config_param ¶m, + const struct mixer_plugin *plugin, + Filter &filter_chain, + Error &error) +{ + Mixer *mixer; + + switch (audio_output_mixer_type(param)) { + case MIXER_TYPE_NONE: + case MIXER_TYPE_UNKNOWN: + return nullptr; + + case MIXER_TYPE_HARDWARE: + if (plugin == nullptr) + return nullptr; + + return mixer_new(plugin, ao, param, error); + + case MIXER_TYPE_SOFTWARE: + mixer = mixer_new(&software_mixer_plugin, nullptr, + config_param(), + IgnoreError()); + assert(mixer != nullptr); + + filter_chain_append(filter_chain, "software_mixer", + software_mixer_get_filter(mixer)); + return mixer; + } + + assert(false); + gcc_unreachable(); +} + +bool +ao_base_init(struct audio_output *ao, + const struct audio_output_plugin *plugin, + const config_param ¶m, Error &error) +{ + assert(ao != nullptr); + assert(plugin != nullptr); + assert(plugin->finish != nullptr); + assert(plugin->open != nullptr); + assert(plugin->close != nullptr); + assert(plugin->play != nullptr); + + if (!param.IsNull()) { + ao->name = param.GetBlockValue(AUDIO_OUTPUT_NAME); + if (ao->name == nullptr) { + error.Set(config_domain, + "Missing \"name\" configuration"); + return false; + } + + const char *p = param.GetBlockValue(AUDIO_OUTPUT_FORMAT); + if (p != nullptr) { + bool success = + audio_format_parse(ao->config_audio_format, + p, true, error); + if (!success) + return false; + } else + ao->config_audio_format.Clear(); + } else { + ao->name = "default detected output"; + + ao->config_audio_format.Clear(); + } + + ao->plugin = plugin; + ao->tags = param.GetBlockValue("tags", true); + ao->always_on = param.GetBlockValue("always_on", false); + ao->enabled = param.GetBlockValue("enabled", true); + ao->really_enabled = false; + ao->open = false; + ao->pause = false; + ao->allow_play = true; + ao->in_playback_loop = false; + ao->woken_for_play = false; + + /* set up the filter chain */ + + ao->filter = filter_chain_new(); + assert(ao->filter != nullptr); + + /* create the normalization filter (if configured) */ + + if (config_get_bool(CONF_VOLUME_NORMALIZATION, false)) { + Filter *normalize_filter = + filter_new(&normalize_filter_plugin, config_param(), + IgnoreError()); + assert(normalize_filter != nullptr); + + filter_chain_append(*ao->filter, "normalize", + autoconvert_filter_new(normalize_filter)); + } + + Error filter_error; + filter_chain_parse(*ao->filter, + param.GetBlockValue(AUDIO_FILTERS, ""), + filter_error); + + // It's not really fatal - Part of the filter chain has been set up already + // and even an empty one will work (if only with unexpected behaviour) + if (filter_error.IsDefined()) + FormatError(filter_error, + "Failed to initialize filter chain for '%s'", + ao->name); + + ao->command = AO_COMMAND_NONE; + + ao->mixer = nullptr; + ao->replay_gain_filter = nullptr; + ao->other_replay_gain_filter = nullptr; + + /* done */ + + return true; +} + +static bool +audio_output_setup(struct audio_output *ao, const config_param ¶m, + Error &error) +{ + + /* create the replay_gain filter */ + + const char *replay_gain_handler = + param.GetBlockValue("replay_gain_handler", "software"); + + if (strcmp(replay_gain_handler, "none") != 0) { + ao->replay_gain_filter = filter_new(&replay_gain_filter_plugin, + param, IgnoreError()); + assert(ao->replay_gain_filter != nullptr); + + ao->replay_gain_serial = 0; + + ao->other_replay_gain_filter = filter_new(&replay_gain_filter_plugin, + param, + IgnoreError()); + assert(ao->other_replay_gain_filter != nullptr); + + ao->other_replay_gain_serial = 0; + } else { + ao->replay_gain_filter = nullptr; + ao->other_replay_gain_filter = nullptr; + } + + /* set up the mixer */ + + Error mixer_error; + ao->mixer = audio_output_load_mixer(ao, param, + ao->plugin->mixer_plugin, + *ao->filter, mixer_error); + if (ao->mixer == nullptr && mixer_error.IsDefined()) + FormatError(mixer_error, + "Failed to initialize hardware mixer for '%s'", + ao->name); + + /* use the hardware mixer for replay gain? */ + + if (strcmp(replay_gain_handler, "mixer") == 0) { + if (ao->mixer != nullptr) + replay_gain_filter_set_mixer(ao->replay_gain_filter, + ao->mixer, 100); + else + FormatError(output_domain, + "No such mixer for output '%s'", ao->name); + } else if (strcmp(replay_gain_handler, "software") != 0 && + ao->replay_gain_filter != nullptr) { + error.Set(config_domain, + "Invalid \"replay_gain_handler\" value"); + return false; + } + + /* the "convert" filter must be the last one in the chain */ + + ao->convert_filter = filter_new(&convert_filter_plugin, config_param(), + IgnoreError()); + assert(ao->convert_filter != nullptr); + + filter_chain_append(*ao->filter, "convert", ao->convert_filter); + + return true; +} + +struct audio_output * +audio_output_new(const config_param ¶m, + PlayerControl &pc, + Error &error) +{ + const struct audio_output_plugin *plugin; + + if (!param.IsNull()) { + const char *p; + + p = param.GetBlockValue(AUDIO_OUTPUT_TYPE); + if (p == nullptr) { + error.Set(config_domain, + "Missing \"type\" configuration"); + return nullptr; + } + + plugin = audio_output_plugin_get(p); + if (plugin == nullptr) { + error.Format(config_domain, + "No such audio output plugin: %s", p); + return nullptr; + } + } else { + LogWarning(output_domain, + "No 'audio_output' defined in config file"); + + plugin = audio_output_detect(error); + if (plugin == nullptr) + return nullptr; + + FormatDefault(output_domain, + "Successfully detected a %s audio device", + plugin->name); + } + + struct audio_output *ao = ao_plugin_init(plugin, param, error); + if (ao == nullptr) + return nullptr; + + if (!audio_output_setup(ao, param, error)) { + ao_plugin_finish(ao); + return nullptr; + } + + ao->player_control = &pc; + return ao; +} diff --git a/src/output/OutputInternal.hxx b/src/output/OutputInternal.hxx new file mode 100644 index 000000000..18404dc01 --- /dev/null +++ b/src/output/OutputInternal.hxx @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OUTPUT_INTERNAL_HXX +#define MPD_OUTPUT_INTERNAL_HXX + +#include "AudioFormat.hxx" +#include "pcm/PcmBuffer.hxx" +#include "pcm/PcmDither.hxx" +#include "thread/Mutex.hxx" +#include "thread/Cond.hxx" +#include "thread/Thread.hxx" +#include "system/PeriodClock.hxx" + +class Error; +class Filter; +class MusicPipe; +struct config_param; +struct PlayerControl; + +enum audio_output_command { + AO_COMMAND_NONE = 0, + AO_COMMAND_ENABLE, + AO_COMMAND_DISABLE, + AO_COMMAND_OPEN, + + /** + * This command is invoked when the input audio format + * changes. + */ + AO_COMMAND_REOPEN, + + AO_COMMAND_CLOSE, + AO_COMMAND_PAUSE, + + /** + * Drains the internal (hardware) buffers of the device. This + * operation may take a while to complete. + */ + AO_COMMAND_DRAIN, + + AO_COMMAND_CANCEL, + AO_COMMAND_KILL +}; + +struct audio_output { + /** + * The device's configured display name. + */ + const char *name; + + /** + * The plugin which implements this output device. + */ + const struct audio_output_plugin *plugin; + + /** + * The #mixer object associated with this audio output device. + * May be nullptr if none is available, or if software volume is + * configured. + */ + class Mixer *mixer; + + /** + * Will this output receive tags from the decoder? The + * default is true, but it may be configured to false to + * suppress sending tags to the output. + */ + bool tags; + + /** + * Shall this output always play something (i.e. silence), + * even when playback is stopped? + */ + bool always_on; + + /** + * Has the user enabled this device? + */ + bool enabled; + + /** + * Is this device actually enabled, i.e. the "enable" method + * has succeeded? + */ + bool really_enabled; + + /** + * Is the device (already) open and functional? + * + * This attribute may only be modified by the output thread. + * It is protected with #mutex: write accesses inside the + * output thread and read accesses outside of it may only be + * performed while the lock is held. + */ + bool open; + + /** + * Is the device paused? i.e. the output thread is in the + * ao_pause() loop. + */ + bool pause; + + /** + * When this flag is set, the output thread will not do any + * playback. It will wait until the flag is cleared. + * + * This is used to synchronize the "clear" operation on the + * shared music pipe during the CANCEL command. + */ + bool allow_play; + + /** + * True while the OutputThread is inside ao_play(). This + * means the PlayerThread does not need to wake up the + * OutputThread when new chunks are added to the MusicPipe, + * because the OutputThread is already watching that. + */ + bool in_playback_loop; + + /** + * Has the OutputThread been woken up to play more chunks? + * This is set by audio_output_play() and reset by ao_play() + * to reduce the number of duplicate wakeups. + */ + bool woken_for_play; + + /** + * If not nullptr, the device has failed, and this timer is used + * to estimate how long it should stay disabled (unless + * explicitly reopened with "play"). + */ + PeriodClock fail_timer; + + /** + * The configured audio format. + */ + AudioFormat config_audio_format; + + /** + * The audio_format in which audio data is received from the + * player thread (which in turn receives it from the decoder). + */ + AudioFormat in_audio_format; + + /** + * The audio_format which is really sent to the device. This + * is basically config_audio_format (if configured) or + * in_audio_format, but may have been modified by + * plugin->open(). + */ + AudioFormat out_audio_format; + + /** + * The buffer used to allocate the cross-fading result. + */ + PcmBuffer cross_fade_buffer; + + /** + * The dithering state for cross-fading two streams. + */ + PcmDither cross_fade_dither; + + /** + * The filter object of this audio output. This is an + * instance of chain_filter_plugin. + */ + Filter *filter; + + /** + * The replay_gain_filter_plugin instance of this audio + * output. + */ + Filter *replay_gain_filter; + + /** + * The serial number of the last replay gain info. 0 means no + * replay gain info was available. + */ + unsigned replay_gain_serial; + + /** + * The replay_gain_filter_plugin instance of this audio + * output, to be applied to the second chunk during + * cross-fading. + */ + Filter *other_replay_gain_filter; + + /** + * The serial number of the last replay gain info by the + * "other" chunk during cross-fading. + */ + unsigned other_replay_gain_serial; + + /** + * The convert_filter_plugin instance of this audio output. + * It is the last item in the filter chain, and is responsible + * for converting the input data into the appropriate format + * for this audio output. + */ + Filter *convert_filter; + + /** + * The thread handle, or nullptr if the output thread isn't + * running. + */ + Thread thread; + + /** + * The next command to be performed by the output thread. + */ + enum audio_output_command command; + + /** + * The music pipe which provides music chunks to be played. + */ + const MusicPipe *pipe; + + /** + * This mutex protects #open, #fail_timer, #chunk and + * #chunk_finished. + */ + Mutex mutex; + + /** + * This condition object wakes up the output thread after + * #command has been set. + */ + Cond cond; + + /** + * The PlayerControl object which "owns" this output. This + * object is needed to signal command completion. + */ + PlayerControl *player_control; + + /** + * The #music_chunk which is currently being played. All + * chunks before this one may be returned to the + * #music_buffer, because they are not going to be used by + * this output anymore. + */ + const struct music_chunk *chunk; + + /** + * Has the output finished playing #chunk? + */ + bool chunk_finished; +}; + +/** + * Notify object used by the thread's client, i.e. we will send a + * notify signal to this object, expecting the caller to wait on it. + */ +extern struct notify audio_output_client_notify; + +static inline bool +audio_output_is_open(const struct audio_output *ao) +{ + return ao->open; +} + +static inline bool +audio_output_command_is_finished(const struct audio_output *ao) +{ + return ao->command == AO_COMMAND_NONE; +} + +struct audio_output * +audio_output_new(const config_param ¶m, + PlayerControl &pc, + Error &error); + +bool +ao_base_init(struct audio_output *ao, + const struct audio_output_plugin *plugin, + const config_param ¶m, Error &error); + +void +ao_base_finish(struct audio_output *ao); + +void +audio_output_free(struct audio_output *ao); + +#endif diff --git a/src/output/OutputList.cxx b/src/output/OutputList.cxx new file mode 100644 index 000000000..b914e6d2e --- /dev/null +++ b/src/output/OutputList.cxx @@ -0,0 +1,100 @@ +/* + * 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 "OutputList.hxx" +#include "OutputAPI.hxx" +#include "plugins/AlsaOutputPlugin.hxx" +#include "plugins/AoOutputPlugin.hxx" +#include "plugins/FifoOutputPlugin.hxx" +#include "plugins/HttpdOutputPlugin.hxx" +#include "plugins/JackOutputPlugin.hxx" +#include "plugins/NullOutputPlugin.hxx" +#include "plugins/OpenALOutputPlugin.hxx" +#include "plugins/OssOutputPlugin.hxx" +#include "plugins/OSXOutputPlugin.hxx" +#include "plugins/PipeOutputPlugin.hxx" +#include "plugins/PulseOutputPlugin.hxx" +#include "plugins/RecorderOutputPlugin.hxx" +#include "plugins/RoarOutputPlugin.hxx" +#include "plugins/ShoutOutputPlugin.hxx" +#include "plugins/SolarisOutputPlugin.hxx" +#include "plugins/WinmmOutputPlugin.hxx" + +#include + +const struct audio_output_plugin *const audio_output_plugins[] = { +#ifdef HAVE_SHOUT + &shout_output_plugin, +#endif + &null_output_plugin, +#ifdef HAVE_FIFO + &fifo_output_plugin, +#endif +#ifdef ENABLE_PIPE_OUTPUT + &pipe_output_plugin, +#endif +#ifdef HAVE_ALSA + &alsa_output_plugin, +#endif +#ifdef HAVE_ROAR + &roar_output_plugin, +#endif +#ifdef HAVE_AO + &ao_output_plugin, +#endif +#ifdef HAVE_OSS + &oss_output_plugin, +#endif +#ifdef HAVE_OPENAL + &openal_output_plugin, +#endif +#ifdef HAVE_OSX + &osx_output_plugin, +#endif +#ifdef ENABLE_SOLARIS_OUTPUT + &solaris_output_plugin, +#endif +#ifdef HAVE_PULSE + &pulse_output_plugin, +#endif +#ifdef HAVE_JACK + &jack_output_plugin, +#endif +#ifdef ENABLE_HTTPD_OUTPUT + &httpd_output_plugin, +#endif +#ifdef ENABLE_RECORDER_OUTPUT + &recorder_output_plugin, +#endif +#ifdef ENABLE_WINMM_OUTPUT + &winmm_output_plugin, +#endif + nullptr +}; + +const struct audio_output_plugin * +audio_output_plugin_get(const char *name) +{ + audio_output_plugins_for_each(plugin) + if (strcmp(plugin->name, name) == 0) + return plugin; + + return nullptr; +} diff --git a/src/output/OutputList.hxx b/src/output/OutputList.hxx new file mode 100644 index 000000000..dfcf7487c --- /dev/null +++ b/src/output/OutputList.hxx @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OUTPUT_LIST_HXX +#define MPD_OUTPUT_LIST_HXX + +extern const struct audio_output_plugin *const audio_output_plugins[]; + +const struct audio_output_plugin * +audio_output_plugin_get(const char *name); + +#define audio_output_plugins_for_each(plugin) \ + for (const struct audio_output_plugin *plugin, \ + *const*output_plugin_iterator = &audio_output_plugins[0]; \ + (plugin = *output_plugin_iterator) != nullptr; ++output_plugin_iterator) + +#endif diff --git a/src/output/OutputPlugin.cxx b/src/output/OutputPlugin.cxx new file mode 100644 index 000000000..29fd6455a --- /dev/null +++ b/src/output/OutputPlugin.cxx @@ -0,0 +1,109 @@ +/* + * 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 "OutputPlugin.hxx" +#include "OutputInternal.hxx" + +struct audio_output * +ao_plugin_init(const struct audio_output_plugin *plugin, + const config_param ¶m, + Error &error) +{ + assert(plugin != nullptr); + assert(plugin->init != nullptr); + + return plugin->init(param, error); +} + +void +ao_plugin_finish(struct audio_output *ao) +{ + ao->plugin->finish(ao); +} + +bool +ao_plugin_enable(struct audio_output *ao, Error &error_r) +{ + return ao->plugin->enable != nullptr + ? ao->plugin->enable(ao, error_r) + : true; +} + +void +ao_plugin_disable(struct audio_output *ao) +{ + if (ao->plugin->disable != nullptr) + ao->plugin->disable(ao); +} + +bool +ao_plugin_open(struct audio_output *ao, AudioFormat &audio_format, + Error &error) +{ + return ao->plugin->open(ao, audio_format, error); +} + +void +ao_plugin_close(struct audio_output *ao) +{ + ao->plugin->close(ao); +} + +unsigned +ao_plugin_delay(struct audio_output *ao) +{ + return ao->plugin->delay != nullptr + ? ao->plugin->delay(ao) + : 0; +} + +void +ao_plugin_send_tag(struct audio_output *ao, const Tag *tag) +{ + if (ao->plugin->send_tag != nullptr) + ao->plugin->send_tag(ao, tag); +} + +size_t +ao_plugin_play(struct audio_output *ao, const void *chunk, size_t size, + Error &error) +{ + return ao->plugin->play(ao, chunk, size, error); +} + +void +ao_plugin_drain(struct audio_output *ao) +{ + if (ao->plugin->drain != nullptr) + ao->plugin->drain(ao); +} + +void +ao_plugin_cancel(struct audio_output *ao) +{ + if (ao->plugin->cancel != nullptr) + ao->plugin->cancel(ao); +} + +bool +ao_plugin_pause(struct audio_output *ao) +{ + return ao->plugin->pause != nullptr && ao->plugin->pause(ao); +} diff --git a/src/output/OutputPlugin.hxx b/src/output/OutputPlugin.hxx new file mode 100644 index 000000000..7d77c92b2 --- /dev/null +++ b/src/output/OutputPlugin.hxx @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OUTPUT_PLUGIN_HXX +#define MPD_OUTPUT_PLUGIN_HXX + +#include "Compiler.h" + +#include + +struct config_param; +struct AudioFormat; +struct Tag; +class Error; + +/** + * A plugin which controls an audio output device. + */ +struct audio_output_plugin { + /** + * the plugin's name + */ + const char *name; + + /** + * Test if this plugin can provide a default output, in case + * none has been configured. This method is optional. + */ + bool (*test_default_device)(void); + + /** + * Configure and initialize the device, but do not open it + * yet. + * + * @param param the configuration section, or nullptr if there is + * no configuration + * @return nullptr on error, or an opaque pointer to the plugin's + * data + */ + struct audio_output *(*init)(const config_param ¶m, + Error &error); + + /** + * Free resources allocated by this device. + */ + void (*finish)(struct audio_output *data); + + /** + * Enable the device. This may allocate resources, preparing + * for the device to be opened. Enabling a device cannot + * fail: if an error occurs during that, it should be reported + * by the open() method. + * + * @return true on success, false on error + */ + bool (*enable)(struct audio_output *data, Error &error); + + /** + * Disables the device. It is closed before this method is + * called. + */ + void (*disable)(struct audio_output *data); + + /** + * Really open the device. + * + * @param audio_format the audio format in which data is going + * to be delivered; may be modified by the plugin + */ + bool (*open)(struct audio_output *data, AudioFormat &audio_format, + Error &error); + + /** + * Close the device. + */ + void (*close)(struct audio_output *data); + + /** + * Returns a positive number if the output thread shall delay + * the next call to play() or pause(). This should be + * implemented instead of doing a sleep inside the plugin, + * because this allows MPD to listen to commands meanwhile. + * + * @return the number of milliseconds to wait + */ + unsigned (*delay)(struct audio_output *data); + + /** + * Display metadata for the next chunk. Optional method, + * because not all devices can display metadata. + */ + void (*send_tag)(struct audio_output *data, const Tag *tag); + + /** + * Play a chunk of audio data. + * + * @return the number of bytes played, or 0 on error + */ + size_t (*play)(struct audio_output *data, + const void *chunk, size_t size, + Error &error); + + /** + * Wait until the device has finished playing. + */ + void (*drain)(struct audio_output *data); + + /** + * Try to cancel data which may still be in the device's + * buffers. + */ + void (*cancel)(struct audio_output *data); + + /** + * Pause the device. If supported, it may perform a special + * action, which keeps the device open, but does not play + * anything. Output plugins like "shout" might want to play + * silence during pause, so their clients won't be + * disconnected. Plugins which do not support pausing will + * simply be closed, and have to be reopened when unpaused. + * + * @return false on error (output will be closed then), true + * for continue to pause + */ + bool (*pause)(struct audio_output *data); + + /** + * The mixer plugin associated with this output plugin. This + * may be nullptr if no mixer plugin is implemented. When + * created, this mixer plugin gets the same #config_param as + * this audio output device. + */ + const struct mixer_plugin *mixer_plugin; +}; + +static inline bool +ao_plugin_test_default_device(const struct audio_output_plugin *plugin) +{ + return plugin->test_default_device != nullptr + ? plugin->test_default_device() + : false; +} + +gcc_malloc +struct audio_output * +ao_plugin_init(const struct audio_output_plugin *plugin, + const config_param ¶m, + Error &error); + +void +ao_plugin_finish(struct audio_output *ao); + +bool +ao_plugin_enable(struct audio_output *ao, Error &error); + +void +ao_plugin_disable(struct audio_output *ao); + +bool +ao_plugin_open(struct audio_output *ao, AudioFormat &audio_format, + Error &error); + +void +ao_plugin_close(struct audio_output *ao); + +gcc_pure +unsigned +ao_plugin_delay(struct audio_output *ao); + +void +ao_plugin_send_tag(struct audio_output *ao, const Tag *tag); + +size_t +ao_plugin_play(struct audio_output *ao, const void *chunk, size_t size, + Error &error); + +void +ao_plugin_drain(struct audio_output *ao); + +void +ao_plugin_cancel(struct audio_output *ao); + +bool +ao_plugin_pause(struct audio_output *ao); + +#endif diff --git a/src/output/OutputPrint.cxx b/src/output/OutputPrint.cxx new file mode 100644 index 000000000..ee4424df2 --- /dev/null +++ b/src/output/OutputPrint.cxx @@ -0,0 +1,45 @@ +/* + * 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. + */ + +/* + * Protocol specific code for the audio output library. + * + */ + +#include "config.h" +#include "OutputPrint.hxx" +#include "OutputAll.hxx" +#include "OutputInternal.hxx" +#include "Client.hxx" + +void +printAudioDevices(Client &client) +{ + const unsigned n = audio_output_count(); + + for (unsigned i = 0; i < n; ++i) { + const struct audio_output *ao = audio_output_get(i); + + client_printf(client, + "outputid: %i\n" + "outputname: %s\n" + "outputenabled: %i\n", + i, ao->name, ao->enabled); + } +} diff --git a/src/output/OutputPrint.hxx b/src/output/OutputPrint.hxx new file mode 100644 index 000000000..2d94226fd --- /dev/null +++ b/src/output/OutputPrint.hxx @@ -0,0 +1,34 @@ + +/* + * 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. + */ + +/* + * Protocol specific code for the audio output library. + * + */ + +#ifndef MPD_OUTPUT_PRINT_HXX +#define MPD_OUTPUT_PRINT_HXX + +class Client; + +void +printAudioDevices(Client &client); + +#endif diff --git a/src/output/OutputState.cxx b/src/output/OutputState.cxx new file mode 100644 index 000000000..1141abeda --- /dev/null +++ b/src/output/OutputState.cxx @@ -0,0 +1,92 @@ +/* + * 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. + */ + +/* + * Saving and loading the audio output states to/from the state file. + * + */ + +#include "config.h" +#include "OutputState.hxx" +#include "OutputAll.hxx" +#include "OutputInternal.hxx" +#include "OutputError.hxx" +#include "Log.hxx" +#include "util/StringUtil.hxx" + +#include +#include + +#define AUDIO_DEVICE_STATE "audio_device_state:" + +unsigned audio_output_state_version; + +void +audio_output_state_save(FILE *fp) +{ + unsigned n = audio_output_count(); + + assert(n > 0); + + for (unsigned i = 0; i < n; ++i) { + const struct audio_output *ao = audio_output_get(i); + + fprintf(fp, AUDIO_DEVICE_STATE "%d:%s\n", + ao->enabled, ao->name); + } +} + +bool +audio_output_state_read(const char *line) +{ + long value; + char *endptr; + const char *name; + struct audio_output *ao; + + if (!StringStartsWith(line, AUDIO_DEVICE_STATE)) + return false; + + line += sizeof(AUDIO_DEVICE_STATE) - 1; + + value = strtol(line, &endptr, 10); + if (*endptr != ':' || (value != 0 && value != 1)) + return false; + + if (value != 0) + /* state is "enabled": no-op */ + return true; + + name = endptr + 1; + ao = audio_output_find(name); + if (ao == NULL) { + FormatDebug(output_domain, + "Ignoring device state for '%s'", name); + return true; + } + + ao->enabled = false; + return true; +} + +unsigned +audio_output_state_get_version(void) +{ + return audio_output_state_version; +} diff --git a/src/output/OutputState.hxx b/src/output/OutputState.hxx new file mode 100644 index 000000000..a68180ae4 --- /dev/null +++ b/src/output/OutputState.hxx @@ -0,0 +1,44 @@ +/* + * 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. + */ + +/* + * Saving and loading the audio output states to/from the state file. + * + */ + +#ifndef MPD_OUTPUT_STATE_HXX +#define MPD_OUTPUT_STATE_HXX + +#include + +bool +audio_output_state_read(const char *line); + +void +audio_output_state_save(FILE *fp); + +/** + * Generates a version number for the current state of the audio + * outputs. This is used by timer_save_state_file() to determine + * whether the state has changed and the state file should be saved. + */ +unsigned +audio_output_state_get_version(void); + +#endif 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 +#include + +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); +} diff --git a/src/output/OutputThread.hxx b/src/output/OutputThread.hxx new file mode 100644 index 000000000..1cdbd65f2 --- /dev/null +++ b/src/output/OutputThread.hxx @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OUTPUT_THREAD_HXX +#define MPD_OUTPUT_THREAD_HXX + +struct audio_output; + +void +audio_output_thread_start(audio_output *ao); + +#endif diff --git a/src/output/PipeOutputPlugin.cxx b/src/output/PipeOutputPlugin.cxx deleted file mode 100644 index 66d5a28ae..000000000 --- a/src/output/PipeOutputPlugin.cxx +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "config.h" -#include "PipeOutputPlugin.hxx" -#include "OutputAPI.hxx" -#include "ConfigError.hxx" -#include "util/Error.hxx" -#include "util/Domain.hxx" - -#include - -#include - -struct PipeOutput { - struct audio_output base; - - std::string cmd; - FILE *fh; - - bool Initialize(const config_param ¶m, Error &error) { - return ao_base_init(&base, &pipe_output_plugin, param, - error); - } - - void Deinitialize() { - ao_base_finish(&base); - } - - bool Configure(const config_param ¶m, Error &error); -}; - -static constexpr Domain pipe_output_domain("pipe_output"); - -inline bool -PipeOutput::Configure(const config_param ¶m, Error &error) -{ - cmd = param.GetBlockValue("command", ""); - if (cmd.empty()) { - error.Set(config_domain, - "No \"command\" parameter specified"); - return false; - } - - return true; -} - -static struct audio_output * -pipe_output_init(const config_param ¶m, Error &error) -{ - PipeOutput *pd = new PipeOutput(); - - if (!pd->Initialize(param, error)) { - delete pd; - return nullptr; - } - - if (!pd->Configure(param, error)) { - pd->Deinitialize(); - delete pd; - return nullptr; - } - - return &pd->base; -} - -static void -pipe_output_finish(struct audio_output *ao) -{ - PipeOutput *pd = (PipeOutput *)ao; - - pd->Deinitialize(); - delete pd; -} - -static bool -pipe_output_open(struct audio_output *ao, - gcc_unused AudioFormat &audio_format, - Error &error) -{ - PipeOutput *pd = (PipeOutput *)ao; - - pd->fh = popen(pd->cmd.c_str(), "w"); - if (pd->fh == nullptr) { - error.FormatErrno("Error opening pipe \"%s\"", - pd->cmd.c_str()); - return false; - } - - return true; -} - -static void -pipe_output_close(struct audio_output *ao) -{ - PipeOutput *pd = (PipeOutput *)ao; - - pclose(pd->fh); -} - -static size_t -pipe_output_play(struct audio_output *ao, const void *chunk, size_t size, - Error &error) -{ - PipeOutput *pd = (PipeOutput *)ao; - size_t ret; - - ret = fwrite(chunk, 1, size, pd->fh); - if (ret == 0) - error.SetErrno("Write error on pipe"); - - return ret; -} - -const struct audio_output_plugin pipe_output_plugin = { - "pipe", - nullptr, - pipe_output_init, - pipe_output_finish, - nullptr, - nullptr, - pipe_output_open, - pipe_output_close, - nullptr, - nullptr, - pipe_output_play, - nullptr, - nullptr, - nullptr, - nullptr, -}; diff --git a/src/output/PipeOutputPlugin.hxx b/src/output/PipeOutputPlugin.hxx deleted file mode 100644 index 42b01b9f7..000000000 --- a/src/output/PipeOutputPlugin.hxx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_PIPE_OUTPUT_PLUGIN_HXX -#define MPD_PIPE_OUTPUT_PLUGIN_HXX - -extern const struct audio_output_plugin pipe_output_plugin; - -#endif diff --git a/src/output/PulseOutputPlugin.cxx b/src/output/PulseOutputPlugin.cxx deleted file mode 100644 index dd1906f53..000000000 --- a/src/output/PulseOutputPlugin.cxx +++ /dev/null @@ -1,889 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "config.h" -#include "PulseOutputPlugin.hxx" -#include "OutputAPI.hxx" -#include "MixerList.hxx" -#include "mixer/PulseMixerPlugin.hxx" -#include "util/Error.hxx" -#include "util/Domain.hxx" -#include "Log.hxx" - -#include - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#define MPD_PULSE_NAME "Music Player Daemon" - -struct PulseOutput { - struct audio_output base; - - const char *name; - const char *server; - const char *sink; - - PulseMixer *mixer; - - struct pa_threaded_mainloop *mainloop; - struct pa_context *context; - struct pa_stream *stream; - - size_t writable; -}; - -static constexpr Domain pulse_output_domain("pulse_output"); - -static void -SetError(Error &error, pa_context *context, const char *msg) -{ - const int e = pa_context_errno(context); - error.Format(pulse_output_domain, e, "%s: %s", msg, pa_strerror(e)); -} - -void -pulse_output_lock(PulseOutput *po) -{ - pa_threaded_mainloop_lock(po->mainloop); -} - -void -pulse_output_unlock(PulseOutput *po) -{ - pa_threaded_mainloop_unlock(po->mainloop); -} - -void -pulse_output_set_mixer(PulseOutput *po, PulseMixer *pm) -{ - assert(po != nullptr); - assert(po->mixer == nullptr); - assert(pm != nullptr); - - po->mixer = pm; - - if (po->mainloop == nullptr) - return; - - pa_threaded_mainloop_lock(po->mainloop); - - if (po->context != nullptr && - pa_context_get_state(po->context) == PA_CONTEXT_READY) { - pulse_mixer_on_connect(pm, po->context); - - if (po->stream != nullptr && - pa_stream_get_state(po->stream) == PA_STREAM_READY) - pulse_mixer_on_change(pm, po->context, po->stream); - } - - pa_threaded_mainloop_unlock(po->mainloop); -} - -void -pulse_output_clear_mixer(PulseOutput *po, gcc_unused PulseMixer *pm) -{ - assert(po != nullptr); - assert(pm != nullptr); - assert(po->mixer == pm); - - po->mixer = nullptr; -} - -bool -pulse_output_set_volume(PulseOutput *po, const pa_cvolume *volume, - Error &error) -{ - pa_operation *o; - - if (po->context == nullptr || po->stream == nullptr || - pa_stream_get_state(po->stream) != PA_STREAM_READY) { - error.Set(pulse_output_domain, "disconnected"); - return false; - } - - o = pa_context_set_sink_input_volume(po->context, - pa_stream_get_index(po->stream), - volume, nullptr, nullptr); - if (o == nullptr) { - SetError(error, po->context, - "failed to set PulseAudio volume"); - return false; - } - - pa_operation_unref(o); - return true; -} - -/** - * \brief waits for a pulseaudio operation to finish, frees it and - * unlocks the mainloop - * \param operation the operation to wait for - * \return true if operation has finished normally (DONE state), - * false otherwise - */ -static bool -pulse_wait_for_operation(struct pa_threaded_mainloop *mainloop, - struct pa_operation *operation) -{ - pa_operation_state_t state; - - assert(mainloop != nullptr); - assert(operation != nullptr); - - 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(gcc_unused pa_stream *s, - gcc_unused int success, void *userdata) -{ - PulseOutput *po = (PulseOutput *)userdata; - - pa_threaded_mainloop_signal(po->mainloop, 0); -} - -static void -pulse_output_context_state_cb(struct pa_context *context, void *userdata) -{ - PulseOutput *po = (PulseOutput *)userdata; - - switch (pa_context_get_state(context)) { - case PA_CONTEXT_READY: - if (po->mixer != nullptr) - pulse_mixer_on_connect(po->mixer, context); - - pa_threaded_mainloop_signal(po->mainloop, 0); - break; - - case PA_CONTEXT_TERMINATED: - case PA_CONTEXT_FAILED: - if (po->mixer != nullptr) - pulse_mixer_on_disconnect(po->mixer); - - /* the caller thread might be waiting for these - states */ - pa_threaded_mainloop_signal(po->mainloop, 0); - break; - - case PA_CONTEXT_UNCONNECTED: - case PA_CONTEXT_CONNECTING: - case PA_CONTEXT_AUTHORIZING: - case PA_CONTEXT_SETTING_NAME: - break; - } -} - -static void -pulse_output_subscribe_cb(pa_context *context, - pa_subscription_event_type_t t, - uint32_t idx, void *userdata) -{ - PulseOutput *po = (PulseOutput *)userdata; - pa_subscription_event_type_t facility = - pa_subscription_event_type_t(t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK); - pa_subscription_event_type_t type = - pa_subscription_event_type_t(t & PA_SUBSCRIPTION_EVENT_TYPE_MASK); - - if (po->mixer != nullptr && - facility == PA_SUBSCRIPTION_EVENT_SINK_INPUT && - po->stream != nullptr && - pa_stream_get_state(po->stream) == PA_STREAM_READY && - idx == pa_stream_get_index(po->stream) && - (type == PA_SUBSCRIPTION_EVENT_NEW || - type == PA_SUBSCRIPTION_EVENT_CHANGE)) - pulse_mixer_on_change(po->mixer, context, po->stream); -} - -/** - * Attempt to connect asynchronously to the PulseAudio server. - * - * @return true on success, false on error - */ -static bool -pulse_output_connect(PulseOutput *po, Error &error) -{ - assert(po != nullptr); - assert(po->context != nullptr); - - if (pa_context_connect(po->context, po->server, - (pa_context_flags_t)0, nullptr) < 0) { - SetError(error, po->context, - "pa_context_connect() has failed"); - return false; - } - - return true; -} - -/** - * Frees and clears the stream. - */ -static void -pulse_output_delete_stream(PulseOutput *po) -{ - assert(po != nullptr); - assert(po->stream != nullptr); - - pa_stream_set_suspended_callback(po->stream, nullptr, nullptr); - - pa_stream_set_state_callback(po->stream, nullptr, nullptr); - pa_stream_set_write_callback(po->stream, nullptr, nullptr); - - pa_stream_disconnect(po->stream); - pa_stream_unref(po->stream); - po->stream = nullptr; -} - -/** - * Frees and clears the context. - * - * Caller must lock the main loop. - */ -static void -pulse_output_delete_context(PulseOutput *po) -{ - assert(po != nullptr); - assert(po->context != nullptr); - - pa_context_set_state_callback(po->context, nullptr, nullptr); - pa_context_set_subscribe_callback(po->context, nullptr, nullptr); - - pa_context_disconnect(po->context); - pa_context_unref(po->context); - po->context = nullptr; -} - -/** - * Create, set up and connect a context. - * - * Caller must lock the main loop. - * - * @return true on success, false on error - */ -static bool -pulse_output_setup_context(PulseOutput *po, Error &error) -{ - assert(po != nullptr); - assert(po->mainloop != nullptr); - - po->context = pa_context_new(pa_threaded_mainloop_get_api(po->mainloop), - MPD_PULSE_NAME); - if (po->context == nullptr) { - error.Set(pulse_output_domain, "pa_context_new() has failed"); - return false; - } - - pa_context_set_state_callback(po->context, - pulse_output_context_state_cb, po); - pa_context_set_subscribe_callback(po->context, - pulse_output_subscribe_cb, po); - - if (!pulse_output_connect(po, error)) { - pulse_output_delete_context(po); - return false; - } - - return true; -} - -static struct audio_output * -pulse_output_init(const config_param ¶m, Error &error) -{ - PulseOutput *po; - - g_setenv("PULSE_PROP_media.role", "music", true); - - po = new PulseOutput(); - if (!ao_base_init(&po->base, &pulse_output_plugin, param, error)) { - delete po; - return nullptr; - } - - po->name = param.GetBlockValue("name", "mpd_pulse"); - po->server = param.GetBlockValue("server"); - po->sink = param.GetBlockValue("sink"); - - po->mixer = nullptr; - po->mainloop = nullptr; - po->context = nullptr; - po->stream = nullptr; - - return &po->base; -} - -static void -pulse_output_finish(struct audio_output *ao) -{ - PulseOutput *po = (PulseOutput *)ao; - - ao_base_finish(&po->base); - delete po; -} - -static bool -pulse_output_enable(struct audio_output *ao, Error &error) -{ - PulseOutput *po = (PulseOutput *)ao; - - assert(po->mainloop == nullptr); - assert(po->context == nullptr); - - /* create the libpulse mainloop and start the thread */ - - po->mainloop = pa_threaded_mainloop_new(); - if (po->mainloop == nullptr) { - g_free(po); - - error.Set(pulse_output_domain, - "pa_threaded_mainloop_new() has failed"); - return false; - } - - pa_threaded_mainloop_lock(po->mainloop); - - if (pa_threaded_mainloop_start(po->mainloop) < 0) { - pa_threaded_mainloop_unlock(po->mainloop); - pa_threaded_mainloop_free(po->mainloop); - po->mainloop = nullptr; - - error.Set(pulse_output_domain, - "pa_threaded_mainloop_start() has failed"); - return false; - } - - /* create the libpulse context and connect it */ - - if (!pulse_output_setup_context(po, error)) { - pa_threaded_mainloop_unlock(po->mainloop); - pa_threaded_mainloop_stop(po->mainloop); - pa_threaded_mainloop_free(po->mainloop); - po->mainloop = nullptr; - return false; - } - - pa_threaded_mainloop_unlock(po->mainloop); - - return true; -} - -static void -pulse_output_disable(struct audio_output *ao) -{ - PulseOutput *po = (PulseOutput *)ao; - - assert(po->mainloop != nullptr); - - pa_threaded_mainloop_stop(po->mainloop); - if (po->context != nullptr) - pulse_output_delete_context(po); - pa_threaded_mainloop_free(po->mainloop); - po->mainloop = nullptr; -} - -/** - * Check if the context is (already) connected, and waits if not. If - * the context has been disconnected, retry to connect. - * - * Caller must lock the main loop. - * - * @return true on success, false on error - */ -static bool -pulse_output_wait_connection(PulseOutput *po, Error &error) -{ - assert(po->mainloop != nullptr); - - pa_context_state_t state; - - if (po->context == nullptr && !pulse_output_setup_context(po, error)) - return false; - - while (true) { - state = pa_context_get_state(po->context); - switch (state) { - case PA_CONTEXT_READY: - /* nothing to do */ - return true; - - case PA_CONTEXT_UNCONNECTED: - case PA_CONTEXT_TERMINATED: - case PA_CONTEXT_FAILED: - /* failure */ - SetError(error, po->context, "failed to connect"); - pulse_output_delete_context(po); - return false; - - case PA_CONTEXT_CONNECTING: - case PA_CONTEXT_AUTHORIZING: - case PA_CONTEXT_SETTING_NAME: - /* wait some more */ - pa_threaded_mainloop_wait(po->mainloop); - break; - } - } -} - -static void -pulse_output_stream_suspended_cb(gcc_unused pa_stream *stream, void *userdata) -{ - PulseOutput *po = (PulseOutput *)userdata; - - assert(stream == po->stream || po->stream == nullptr); - assert(po->mainloop != nullptr); - - /* wake up the main loop to break out of the loop in - pulse_output_play() */ - pa_threaded_mainloop_signal(po->mainloop, 0); -} - -static void -pulse_output_stream_state_cb(pa_stream *stream, void *userdata) -{ - PulseOutput *po = (PulseOutput *)userdata; - - assert(stream == po->stream || po->stream == nullptr); - assert(po->mainloop != nullptr); - assert(po->context != nullptr); - - switch (pa_stream_get_state(stream)) { - case PA_STREAM_READY: - if (po->mixer != nullptr) - pulse_mixer_on_change(po->mixer, po->context, stream); - - pa_threaded_mainloop_signal(po->mainloop, 0); - break; - - case PA_STREAM_FAILED: - case PA_STREAM_TERMINATED: - if (po->mixer != nullptr) - pulse_mixer_on_disconnect(po->mixer); - - pa_threaded_mainloop_signal(po->mainloop, 0); - break; - - case PA_STREAM_UNCONNECTED: - case PA_STREAM_CREATING: - break; - } -} - -static void -pulse_output_stream_write_cb(gcc_unused pa_stream *stream, size_t nbytes, - void *userdata) -{ - PulseOutput *po = (PulseOutput *)userdata; - - assert(po->mainloop != nullptr); - - po->writable = nbytes; - pa_threaded_mainloop_signal(po->mainloop, 0); -} - -/** - * Create, set up and connect a context. - * - * Caller must lock the main loop. - * - * @return true on success, false on error - */ -static bool -pulse_output_setup_stream(PulseOutput *po, const pa_sample_spec *ss, - Error &error) -{ - assert(po != nullptr); - assert(po->context != nullptr); - - po->stream = pa_stream_new(po->context, po->name, ss, nullptr); - if (po->stream == nullptr) { - SetError(error, po->context, "pa_stream_new() has failed"); - return false; - } - - pa_stream_set_suspended_callback(po->stream, - pulse_output_stream_suspended_cb, po); - - pa_stream_set_state_callback(po->stream, - pulse_output_stream_state_cb, po); - pa_stream_set_write_callback(po->stream, - pulse_output_stream_write_cb, po); - - return true; -} - -static bool -pulse_output_open(struct audio_output *ao, AudioFormat &audio_format, - Error &error) -{ - PulseOutput *po = (PulseOutput *)ao; - pa_sample_spec ss; - - assert(po->mainloop != nullptr); - - pa_threaded_mainloop_lock(po->mainloop); - - if (po->context != nullptr) { - switch (pa_context_get_state(po->context)) { - case PA_CONTEXT_UNCONNECTED: - case PA_CONTEXT_TERMINATED: - case PA_CONTEXT_FAILED: - /* the connection was closed meanwhile; delete - it, and pulse_output_wait_connection() will - reopen it */ - pulse_output_delete_context(po); - break; - - case PA_CONTEXT_READY: - case PA_CONTEXT_CONNECTING: - case PA_CONTEXT_AUTHORIZING: - case PA_CONTEXT_SETTING_NAME: - break; - } - } - - if (!pulse_output_wait_connection(po, error)) { - pa_threaded_mainloop_unlock(po->mainloop); - return false; - } - - /* MPD doesn't support the other pulseaudio sample formats, so - we just force MPD to send us everything as 16 bit */ - audio_format.format = SampleFormat::S16; - - ss.format = PA_SAMPLE_S16NE; - ss.rate = audio_format.sample_rate; - ss.channels = audio_format.channels; - - /* create a stream .. */ - - if (!pulse_output_setup_stream(po, &ss, error)) { - pa_threaded_mainloop_unlock(po->mainloop); - return false; - } - - /* .. and connect it (asynchronously) */ - - if (pa_stream_connect_playback(po->stream, po->sink, - nullptr, pa_stream_flags_t(0), - nullptr, nullptr) < 0) { - pulse_output_delete_stream(po); - - SetError(error, po->context, - "pa_stream_connect_playback() has failed"); - pa_threaded_mainloop_unlock(po->mainloop); - return false; - } - - pa_threaded_mainloop_unlock(po->mainloop); - - return true; -} - -static void -pulse_output_close(struct audio_output *ao) -{ - PulseOutput *po = (PulseOutput *)ao; - pa_operation *o; - - assert(po->mainloop != nullptr); - - pa_threaded_mainloop_lock(po->mainloop); - - if (pa_stream_get_state(po->stream) == PA_STREAM_READY) { - o = pa_stream_drain(po->stream, - pulse_output_stream_success_cb, po); - if (o == nullptr) { - FormatWarning(pulse_output_domain, - "pa_stream_drain() has failed: %s", - pa_strerror(pa_context_errno(po->context))); - } else - pulse_wait_for_operation(po->mainloop, o); - } - - pulse_output_delete_stream(po); - - if (po->context != nullptr && - pa_context_get_state(po->context) != PA_CONTEXT_READY) - pulse_output_delete_context(po); - - pa_threaded_mainloop_unlock(po->mainloop); -} - -/** - * Check if the stream is (already) connected, and waits if not. The - * mainloop must be locked before calling this function. - * - * @return true on success, false on error - */ -static bool -pulse_output_wait_stream(PulseOutput *po, Error &error) -{ - while (true) { - switch (pa_stream_get_state(po->stream)) { - case PA_STREAM_READY: - return true; - - case PA_STREAM_FAILED: - case PA_STREAM_TERMINATED: - case PA_STREAM_UNCONNECTED: - SetError(error, po->context, - "failed to connect the stream"); - return false; - - case PA_STREAM_CREATING: - pa_threaded_mainloop_wait(po->mainloop); - break; - } - } -} - -/** - * Sets cork mode on the stream. - */ -static bool -pulse_output_stream_pause(PulseOutput *po, bool pause, - Error &error) -{ - pa_operation *o; - - assert(po->mainloop != nullptr); - assert(po->context != nullptr); - assert(po->stream != nullptr); - - o = pa_stream_cork(po->stream, pause, - pulse_output_stream_success_cb, po); - if (o == nullptr) { - SetError(error, po->context, "pa_stream_cork() has failed"); - return false; - } - - if (!pulse_wait_for_operation(po->mainloop, o)) { - SetError(error, po->context, "pa_stream_cork() has failed"); - return false; - } - - return true; -} - -static unsigned -pulse_output_delay(struct audio_output *ao) -{ - PulseOutput *po = (PulseOutput *)ao; - unsigned result = 0; - - pa_threaded_mainloop_lock(po->mainloop); - - if (po->base.pause && pa_stream_is_corked(po->stream) && - pa_stream_get_state(po->stream) == PA_STREAM_READY) - /* idle while paused */ - result = 1000; - - pa_threaded_mainloop_unlock(po->mainloop); - - return result; -} - -static size_t -pulse_output_play(struct audio_output *ao, const void *chunk, size_t size, - Error &error) -{ - PulseOutput *po = (PulseOutput *)ao; - - assert(po->mainloop != nullptr); - assert(po->stream != nullptr); - - pa_threaded_mainloop_lock(po->mainloop); - - /* check if the stream is (already) connected */ - - if (!pulse_output_wait_stream(po, error)) { - pa_threaded_mainloop_unlock(po->mainloop); - return 0; - } - - assert(po->context != nullptr); - - /* unpause if previously paused */ - - if (pa_stream_is_corked(po->stream) && - !pulse_output_stream_pause(po, false, error)) { - pa_threaded_mainloop_unlock(po->mainloop); - return 0; - } - - /* wait until the server allows us to write */ - - while (po->writable == 0) { - if (pa_stream_is_suspended(po->stream)) { - pa_threaded_mainloop_unlock(po->mainloop); - error.Set(pulse_output_domain, "suspended"); - return 0; - } - - pa_threaded_mainloop_wait(po->mainloop); - - if (pa_stream_get_state(po->stream) != PA_STREAM_READY) { - pa_threaded_mainloop_unlock(po->mainloop); - error.Set(pulse_output_domain, "disconnected"); - return 0; - } - } - - /* now write */ - - if (size > po->writable) - /* don't send more than possible */ - size = po->writable; - - po->writable -= size; - - int result = pa_stream_write(po->stream, chunk, size, nullptr, - 0, PA_SEEK_RELATIVE); - pa_threaded_mainloop_unlock(po->mainloop); - if (result < 0) { - SetError(error, po->context, "pa_stream_write() failed"); - return 0; - } - - return size; -} - -static void -pulse_output_cancel(struct audio_output *ao) -{ - PulseOutput *po = (PulseOutput *)ao; - pa_operation *o; - - assert(po->mainloop != nullptr); - assert(po->stream != nullptr); - - pa_threaded_mainloop_lock(po->mainloop); - - if (pa_stream_get_state(po->stream) != PA_STREAM_READY) { - /* no need to flush when the stream isn't connected - yet */ - pa_threaded_mainloop_unlock(po->mainloop); - return; - } - - assert(po->context != nullptr); - - o = pa_stream_flush(po->stream, pulse_output_stream_success_cb, po); - if (o == nullptr) { - FormatWarning(pulse_output_domain, - "pa_stream_flush() has failed: %s", - pa_strerror(pa_context_errno(po->context))); - pa_threaded_mainloop_unlock(po->mainloop); - return; - } - - pulse_wait_for_operation(po->mainloop, o); - pa_threaded_mainloop_unlock(po->mainloop); -} - -static bool -pulse_output_pause(struct audio_output *ao) -{ - PulseOutput *po = (PulseOutput *)ao; - - assert(po->mainloop != nullptr); - assert(po->stream != nullptr); - - pa_threaded_mainloop_lock(po->mainloop); - - /* check if the stream is (already/still) connected */ - - Error error; - if (!pulse_output_wait_stream(po, error)) { - pa_threaded_mainloop_unlock(po->mainloop); - LogError(error); - return false; - } - - assert(po->context != nullptr); - - /* cork the stream */ - - if (!pa_stream_is_corked(po->stream) && - !pulse_output_stream_pause(po, true, error)) { - pa_threaded_mainloop_unlock(po->mainloop); - LogError(error); - return false; - } - - pa_threaded_mainloop_unlock(po->mainloop); - - return true; -} - -static bool -pulse_output_test_default_device(void) -{ - bool success; - - const config_param empty; - PulseOutput *po = (PulseOutput *) - pulse_output_init(empty, IgnoreError()); - if (po == nullptr) - return false; - - success = pulse_output_wait_connection(po, IgnoreError()); - pulse_output_finish(&po->base); - - return success; -} - -const struct audio_output_plugin pulse_output_plugin = { - "pulse", - pulse_output_test_default_device, - pulse_output_init, - pulse_output_finish, - pulse_output_enable, - pulse_output_disable, - pulse_output_open, - pulse_output_close, - pulse_output_delay, - nullptr, - pulse_output_play, - nullptr, - pulse_output_cancel, - pulse_output_pause, - - &pulse_mixer_plugin, -}; diff --git a/src/output/PulseOutputPlugin.hxx b/src/output/PulseOutputPlugin.hxx deleted file mode 100644 index 9df557282..000000000 --- a/src/output/PulseOutputPlugin.hxx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_PULSE_OUTPUT_PLUGIN_HXX -#define MPD_PULSE_OUTPUT_PLUGIN_HXX - -struct PulseOutput; -struct PulseMixer; -struct pa_cvolume; -class Error; - -extern const struct audio_output_plugin pulse_output_plugin; - -void -pulse_output_lock(PulseOutput *po); - -void -pulse_output_unlock(PulseOutput *po); - -void -pulse_output_set_mixer(PulseOutput *po, PulseMixer *pm); - -void -pulse_output_clear_mixer(PulseOutput *po, PulseMixer *pm); - -bool -pulse_output_set_volume(PulseOutput *po, - const pa_cvolume *volume, Error &error); - -#endif diff --git a/src/output/RecorderOutputPlugin.cxx b/src/output/RecorderOutputPlugin.cxx deleted file mode 100644 index cebbc2cec..000000000 --- a/src/output/RecorderOutputPlugin.cxx +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "config.h" -#include "RecorderOutputPlugin.hxx" -#include "OutputAPI.hxx" -#include "encoder/EncoderPlugin.hxx" -#include "encoder/EncoderList.hxx" -#include "ConfigError.hxx" -#include "util/Error.hxx" -#include "util/Domain.hxx" -#include "system/fd_util.h" -#include "open.h" - -#include -#include -#include -#include -#include - -struct RecorderOutput { - struct audio_output base; - - /** - * The configured encoder plugin. - */ - Encoder *encoder; - - /** - * The destination file name. - */ - const char *path; - - /** - * The destination file descriptor. - */ - int fd; - - /** - * The buffer for encoder_read(). - */ - char buffer[32768]; - - bool Initialize(const config_param ¶m, Error &error_r) { - return ao_base_init(&base, &recorder_output_plugin, param, - error_r); - } - - void Deinitialize() { - ao_base_finish(&base); - } - - bool Configure(const config_param ¶m, Error &error); - - bool WriteToFile(const void *data, size_t length, Error &error); - - /** - * Writes pending data from the encoder to the output file. - */ - bool EncoderToFile(Error &error); -}; - -static constexpr Domain recorder_output_domain("recorder_output"); - -inline bool -RecorderOutput::Configure(const config_param ¶m, Error &error) -{ - /* read configuration */ - - const char *encoder_name = - param.GetBlockValue("encoder", "vorbis"); - const auto encoder_plugin = encoder_plugin_get(encoder_name); - if (encoder_plugin == nullptr) { - error.Format(config_domain, - "No such encoder: %s", encoder_name); - return false; - } - - path = param.GetBlockValue("path"); - if (path == nullptr) { - error.Set(config_domain, "'path' not configured"); - return false; - } - - /* initialize encoder */ - - encoder = encoder_init(*encoder_plugin, param, error); - if (encoder == nullptr) - return false; - - return true; -} - -static audio_output * -recorder_output_init(const config_param ¶m, Error &error) -{ - RecorderOutput *recorder = new RecorderOutput(); - - if (!recorder->Initialize(param, error)) { - delete recorder; - return nullptr; - } - - if (!recorder->Configure(param, error)) { - recorder->Deinitialize(); - delete recorder; - return nullptr; - } - - return &recorder->base; -} - -static void -recorder_output_finish(struct audio_output *ao) -{ - RecorderOutput *recorder = (RecorderOutput *)ao; - - encoder_finish(recorder->encoder); - recorder->Deinitialize(); - delete recorder; -} - -inline bool -RecorderOutput::WriteToFile(const void *_data, size_t length, Error &error) -{ - assert(length > 0); - - const uint8_t *data = (const uint8_t *)_data, *end = data + length; - - while (true) { - ssize_t nbytes = write(fd, data, end - data); - if (nbytes > 0) { - data += nbytes; - if (data == end) - return true; - } else if (nbytes == 0) { - /* shouldn't happen for files */ - error.Set(recorder_output_domain, - "write() returned 0"); - return false; - } else if (errno != EINTR) { - error.FormatErrno("Failed to write to '%s'", path); - return false; - } - } -} - -inline bool -RecorderOutput::EncoderToFile(Error &error) -{ - assert(fd >= 0); - - while (true) { - /* read from the encoder */ - - size_t size = encoder_read(encoder, buffer, sizeof(buffer)); - if (size == 0) - return true; - - /* write everything into the file */ - - if (!WriteToFile(buffer, size, error)) - return false; - } -} - -static bool -recorder_output_open(struct audio_output *ao, - AudioFormat &audio_format, - Error &error) -{ - RecorderOutput *recorder = (RecorderOutput *)ao; - - /* create the output file */ - - recorder->fd = open_cloexec(recorder->path, - O_CREAT|O_WRONLY|O_TRUNC|O_BINARY, - 0666); - if (recorder->fd < 0) { - error.FormatErrno("Failed to create '%s'", recorder->path); - return false; - } - - /* open the encoder */ - - if (!encoder_open(recorder->encoder, audio_format, error)) { - close(recorder->fd); - unlink(recorder->path); - return false; - } - - if (!recorder->EncoderToFile(error)) { - encoder_close(recorder->encoder); - close(recorder->fd); - unlink(recorder->path); - return false; - } - - return true; -} - -static void -recorder_output_close(struct audio_output *ao) -{ - RecorderOutput *recorder = (RecorderOutput *)ao; - - /* flush the encoder and write the rest to the file */ - - if (encoder_end(recorder->encoder, IgnoreError())) - recorder->EncoderToFile(IgnoreError()); - - /* now really close everything */ - - encoder_close(recorder->encoder); - - close(recorder->fd); -} - -static size_t -recorder_output_play(struct audio_output *ao, const void *chunk, size_t size, - Error &error) -{ - RecorderOutput *recorder = (RecorderOutput *)ao; - - return encoder_write(recorder->encoder, chunk, size, error) && - recorder->EncoderToFile(error) - ? size : 0; -} - -const struct audio_output_plugin recorder_output_plugin = { - "recorder", - nullptr, - recorder_output_init, - recorder_output_finish, - nullptr, - nullptr, - recorder_output_open, - recorder_output_close, - nullptr, - nullptr, - recorder_output_play, - nullptr, - nullptr, - nullptr, - nullptr, -}; diff --git a/src/output/RecorderOutputPlugin.hxx b/src/output/RecorderOutputPlugin.hxx deleted file mode 100644 index 4fac911a1..000000000 --- a/src/output/RecorderOutputPlugin.hxx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_RECORDER_OUTPUT_PLUGIN_HXX -#define MPD_RECORDER_OUTPUT_PLUGIN_HXX - -extern const struct audio_output_plugin recorder_output_plugin; - -#endif diff --git a/src/output/RoarOutputPlugin.cxx b/src/output/RoarOutputPlugin.cxx deleted file mode 100644 index 9634379c5..000000000 --- a/src/output/RoarOutputPlugin.cxx +++ /dev/null @@ -1,428 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * Copyright (C) 2010-2011 Philipp 'ph3-der-loewe' Schafft - * Copyright (C) 2010-2011 Hans-Kristian 'maister' Arntzen - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "config.h" -#include "RoarOutputPlugin.hxx" -#include "OutputAPI.hxx" -#include "MixerList.hxx" -#include "thread/Mutex.hxx" -#include "util/Error.hxx" -#include "util/Domain.hxx" -#include "Log.hxx" - -#include - -/* libroar/services.h declares roar_service_stream::new - work around - this C++ problem */ -#define new _new -#include -#undef new - -class RoarOutput { - struct audio_output base; - - std::string host, name; - - roar_vs_t * vss; - int err; - int role; - struct roar_connection con; - struct roar_audio_info info; - mutable Mutex mutex; - volatile bool alive; - -public: - RoarOutput() - :err(ROAR_ERROR_NONE) {} - - operator audio_output *() { - return &base; - } - - bool Initialize(const config_param ¶m, Error &error) { - return ao_base_init(&base, &roar_output_plugin, param, - error); - } - - void Deinitialize() { - ao_base_finish(&base); - } - - void Configure(const config_param ¶m); - - bool Open(AudioFormat &audio_format, Error &error); - void Close(); - - void SendTag(const Tag &tag); - size_t Play(const void *chunk, size_t size, Error &error); - void Cancel(); - - int GetVolume() const; - bool SetVolume(unsigned volume); -}; - -static constexpr Domain roar_output_domain("roar_output"); - -inline int -RoarOutput::GetVolume() const -{ - const ScopeLock protect(mutex); - - if (vss == nullptr || !alive) - return -1; - - float l, r; - int error; - if (roar_vs_volume_get(vss, &l, &r, &error) < 0) - return -1; - - return (l + r) * 50; -} - -int -roar_output_get_volume(RoarOutput *roar) -{ - return roar->GetVolume(); -} - -bool -RoarOutput::SetVolume(unsigned volume) -{ - assert(volume <= 100); - - const ScopeLock protect(mutex); - if (vss == nullptr || !alive) - return false; - - int error; - float level = volume / 100.0; - - roar_vs_volume_mono(vss, level, &error); - return true; -} - -bool -roar_output_set_volume(RoarOutput *roar, unsigned volume) -{ - return roar->SetVolume(volume); -} - -inline void -RoarOutput::Configure(const config_param ¶m) -{ - host = param.GetBlockValue("server", ""); - name = param.GetBlockValue("name", "MPD"); - - const char *_role = param.GetBlockValue("role", "music"); - role = _role != nullptr - ? roar_str2role(_role) - : ROAR_ROLE_MUSIC; -} - -static struct audio_output * -roar_init(const config_param ¶m, Error &error) -{ - RoarOutput *self = new RoarOutput(); - - if (!self->Initialize(param, error)) { - delete self; - return nullptr; - } - - self->Configure(param); - return *self; -} - -static void -roar_finish(struct audio_output *ao) -{ - RoarOutput *self = (RoarOutput *)ao; - - self->Deinitialize(); - delete self; -} - -static void -roar_use_audio_format(struct roar_audio_info *info, - AudioFormat &audio_format) -{ - info->rate = audio_format.sample_rate; - info->channels = audio_format.channels; - info->codec = ROAR_CODEC_PCM_S; - - switch (audio_format.format) { - case SampleFormat::UNDEFINED: - case SampleFormat::FLOAT: - case SampleFormat::DSD: - info->bits = 16; - audio_format.format = SampleFormat::S16; - break; - - case SampleFormat::S8: - info->bits = 8; - break; - - case SampleFormat::S16: - info->bits = 16; - break; - - case SampleFormat::S24_P32: - info->bits = 32; - audio_format.format = SampleFormat::S32; - break; - - case SampleFormat::S32: - info->bits = 32; - break; - } -} - -inline bool -RoarOutput::Open(AudioFormat &audio_format, Error &error) -{ - const ScopeLock protect(mutex); - - if (roar_simple_connect(&con, - host.empty() ? nullptr : host.c_str(), - name.c_str()) < 0) { - error.Set(roar_output_domain, - "Failed to connect to Roar server"); - return false; - } - - vss = roar_vs_new_from_con(&con, &err); - - if (vss == nullptr || err != ROAR_ERROR_NONE) { - error.Set(roar_output_domain, "Failed to connect to server"); - return false; - } - - roar_use_audio_format(&info, audio_format); - - if (roar_vs_stream(vss, &info, ROAR_DIR_PLAY, &err) < 0) { - error.Set(roar_output_domain, "Failed to start stream"); - return false; - } - - roar_vs_role(vss, role, &err); - alive = true; - return true; -} - -static bool -roar_open(struct audio_output *ao, AudioFormat &audio_format, Error &error) -{ - RoarOutput *self = (RoarOutput *)ao; - - return self->Open(audio_format, error); -} - -inline void -RoarOutput::Close() -{ - const ScopeLock protect(mutex); - - alive = false; - - if (vss != nullptr) - roar_vs_close(vss, ROAR_VS_TRUE, &err); - vss = nullptr; - roar_disconnect(&con); -} - -static void -roar_close(struct audio_output *ao) -{ - RoarOutput *self = (RoarOutput *)ao; - self->Close(); -} - -inline void -RoarOutput::Cancel() -{ - const ScopeLock protect(mutex); - - if (vss == nullptr) - return; - - roar_vs_t *_vss = vss; - vss = nullptr; - roar_vs_close(_vss, ROAR_VS_TRUE, &err); - alive = false; - - _vss = roar_vs_new_from_con(&con, &err); - if (_vss == nullptr) - return; - - if (roar_vs_stream(_vss, &info, ROAR_DIR_PLAY, &err) < 0) { - roar_vs_close(_vss, ROAR_VS_TRUE, &err); - LogError(roar_output_domain, "Failed to start stream"); - return; - } - - roar_vs_role(_vss, role, &err); - vss = _vss; - alive = true; -} - -static void -roar_cancel(struct audio_output *ao) -{ - RoarOutput *self = (RoarOutput *)ao; - - self->Cancel(); -} - -inline size_t -RoarOutput::Play(const void *chunk, size_t size, Error &error) -{ - if (vss == nullptr) { - error.Set(roar_output_domain, "Connection is invalid"); - return 0; - } - - ssize_t nbytes = roar_vs_write(vss, chunk, size, &err); - if (nbytes <= 0) { - error.Set(roar_output_domain, "Failed to play data"); - return 0; - } - - return nbytes; -} - -static size_t -roar_play(struct audio_output *ao, const void *chunk, size_t size, - Error &error) -{ - RoarOutput *self = (RoarOutput *)ao; - return self->Play(chunk, size, error); -} - -static const char* -roar_tag_convert(TagType type, bool *is_uuid) -{ - *is_uuid = false; - switch (type) - { - case TAG_ARTIST: - case TAG_ALBUM_ARTIST: - return "AUTHOR"; - case TAG_ALBUM: - return "ALBUM"; - case TAG_TITLE: - return "TITLE"; - case TAG_TRACK: - return "TRACK"; - case TAG_NAME: - return "NAME"; - case TAG_GENRE: - return "GENRE"; - case TAG_DATE: - return "DATE"; - case TAG_PERFORMER: - return "PERFORMER"; - case TAG_COMMENT: - return "COMMENT"; - case TAG_DISC: - return "DISCID"; - case TAG_COMPOSER: -#ifdef ROAR_META_TYPE_COMPOSER - return "COMPOSER"; -#else - return "AUTHOR"; -#endif - case TAG_MUSICBRAINZ_ARTISTID: - case TAG_MUSICBRAINZ_ALBUMID: - case TAG_MUSICBRAINZ_ALBUMARTISTID: - case TAG_MUSICBRAINZ_TRACKID: - *is_uuid = true; - return "HASH"; - - default: - return nullptr; - } -} - -inline void -RoarOutput::SendTag(const Tag &tag) -{ - if (vss == nullptr) - return; - - const ScopeLock protect(mutex); - - size_t cnt = 1; - struct roar_keyval vals[32]; - char uuid_buf[32][64]; - - char timebuf[16]; - snprintf(timebuf, sizeof(timebuf), "%02d:%02d:%02d", - tag.time / 3600, (tag.time % 3600) / 60, tag.time % 60); - - vals[0].key = const_cast("LENGTH"); - vals[0].value = timebuf; - - for (unsigned i = 0; i < tag.num_items && cnt < 32; i++) - { - bool is_uuid = false; - const char *key = roar_tag_convert(tag.items[i]->type, - &is_uuid); - if (key != nullptr) { - vals[cnt].key = const_cast(key); - - if (is_uuid) { - snprintf(uuid_buf[cnt], sizeof(uuid_buf[0]), "{UUID}%s", - tag.items[i]->value); - vals[cnt].value = uuid_buf[cnt]; - } else { - vals[cnt].value = tag.items[i]->value; - } - - cnt++; - } - } - - roar_vs_meta(vss, vals, cnt, &(err)); -} - -static void -roar_send_tag(struct audio_output *ao, const Tag *meta) -{ - RoarOutput *self = (RoarOutput *)ao; - self->SendTag(*meta); -} - -const struct audio_output_plugin roar_output_plugin = { - "roar", - nullptr, - roar_init, - roar_finish, - nullptr, - nullptr, - roar_open, - roar_close, - nullptr, - roar_send_tag, - roar_play, - nullptr, - roar_cancel, - nullptr, - &roar_mixer_plugin, -}; diff --git a/src/output/RoarOutputPlugin.hxx b/src/output/RoarOutputPlugin.hxx deleted file mode 100644 index 27c5dc420..000000000 --- a/src/output/RoarOutputPlugin.hxx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_ROAR_OUTPUT_PLUGIN_H -#define MPD_ROAR_OUTPUT_PLUGIN_H - -class RoarOutput; - -extern const struct audio_output_plugin roar_output_plugin; - -int -roar_output_get_volume(RoarOutput *roar); - -bool -roar_output_set_volume(RoarOutput *roar, unsigned volume); - -#endif diff --git a/src/output/ShoutOutputPlugin.cxx b/src/output/ShoutOutputPlugin.cxx deleted file mode 100644 index cccd3e0ba..000000000 --- a/src/output/ShoutOutputPlugin.cxx +++ /dev/null @@ -1,544 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "config.h" -#include "ShoutOutputPlugin.hxx" -#include "OutputAPI.hxx" -#include "encoder/EncoderPlugin.hxx" -#include "encoder/EncoderList.hxx" -#include "ConfigError.hxx" -#include "util/Error.hxx" -#include "util/Domain.hxx" -#include "system/FatalError.hxx" -#include "Log.hxx" - -#include -#include - -#include -#include -#include -#include - -static constexpr unsigned DEFAULT_CONN_TIMEOUT = 2; - -struct ShoutOutput final { - struct audio_output base; - - shout_t *shout_conn; - shout_metadata_t *shout_meta; - - Encoder *encoder; - - float quality; - int bitrate; - - int timeout; - - uint8_t buffer[32768]; - - ShoutOutput() - :shout_conn(shout_new()), - shout_meta(shout_metadata_new()), - quality(-2.0), - bitrate(-1), - timeout(DEFAULT_CONN_TIMEOUT) {} - - ~ShoutOutput() { - if (shout_meta != nullptr) - shout_metadata_free(shout_meta); - if (shout_conn != nullptr) - shout_free(shout_conn); - } - - bool Initialize(const config_param ¶m, Error &error) { - return ao_base_init(&base, &shout_output_plugin, param, - error); - } - - void Deinitialize() { - ao_base_finish(&base); - } - - bool Configure(const config_param ¶m, Error &error); -}; - -static int shout_init_count; - -static constexpr Domain shout_output_domain("shout_output"); - -static const EncoderPlugin * -shout_encoder_plugin_get(const char *name) -{ - if (strcmp(name, "ogg") == 0) - name = "vorbis"; - else if (strcmp(name, "mp3") == 0) - name = "lame"; - - return encoder_plugin_get(name); -} - -gcc_pure -static const char * -require_block_string(const config_param ¶m, const char *name) -{ - const char *value = param.GetBlockValue(name); - if (value == nullptr) - FormatFatalError("no \"%s\" defined for shout device defined " - "at line %u\n", name, param.line); - - return value; -} - -inline bool -ShoutOutput::Configure(const config_param ¶m, Error &error) -{ - - const AudioFormat audio_format = base.config_audio_format; - if (!audio_format.IsFullyDefined()) { - error.Set(config_domain, - "Need full audio format specification"); - return nullptr; - } - - const char *host = require_block_string(param, "host"); - const char *mount = require_block_string(param, "mount"); - unsigned port = param.GetBlockValue("port", 0u); - if (port == 0) { - error.Set(config_domain, "shout port must be configured"); - return false; - } - - const char *passwd = require_block_string(param, "password"); - const char *name = require_block_string(param, "name"); - - bool is_public = param.GetBlockValue("public", false); - - const char *user = param.GetBlockValue("user", "source"); - - const char *value = param.GetBlockValue("quality"); - if (value != nullptr) { - char *test; - quality = strtod(value, &test); - - if (*test != '\0' || quality < -1.0 || quality > 10.0) { - error.Format(config_domain, - "shout quality \"%s\" is not a number in the " - "range -1 to 10", - value); - return false; - } - - if (param.GetBlockValue("bitrate") != nullptr) { - error.Set(config_domain, - "quality and bitrate are " - "both defined"); - return false; - } - } else { - value = param.GetBlockValue("bitrate"); - if (value == nullptr) { - error.Set(config_domain, - "neither bitrate nor quality defined"); - return false; - } - - char *test; - bitrate = strtol(value, &test, 10); - - if (*test != '\0' || bitrate <= 0) { - error.Set(config_domain, - "bitrate must be a positive integer"); - return false; - } - } - - const char *encoding = param.GetBlockValue("encoding", "ogg"); - const auto encoder_plugin = shout_encoder_plugin_get(encoding); - if (encoder_plugin == nullptr) { - error.Format(config_domain, - "couldn't find shout encoder plugin \"%s\"", - encoding); - return false; - } - - encoder = encoder_init(*encoder_plugin, param, error); - if (encoder == nullptr) - return false; - - unsigned shout_format; - if (strcmp(encoding, "mp3") == 0 || strcmp(encoding, "lame") == 0) - shout_format = SHOUT_FORMAT_MP3; - else - shout_format = SHOUT_FORMAT_OGG; - - unsigned protocol; - value = param.GetBlockValue("protocol"); - if (value != nullptr) { - if (0 == strcmp(value, "shoutcast") && - 0 != strcmp(encoding, "mp3")) { - error.Format(config_domain, - "you cannot stream \"%s\" to shoutcast, use mp3", - encoding); - return false; - } else if (0 == strcmp(value, "shoutcast")) - protocol = SHOUT_PROTOCOL_ICY; - else if (0 == strcmp(value, "icecast1")) - protocol = SHOUT_PROTOCOL_XAUDIOCAST; - else if (0 == strcmp(value, "icecast2")) - protocol = SHOUT_PROTOCOL_HTTP; - else { - error.Format(config_domain, - "shout protocol \"%s\" is not \"shoutcast\" or " - "\"icecast1\"or \"icecast2\"", - value); - return false; - } - } else { - protocol = SHOUT_PROTOCOL_HTTP; - } - - if (shout_set_host(shout_conn, host) != SHOUTERR_SUCCESS || - shout_set_port(shout_conn, port) != SHOUTERR_SUCCESS || - shout_set_password(shout_conn, passwd) != SHOUTERR_SUCCESS || - shout_set_mount(shout_conn, mount) != SHOUTERR_SUCCESS || - shout_set_name(shout_conn, name) != SHOUTERR_SUCCESS || - shout_set_user(shout_conn, user) != SHOUTERR_SUCCESS || - shout_set_public(shout_conn, is_public) != SHOUTERR_SUCCESS || - shout_set_format(shout_conn, shout_format) - != SHOUTERR_SUCCESS || - shout_set_protocol(shout_conn, protocol) != SHOUTERR_SUCCESS || - shout_set_agent(shout_conn, "MPD") != SHOUTERR_SUCCESS) { - error.Set(shout_output_domain, shout_get_error(shout_conn)); - return false; - } - - /* optional paramters */ - timeout = param.GetBlockValue("timeout", DEFAULT_CONN_TIMEOUT); - - value = param.GetBlockValue("genre"); - if (value != nullptr && shout_set_genre(shout_conn, value)) { - error.Set(shout_output_domain, shout_get_error(shout_conn)); - return false; - } - - value = param.GetBlockValue("description"); - if (value != nullptr && shout_set_description(shout_conn, value)) { - error.Set(shout_output_domain, shout_get_error(shout_conn)); - return false; - } - - value = param.GetBlockValue("url"); - if (value != nullptr && shout_set_url(shout_conn, value)) { - error.Set(shout_output_domain, shout_get_error(shout_conn)); - return false; - } - - { - char temp[11]; - memset(temp, 0, sizeof(temp)); - - snprintf(temp, sizeof(temp), "%u", audio_format.channels); - shout_set_audio_info(shout_conn, SHOUT_AI_CHANNELS, temp); - - snprintf(temp, sizeof(temp), "%u", audio_format.sample_rate); - - shout_set_audio_info(shout_conn, SHOUT_AI_SAMPLERATE, temp); - - if (quality >= -1.0) { - snprintf(temp, sizeof(temp), "%2.2f", quality); - shout_set_audio_info(shout_conn, SHOUT_AI_QUALITY, - temp); - } else { - snprintf(temp, sizeof(temp), "%d", bitrate); - shout_set_audio_info(shout_conn, SHOUT_AI_BITRATE, - temp); - } - } - - return true; -} - -static struct audio_output * -my_shout_init_driver(const config_param ¶m, Error &error) -{ - ShoutOutput *sd = new ShoutOutput(); - if (!sd->Initialize(param, error)) { - delete sd; - return nullptr; - } - - if (!sd->Configure(param, error)) { - sd->Deinitialize(); - delete sd; - return nullptr; - } - - if (shout_init_count == 0) - shout_init(); - - shout_init_count++; - - return &sd->base; -} - -static bool -handle_shout_error(ShoutOutput *sd, int err, Error &error) -{ - switch (err) { - case SHOUTERR_SUCCESS: - break; - - case SHOUTERR_UNCONNECTED: - case SHOUTERR_SOCKET: - error.Format(shout_output_domain, err, - "Lost shout connection to %s:%i: %s", - shout_get_host(sd->shout_conn), - shout_get_port(sd->shout_conn), - shout_get_error(sd->shout_conn)); - return false; - - default: - error.Format(shout_output_domain, err, - "connection to %s:%i error: %s", - shout_get_host(sd->shout_conn), - shout_get_port(sd->shout_conn), - shout_get_error(sd->shout_conn)); - return false; - } - - return true; -} - -static bool -write_page(ShoutOutput *sd, Error &error) -{ - assert(sd->encoder != nullptr); - - while (true) { - size_t nbytes = encoder_read(sd->encoder, - sd->buffer, sizeof(sd->buffer)); - if (nbytes == 0) - return true; - - int err = shout_send(sd->shout_conn, sd->buffer, nbytes); - if (!handle_shout_error(sd, err, error)) - return false; - } - - return true; -} - -static void close_shout_conn(ShoutOutput * sd) -{ - if (sd->encoder != nullptr) { - if (encoder_end(sd->encoder, IgnoreError())) - write_page(sd, IgnoreError()); - - encoder_close(sd->encoder); - } - - if (shout_get_connected(sd->shout_conn) != SHOUTERR_UNCONNECTED && - shout_close(sd->shout_conn) != SHOUTERR_SUCCESS) { - FormatWarning(shout_output_domain, - "problem closing connection to shout server: %s", - shout_get_error(sd->shout_conn)); - } -} - -static void -my_shout_finish_driver(struct audio_output *ao) -{ - ShoutOutput *sd = (ShoutOutput *)ao; - - encoder_finish(sd->encoder); - - sd->Deinitialize(); - delete sd; - - shout_init_count--; - - if (shout_init_count == 0) - shout_shutdown(); -} - -static void -my_shout_drop_buffered_audio(struct audio_output *ao) -{ - gcc_unused - ShoutOutput *sd = (ShoutOutput *)ao; - - /* needs to be implemented for shout */ -} - -static void -my_shout_close_device(struct audio_output *ao) -{ - ShoutOutput *sd = (ShoutOutput *)ao; - - close_shout_conn(sd); -} - -static bool -shout_connect(ShoutOutput *sd, Error &error) -{ - switch (shout_open(sd->shout_conn)) { - case SHOUTERR_SUCCESS: - case SHOUTERR_CONNECTED: - return true; - - default: - error.Format(shout_output_domain, - "problem opening connection to shout server %s:%i: %s", - shout_get_host(sd->shout_conn), - shout_get_port(sd->shout_conn), - shout_get_error(sd->shout_conn)); - return false; - } -} - -static bool -my_shout_open_device(struct audio_output *ao, AudioFormat &audio_format, - Error &error) -{ - ShoutOutput *sd = (ShoutOutput *)ao; - - if (!shout_connect(sd, error)) - return false; - - if (!encoder_open(sd->encoder, audio_format, error)) { - shout_close(sd->shout_conn); - return false; - } - - if (!write_page(sd, error)) { - encoder_close(sd->encoder); - shout_close(sd->shout_conn); - return false; - } - - return true; -} - -static unsigned -my_shout_delay(struct audio_output *ao) -{ - ShoutOutput *sd = (ShoutOutput *)ao; - - int delay = shout_delay(sd->shout_conn); - if (delay < 0) - delay = 0; - - return delay; -} - -static size_t -my_shout_play(struct audio_output *ao, const void *chunk, size_t size, - Error &error) -{ - ShoutOutput *sd = (ShoutOutput *)ao; - - return encoder_write(sd->encoder, chunk, size, error) && - write_page(sd, error) - ? size - : 0; -} - -static bool -my_shout_pause(struct audio_output *ao) -{ - static char silence[1020]; - - return my_shout_play(ao, silence, sizeof(silence), IgnoreError()); -} - -static void -shout_tag_to_metadata(const Tag *tag, char *dest, size_t size) -{ - char artist[size]; - char title[size]; - - artist[0] = 0; - title[0] = 0; - - for (unsigned i = 0; i < tag->num_items; i++) { - switch (tag->items[i]->type) { - case TAG_ARTIST: - strncpy(artist, tag->items[i]->value, size); - break; - case TAG_TITLE: - strncpy(title, tag->items[i]->value, size); - break; - - default: - break; - } - } - - snprintf(dest, size, "%s - %s", artist, title); -} - -static void my_shout_set_tag(struct audio_output *ao, - const Tag *tag) -{ - ShoutOutput *sd = (ShoutOutput *)ao; - - if (sd->encoder->plugin.tag != nullptr) { - /* encoder plugin supports stream tags */ - - Error error; - if (!encoder_pre_tag(sd->encoder, error) || - !write_page(sd, error) || - !encoder_tag(sd->encoder, tag, error)) { - LogError(error); - return; - } - } else { - /* no stream tag support: fall back to icy-metadata */ - char song[1024]; - shout_tag_to_metadata(tag, song, sizeof(song)); - - shout_metadata_add(sd->shout_meta, "song", song); - if (SHOUTERR_SUCCESS != shout_set_metadata(sd->shout_conn, - sd->shout_meta)) { - LogWarning(shout_output_domain, - "error setting shout metadata"); - } - } - - write_page(sd, IgnoreError()); -} - -const struct audio_output_plugin shout_output_plugin = { - "shout", - nullptr, - my_shout_init_driver, - my_shout_finish_driver, - nullptr, - nullptr, - my_shout_open_device, - my_shout_close_device, - my_shout_delay, - my_shout_set_tag, - my_shout_play, - nullptr, - my_shout_drop_buffered_audio, - my_shout_pause, - nullptr, -}; diff --git a/src/output/ShoutOutputPlugin.hxx b/src/output/ShoutOutputPlugin.hxx deleted file mode 100644 index d437e0b0d..000000000 --- a/src/output/ShoutOutputPlugin.hxx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_SHOUT_OUTPUT_PLUGIN_HXX -#define MPD_SHOUT_OUTPUT_PLUGIN_HXX - -extern const struct audio_output_plugin shout_output_plugin; - -#endif diff --git a/src/output/SolarisOutputPlugin.cxx b/src/output/SolarisOutputPlugin.cxx deleted file mode 100644 index c06c848d1..000000000 --- a/src/output/SolarisOutputPlugin.cxx +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "config.h" -#include "SolarisOutputPlugin.hxx" -#include "OutputAPI.hxx" -#include "system/fd_util.h" -#include "util/Error.hxx" - -#include -#include -#include -#include -#include -#include - -#ifdef __sun -#include -#else - -/* some fake declarations that allow build this plugin on systems - other than Solaris, just to see if it compiles */ - -#define AUDIO_GETINFO 0 -#define AUDIO_SETINFO 0 -#define AUDIO_ENCODING_LINEAR 0 - -struct audio_info { - struct { - unsigned sample_rate, channels, precision, encoding; - } play; -}; - -#endif - -struct SolarisOutput { - struct audio_output base; - - /* configuration */ - const char *device; - - int fd; - - bool Initialize(const config_param ¶m, Error &error_r) { - return ao_base_init(&base, &solaris_output_plugin, param, - error_r); - } - - void Deinitialize() { - ao_base_finish(&base); - } -}; - -static bool -solaris_output_test_default_device(void) -{ - struct stat st; - - return stat("/dev/audio", &st) == 0 && S_ISCHR(st.st_mode) && - access("/dev/audio", W_OK) == 0; -} - -static struct audio_output * -solaris_output_init(const config_param ¶m, Error &error_r) -{ - SolarisOutput *so = new SolarisOutput(); - if (!so->Initialize(param, error_r)) { - delete so; - return nullptr; - } - - so->device = param.GetBlockValue("device", "/dev/audio"); - - return &so->base; -} - -static void -solaris_output_finish(struct audio_output *ao) -{ - SolarisOutput *so = (SolarisOutput *)ao; - - so->Deinitialize(); - delete so; -} - -static bool -solaris_output_open(struct audio_output *ao, AudioFormat &audio_format, - Error &error) -{ - SolarisOutput *so = (SolarisOutput *)ao; - struct audio_info info; - int ret, flags; - - /* support only 16 bit mono/stereo for now; nothing else has - been tested */ - audio_format.format = SampleFormat::S16; - - /* open the device in non-blocking mode */ - - so->fd = open_cloexec(so->device, O_WRONLY|O_NONBLOCK, 0); - if (so->fd < 0) { - error.FormatErrno("Failed to open %s", - so->device); - return false; - } - - /* restore blocking mode */ - - flags = fcntl(so->fd, F_GETFL); - if (flags > 0 && (flags & O_NONBLOCK) != 0) - fcntl(so->fd, F_SETFL, flags & ~O_NONBLOCK); - - /* configure the audio device */ - - ret = ioctl(so->fd, AUDIO_GETINFO, &info); - if (ret < 0) { - error.SetErrno("AUDIO_GETINFO failed"); - close(so->fd); - return false; - } - - info.play.sample_rate = audio_format.sample_rate; - info.play.channels = audio_format.channels; - info.play.precision = 16; - info.play.encoding = AUDIO_ENCODING_LINEAR; - - ret = ioctl(so->fd, AUDIO_SETINFO, &info); - if (ret < 0) { - error.SetErrno("AUDIO_SETINFO failed"); - close(so->fd); - return false; - } - - return true; -} - -static void -solaris_output_close(struct audio_output *ao) -{ - SolarisOutput *so = (SolarisOutput *)ao; - - close(so->fd); -} - -static size_t -solaris_output_play(struct audio_output *ao, const void *chunk, size_t size, - Error &error) -{ - SolarisOutput *so = (SolarisOutput *)ao; - ssize_t nbytes; - - nbytes = write(so->fd, chunk, size); - if (nbytes <= 0) { - error.SetErrno("Write failed"); - return 0; - } - - return nbytes; -} - -static void -solaris_output_cancel(struct audio_output *ao) -{ - SolarisOutput *so = (SolarisOutput *)ao; - - ioctl(so->fd, I_FLUSH); -} - -const struct audio_output_plugin solaris_output_plugin = { - "solaris", - solaris_output_test_default_device, - solaris_output_init, - solaris_output_finish, - nullptr, - nullptr, - solaris_output_open, - solaris_output_close, - nullptr, - nullptr, - solaris_output_play, - nullptr, - solaris_output_cancel, - nullptr, - nullptr, -}; diff --git a/src/output/SolarisOutputPlugin.hxx b/src/output/SolarisOutputPlugin.hxx deleted file mode 100644 index 9ce848a40..000000000 --- a/src/output/SolarisOutputPlugin.hxx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_SOLARIS_OUTPUT_PLUGIN_HXX -#define MPD_SOLARIS_OUTPUT_PLUGIN_HXX - -extern const struct audio_output_plugin solaris_output_plugin; - -#endif diff --git a/src/output/WinmmOutputPlugin.cxx b/src/output/WinmmOutputPlugin.cxx deleted file mode 100644 index bc4ca9386..000000000 --- a/src/output/WinmmOutputPlugin.cxx +++ /dev/null @@ -1,353 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "config.h" -#include "WinmmOutputPlugin.hxx" -#include "OutputAPI.hxx" -#include "pcm/PcmBuffer.hxx" -#include "MixerList.hxx" -#include "util/Error.hxx" -#include "util/Domain.hxx" -#include "util/Macros.hxx" - -#include - -#include -#include - -struct WinmmBuffer { - PcmBuffer buffer; - - WAVEHDR hdr; -}; - -struct WinmmOutput { - struct audio_output base; - - UINT device_id; - HWAVEOUT handle; - - /** - * This event is triggered by Windows when a buffer is - * finished. - */ - HANDLE event; - - WinmmBuffer buffers[8]; - unsigned next_buffer; -}; - -static constexpr Domain winmm_output_domain("winmm_output"); - -HWAVEOUT -winmm_output_get_handle(WinmmOutput *output) -{ - return output->handle; -} - -static bool -winmm_output_test_default_device(void) -{ - return waveOutGetNumDevs() > 0; -} - -static bool -get_device_id(const char *device_name, UINT *device_id, Error &error) -{ - /* if device is not specified use wave mapper */ - if (device_name == nullptr) { - *device_id = WAVE_MAPPER; - return true; - } - - UINT numdevs = waveOutGetNumDevs(); - - /* check for device id */ - char *endptr; - UINT id = strtoul(device_name, &endptr, 0); - if (endptr > device_name && *endptr == 0) { - if (id >= numdevs) - goto fail; - *device_id = id; - return true; - } - - /* check for device name */ - for (UINT i = 0; i < numdevs; i++) { - WAVEOUTCAPS caps; - MMRESULT result = waveOutGetDevCaps(i, &caps, sizeof(caps)); - if (result != MMSYSERR_NOERROR) - continue; - /* szPname is only 32 chars long, so it is often truncated. - Use partial match to work around this. */ - if (strstr(device_name, caps.szPname) == device_name) { - *device_id = i; - return true; - } - } - -fail: - error.Format(winmm_output_domain, - "device \"%s\" is not found", device_name); - return false; -} - -static struct audio_output * -winmm_output_init(const config_param ¶m, Error &error) -{ - WinmmOutput *wo = new WinmmOutput(); - if (!ao_base_init(&wo->base, &winmm_output_plugin, param, error)) { - delete wo; - return nullptr; - } - - const char *device = param.GetBlockValue("device"); - if (!get_device_id(device, &wo->device_id, error)) { - ao_base_finish(&wo->base); - delete wo; - return nullptr; - } - - return &wo->base; -} - -static void -winmm_output_finish(struct audio_output *ao) -{ - WinmmOutput *wo = (WinmmOutput *)ao; - - ao_base_finish(&wo->base); - delete wo; -} - -static bool -winmm_output_open(struct audio_output *ao, AudioFormat &audio_format, - Error &error) -{ - WinmmOutput *wo = (WinmmOutput *)ao; - - wo->event = CreateEvent(nullptr, false, false, nullptr); - if (wo->event == nullptr) { - error.Set(winmm_output_domain, "CreateEvent() failed"); - return false; - } - - switch (audio_format.format) { - case SampleFormat::S8: - case SampleFormat::S16: - break; - - case SampleFormat::S24_P32: - case SampleFormat::S32: - case SampleFormat::FLOAT: - case SampleFormat::DSD: - case SampleFormat::UNDEFINED: - /* we havn't tested formats other than S16 */ - audio_format.format = SampleFormat::S16; - break; - } - - if (audio_format.channels > 2) - /* same here: more than stereo was not tested */ - audio_format.channels = 2; - - WAVEFORMATEX format; - format.wFormatTag = WAVE_FORMAT_PCM; - format.nChannels = audio_format.channels; - format.nSamplesPerSec = audio_format.sample_rate; - format.nBlockAlign = audio_format.GetFrameSize(); - format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign; - format.wBitsPerSample = audio_format.GetSampleSize() * 8; - format.cbSize = 0; - - MMRESULT result = waveOutOpen(&wo->handle, wo->device_id, &format, - (DWORD_PTR)wo->event, 0, CALLBACK_EVENT); - if (result != MMSYSERR_NOERROR) { - CloseHandle(wo->event); - error.Set(winmm_output_domain, "waveOutOpen() failed"); - return false; - } - - for (unsigned i = 0; i < ARRAY_SIZE(wo->buffers); ++i) { - memset(&wo->buffers[i].hdr, 0, sizeof(wo->buffers[i].hdr)); - } - - wo->next_buffer = 0; - - return true; -} - -static void -winmm_output_close(struct audio_output *ao) -{ - WinmmOutput *wo = (WinmmOutput *)ao; - - for (unsigned i = 0; i < ARRAY_SIZE(wo->buffers); ++i) - wo->buffers[i].buffer.Clear(); - - waveOutClose(wo->handle); - - CloseHandle(wo->event); -} - -/** - * Copy data into a buffer, and prepare the wave header. - */ -static bool -winmm_set_buffer(WinmmOutput *wo, WinmmBuffer *buffer, - const void *data, size_t size, - Error &error) -{ - void *dest = buffer->buffer.Get(size); - assert(dest != nullptr); - - memcpy(dest, data, size); - - memset(&buffer->hdr, 0, sizeof(buffer->hdr)); - buffer->hdr.lpData = (LPSTR)dest; - buffer->hdr.dwBufferLength = size; - - MMRESULT result = waveOutPrepareHeader(wo->handle, &buffer->hdr, - sizeof(buffer->hdr)); - if (result != MMSYSERR_NOERROR) { - error.Set(winmm_output_domain, result, - "waveOutPrepareHeader() failed"); - return false; - } - - return true; -} - -/** - * Wait until the buffer is finished. - */ -static bool -winmm_drain_buffer(WinmmOutput *wo, WinmmBuffer *buffer, - Error &error) -{ - if ((buffer->hdr.dwFlags & WHDR_DONE) == WHDR_DONE) - /* already finished */ - return true; - - while (true) { - MMRESULT result = waveOutUnprepareHeader(wo->handle, - &buffer->hdr, - sizeof(buffer->hdr)); - if (result == MMSYSERR_NOERROR) - return true; - else if (result != WAVERR_STILLPLAYING) { - error.Set(winmm_output_domain, result, - "waveOutUnprepareHeader() failed"); - return false; - } - - /* wait some more */ - WaitForSingleObject(wo->event, INFINITE); - } -} - -static size_t -winmm_output_play(struct audio_output *ao, const void *chunk, size_t size, Error &error) -{ - WinmmOutput *wo = (WinmmOutput *)ao; - - /* get the next buffer from the ring and prepare it */ - WinmmBuffer *buffer = &wo->buffers[wo->next_buffer]; - if (!winmm_drain_buffer(wo, buffer, error) || - !winmm_set_buffer(wo, buffer, chunk, size, error)) - return 0; - - /* enqueue the buffer */ - MMRESULT result = waveOutWrite(wo->handle, &buffer->hdr, - sizeof(buffer->hdr)); - if (result != MMSYSERR_NOERROR) { - waveOutUnprepareHeader(wo->handle, &buffer->hdr, - sizeof(buffer->hdr)); - error.Set(winmm_output_domain, result, - "waveOutWrite() failed"); - return 0; - } - - /* mark our buffer as "used" */ - wo->next_buffer = (wo->next_buffer + 1) % - ARRAY_SIZE(wo->buffers); - - return size; -} - -static bool -winmm_drain_all_buffers(WinmmOutput *wo, Error &error) -{ - for (unsigned i = wo->next_buffer; i < ARRAY_SIZE(wo->buffers); ++i) - if (!winmm_drain_buffer(wo, &wo->buffers[i], error)) - return false; - - for (unsigned i = 0; i < wo->next_buffer; ++i) - if (!winmm_drain_buffer(wo, &wo->buffers[i], error)) - return false; - - return true; -} - -static void -winmm_stop(WinmmOutput *wo) -{ - waveOutReset(wo->handle); - - for (unsigned i = 0; i < ARRAY_SIZE(wo->buffers); ++i) { - WinmmBuffer *buffer = &wo->buffers[i]; - waveOutUnprepareHeader(wo->handle, &buffer->hdr, - sizeof(buffer->hdr)); - } -} - -static void -winmm_output_drain(struct audio_output *ao) -{ - WinmmOutput *wo = (WinmmOutput *)ao; - - if (!winmm_drain_all_buffers(wo, IgnoreError())) - winmm_stop(wo); -} - -static void -winmm_output_cancel(struct audio_output *ao) -{ - WinmmOutput *wo = (WinmmOutput *)ao; - - winmm_stop(wo); -} - -const struct audio_output_plugin winmm_output_plugin = { - "winmm", - winmm_output_test_default_device, - winmm_output_init, - winmm_output_finish, - nullptr, - nullptr, - winmm_output_open, - winmm_output_close, - nullptr, - nullptr, - winmm_output_play, - winmm_output_drain, - winmm_output_cancel, - nullptr, - &winmm_mixer_plugin, -}; diff --git a/src/output/WinmmOutputPlugin.hxx b/src/output/WinmmOutputPlugin.hxx deleted file mode 100644 index 1409a2e8c..000000000 --- a/src/output/WinmmOutputPlugin.hxx +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2003-2014 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_WINMM_OUTPUT_PLUGIN_HXX -#define MPD_WINMM_OUTPUT_PLUGIN_HXX - -#include "check.h" - -#ifdef ENABLE_WINMM_OUTPUT - -#include "Compiler.h" - -#include -#include - -struct WinmmOutput; - -extern const struct audio_output_plugin winmm_output_plugin; - -gcc_pure -HWAVEOUT -winmm_output_get_handle(WinmmOutput *); - -#endif - -#endif diff --git a/src/output/plugins/AlsaOutputPlugin.cxx b/src/output/plugins/AlsaOutputPlugin.cxx new file mode 100644 index 000000000..f2e4fc643 --- /dev/null +++ b/src/output/plugins/AlsaOutputPlugin.cxx @@ -0,0 +1,868 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "AlsaOutputPlugin.hxx" +#include "../OutputAPI.hxx" +#include "MixerList.hxx" +#include "pcm/PcmExport.hxx" +#include "util/Manual.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "Log.hxx" + +#include + +#include + +#define ALSA_PCM_NEW_HW_PARAMS_API +#define ALSA_PCM_NEW_SW_PARAMS_API + +static const char default_device[] = "default"; + +static constexpr unsigned MPD_ALSA_BUFFER_TIME_US = 500000; + +#define MPD_ALSA_RETRY_NR 5 + +typedef snd_pcm_sframes_t alsa_writei_t(snd_pcm_t * pcm, const void *buffer, + snd_pcm_uframes_t size); + +struct AlsaOutput { + struct audio_output base; + + Manual pcm_export; + + /** + * The configured name of the ALSA device; empty for the + * default device + */ + std::string device; + + /** use memory mapped I/O? */ + bool use_mmap; + + /** + * Enable DSD over USB according to the dCS suggested + * standard? + * + * @see http://www.dcsltd.co.uk/page/assets/DSDoverUSB.pdf + */ + bool dsd_usb; + + /** libasound's buffer_time setting (in microseconds) */ + unsigned int buffer_time; + + /** libasound's period_time setting (in microseconds) */ + unsigned int period_time; + + /** the mode flags passed to snd_pcm_open */ + int mode; + + /** the libasound PCM device handle */ + snd_pcm_t *pcm; + + /** + * a pointer to the libasound writei() function, which is + * snd_pcm_writei() or snd_pcm_mmap_writei(), depending on the + * use_mmap configuration + */ + alsa_writei_t *writei; + + /** + * The size of one audio frame passed to method play(). + */ + size_t in_frame_size; + + /** + * The size of one audio frame passed to libasound. + */ + size_t out_frame_size; + + /** + * The size of one period, in number of frames. + */ + snd_pcm_uframes_t period_frames; + + /** + * The number of frames written in the current period. + */ + snd_pcm_uframes_t period_position; + + /** + * Set to non-zero when the Raspberry Pi workaround has been + * activated in alsa_recover(); decremented by each write. + * This will avoid activating it again, leading to an endless + * loop. This problem was observed with a "RME Digi9636/52". + */ + unsigned pi_workaround; + + /** + * This buffer gets allocated after opening the ALSA device. + * It contains silence samples, enough to fill one period (see + * #period_frames). + */ + uint8_t *silence; + + AlsaOutput():mode(0), writei(snd_pcm_writei) { + } + + bool Init(const config_param ¶m, Error &error) { + return ao_base_init(&base, &alsa_output_plugin, + param, error); + } + + void Deinit() { + ao_base_finish(&base); + } +}; + +static constexpr Domain alsa_output_domain("alsa_output"); + +static const char * +alsa_device(const AlsaOutput *ad) +{ + return ad->device.empty() ? default_device : ad->device.c_str(); +} + +static void +alsa_configure(AlsaOutput *ad, const config_param ¶m) +{ + ad->device = param.GetBlockValue("device", ""); + + ad->use_mmap = param.GetBlockValue("use_mmap", false); + + ad->dsd_usb = param.GetBlockValue("dsd_usb", false); + + ad->buffer_time = param.GetBlockValue("buffer_time", + MPD_ALSA_BUFFER_TIME_US); + ad->period_time = param.GetBlockValue("period_time", 0u); + +#ifdef SND_PCM_NO_AUTO_RESAMPLE + if (!param.GetBlockValue("auto_resample", true)) + ad->mode |= SND_PCM_NO_AUTO_RESAMPLE; +#endif + +#ifdef SND_PCM_NO_AUTO_CHANNELS + if (!param.GetBlockValue("auto_channels", true)) + ad->mode |= SND_PCM_NO_AUTO_CHANNELS; +#endif + +#ifdef SND_PCM_NO_AUTO_FORMAT + if (!param.GetBlockValue("auto_format", true)) + ad->mode |= SND_PCM_NO_AUTO_FORMAT; +#endif +} + +static struct audio_output * +alsa_init(const config_param ¶m, Error &error) +{ + AlsaOutput *ad = new AlsaOutput(); + + if (!ad->Init(param, error)) { + delete ad; + return nullptr; + } + + alsa_configure(ad, param); + + return &ad->base; +} + +static void +alsa_finish(struct audio_output *ao) +{ + AlsaOutput *ad = (AlsaOutput *)ao; + + ad->Deinit(); + delete ad; + + /* free libasound's config cache */ + snd_config_update_free_global(); +} + +static bool +alsa_output_enable(struct audio_output *ao, gcc_unused Error &error) +{ + AlsaOutput *ad = (AlsaOutput *)ao; + + ad->pcm_export.Construct(); + return true; +} + +static void +alsa_output_disable(struct audio_output *ao) +{ + AlsaOutput *ad = (AlsaOutput *)ao; + + ad->pcm_export.Destruct(); +} + +static bool +alsa_test_default_device(void) +{ + snd_pcm_t *handle; + + int ret = snd_pcm_open(&handle, default_device, + SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); + if (ret) { + FormatError(alsa_output_domain, + "Error opening default ALSA device: %s", + snd_strerror(-ret)); + return false; + } else + snd_pcm_close(handle); + + return true; +} + +static snd_pcm_format_t +get_bitformat(SampleFormat sample_format) +{ + switch (sample_format) { + case SampleFormat::UNDEFINED: + case SampleFormat::DSD: + return SND_PCM_FORMAT_UNKNOWN; + + case SampleFormat::S8: + return SND_PCM_FORMAT_S8; + + case SampleFormat::S16: + return SND_PCM_FORMAT_S16; + + case SampleFormat::S24_P32: + return SND_PCM_FORMAT_S24; + + case SampleFormat::S32: + return SND_PCM_FORMAT_S32; + + case SampleFormat::FLOAT: + return SND_PCM_FORMAT_FLOAT; + } + + assert(false); + gcc_unreachable(); +} + +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; + } +} + +static snd_pcm_format_t +alsa_to_packed_format(snd_pcm_format_t fmt) +{ + switch (fmt) { + case SND_PCM_FORMAT_S24_LE: + return SND_PCM_FORMAT_S24_3LE; + + case SND_PCM_FORMAT_S24_BE: + return SND_PCM_FORMAT_S24_3BE; + + default: + return SND_PCM_FORMAT_UNKNOWN; + } +} + +static int +alsa_try_format_or_packed(snd_pcm_t *pcm, snd_pcm_hw_params_t *hwparams, + snd_pcm_format_t fmt, bool *packed_r) +{ + int err = snd_pcm_hw_params_set_format(pcm, hwparams, fmt); + if (err == 0) + *packed_r = false; + + if (err != -EINVAL) + return err; + + fmt = alsa_to_packed_format(fmt); + if (fmt == SND_PCM_FORMAT_UNKNOWN) + return -EINVAL; + + err = snd_pcm_hw_params_set_format(pcm, hwparams, fmt); + if (err == 0) + *packed_r = true; + + return err; +} + +/** + * Attempts to configure the specified sample format, and tries the + * reversed host byte order if was not supported. + */ +static int +alsa_output_try_format(snd_pcm_t *pcm, snd_pcm_hw_params_t *hwparams, + SampleFormat sample_format, + bool *packed_r, bool *reverse_endian_r) +{ + snd_pcm_format_t alsa_format = get_bitformat(sample_format); + if (alsa_format == SND_PCM_FORMAT_UNKNOWN) + return -EINVAL; + + int err = alsa_try_format_or_packed(pcm, hwparams, alsa_format, + packed_r); + if (err == 0) + *reverse_endian_r = false; + + if (err != -EINVAL) + return err; + + alsa_format = byteswap_bitformat(alsa_format); + if (alsa_format == SND_PCM_FORMAT_UNKNOWN) + return -EINVAL; + + err = alsa_try_format_or_packed(pcm, hwparams, alsa_format, packed_r); + if (err == 0) + *reverse_endian_r = true; + + return err; +} + +/** + * Configure a sample format, and probe other formats if that fails. + */ +static int +alsa_output_setup_format(snd_pcm_t *pcm, snd_pcm_hw_params_t *hwparams, + AudioFormat &audio_format, + bool *packed_r, bool *reverse_endian_r) +{ + /* try the input format first */ + + int err = alsa_output_try_format(pcm, hwparams, + audio_format.format, + packed_r, reverse_endian_r); + + /* if unsupported by the hardware, try other formats */ + + static const SampleFormat probe_formats[] = { + SampleFormat::S24_P32, + SampleFormat::S32, + SampleFormat::S16, + SampleFormat::S8, + SampleFormat::UNDEFINED, + }; + + for (unsigned i = 0; + err == -EINVAL && probe_formats[i] != SampleFormat::UNDEFINED; + ++i) { + const SampleFormat mpd_format = probe_formats[i]; + if (mpd_format == audio_format.format) + continue; + + err = alsa_output_try_format(pcm, hwparams, mpd_format, + packed_r, reverse_endian_r); + if (err == 0) + audio_format.format = mpd_format; + } + + return err; +} + +/** + * Set up the snd_pcm_t object which was opened by the caller. Set up + * the configured settings and the audio format. + */ +static bool +alsa_setup(AlsaOutput *ad, AudioFormat &audio_format, + bool *packed_r, bool *reverse_endian_r, Error &error) +{ + unsigned int sample_rate = audio_format.sample_rate; + unsigned int channels = audio_format.channels; + int err; + const char *cmd = nullptr; + int retry = MPD_ALSA_RETRY_NR; + unsigned int period_time, period_time_ro; + unsigned int buffer_time; + + period_time_ro = period_time = ad->period_time; +configure_hw: + /* configure HW params */ + snd_pcm_hw_params_t *hwparams; + snd_pcm_hw_params_alloca(&hwparams); + cmd = "snd_pcm_hw_params_any"; + err = snd_pcm_hw_params_any(ad->pcm, hwparams); + if (err < 0) + goto error; + + if (ad->use_mmap) { + err = snd_pcm_hw_params_set_access(ad->pcm, hwparams, + SND_PCM_ACCESS_MMAP_INTERLEAVED); + if (err < 0) { + FormatWarning(alsa_output_domain, + "Cannot set mmap'ed mode on ALSA device \"%s\": %s", + alsa_device(ad), snd_strerror(-err)); + LogWarning(alsa_output_domain, + "Falling back to direct write mode"); + ad->use_mmap = false; + } else + ad->writei = snd_pcm_mmap_writei; + } + + if (!ad->use_mmap) { + cmd = "snd_pcm_hw_params_set_access"; + err = snd_pcm_hw_params_set_access(ad->pcm, hwparams, + SND_PCM_ACCESS_RW_INTERLEAVED); + if (err < 0) + goto error; + ad->writei = snd_pcm_writei; + } + + err = alsa_output_setup_format(ad->pcm, hwparams, audio_format, + packed_r, reverse_endian_r); + if (err < 0) { + error.Format(alsa_output_domain, err, + "ALSA device \"%s\" does not support format %s: %s", + alsa_device(ad), + sample_format_to_string(audio_format.format), + snd_strerror(-err)); + return false; + } + + snd_pcm_format_t format; + if (snd_pcm_hw_params_get_format(hwparams, &format) == 0) + FormatDebug(alsa_output_domain, + "format=%s (%s)", snd_pcm_format_name(format), + snd_pcm_format_description(format)); + + err = snd_pcm_hw_params_set_channels_near(ad->pcm, hwparams, + &channels); + if (err < 0) { + error.Format(alsa_output_domain, err, + "ALSA device \"%s\" does not support %i channels: %s", + alsa_device(ad), (int)audio_format.channels, + snd_strerror(-err)); + return false; + } + audio_format.channels = (int8_t)channels; + + err = snd_pcm_hw_params_set_rate_near(ad->pcm, hwparams, + &sample_rate, nullptr); + if (err < 0 || sample_rate == 0) { + error.Format(alsa_output_domain, err, + "ALSA device \"%s\" does not support %u Hz audio", + alsa_device(ad), audio_format.sample_rate); + return false; + } + audio_format.sample_rate = sample_rate; + + snd_pcm_uframes_t buffer_size_min, buffer_size_max; + snd_pcm_hw_params_get_buffer_size_min(hwparams, &buffer_size_min); + snd_pcm_hw_params_get_buffer_size_max(hwparams, &buffer_size_max); + unsigned buffer_time_min, buffer_time_max; + snd_pcm_hw_params_get_buffer_time_min(hwparams, &buffer_time_min, 0); + snd_pcm_hw_params_get_buffer_time_max(hwparams, &buffer_time_max, 0); + FormatDebug(alsa_output_domain, "buffer: size=%u..%u time=%u..%u", + (unsigned)buffer_size_min, (unsigned)buffer_size_max, + buffer_time_min, buffer_time_max); + + snd_pcm_uframes_t period_size_min, period_size_max; + snd_pcm_hw_params_get_period_size_min(hwparams, &period_size_min, 0); + snd_pcm_hw_params_get_period_size_max(hwparams, &period_size_max, 0); + unsigned period_time_min, period_time_max; + snd_pcm_hw_params_get_period_time_min(hwparams, &period_time_min, 0); + snd_pcm_hw_params_get_period_time_max(hwparams, &period_time_max, 0); + FormatDebug(alsa_output_domain, "period: size=%u..%u time=%u..%u", + (unsigned)period_size_min, (unsigned)period_size_max, + period_time_min, period_time_max); + + if (ad->buffer_time > 0) { + buffer_time = ad->buffer_time; + cmd = "snd_pcm_hw_params_set_buffer_time_near"; + err = snd_pcm_hw_params_set_buffer_time_near(ad->pcm, hwparams, + &buffer_time, nullptr); + if (err < 0) + goto error; + } else { + err = snd_pcm_hw_params_get_buffer_time(hwparams, &buffer_time, + nullptr); + if (err < 0) + buffer_time = 0; + } + + if (period_time_ro == 0 && buffer_time >= 10000) { + period_time_ro = period_time = buffer_time / 4; + + FormatDebug(alsa_output_domain, + "default period_time = buffer_time/4 = %u/4 = %u", + buffer_time, period_time); + } + + if (period_time_ro > 0) { + period_time = period_time_ro; + cmd = "snd_pcm_hw_params_set_period_time_near"; + err = snd_pcm_hw_params_set_period_time_near(ad->pcm, hwparams, + &period_time, nullptr); + if (err < 0) + goto error; + } + + cmd = "snd_pcm_hw_params"; + err = snd_pcm_hw_params(ad->pcm, hwparams); + if (err == -EPIPE && --retry > 0 && period_time_ro > 0) { + period_time_ro = period_time_ro >> 1; + goto configure_hw; + } else if (err < 0) + goto error; + if (retry != MPD_ALSA_RETRY_NR) + FormatDebug(alsa_output_domain, + "ALSA period_time set to %d", period_time); + + snd_pcm_uframes_t alsa_buffer_size; + cmd = "snd_pcm_hw_params_get_buffer_size"; + err = snd_pcm_hw_params_get_buffer_size(hwparams, &alsa_buffer_size); + if (err < 0) + goto error; + + snd_pcm_uframes_t alsa_period_size; + cmd = "snd_pcm_hw_params_get_period_size"; + err = snd_pcm_hw_params_get_period_size(hwparams, &alsa_period_size, + nullptr); + if (err < 0) + goto error; + + /* configure SW params */ + snd_pcm_sw_params_t *swparams; + snd_pcm_sw_params_alloca(&swparams); + + cmd = "snd_pcm_sw_params_current"; + err = snd_pcm_sw_params_current(ad->pcm, swparams); + if (err < 0) + goto error; + + cmd = "snd_pcm_sw_params_set_start_threshold"; + err = snd_pcm_sw_params_set_start_threshold(ad->pcm, swparams, + alsa_buffer_size - + alsa_period_size); + if (err < 0) + goto error; + + cmd = "snd_pcm_sw_params_set_avail_min"; + err = snd_pcm_sw_params_set_avail_min(ad->pcm, swparams, + alsa_period_size); + if (err < 0) + goto error; + + cmd = "snd_pcm_sw_params"; + err = snd_pcm_sw_params(ad->pcm, swparams); + if (err < 0) + goto error; + + FormatDebug(alsa_output_domain, "buffer_size=%u period_size=%u", + (unsigned)alsa_buffer_size, (unsigned)alsa_period_size); + + if (alsa_period_size == 0) + /* this works around a SIGFPE bug that occurred when + an ALSA driver indicated period_size==0; this + caused a division by zero in alsa_play(). By using + the fallback "1", we make sure that this won't + happen again. */ + alsa_period_size = 1; + + ad->period_frames = alsa_period_size; + ad->period_position = 0; + + ad->silence = new uint8_t[snd_pcm_frames_to_bytes(ad->pcm, + alsa_period_size)]; + snd_pcm_format_set_silence(format, ad->silence, + alsa_period_size * channels); + + return true; + +error: + error.Format(alsa_output_domain, err, + "Error opening ALSA device \"%s\" (%s): %s", + alsa_device(ad), cmd, snd_strerror(-err)); + return false; +} + +static bool +alsa_setup_dsd(AlsaOutput *ad, const AudioFormat audio_format, + bool *shift8_r, bool *packed_r, bool *reverse_endian_r, + Error &error) +{ + assert(ad->dsd_usb); + assert(audio_format.format == SampleFormat::DSD); + + /* pass 24 bit to alsa_setup() */ + + AudioFormat usb_format = audio_format; + usb_format.format = SampleFormat::S24_P32; + usb_format.sample_rate /= 2; + + const AudioFormat check = usb_format; + + if (!alsa_setup(ad, usb_format, packed_r, reverse_endian_r, error)) + return false; + + /* if the device allows only 32 bit, shift all DSD-over-USB + samples left by 8 bit and leave the lower 8 bit cleared; + the DSD-over-USB documentation does not specify whether + this is legal, but there is anecdotical evidence that this + is possible (and the only option for some devices) */ + *shift8_r = usb_format.format == SampleFormat::S32; + if (usb_format.format == SampleFormat::S32) + usb_format.format = SampleFormat::S24_P32; + + if (usb_format != check) { + /* no bit-perfect playback, which is required + for DSD over USB */ + error.Format(alsa_output_domain, + "Failed to configure DSD-over-USB on ALSA device \"%s\"", + alsa_device(ad)); + delete[] ad->silence; + return false; + } + + return true; +} + +static bool +alsa_setup_or_dsd(AlsaOutput *ad, AudioFormat &audio_format, + Error &error) +{ + bool shift8 = false, packed, reverse_endian; + + const bool dsd_usb = ad->dsd_usb && + audio_format.format == SampleFormat::DSD; + const bool success = dsd_usb + ? alsa_setup_dsd(ad, audio_format, + &shift8, &packed, &reverse_endian, + error) + : alsa_setup(ad, audio_format, &packed, &reverse_endian, + error); + if (!success) + return false; + + ad->pcm_export->Open(audio_format.format, + audio_format.channels, + dsd_usb, shift8, packed, reverse_endian); + return true; +} + +static bool +alsa_open(struct audio_output *ao, AudioFormat &audio_format, Error &error) +{ + AlsaOutput *ad = (AlsaOutput *)ao; + + ad->pi_workaround = 0; + + int err = snd_pcm_open(&ad->pcm, alsa_device(ad), + SND_PCM_STREAM_PLAYBACK, ad->mode); + if (err < 0) { + error.Format(alsa_output_domain, err, + "Failed to open ALSA device \"%s\": %s", + alsa_device(ad), snd_strerror(err)); + return false; + } + + FormatDebug(alsa_output_domain, "opened %s type=%s", + snd_pcm_name(ad->pcm), + snd_pcm_type_name(snd_pcm_type(ad->pcm))); + + if (!alsa_setup_or_dsd(ad, audio_format, error)) { + snd_pcm_close(ad->pcm); + return false; + } + + ad->in_frame_size = audio_format.GetFrameSize(); + ad->out_frame_size = ad->pcm_export->GetFrameSize(audio_format); + + return true; +} + +/** + * Write silence to the ALSA device. + */ +static void +alsa_write_silence(AlsaOutput *ad, snd_pcm_uframes_t nframes) +{ + ad->writei(ad->pcm, ad->silence, nframes); +} + +static int +alsa_recover(AlsaOutput *ad, int err) +{ + if (err == -EPIPE) { + FormatDebug(alsa_output_domain, + "Underrun on ALSA device \"%s\"", alsa_device(ad)); + } else if (err == -ESTRPIPE) { + FormatDebug(alsa_output_domain, + "ALSA device \"%s\" was suspended", + alsa_device(ad)); + } + + switch (snd_pcm_state(ad->pcm)) { + case SND_PCM_STATE_PAUSED: + err = snd_pcm_pause(ad->pcm, /* disable */ 0); + break; + case SND_PCM_STATE_SUSPENDED: + err = snd_pcm_resume(ad->pcm); + if (err == -EAGAIN) + return 0; + /* fall-through to snd_pcm_prepare: */ + case SND_PCM_STATE_SETUP: + case SND_PCM_STATE_XRUN: + ad->period_position = 0; + err = snd_pcm_prepare(ad->pcm); + + if (err == 0 && ad->pi_workaround == 0) { + /* this works around a driver bug observed on + the Raspberry Pi: after snd_pcm_drop(), the + whole ring buffer must be invalidated, but + the snd_pcm_prepare() call above makes the + driver play random data that just happens + to be still in the buffer; by adding and + cancelling some silence, this bug does not + occur */ + alsa_write_silence(ad, ad->period_frames); + + /* cancel the silence data right away to avoid + increasing latency; even though this + function call invalidates the portion of + silence, the driver seems to avoid the + bug */ + snd_pcm_reset(ad->pcm); + + /* disable the workaround for some time */ + ad->pi_workaround = 8; + } + + break; + case SND_PCM_STATE_DISCONNECTED: + break; + /* this is no error, so just keep running */ + case SND_PCM_STATE_RUNNING: + err = 0; + break; + default: + /* unknown state, do nothing */ + break; + } + + return err; +} + +static void +alsa_drain(struct audio_output *ao) +{ + AlsaOutput *ad = (AlsaOutput *)ao; + + if (snd_pcm_state(ad->pcm) != SND_PCM_STATE_RUNNING) + return; + + if (ad->period_position > 0) { + /* generate some silence to finish the partial + period */ + snd_pcm_uframes_t nframes = + ad->period_frames - ad->period_position; + alsa_write_silence(ad, nframes); + } + + snd_pcm_drain(ad->pcm); + + ad->period_position = 0; +} + +static void +alsa_cancel(struct audio_output *ao) +{ + AlsaOutput *ad = (AlsaOutput *)ao; + + ad->period_position = 0; + + snd_pcm_drop(ad->pcm); +} + +static void +alsa_close(struct audio_output *ao) +{ + AlsaOutput *ad = (AlsaOutput *)ao; + + snd_pcm_close(ad->pcm); + delete[] ad->silence; +} + +static size_t +alsa_play(struct audio_output *ao, const void *chunk, size_t size, + Error &error) +{ + AlsaOutput *ad = (AlsaOutput *)ao; + + assert(size % ad->in_frame_size == 0); + + chunk = ad->pcm_export->Export(chunk, size, size); + + assert(size % ad->out_frame_size == 0); + + size /= ad->out_frame_size; + + while (true) { + snd_pcm_sframes_t ret = ad->writei(ad->pcm, chunk, size); + if (ret > 0) { + ad->period_position = (ad->period_position + ret) + % ad->period_frames; + + if (ad->pi_workaround > 0) + --ad->pi_workaround; + + size_t bytes_written = ret * ad->out_frame_size; + return ad->pcm_export->CalcSourceSize(bytes_written); + } + + if (ret < 0 && ret != -EAGAIN && ret != -EINTR && + alsa_recover(ad, ret) < 0) { + error.Set(alsa_output_domain, ret, snd_strerror(-ret)); + return 0; + } + } +} + +const struct audio_output_plugin alsa_output_plugin = { + "alsa", + alsa_test_default_device, + alsa_init, + alsa_finish, + alsa_output_enable, + alsa_output_disable, + alsa_open, + alsa_close, + nullptr, + nullptr, + alsa_play, + alsa_drain, + alsa_cancel, + nullptr, + + &alsa_mixer_plugin, +}; diff --git a/src/output/plugins/AlsaOutputPlugin.hxx b/src/output/plugins/AlsaOutputPlugin.hxx new file mode 100644 index 000000000..63508e041 --- /dev/null +++ b/src/output/plugins/AlsaOutputPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_ALSA_OUTPUT_PLUGIN_HXX +#define MPD_ALSA_OUTPUT_PLUGIN_HXX + +extern const struct audio_output_plugin alsa_output_plugin; + +#endif diff --git a/src/output/plugins/AoOutputPlugin.cxx b/src/output/plugins/AoOutputPlugin.cxx new file mode 100644 index 000000000..efc1e0c6e --- /dev/null +++ b/src/output/plugins/AoOutputPlugin.cxx @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "AoOutputPlugin.hxx" +#include "../OutputAPI.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "Log.hxx" + +#include +#include + +#include + +/* An ao_sample_format, with all fields set to zero: */ +static ao_sample_format OUR_AO_FORMAT_INITIALIZER; + +static unsigned ao_output_ref; + +struct AoOutput { + struct audio_output base; + + size_t write_size; + int driver; + ao_option *options; + ao_device *device; + + bool Initialize(const config_param ¶m, Error &error) { + return ao_base_init(&base, &ao_output_plugin, param, + error); + } + + void Deinitialize() { + ao_base_finish(&base); + } + + bool Configure(const config_param ¶m, Error &error); +}; + +static constexpr Domain ao_output_domain("ao_output"); + +static void +ao_output_error(Error &error_r) +{ + const char *error; + + switch (errno) { + case AO_ENODRIVER: + error = "No such libao driver"; + break; + + case AO_ENOTLIVE: + error = "This driver is not a libao live device"; + break; + + case AO_EBADOPTION: + error = "Invalid libao option"; + break; + + case AO_EOPENDEVICE: + error = "Cannot open the libao device"; + break; + + case AO_EFAIL: + error = "Generic libao failure"; + break; + + default: + error_r.SetErrno(); + return; + } + + error_r.Set(ao_output_domain, errno, error); +} + +inline bool +AoOutput::Configure(const config_param ¶m, Error &error) +{ + const char *value; + + options = nullptr; + + write_size = param.GetBlockValue("write_size", 1024u); + + if (ao_output_ref == 0) { + ao_initialize(); + } + ao_output_ref++; + + value = param.GetBlockValue("driver", "default"); + if (0 == strcmp(value, "default")) + driver = ao_default_driver_id(); + else + driver = ao_driver_id(value); + + if (driver < 0) { + error.Format(ao_output_domain, + "\"%s\" is not a valid ao driver", + value); + return false; + } + + ao_info *ai = ao_driver_info(driver); + if (ai == nullptr) { + error.Set(ao_output_domain, "problems getting driver info"); + return false; + } + + FormatDebug(ao_output_domain, "using ao driver \"%s\" for \"%s\"\n", + ai->short_name, param.GetBlockValue("name", nullptr)); + + value = param.GetBlockValue("options", nullptr); + if (value != nullptr) { + gchar **_options = g_strsplit(value, ";", 0); + + for (unsigned i = 0; _options[i] != nullptr; ++i) { + gchar **key_value = g_strsplit(_options[i], "=", 2); + + if (key_value[0] == nullptr || key_value[1] == nullptr) { + error.Format(ao_output_domain, + "problems parsing options \"%s\"", + _options[i]); + return false; + } + + ao_append_option(&options, key_value[0], + key_value[1]); + + g_strfreev(key_value); + } + + g_strfreev(_options); + } + + return true; +} + +static struct audio_output * +ao_output_init(const config_param ¶m, Error &error) +{ + AoOutput *ad = new AoOutput(); + + if (!ad->Initialize(param, error)) { + delete ad; + return nullptr; + } + + if (!ad->Configure(param, error)) { + ad->Deinitialize(); + delete ad; + return nullptr; + } + + return &ad->base; +} + +static void +ao_output_finish(struct audio_output *ao) +{ + AoOutput *ad = (AoOutput *)ao; + + ao_free_options(ad->options); + ad->Deinitialize(); + delete ad; + + ao_output_ref--; + + if (ao_output_ref == 0) + ao_shutdown(); +} + +static void +ao_output_close(struct audio_output *ao) +{ + AoOutput *ad = (AoOutput *)ao; + + ao_close(ad->device); +} + +static bool +ao_output_open(struct audio_output *ao, AudioFormat &audio_format, + Error &error) +{ + ao_sample_format format = OUR_AO_FORMAT_INITIALIZER; + AoOutput *ad = (AoOutput *)ao; + + switch (audio_format.format) { + case SampleFormat::S8: + format.bits = 8; + break; + + case SampleFormat::S16: + format.bits = 16; + break; + + default: + /* support for 24 bit samples in libao is currently + dubious, and until we have sorted that out, + convert everything to 16 bit */ + audio_format.format = SampleFormat::S16; + format.bits = 16; + break; + } + + format.rate = audio_format.sample_rate; + format.byte_format = AO_FMT_NATIVE; + format.channels = audio_format.channels; + + ad->device = ao_open_live(ad->driver, &format, ad->options); + + if (ad->device == nullptr) { + ao_output_error(error); + return false; + } + + return true; +} + +/** + * For whatever reason, libao wants a non-const pointer. Let's hope + * it does not write to the buffer, and use the union deconst hack to + * work around this API misdesign. + */ +static int ao_play_deconst(ao_device *device, const void *output_samples, + uint_32 num_bytes) +{ + union { + const void *in; + char *out; + } u; + + u.in = output_samples; + return ao_play(device, u.out, num_bytes); +} + +static size_t +ao_output_play(struct audio_output *ao, const void *chunk, size_t size, + Error &error) +{ + AoOutput *ad = (AoOutput *)ao; + + if (size > ad->write_size) + size = ad->write_size; + + if (ao_play_deconst(ad->device, chunk, size) == 0) { + ao_output_error(error); + return 0; + } + + return size; +} + +const struct audio_output_plugin ao_output_plugin = { + "ao", + nullptr, + ao_output_init, + ao_output_finish, + nullptr, + nullptr, + ao_output_open, + ao_output_close, + nullptr, + nullptr, + ao_output_play, + nullptr, + nullptr, + nullptr, + nullptr, +}; diff --git a/src/output/plugins/AoOutputPlugin.hxx b/src/output/plugins/AoOutputPlugin.hxx new file mode 100644 index 000000000..cbf2fd589 --- /dev/null +++ b/src/output/plugins/AoOutputPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_AO_OUTPUT_PLUGIN_HXX +#define MPD_AO_OUTPUT_PLUGIN_HXX + +extern const struct audio_output_plugin ao_output_plugin; + +#endif diff --git a/src/output/plugins/FifoOutputPlugin.cxx b/src/output/plugins/FifoOutputPlugin.cxx new file mode 100644 index 000000000..5f14bcbbe --- /dev/null +++ b/src/output/plugins/FifoOutputPlugin.cxx @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "FifoOutputPlugin.hxx" +#include "ConfigError.hxx" +#include "../OutputAPI.hxx" +#include "Timer.hxx" +#include "fs/AllocatedPath.hxx" +#include "fs/FileSystem.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "Log.hxx" +#include "open.h" + +#include +#include +#include + +#define FIFO_BUFFER_SIZE 65536 /* pipe capacity on Linux >= 2.6.11 */ + +struct FifoOutput { + struct audio_output base; + + AllocatedPath path; + std::string path_utf8; + + int input; + int output; + bool created; + Timer *timer; + + FifoOutput() + :path(AllocatedPath::Null()), input(-1), output(-1), + created(false) {} + + bool Initialize(const config_param ¶m, Error &error) { + return ao_base_init(&base, &fifo_output_plugin, param, + error); + } + + void Deinitialize() { + ao_base_finish(&base); + } + + bool Create(Error &error); + bool Check(Error &error); + void Delete(); + + bool Open(Error &error); + void Close(); +}; + +static constexpr Domain fifo_output_domain("fifo_output"); + +inline void +FifoOutput::Delete() +{ + FormatDebug(fifo_output_domain, + "Removing FIFO \"%s\"", path_utf8.c_str()); + + if (!RemoveFile(path)) { + FormatErrno(fifo_output_domain, + "Could not remove FIFO \"%s\"", + path_utf8.c_str()); + return; + } + + created = false; +} + +void +FifoOutput::Close() +{ + if (input >= 0) { + close(input); + input = -1; + } + + if (output >= 0) { + close(output); + output = -1; + } + + struct stat st; + if (created && StatFile(path, st)) + Delete(); +} + +inline bool +FifoOutput::Create(Error &error) +{ + if (!MakeFifo(path, 0666)) { + error.FormatErrno("Couldn't create FIFO \"%s\"", + path_utf8.c_str()); + return false; + } + + created = true; + return true; +} + +inline bool +FifoOutput::Check(Error &error) +{ + struct stat st; + if (!StatFile(path, st)) { + if (errno == ENOENT) { + /* Path doesn't exist */ + return Create(error); + } + + error.FormatErrno("Failed to stat FIFO \"%s\"", + path_utf8.c_str()); + return false; + } + + if (!S_ISFIFO(st.st_mode)) { + error.Format(fifo_output_domain, + "\"%s\" already exists, but is not a FIFO", + path_utf8.c_str()); + return false; + } + + return true; +} + +inline bool +FifoOutput::Open(Error &error) +{ + if (!Check(error)) + return false; + + input = OpenFile(path, O_RDONLY|O_NONBLOCK|O_BINARY, 0); + if (input < 0) { + error.FormatErrno("Could not open FIFO \"%s\" for reading", + path_utf8.c_str()); + Close(); + return false; + } + + output = OpenFile(path, O_WRONLY|O_NONBLOCK|O_BINARY, 0); + if (output < 0) { + error.FormatErrno("Could not open FIFO \"%s\" for writing", + path_utf8.c_str()); + Close(); + return false; + } + + return true; +} + +static bool +fifo_open(FifoOutput *fd, Error &error) +{ + return fd->Open(error); +} + +static struct audio_output * +fifo_output_init(const config_param ¶m, Error &error) +{ + FifoOutput *fd = new FifoOutput(); + + fd->path = param.GetBlockPath("path", error); + if (fd->path.IsNull()) { + delete fd; + + if (!error.IsDefined()) + error.Set(config_domain, + "No \"path\" parameter specified"); + return nullptr; + } + + fd->path_utf8 = fd->path.ToUTF8(); + + if (!fd->Initialize(param, error)) { + delete fd; + return nullptr; + } + + if (!fifo_open(fd, error)) { + fd->Deinitialize(); + delete fd; + return nullptr; + } + + return &fd->base; +} + +static void +fifo_output_finish(struct audio_output *ao) +{ + FifoOutput *fd = (FifoOutput *)ao; + + fd->Close(); + fd->Deinitialize(); + delete fd; +} + +static bool +fifo_output_open(struct audio_output *ao, AudioFormat &audio_format, + gcc_unused Error &error) +{ + FifoOutput *fd = (FifoOutput *)ao; + + fd->timer = new Timer(audio_format); + + return true; +} + +static void +fifo_output_close(struct audio_output *ao) +{ + FifoOutput *fd = (FifoOutput *)ao; + + delete fd->timer; +} + +static void +fifo_output_cancel(struct audio_output *ao) +{ + FifoOutput *fd = (FifoOutput *)ao; + char buf[FIFO_BUFFER_SIZE]; + int bytes = 1; + + fd->timer->Reset(); + + while (bytes > 0 && errno != EINTR) + bytes = read(fd->input, buf, FIFO_BUFFER_SIZE); + + if (bytes < 0 && errno != EAGAIN) { + FormatErrno(fifo_output_domain, + "Flush of FIFO \"%s\" failed", + fd->path_utf8.c_str()); + } +} + +static unsigned +fifo_output_delay(struct audio_output *ao) +{ + FifoOutput *fd = (FifoOutput *)ao; + + return fd->timer->IsStarted() + ? fd->timer->GetDelay() + : 0; +} + +static size_t +fifo_output_play(struct audio_output *ao, const void *chunk, size_t size, + Error &error) +{ + FifoOutput *fd = (FifoOutput *)ao; + ssize_t bytes; + + if (!fd->timer->IsStarted()) + fd->timer->Start(); + fd->timer->Add(size); + + while (true) { + bytes = write(fd->output, chunk, size); + if (bytes > 0) + return (size_t)bytes; + + if (bytes < 0) { + switch (errno) { + case EAGAIN: + /* The pipe is full, so empty it */ + fifo_output_cancel(&fd->base); + continue; + case EINTR: + continue; + } + + error.FormatErrno("Failed to write to FIFO %s", + fd->path_utf8.c_str()); + return 0; + } + } +} + +const struct audio_output_plugin fifo_output_plugin = { + "fifo", + nullptr, + fifo_output_init, + fifo_output_finish, + nullptr, + nullptr, + fifo_output_open, + fifo_output_close, + fifo_output_delay, + nullptr, + fifo_output_play, + nullptr, + fifo_output_cancel, + nullptr, + nullptr, +}; diff --git a/src/output/plugins/FifoOutputPlugin.hxx b/src/output/plugins/FifoOutputPlugin.hxx new file mode 100644 index 000000000..394ec3ae9 --- /dev/null +++ b/src/output/plugins/FifoOutputPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_FIFO_OUTPUT_PLUGIN_HXX +#define MPD_FIFO_OUTPUT_PLUGIN_HXX + +extern const struct audio_output_plugin fifo_output_plugin; + +#endif diff --git a/src/output/plugins/HttpdClient.cxx b/src/output/plugins/HttpdClient.cxx new file mode 100644 index 000000000..d761bdf57 --- /dev/null +++ b/src/output/plugins/HttpdClient.cxx @@ -0,0 +1,485 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "HttpdClient.hxx" +#include "HttpdInternal.hxx" +#include "util/ASCII.hxx" +#include "Page.hxx" +#include "IcyMetaDataServer.hxx" +#include "system/SocketError.hxx" +#include "Log.hxx" + +#include + +#include +#include + +HttpdClient::~HttpdClient() +{ + if (state == RESPONSE) { + if (current_page != nullptr) + current_page->Unref(); + + ClearQueue(); + } + + if (metadata) + metadata->Unref(); + + if (IsDefined()) + BufferedSocket::Close(); +} + +void +HttpdClient::Close() +{ + httpd.RemoveClient(*this); +} + +void +HttpdClient::LockClose() +{ + const ScopeLock protect(httpd.mutex); + Close(); +} + +void +HttpdClient::BeginResponse() +{ + assert(state != RESPONSE); + + state = RESPONSE; + current_page = nullptr; + + if (!head_method) + httpd.SendHeader(*this); +} + +/** + * Handle a line of the HTTP request. + */ +bool +HttpdClient::HandleLine(const char *line) +{ + assert(state != RESPONSE); + + if (state == REQUEST) { + if (memcmp(line, "HEAD /", 6) == 0) { + line += 6; + head_method = true; + } else if (memcmp(line, "GET /", 5) == 0) { + line += 5; + } else { + /* only GET is supported */ + LogWarning(httpd_output_domain, + "malformed request line from client"); + return false; + } + + line = strchr(line, ' '); + if (line == nullptr || memcmp(line + 1, "HTTP/", 5) != 0) { + /* HTTP/0.9 without request headers */ + + if (head_method) + return false; + + BeginResponse(); + return true; + } + + /* after the request line, request headers follow */ + state = HEADERS; + return true; + } else { + if (*line == 0) { + /* empty line: request is finished */ + + BeginResponse(); + return true; + } + + if (StringEqualsCaseASCII(line, "Icy-MetaData: 1", 15) || + StringEqualsCaseASCII(line, "Icy-MetaData:1", 14)) { + /* Send icy metadata */ + metadata_requested = metadata_supported; + return true; + } + + if (StringEqualsCaseASCII(line, "transferMode.dlna.org: Streaming", 32)) { + /* Send as dlna */ + dlna_streaming_requested = true; + /* metadata is not supported by dlna streaming, so disable it */ + metadata_supported = false; + metadata_requested = false; + return true; + } + + /* expect more request headers */ + return true; + } +} + +/** + * Sends the status line and response headers to the client. + */ +bool +HttpdClient::SendResponse() +{ + char buffer[1024]; + assert(state == RESPONSE); + + if (dlna_streaming_requested) { + snprintf(buffer, sizeof(buffer), + "HTTP/1.1 206 OK\r\n" + "Content-Type: %s\r\n" + "Content-Length: 10000\r\n" + "Content-RangeX: 0-1000000/1000000\r\n" + "transferMode.dlna.org: Streaming\r\n" + "Accept-Ranges: bytes\r\n" + "Connection: close\r\n" + "realTimeInfo.dlna.org: DLNA.ORG_TLAG=*\r\n" + "contentFeatures.dlna.org: DLNA.ORG_OP=01;DLNA.ORG_CI=0\r\n" + "\r\n", + httpd.content_type); + + } else if (metadata_requested) { + char *metadata_header = + icy_server_metadata_header(httpd.name, httpd.genre, + httpd.website, + httpd.content_type, + metaint); + + g_strlcpy(buffer, metadata_header, sizeof(buffer)); + + delete[] metadata_header; + + } else { /* revert to a normal HTTP request */ + snprintf(buffer, sizeof(buffer), + "HTTP/1.1 200 OK\r\n" + "Content-Type: %s\r\n" + "Connection: close\r\n" + "Pragma: no-cache\r\n" + "Cache-Control: no-cache, no-store\r\n" + "\r\n", + httpd.content_type); + } + + ssize_t nbytes = SocketMonitor::Write(buffer, strlen(buffer)); + if (gcc_unlikely(nbytes < 0)) { + const SocketErrorMessage msg; + FormatWarning(httpd_output_domain, + "failed to write to client: %s", + (const char *)msg); + Close(); + return false; + } + + return true; +} + +HttpdClient::HttpdClient(HttpdOutput &_httpd, int _fd, EventLoop &_loop, + bool _metadata_supported) + :BufferedSocket(_fd, _loop), + httpd(_httpd), + state(REQUEST), + queue_size(0), + head_method(false), + dlna_streaming_requested(false), + metadata_supported(_metadata_supported), + metadata_requested(false), metadata_sent(true), + metaint(8192), /*TODO: just a std value */ + metadata(nullptr), + metadata_current_position(0), metadata_fill(0) +{ +} + +void +HttpdClient::ClearQueue() +{ + assert(state == RESPONSE); + + while (!pages.empty()) { + Page *page = pages.front(); + pages.pop(); + +#ifndef NDEBUG + assert(queue_size >= page->size); + queue_size -= page->size; +#endif + + page->Unref(); + } + + assert(queue_size == 0); +} + +void +HttpdClient::CancelQueue() +{ + if (state != RESPONSE) + return; + + ClearQueue(); + + if (current_page == nullptr) + CancelWrite(); +} + +ssize_t +HttpdClient::TryWritePage(const Page &page, size_t position) +{ + assert(position < page.size); + + return Write(page.data + position, page.size - position); +} + +ssize_t +HttpdClient::TryWritePageN(const Page &page, size_t position, ssize_t n) +{ + return n >= 0 + ? Write(page.data + position, n) + : TryWritePage(page, position); +} + +ssize_t +HttpdClient::GetBytesTillMetaData() const +{ + if (metadata_requested && + current_page->size - current_position > metaint - metadata_fill) + return metaint - metadata_fill; + + return -1; +} + +inline bool +HttpdClient::TryWrite() +{ + const ScopeLock protect(httpd.mutex); + + assert(state == RESPONSE); + + if (current_page == nullptr) { + if (pages.empty()) { + /* another thread has removed the event source + while this thread was waiting for + httpd.mutex */ + CancelWrite(); + return true; + } + + current_page = pages.front(); + pages.pop(); + current_position = 0; + + assert(queue_size >= current_page->size); + queue_size -= current_page->size; + } + + const ssize_t bytes_to_write = GetBytesTillMetaData(); + if (bytes_to_write == 0) { + if (!metadata_sent) { + ssize_t nbytes = TryWritePage(*metadata, + metadata_current_position); + if (nbytes < 0) { + auto e = GetSocketError(); + if (IsSocketErrorAgain(e)) + return true; + + if (!IsSocketErrorClosed(e)) { + SocketErrorMessage msg(e); + FormatWarning(httpd_output_domain, + "failed to write to client: %s", + (const char *)msg); + } + + Close(); + return false; + } + + metadata_current_position += nbytes; + + if (metadata->size - metadata_current_position == 0) { + metadata_fill = 0; + metadata_current_position = 0; + metadata_sent = true; + } + } else { + guchar empty_data = 0; + + ssize_t nbytes = Write(&empty_data, 1); + if (nbytes < 0) { + auto e = GetSocketError(); + if (IsSocketErrorAgain(e)) + return true; + + if (!IsSocketErrorClosed(e)) { + SocketErrorMessage msg(e); + FormatWarning(httpd_output_domain, + "failed to write to client: %s", + (const char *)msg); + } + + Close(); + return false; + } + + metadata_fill = 0; + metadata_current_position = 0; + } + } else { + ssize_t nbytes = + TryWritePageN(*current_page, current_position, + bytes_to_write); + if (nbytes < 0) { + auto e = GetSocketError(); + if (IsSocketErrorAgain(e)) + return true; + + if (!IsSocketErrorClosed(e)) { + SocketErrorMessage msg(e); + FormatWarning(httpd_output_domain, + "failed to write to client: %s", + (const char *)msg); + } + + Close(); + return false; + } + + current_position += nbytes; + assert(current_position <= current_page->size); + + if (metadata_requested) + metadata_fill += nbytes; + + if (current_position >= current_page->size) { + current_page->Unref(); + current_page = nullptr; + + if (pages.empty()) + /* all pages are sent: remove the + event source */ + CancelWrite(); + } + } + + return true; +} + +void +HttpdClient::PushPage(Page *page) +{ + if (state != RESPONSE) + /* the client is still writing the HTTP request */ + return; + + if (queue_size > 256 * 1024) { + FormatDebug(httpd_output_domain, + "client is too slow, flushing its queue"); + ClearQueue(); + } + + page->Ref(); + pages.push(page); + queue_size += page->size; + + ScheduleWrite(); +} + +void +HttpdClient::PushMetaData(Page *page) +{ + if (metadata) { + metadata->Unref(); + metadata = nullptr; + } + + g_return_if_fail (page); + + page->Ref(); + metadata = page; + metadata_sent = false; +} + +bool +HttpdClient::OnSocketReady(unsigned flags) +{ + if (!BufferedSocket::OnSocketReady(flags)) + return false; + + if (flags & WRITE) + if (!TryWrite()) + return false; + + return true; +} + +BufferedSocket::InputResult +HttpdClient::OnSocketInput(void *data, size_t length) +{ + if (state == RESPONSE) { + LogWarning(httpd_output_domain, + "unexpected input from client"); + LockClose(); + return InputResult::CLOSED; + } + + char *line = (char *)data; + char *newline = (char *)memchr(line, '\n', length); + if (newline == nullptr) + return InputResult::MORE; + + ConsumeInput(newline + 1 - line); + + if (newline > line && newline[-1] == '\r') + --newline; + + /* terminate the string at the end of the line */ + *newline = 0; + + if (!HandleLine(line)) { + LockClose(); + return InputResult::CLOSED; + } + + if (state == RESPONSE) { + if (!SendResponse()) + return InputResult::CLOSED; + + if (head_method) { + LockClose(); + return InputResult::CLOSED; + } + } + + return InputResult::AGAIN; +} + +void +HttpdClient::OnSocketError(Error &&error) +{ + LogError(error); +} + +void +HttpdClient::OnSocketClosed() +{ + LockClose(); +} diff --git a/src/output/plugins/HttpdClient.hxx b/src/output/plugins/HttpdClient.hxx new file mode 100644 index 000000000..f94f05769 --- /dev/null +++ b/src/output/plugins/HttpdClient.hxx @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OUTPUT_HTTPD_CLIENT_HXX +#define MPD_OUTPUT_HTTPD_CLIENT_HXX + +#include "event/BufferedSocket.hxx" +#include "Compiler.h" + +#include +#include + +#include + +class HttpdOutput; +class Page; + +class HttpdClient final : BufferedSocket { + /** + * The httpd output object this client is connected to. + */ + HttpdOutput &httpd; + + /** + * The current state of the client. + */ + enum { + /** reading the request line */ + REQUEST, + + /** reading the request headers */ + HEADERS, + + /** sending the HTTP response */ + RESPONSE, + } state; + + /** + * A queue of #Page objects to be sent to the client. + */ + std::queue> pages; + + /** + * The sum of all page sizes in #pages. + */ + size_t queue_size; + + /** + * The #page which is currently being sent to the client. + */ + Page *current_page; + + /** + * The amount of bytes which were already sent from + * #current_page. + */ + size_t current_position; + + /** + * Is this a HEAD request? + */ + bool head_method; + + /** + * If DLNA streaming was an option. + */ + bool dlna_streaming_requested; + + /* ICY */ + + /** + * Do we support sending Icy-Metadata to the client? This is + * disabled if the httpd audio output uses encoder tags. + */ + bool metadata_supported; + + /** + * If we should sent icy metadata. + */ + bool metadata_requested; + + /** + * If the current metadata was already sent to the client. + */ + bool metadata_sent; + + /** + * The amount of streaming data between each metadata block + */ + unsigned metaint; + + /** + * The metadata as #Page which is currently being sent to the client. + */ + Page *metadata; + + /* + * The amount of bytes which were already sent from the metadata. + */ + size_t metadata_current_position; + + /** + * The amount of streaming data sent to the client + * since the last icy information was sent. + */ + unsigned metadata_fill; + +public: + /** + * @param httpd the HTTP output device + * @param fd the socket file descriptor + */ + HttpdClient(HttpdOutput &httpd, int _fd, EventLoop &_loop, + bool _metadata_supported); + + /** + * Note: this does not remove the client from the + * #HttpdOutput object. + */ + ~HttpdClient(); + + /** + * Frees the client and removes it from the server's client list. + */ + void Close(); + + void LockClose(); + + /** + * Clears the page queue. + */ + void CancelQueue(); + + /** + * Handle a line of the HTTP request. + */ + bool HandleLine(const char *line); + + /** + * Switch the client to the "RESPONSE" state. + */ + void BeginResponse(); + + /** + * Sends the status line and response headers to the client. + */ + bool SendResponse(); + + gcc_pure + ssize_t GetBytesTillMetaData() const; + + ssize_t TryWritePage(const Page &page, size_t position); + ssize_t TryWritePageN(const Page &page, size_t position, ssize_t n); + + bool TryWrite(); + + /** + * Appends a page to the client's queue. + */ + void PushPage(Page *page); + + /** + * Sends the passed metadata. + */ + void PushMetaData(Page *page); + +private: + void ClearQueue(); + +protected: + virtual bool OnSocketReady(unsigned flags) override; + virtual InputResult OnSocketInput(void *data, size_t length) override; + virtual void OnSocketError(Error &&error) override; + virtual void OnSocketClosed() override; +}; + +#endif diff --git a/src/output/plugins/HttpdInternal.hxx b/src/output/plugins/HttpdInternal.hxx new file mode 100644 index 000000000..506730d11 --- /dev/null +++ b/src/output/plugins/HttpdInternal.hxx @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/** \file + * + * Internal declarations for the "httpd" audio output plugin. + */ + +#ifndef MPD_OUTPUT_HTTPD_INTERNAL_H +#define MPD_OUTPUT_HTTPD_INTERNAL_H + +#include "../OutputInternal.hxx" +#include "Timer.hxx" +#include "thread/Mutex.hxx" +#include "event/ServerSocket.hxx" +#include "event/DeferredMonitor.hxx" +#include "util/Cast.hxx" + +#ifdef _LIBCPP_VERSION +/* can't use incomplete template arguments with libc++ */ +#include "HttpdClient.hxx" +#endif + +#include +#include +#include + +struct config_param; +class Error; +class EventLoop; +class ServerSocket; +class HttpdClient; +class Page; +struct Encoder; +struct Tag; + +class HttpdOutput final : ServerSocket, DeferredMonitor { + struct audio_output base; + + /** + * True if the audio output is open and accepts client + * connections. + */ + bool open; + + /** + * The configured encoder plugin. + */ + Encoder *encoder; + + /** + * Number of bytes which were fed into the encoder, without + * ever receiving new output. This is used to estimate + * whether MPD should manually flush the encoder, to avoid + * buffer underruns in the client. + */ + size_t unflushed_input; + +public: + /** + * The MIME type produced by the #encoder. + */ + const char *content_type; + + /** + * This mutex protects the listener socket and the client + * list. + */ + mutable Mutex mutex; + + /** + * This condition gets signalled when an item is removed from + * #pages. + */ + Cond cond; + +private: + /** + * A #Timer object to synchronize this output with the + * wallclock. + */ + Timer *timer; + + /** + * The header page, which is sent to every client on connect. + */ + Page *header; + + /** + * The metadata, which is sent to every client. + */ + Page *metadata; + + /** + * The page queue, i.e. pages from the encoder to be + * broadcasted to all clients. This container is necessary to + * pass pages from the OutputThread to the IOThread. It is + * protected by #mutex, and removing signals #cond. + */ + std::queue> pages; + + public: + /** + * The configured name. + */ + char const *name; + /** + * The configured genre. + */ + char const *genre; + /** + * The configured website address. + */ + char const *website; + +private: + /** + * A linked list containing all clients which are currently + * connected. + */ + std::forward_list clients; + + /** + * A temporary buffer for the httpd_output_read_page() + * function. + */ + char buffer[32768]; + + /** + * The maximum and current number of clients connected + * at the same time. + */ + unsigned clients_max, clients_cnt; + +public: + HttpdOutput(EventLoop &_loop); + ~HttpdOutput(); + +#if GCC_CHECK_VERSION(4,6) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Winvalid-offsetof" +#endif + + static constexpr HttpdOutput *Cast(audio_output *ao) { + return ContainerCast(ao, HttpdOutput, base); + } + +#if GCC_CHECK_VERSION(4,6) || defined(__clang__) +#pragma GCC diagnostic pop +#endif + + using DeferredMonitor::GetEventLoop; + + bool Init(const config_param ¶m, Error &error); + + void Finish() { + ao_base_finish(&base); + } + + bool Configure(const config_param ¶m, Error &error); + + audio_output *InitAndConfigure(const config_param ¶m, + Error &error) { + if (!Init(param, error)) + return nullptr; + + if (!Configure(param, error)) { + Finish(); + return nullptr; + } + + return &base; + } + + bool Bind(Error &error); + void Unbind(); + + /** + * Caller must lock the mutex. + */ + bool OpenEncoder(AudioFormat &audio_format, Error &error); + + /** + * Caller must lock the mutex. + */ + bool Open(AudioFormat &audio_format, Error &error); + + /** + * Caller must lock the mutex. + */ + void Close(); + + /** + * Check whether there is at least one client. + * + * Caller must lock the mutex. + */ + gcc_pure + bool HasClients() const { + return !clients.empty(); + } + + /** + * Check whether there is at least one client. + */ + gcc_pure + bool LockHasClients() const { + const ScopeLock protect(mutex); + return HasClients(); + } + + void AddClient(int fd); + + /** + * Removes a client from the httpd_output.clients linked list. + */ + void RemoveClient(HttpdClient &client); + + /** + * Sends the encoder header to the client. This is called + * right after the response headers have been sent. + */ + void SendHeader(HttpdClient &client) const; + + gcc_pure + unsigned Delay() const; + + /** + * Reads data from the encoder (as much as available) and + * returns it as a new #page object. + */ + Page *ReadPage(); + + /** + * Broadcasts a page struct to all clients. + * + * Mutext must not be locked. + */ + void BroadcastPage(Page *page); + + /** + * Broadcasts data from the encoder to all clients. + */ + void BroadcastFromEncoder(); + + bool EncodeAndPlay(const void *chunk, size_t size, Error &error); + + void SendTag(const Tag *tag); + + size_t Play(const void *chunk, size_t size, Error &error); + + void CancelAllClients(); + +private: + virtual void RunDeferred() override; + + virtual void OnAccept(int fd, const sockaddr &address, + size_t address_length, int uid) override; +}; + +extern const class Domain httpd_output_domain; + +#endif diff --git a/src/output/plugins/HttpdOutputPlugin.cxx b/src/output/plugins/HttpdOutputPlugin.cxx new file mode 100644 index 000000000..6921cb808 --- /dev/null +++ b/src/output/plugins/HttpdOutputPlugin.cxx @@ -0,0 +1,601 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "HttpdOutputPlugin.hxx" +#include "HttpdInternal.hxx" +#include "HttpdClient.hxx" +#include "../OutputAPI.hxx" +#include "encoder/EncoderPlugin.hxx" +#include "encoder/EncoderList.hxx" +#include "system/Resolver.hxx" +#include "Page.hxx" +#include "IcyMetaDataServer.hxx" +#include "system/fd_util.h" +#include "IOThread.hxx" +#include "event/Call.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "Log.hxx" + +#include + +#include +#include +#include +#include + +#ifdef HAVE_LIBWRAP +#include /* needed for AF_UNIX */ +#include +#endif + +const Domain httpd_output_domain("httpd_output"); + +inline +HttpdOutput::HttpdOutput(EventLoop &_loop) + :ServerSocket(_loop), DeferredMonitor(_loop), + encoder(nullptr), unflushed_input(0), + metadata(nullptr) +{ +} + +HttpdOutput::~HttpdOutput() +{ + if (metadata != nullptr) + metadata->Unref(); + + if (encoder != nullptr) + encoder_finish(encoder); + +} + +inline bool +HttpdOutput::Bind(Error &error) +{ + open = false; + + bool result = false; + BlockingCall(GetEventLoop(), [this, &error, &result](){ + result = ServerSocket::Open(error); + }); + return result; +} + +inline void +HttpdOutput::Unbind() +{ + assert(!open); + + BlockingCall(GetEventLoop(), [this](){ + ServerSocket::Close(); + }); +} + +inline bool +HttpdOutput::Configure(const config_param ¶m, Error &error) +{ + /* read configuration */ + name = param.GetBlockValue("name", "Set name in config"); + genre = param.GetBlockValue("genre", "Set genre in config"); + website = param.GetBlockValue("website", "Set website in config"); + + unsigned port = param.GetBlockValue("port", 8000u); + + const char *encoder_name = + param.GetBlockValue("encoder", "vorbis"); + const auto encoder_plugin = encoder_plugin_get(encoder_name); + if (encoder_plugin == nullptr) { + error.Format(httpd_output_domain, + "No such encoder: %s", encoder_name); + return false; + } + + clients_max = param.GetBlockValue("max_clients", 0u); + + /* set up bind_to_address */ + + const char *bind_to_address = param.GetBlockValue("bind_to_address"); + bool success = bind_to_address != nullptr && + strcmp(bind_to_address, "any") != 0 + ? AddHost(bind_to_address, port, error) + : AddPort(port, error); + if (!success) + return false; + + /* initialize encoder */ + + encoder = encoder_init(*encoder_plugin, param, error); + if (encoder == nullptr) + return false; + + /* determine content type */ + content_type = encoder_get_mime_type(encoder); + if (content_type == nullptr) + content_type = "application/octet-stream"; + + return true; +} + +inline bool +HttpdOutput::Init(const config_param ¶m, Error &error) +{ + return ao_base_init(&base, &httpd_output_plugin, param, error); +} + +static struct audio_output * +httpd_output_init(const config_param ¶m, Error &error) +{ + HttpdOutput *httpd = new HttpdOutput(io_thread_get()); + + audio_output *result = httpd->InitAndConfigure(param, error); + if (result == nullptr) + delete httpd; + + return result; +} + +static void +httpd_output_finish(struct audio_output *ao) +{ + HttpdOutput *httpd = HttpdOutput::Cast(ao); + + httpd->Finish(); + delete httpd; +} + +/** + * Creates a new #HttpdClient object and adds it into the + * HttpdOutput.clients linked list. + */ +inline void +HttpdOutput::AddClient(int fd) +{ + clients.emplace_front(*this, fd, GetEventLoop(), + encoder->plugin.tag == nullptr); + ++clients_cnt; + + /* pass metadata to client */ + if (metadata != nullptr) + clients.front().PushMetaData(metadata); +} + +void +HttpdOutput::RunDeferred() +{ + /* this method runs in the IOThread; it broadcasts pages from + our own queue to all clients */ + + const ScopeLock protect(mutex); + + while (!pages.empty()) { + Page *page = pages.front(); + pages.pop(); + + for (auto &client : clients) + client.PushPage(page); + + page->Unref(); + } + + /* wake up the client that may be waiting for the queue to be + flushed */ + cond.broadcast(); +} + +void +HttpdOutput::OnAccept(int fd, const sockaddr &address, + size_t address_length, gcc_unused int uid) +{ + /* the listener socket has become readable - a client has + connected */ + +#ifdef HAVE_LIBWRAP + if (address.sa_family != AF_UNIX) { + const auto hostaddr = sockaddr_to_string(&address, + address_length); + // TODO: shall we obtain the program name from argv[0]? + const char *progname = "mpd"; + + struct request_info req; + request_init(&req, RQ_FILE, fd, RQ_DAEMON, progname, 0); + + fromhost(&req); + + if (!hosts_access(&req)) { + /* tcp wrappers says no */ + FormatWarning(httpd_output_domain, + "libwrap refused connection (libwrap=%s) from %s", + progname, hostaddr.c_str()); + close_socket(fd); + return; + } + } +#else + (void)address; + (void)address_length; +#endif /* HAVE_WRAP */ + + const ScopeLock protect(mutex); + + if (fd >= 0) { + /* can we allow additional client */ + if (open && (clients_max == 0 || clients_cnt < clients_max)) + AddClient(fd); + else + close_socket(fd); + } else if (fd < 0 && errno != EINTR) { + LogErrno(httpd_output_domain, "accept() failed"); + } +} + +Page * +HttpdOutput::ReadPage() +{ + if (unflushed_input >= 65536) { + /* we have fed a lot of input into the encoder, but it + didn't give anything back yet - flush now to avoid + buffer underruns */ + encoder_flush(encoder, IgnoreError()); + unflushed_input = 0; + } + + size_t size = 0; + do { + size_t nbytes = encoder_read(encoder, + buffer + size, + sizeof(buffer) - size); + if (nbytes == 0) + break; + + unflushed_input = 0; + + size += nbytes; + } while (size < sizeof(buffer)); + + if (size == 0) + return nullptr; + + return Page::Copy(buffer, size); +} + +static bool +httpd_output_enable(struct audio_output *ao, Error &error) +{ + HttpdOutput *httpd = HttpdOutput::Cast(ao); + + return httpd->Bind(error); +} + +static void +httpd_output_disable(struct audio_output *ao) +{ + HttpdOutput *httpd = HttpdOutput::Cast(ao); + + httpd->Unbind(); +} + +inline bool +HttpdOutput::OpenEncoder(AudioFormat &audio_format, Error &error) +{ + if (!encoder_open(encoder, audio_format, error)) + return false; + + /* we have to remember the encoder header, i.e. the first + bytes of encoder output after opening it, because it has to + be sent to every new client */ + header = ReadPage(); + + unflushed_input = 0; + + return true; +} + +inline bool +HttpdOutput::Open(AudioFormat &audio_format, Error &error) +{ + assert(!open); + assert(clients.empty()); + + /* open the encoder */ + + if (!OpenEncoder(audio_format, error)) + return false; + + /* initialize other attributes */ + + clients_cnt = 0; + timer = new Timer(audio_format); + + open = true; + + return true; +} + +static bool +httpd_output_open(struct audio_output *ao, AudioFormat &audio_format, + Error &error) +{ + HttpdOutput *httpd = HttpdOutput::Cast(ao); + + const ScopeLock protect(httpd->mutex); + return httpd->Open(audio_format, error); +} + +inline void +HttpdOutput::Close() +{ + assert(open); + + open = false; + + delete timer; + + BlockingCall(GetEventLoop(), [this](){ + clients.clear(); + }); + + if (header != nullptr) + header->Unref(); + + encoder_close(encoder); +} + +static void +httpd_output_close(struct audio_output *ao) +{ + HttpdOutput *httpd = HttpdOutput::Cast(ao); + + const ScopeLock protect(httpd->mutex); + httpd->Close(); +} + +void +HttpdOutput::RemoveClient(HttpdClient &client) +{ + assert(clients_cnt > 0); + + for (auto prev = clients.before_begin(), i = std::next(prev);; + prev = i, i = std::next(prev)) { + assert(i != clients.end()); + if (&*i == &client) { + clients.erase_after(prev); + clients_cnt--; + break; + } + } +} + +void +HttpdOutput::SendHeader(HttpdClient &client) const +{ + if (header != nullptr) + client.PushPage(header); +} + +inline unsigned +HttpdOutput::Delay() const +{ + if (!LockHasClients() && base.pause) { + /* if there's no client and this output is paused, + then httpd_output_pause() will not do anything, it + will not fill the buffer and it will not update the + timer; therefore, we reset the timer here */ + timer->Reset(); + + /* some arbitrary delay that is long enough to avoid + consuming too much CPU, and short enough to notice + new clients quickly enough */ + return 1000; + } + + return timer->IsStarted() + ? timer->GetDelay() + : 0; +} + +static unsigned +httpd_output_delay(struct audio_output *ao) +{ + HttpdOutput *httpd = HttpdOutput::Cast(ao); + + return httpd->Delay(); +} + +void +HttpdOutput::BroadcastPage(Page *page) +{ + assert(page != nullptr); + + mutex.lock(); + pages.push(page); + page->Ref(); + mutex.unlock(); + + DeferredMonitor::Schedule(); +} + +void +HttpdOutput::BroadcastFromEncoder() +{ + /* synchronize with the IOThread */ + mutex.lock(); + while (!pages.empty()) + cond.wait(mutex); + + Page *page; + while ((page = ReadPage()) != nullptr) + pages.push(page); + + mutex.unlock(); + + DeferredMonitor::Schedule(); +} + +inline bool +HttpdOutput::EncodeAndPlay(const void *chunk, size_t size, Error &error) +{ + if (!encoder_write(encoder, chunk, size, error)) + return false; + + unflushed_input += size; + + BroadcastFromEncoder(); + return true; +} + +inline size_t +HttpdOutput::Play(const void *chunk, size_t size, Error &error) +{ + if (LockHasClients()) { + if (!EncodeAndPlay(chunk, size, error)) + return 0; + } + + if (!timer->IsStarted()) + timer->Start(); + timer->Add(size); + + return size; +} + +static size_t +httpd_output_play(struct audio_output *ao, const void *chunk, size_t size, + Error &error) +{ + HttpdOutput *httpd = HttpdOutput::Cast(ao); + + return httpd->Play(chunk, size, error); +} + +static bool +httpd_output_pause(struct audio_output *ao) +{ + HttpdOutput *httpd = HttpdOutput::Cast(ao); + + if (httpd->LockHasClients()) { + static const char silence[1020] = { 0 }; + return httpd_output_play(ao, silence, sizeof(silence), + IgnoreError()) > 0; + } else { + return true; + } +} + +inline void +HttpdOutput::SendTag(const Tag *tag) +{ + assert(tag != nullptr); + + if (encoder->plugin.tag != nullptr) { + /* embed encoder tags */ + + /* flush the current stream, and end it */ + + encoder_pre_tag(encoder, IgnoreError()); + BroadcastFromEncoder(); + + /* send the tag to the encoder - which starts a new + stream now */ + + encoder_tag(encoder, tag, IgnoreError()); + + /* the first page generated by the encoder will now be + used as the new "header" page, which is sent to all + new clients */ + + Page *page = ReadPage(); + if (page != nullptr) { + if (header != nullptr) + header->Unref(); + header = page; + BroadcastPage(page); + } + } else { + /* use Icy-Metadata */ + + if (metadata != nullptr) + metadata->Unref(); + + static constexpr TagType types[] = { + TAG_ALBUM, TAG_ARTIST, TAG_TITLE, + TAG_NUM_OF_ITEM_TYPES + }; + + metadata = icy_server_metadata_page(*tag, &types[0]); + if (metadata != nullptr) { + const ScopeLock protect(mutex); + for (auto &client : clients) + client.PushMetaData(metadata); + } + } +} + +static void +httpd_output_tag(struct audio_output *ao, const Tag *tag) +{ + HttpdOutput *httpd = HttpdOutput::Cast(ao); + + httpd->SendTag(tag); +} + +inline void +HttpdOutput::CancelAllClients() +{ + const ScopeLock protect(mutex); + + while (!pages.empty()) { + Page *page = pages.front(); + pages.pop(); + page->Unref(); + } + + for (auto &client : clients) + client.CancelQueue(); + + cond.broadcast(); +} + +static void +httpd_output_cancel(struct audio_output *ao) +{ + HttpdOutput *httpd = HttpdOutput::Cast(ao); + + BlockingCall(io_thread_get(), [httpd](){ + httpd->CancelAllClients(); + }); +} + +const struct audio_output_plugin httpd_output_plugin = { + "httpd", + nullptr, + httpd_output_init, + httpd_output_finish, + httpd_output_enable, + httpd_output_disable, + httpd_output_open, + httpd_output_close, + httpd_output_delay, + httpd_output_tag, + httpd_output_play, + nullptr, + httpd_output_cancel, + httpd_output_pause, + nullptr, +}; diff --git a/src/output/plugins/HttpdOutputPlugin.hxx b/src/output/plugins/HttpdOutputPlugin.hxx new file mode 100644 index 000000000..78218e5f0 --- /dev/null +++ b/src/output/plugins/HttpdOutputPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_HTTPD_OUTPUT_PLUGIN_HXX +#define MPD_HTTPD_OUTPUT_PLUGIN_HXX + +extern const struct audio_output_plugin httpd_output_plugin; + +#endif diff --git a/src/output/plugins/JackOutputPlugin.cxx b/src/output/plugins/JackOutputPlugin.cxx new file mode 100644 index 000000000..5a0d2bf16 --- /dev/null +++ b/src/output/plugins/JackOutputPlugin.cxx @@ -0,0 +1,765 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "JackOutputPlugin.hxx" +#include "../OutputAPI.hxx" +#include "ConfigError.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "Log.hxx" + +#include + +#include +#include +#include +#include + +#include +#include + +enum { + MAX_PORTS = 16, +}; + +static const size_t jack_sample_size = sizeof(jack_default_audio_sample_t); + +struct JackOutput { + struct audio_output base; + + /** + * libjack options passed to jack_client_open(). + */ + jack_options_t options; + + const char *name; + + const char *server_name; + + /* configuration */ + + char *source_ports[MAX_PORTS]; + unsigned num_source_ports; + + char *destination_ports[MAX_PORTS]; + unsigned num_destination_ports; + + size_t ringbuffer_size; + + /* the current audio format */ + AudioFormat audio_format; + + /* jack library stuff */ + jack_port_t *ports[MAX_PORTS]; + jack_client_t *client; + jack_ringbuffer_t *ringbuffer[MAX_PORTS]; + + bool shutdown; + + /** + * While this flag is set, the "process" callback generates + * silence. + */ + bool pause; + + bool Initialize(const config_param ¶m, Error &error_r) { + return ao_base_init(&base, &jack_output_plugin, param, + error_r); + } + + void Deinitialize() { + ao_base_finish(&base); + } +}; + +static constexpr Domain jack_output_domain("jack_output"); + +/** + * Determine the number of frames guaranteed to be available on all + * channels. + */ +static jack_nframes_t +mpd_jack_available(const JackOutput *jd) +{ + size_t min = jack_ringbuffer_read_space(jd->ringbuffer[0]); + + for (unsigned i = 1; i < jd->audio_format.channels; ++i) { + size_t current = jack_ringbuffer_read_space(jd->ringbuffer[i]); + if (current < min) + min = current; + } + + assert(min % jack_sample_size == 0); + + return min / jack_sample_size; +} + +static int +mpd_jack_process(jack_nframes_t nframes, void *arg) +{ + JackOutput *jd = (JackOutput *) arg; + + if (nframes <= 0) + return 0; + + if (jd->pause) { + /* empty the ring buffers */ + + const jack_nframes_t available = mpd_jack_available(jd); + for (unsigned i = 0; i < jd->audio_format.channels; ++i) + jack_ringbuffer_read_advance(jd->ringbuffer[i], + available * jack_sample_size); + + /* generate silence while MPD is paused */ + + for (unsigned i = 0; i < jd->audio_format.channels; ++i) { + jack_default_audio_sample_t *out = + (jack_default_audio_sample_t *) + jack_port_get_buffer(jd->ports[i], nframes); + + for (jack_nframes_t f = 0; f < nframes; ++f) + out[f] = 0.0; + } + + return 0; + } + + jack_nframes_t available = mpd_jack_available(jd); + if (available > nframes) + available = nframes; + + for (unsigned i = 0; i < jd->audio_format.channels; ++i) { + jack_default_audio_sample_t *out = + (jack_default_audio_sample_t *) + jack_port_get_buffer(jd->ports[i], nframes); + if (out == nullptr) + /* workaround for libjack1 bug: if the server + connection fails, the process callback is + invoked anyway, but unable to get a + buffer */ + continue; + + jack_ringbuffer_read(jd->ringbuffer[i], + (char *)out, available * jack_sample_size); + + for (jack_nframes_t f = available; f < nframes; ++f) + /* ringbuffer underrun, fill with silence */ + out[f] = 0.0; + } + + /* generate silence for the unused source ports */ + + for (unsigned i = jd->audio_format.channels; + i < jd->num_source_ports; ++i) { + jack_default_audio_sample_t *out = + (jack_default_audio_sample_t *) + jack_port_get_buffer(jd->ports[i], nframes); + if (out == nullptr) + /* workaround for libjack1 bug: if the server + connection fails, the process callback is + invoked anyway, but unable to get a + buffer */ + continue; + + for (jack_nframes_t f = 0; f < nframes; ++f) + out[f] = 0.0; + } + + return 0; +} + +static void +mpd_jack_shutdown(void *arg) +{ + JackOutput *jd = (JackOutput *) arg; + jd->shutdown = true; +} + +static void +set_audioformat(JackOutput *jd, AudioFormat &audio_format) +{ + audio_format.sample_rate = jack_get_sample_rate(jd->client); + + if (jd->num_source_ports == 1) + audio_format.channels = 1; + else if (audio_format.channels > jd->num_source_ports) + audio_format.channels = 2; + + if (audio_format.format != SampleFormat::S16 && + audio_format.format != SampleFormat::S24_P32) + audio_format.format = SampleFormat::S24_P32; +} + +static void +mpd_jack_error(const char *msg) +{ + LogError(jack_output_domain, msg); +} + +#ifdef HAVE_JACK_SET_INFO_FUNCTION +static void +mpd_jack_info(const char *msg) +{ + LogDefault(jack_output_domain, msg); +} +#endif + +/** + * Disconnect the JACK client. + */ +static void +mpd_jack_disconnect(JackOutput *jd) +{ + assert(jd != nullptr); + assert(jd->client != nullptr); + + jack_deactivate(jd->client); + jack_client_close(jd->client); + jd->client = nullptr; +} + +/** + * Connect the JACK client and performs some basic setup + * (e.g. register callbacks). + */ +static bool +mpd_jack_connect(JackOutput *jd, Error &error) +{ + jack_status_t status; + + assert(jd != nullptr); + + jd->shutdown = false; + + jd->client = jack_client_open(jd->name, jd->options, &status, + jd->server_name); + if (jd->client == nullptr) { + error.Format(jack_output_domain, status, + "Failed to connect to JACK server, status=%d", + status); + return false; + } + + jack_set_process_callback(jd->client, mpd_jack_process, jd); + jack_on_shutdown(jd->client, mpd_jack_shutdown, jd); + + for (unsigned i = 0; i < jd->num_source_ports; ++i) { + jd->ports[i] = jack_port_register(jd->client, + jd->source_ports[i], + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsOutput, 0); + if (jd->ports[i] == nullptr) { + error.Format(jack_output_domain, + "Cannot register output port \"%s\"", + jd->source_ports[i]); + mpd_jack_disconnect(jd); + return false; + } + } + + return true; +} + +static bool +mpd_jack_test_default_device(void) +{ + return true; +} + +static unsigned +parse_port_list(const char *source, char **dest, Error &error) +{ + char **list = g_strsplit(source, ",", 0); + unsigned n = 0; + + for (n = 0; list[n] != nullptr; ++n) { + if (n >= MAX_PORTS) { + error.Set(config_domain, + "too many port names"); + return 0; + } + + dest[n] = list[n]; + } + + g_free(list); + + if (n == 0) { + error.Format(config_domain, + "at least one port name expected"); + return 0; + } + + return n; +} + +static struct audio_output * +mpd_jack_init(const config_param ¶m, Error &error) +{ + JackOutput *jd = new JackOutput(); + + if (!jd->Initialize(param, error)) { + delete jd; + return nullptr; + } + + const char *value; + + jd->options = JackNullOption; + + jd->name = param.GetBlockValue("client_name", nullptr); + if (jd->name != nullptr) + jd->options = jack_options_t(jd->options | JackUseExactName); + else + /* if there's a no configured client name, we don't + care about the JackUseExactName option */ + jd->name = "Music Player Daemon"; + + jd->server_name = param.GetBlockValue("server_name", nullptr); + if (jd->server_name != nullptr) + jd->options = jack_options_t(jd->options | JackServerName); + + if (!param.GetBlockValue("autostart", false)) + jd->options = jack_options_t(jd->options | JackNoStartServer); + + /* configure the source ports */ + + value = param.GetBlockValue("source_ports", "left,right"); + jd->num_source_ports = parse_port_list(value, + jd->source_ports, error); + if (jd->num_source_ports == 0) + return nullptr; + + /* configure the destination ports */ + + value = param.GetBlockValue("destination_ports", nullptr); + if (value == nullptr) { + /* compatibility with MPD < 0.16 */ + value = param.GetBlockValue("ports", nullptr); + if (value != nullptr) + FormatWarning(jack_output_domain, + "deprecated option 'ports' in line %d", + param.line); + } + + if (value != nullptr) { + jd->num_destination_ports = + parse_port_list(value, + jd->destination_ports, error); + if (jd->num_destination_ports == 0) + return nullptr; + } else { + jd->num_destination_ports = 0; + } + + if (jd->num_destination_ports > 0 && + jd->num_destination_ports != jd->num_source_ports) + FormatWarning(jack_output_domain, + "number of source ports (%u) mismatches the " + "number of destination ports (%u) in line %d", + jd->num_source_ports, jd->num_destination_ports, + param.line); + + jd->ringbuffer_size = param.GetBlockValue("ringbuffer_size", 32768u); + + jack_set_error_function(mpd_jack_error); + +#ifdef HAVE_JACK_SET_INFO_FUNCTION + jack_set_info_function(mpd_jack_info); +#endif + + return &jd->base; +} + +static void +mpd_jack_finish(struct audio_output *ao) +{ + JackOutput *jd = (JackOutput *)ao; + + for (unsigned i = 0; i < jd->num_source_ports; ++i) + g_free(jd->source_ports[i]); + + for (unsigned i = 0; i < jd->num_destination_ports; ++i) + g_free(jd->destination_ports[i]); + + jd->Deinitialize(); + delete jd; +} + +static bool +mpd_jack_enable(struct audio_output *ao, Error &error) +{ + JackOutput *jd = (JackOutput *)ao; + + for (unsigned i = 0; i < jd->num_source_ports; ++i) + jd->ringbuffer[i] = nullptr; + + return mpd_jack_connect(jd, error); +} + +static void +mpd_jack_disable(struct audio_output *ao) +{ + JackOutput *jd = (JackOutput *)ao; + + if (jd->client != nullptr) + mpd_jack_disconnect(jd); + + for (unsigned i = 0; i < jd->num_source_ports; ++i) { + if (jd->ringbuffer[i] != nullptr) { + jack_ringbuffer_free(jd->ringbuffer[i]); + jd->ringbuffer[i] = nullptr; + } + } +} + +/** + * Stops the playback on the JACK connection. + */ +static void +mpd_jack_stop(JackOutput *jd) +{ + assert(jd != nullptr); + + if (jd->client == nullptr) + return; + + if (jd->shutdown) + /* the connection has failed; close it */ + mpd_jack_disconnect(jd); + else + /* the connection is alive: just stop playback */ + jack_deactivate(jd->client); +} + +static bool +mpd_jack_start(JackOutput *jd, Error &error) +{ + const char *destination_ports[MAX_PORTS], **jports; + const char *duplicate_port = nullptr; + unsigned num_destination_ports; + + assert(jd->client != nullptr); + assert(jd->audio_format.channels <= jd->num_source_ports); + + /* allocate the ring buffers on the first open(); these + persist until MPD exits. It's too unsafe to delete them + because we can never know when mpd_jack_process() gets + called */ + for (unsigned i = 0; i < jd->num_source_ports; ++i) { + if (jd->ringbuffer[i] == nullptr) + jd->ringbuffer[i] = + jack_ringbuffer_create(jd->ringbuffer_size); + + /* clear the ring buffer to be sure that data from + previous playbacks are gone */ + jack_ringbuffer_reset(jd->ringbuffer[i]); + } + + if ( jack_activate(jd->client) ) { + error.Set(jack_output_domain, "cannot activate client"); + mpd_jack_stop(jd); + return false; + } + + if (jd->num_destination_ports == 0) { + /* no output ports were configured - ask libjack for + defaults */ + jports = jack_get_ports(jd->client, nullptr, nullptr, + JackPortIsPhysical | JackPortIsInput); + if (jports == nullptr) { + error.Set(jack_output_domain, "no ports found"); + mpd_jack_stop(jd); + return false; + } + + assert(*jports != nullptr); + + for (num_destination_ports = 0; + num_destination_ports < MAX_PORTS && + jports[num_destination_ports] != nullptr; + ++num_destination_ports) { + FormatDebug(jack_output_domain, + "destination_port[%u] = '%s'\n", + num_destination_ports, + jports[num_destination_ports]); + destination_ports[num_destination_ports] = + jports[num_destination_ports]; + } + } else { + /* use the configured output ports */ + + num_destination_ports = jd->num_destination_ports; + memcpy(destination_ports, jd->destination_ports, + num_destination_ports * sizeof(*destination_ports)); + + jports = nullptr; + } + + assert(num_destination_ports > 0); + + if (jd->audio_format.channels >= 2 && num_destination_ports == 1) { + /* mix stereo signal on one speaker */ + + while (num_destination_ports < jd->audio_format.channels) + destination_ports[num_destination_ports++] = + destination_ports[0]; + } else if (num_destination_ports > jd->audio_format.channels) { + if (jd->audio_format.channels == 1 && num_destination_ports > 2) { + /* mono input file: connect the one source + channel to the both destination channels */ + duplicate_port = destination_ports[1]; + num_destination_ports = 1; + } else + /* connect only as many ports as we need */ + num_destination_ports = jd->audio_format.channels; + } + + assert(num_destination_ports <= jd->num_source_ports); + + for (unsigned i = 0; i < num_destination_ports; ++i) { + int ret; + + ret = jack_connect(jd->client, jack_port_name(jd->ports[i]), + destination_ports[i]); + if (ret != 0) { + error.Format(jack_output_domain, + "Not a valid JACK port: %s", + destination_ports[i]); + + if (jports != nullptr) + free(jports); + + mpd_jack_stop(jd); + return false; + } + } + + if (duplicate_port != nullptr) { + /* mono input file: connect the one source channel to + the both destination channels */ + int ret; + + ret = jack_connect(jd->client, jack_port_name(jd->ports[0]), + duplicate_port); + if (ret != 0) { + error.Format(jack_output_domain, + "Not a valid JACK port: %s", + duplicate_port); + + if (jports != nullptr) + free(jports); + + mpd_jack_stop(jd); + return false; + } + } + + if (jports != nullptr) + free(jports); + + return true; +} + +static bool +mpd_jack_open(struct audio_output *ao, AudioFormat &audio_format, + Error &error) +{ + JackOutput *jd = (JackOutput *)ao; + + assert(jd != nullptr); + + jd->pause = false; + + if (jd->client != nullptr && jd->shutdown) + mpd_jack_disconnect(jd); + + if (jd->client == nullptr && !mpd_jack_connect(jd, error)) + return false; + + set_audioformat(jd, audio_format); + jd->audio_format = audio_format; + + if (!mpd_jack_start(jd, error)) + return false; + + return true; +} + +static void +mpd_jack_close(gcc_unused struct audio_output *ao) +{ + JackOutput *jd = (JackOutput *)ao; + + mpd_jack_stop(jd); +} + +static unsigned +mpd_jack_delay(struct audio_output *ao) +{ + JackOutput *jd = (JackOutput *)ao; + + return jd->base.pause && jd->pause && !jd->shutdown + ? 1000 + : 0; +} + +static inline jack_default_audio_sample_t +sample_16_to_jack(int16_t sample) +{ + return sample / (jack_default_audio_sample_t)(1 << (16 - 1)); +} + +static void +mpd_jack_write_samples_16(JackOutput *jd, const int16_t *src, + unsigned num_samples) +{ + jack_default_audio_sample_t sample; + unsigned i; + + while (num_samples-- > 0) { + for (i = 0; i < jd->audio_format.channels; ++i) { + sample = sample_16_to_jack(*src++); + jack_ringbuffer_write(jd->ringbuffer[i], + (const char *)&sample, + sizeof(sample)); + } + } +} + +static inline jack_default_audio_sample_t +sample_24_to_jack(int32_t sample) +{ + return sample / (jack_default_audio_sample_t)(1 << (24 - 1)); +} + +static void +mpd_jack_write_samples_24(JackOutput *jd, const int32_t *src, + unsigned num_samples) +{ + jack_default_audio_sample_t sample; + unsigned i; + + while (num_samples-- > 0) { + for (i = 0; i < jd->audio_format.channels; ++i) { + sample = sample_24_to_jack(*src++); + jack_ringbuffer_write(jd->ringbuffer[i], + (const char *)&sample, + sizeof(sample)); + } + } +} + +static void +mpd_jack_write_samples(JackOutput *jd, const void *src, + unsigned num_samples) +{ + switch (jd->audio_format.format) { + case SampleFormat::S16: + mpd_jack_write_samples_16(jd, (const int16_t*)src, + num_samples); + break; + + case SampleFormat::S24_P32: + mpd_jack_write_samples_24(jd, (const int32_t*)src, + num_samples); + break; + + default: + assert(false); + gcc_unreachable(); + } +} + +static size_t +mpd_jack_play(struct audio_output *ao, const void *chunk, size_t size, + Error &error) +{ + JackOutput *jd = (JackOutput *)ao; + const size_t frame_size = jd->audio_format.GetFrameSize(); + size_t space = 0, space1; + + jd->pause = false; + + assert(size % frame_size == 0); + size /= frame_size; + + while (true) { + if (jd->shutdown) { + error.Set(jack_output_domain, + "Refusing to play, because " + "there is no client thread"); + return 0; + } + + space = jack_ringbuffer_write_space(jd->ringbuffer[0]); + for (unsigned i = 1; i < jd->audio_format.channels; ++i) { + space1 = jack_ringbuffer_write_space(jd->ringbuffer[i]); + if (space > space1) + /* send data symmetrically */ + space = space1; + } + + if (space >= jack_sample_size) + break; + + /* XXX do something more intelligent to + synchronize */ + g_usleep(1000); + } + + space /= jack_sample_size; + if (space < size) + size = space; + + mpd_jack_write_samples(jd, chunk, size); + return size * frame_size; +} + +static bool +mpd_jack_pause(struct audio_output *ao) +{ + JackOutput *jd = (JackOutput *)ao; + + if (jd->shutdown) + return false; + + jd->pause = true; + + return true; +} + +const struct audio_output_plugin jack_output_plugin = { + "jack", + mpd_jack_test_default_device, + mpd_jack_init, + mpd_jack_finish, + mpd_jack_enable, + mpd_jack_disable, + mpd_jack_open, + mpd_jack_close, + mpd_jack_delay, + nullptr, + mpd_jack_play, + nullptr, + nullptr, + mpd_jack_pause, + nullptr, +}; diff --git a/src/output/plugins/JackOutputPlugin.hxx b/src/output/plugins/JackOutputPlugin.hxx new file mode 100644 index 000000000..ee3fe9238 --- /dev/null +++ b/src/output/plugins/JackOutputPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_JACK_OUTPUT_PLUGIN_HXX +#define MPD_JACK_OUTPUT_PLUGIN_HXX + +extern const struct audio_output_plugin jack_output_plugin; + +#endif diff --git a/src/output/plugins/NullOutputPlugin.cxx b/src/output/plugins/NullOutputPlugin.cxx new file mode 100644 index 000000000..c336d86e6 --- /dev/null +++ b/src/output/plugins/NullOutputPlugin.cxx @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "NullOutputPlugin.hxx" +#include "../OutputAPI.hxx" +#include "Timer.hxx" + +struct NullOutput { + struct audio_output base; + + bool sync; + + Timer *timer; + + bool Initialize(const config_param ¶m, Error &error) { + return ao_base_init(&base, &null_output_plugin, param, + error); + } + + void Deinitialize() { + ao_base_finish(&base); + } +}; + +static struct audio_output * +null_init(const config_param ¶m, Error &error) +{ + NullOutput *nd = new NullOutput(); + + if (!nd->Initialize(param, error)) { + delete nd; + return nullptr; + } + + nd->sync = param.GetBlockValue("sync", true); + + return &nd->base; +} + +static void +null_finish(struct audio_output *ao) +{ + NullOutput *nd = (NullOutput *)ao; + + nd->Deinitialize(); + delete nd; +} + +static bool +null_open(struct audio_output *ao, AudioFormat &audio_format, + gcc_unused Error &error) +{ + NullOutput *nd = (NullOutput *)ao; + + if (nd->sync) + nd->timer = new Timer(audio_format); + + return true; +} + +static void +null_close(struct audio_output *ao) +{ + NullOutput *nd = (NullOutput *)ao; + + if (nd->sync) + delete nd->timer; +} + +static unsigned +null_delay(struct audio_output *ao) +{ + NullOutput *nd = (NullOutput *)ao; + + return nd->sync && nd->timer->IsStarted() + ? nd->timer->GetDelay() + : 0; +} + +static size_t +null_play(struct audio_output *ao, gcc_unused const void *chunk, size_t size, + gcc_unused Error &error) +{ + NullOutput *nd = (NullOutput *)ao; + Timer *timer = nd->timer; + + if (!nd->sync) + return size; + + if (!timer->IsStarted()) + timer->Start(); + timer->Add(size); + + return size; +} + +static void +null_cancel(struct audio_output *ao) +{ + NullOutput *nd = (NullOutput *)ao; + + if (!nd->sync) + return; + + nd->timer->Reset(); +} + +const struct audio_output_plugin null_output_plugin = { + "null", + nullptr, + null_init, + null_finish, + nullptr, + nullptr, + null_open, + null_close, + null_delay, + nullptr, + null_play, + nullptr, + null_cancel, + nullptr, + nullptr, +}; diff --git a/src/output/plugins/NullOutputPlugin.hxx b/src/output/plugins/NullOutputPlugin.hxx new file mode 100644 index 000000000..05b8ef3d8 --- /dev/null +++ b/src/output/plugins/NullOutputPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_NULL_OUTPUT_PLUGIN_HXX +#define MPD_NULL_OUTPUT_PLUGIN_HXX + +extern const struct audio_output_plugin null_output_plugin; + +#endif diff --git a/src/output/plugins/OSXOutputPlugin.cxx b/src/output/plugins/OSXOutputPlugin.cxx new file mode 100644 index 000000000..c247336d7 --- /dev/null +++ b/src/output/plugins/OSXOutputPlugin.cxx @@ -0,0 +1,428 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "OSXOutputPlugin.hxx" +#include "../OutputAPI.hxx" +#include "util/DynamicFifoBuffer.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "thread/Mutex.hxx" +#include "thread/Cond.hxx" +#include "system/ByteOrder.hxx" +#include "Log.hxx" + +#include +#include +#include + +struct OSXOutput { + struct audio_output base; + + /* configuration settings */ + OSType component_subtype; + /* only applicable with kAudioUnitSubType_HALOutput */ + const char *device_name; + + AudioUnit au; + Mutex mutex; + Cond condition; + + DynamicFifoBuffer *buffer; +}; + +static constexpr Domain osx_output_domain("osx_output"); + +static bool +osx_output_test_default_device(void) +{ + /* on a Mac, this is always the default plugin, if nothing + else is configured */ + return true; +} + +static void +osx_output_configure(OSXOutput *oo, const config_param ¶m) +{ + const char *device = param.GetBlockValue("device"); + + if (device == NULL || 0 == strcmp(device, "default")) { + oo->component_subtype = kAudioUnitSubType_DefaultOutput; + oo->device_name = NULL; + } + else if (0 == strcmp(device, "system")) { + oo->component_subtype = kAudioUnitSubType_SystemOutput; + oo->device_name = NULL; + } + else { + oo->component_subtype = kAudioUnitSubType_HALOutput; + /* XXX am I supposed to strdup() this? */ + oo->device_name = device; + } +} + +static struct audio_output * +osx_output_init(const config_param ¶m, Error &error) +{ + OSXOutput *oo = new OSXOutput(); + if (!ao_base_init(&oo->base, &osx_output_plugin, param, error)) { + delete oo; + return NULL; + } + + osx_output_configure(oo, param); + + return &oo->base; +} + +static void +osx_output_finish(struct audio_output *ao) +{ + OSXOutput *oo = (OSXOutput *)ao; + + delete oo; +} + +static bool +osx_output_set_device(OSXOutput *oo, Error &error) +{ + bool ret = true; + OSStatus status; + UInt32 size, numdevices; + AudioDeviceID *deviceids = NULL; + char name[256]; + unsigned int i; + + if (oo->component_subtype != kAudioUnitSubType_HALOutput) + goto done; + + /* how many audio devices are there? */ + status = AudioHardwareGetPropertyInfo(kAudioHardwarePropertyDevices, + &size, + NULL); + if (status != noErr) { + error.Format(osx_output_domain, status, + "Unable to determine number of OS X audio devices: %s", + GetMacOSStatusCommentString(status)); + ret = false; + goto done; + } + + /* what are the available audio device IDs? */ + numdevices = size / sizeof(AudioDeviceID); + deviceids = new AudioDeviceID[numdevices]; + status = AudioHardwareGetProperty(kAudioHardwarePropertyDevices, + &size, + deviceids); + if (status != noErr) { + error.Format(osx_output_domain, status, + "Unable to determine OS X audio device IDs: %s", + GetMacOSStatusCommentString(status)); + ret = false; + goto done; + } + + /* which audio device matches oo->device_name? */ + for (i = 0; i < numdevices; i++) { + size = sizeof(name); + status = AudioDeviceGetProperty(deviceids[i], 0, false, + kAudioDevicePropertyDeviceName, + &size, name); + if (status != noErr) { + error.Format(osx_output_domain, status, + "Unable to determine OS X device name " + "(device %u): %s", + (unsigned int) deviceids[i], + GetMacOSStatusCommentString(status)); + ret = false; + goto done; + } + if (strcmp(oo->device_name, name) == 0) { + FormatDebug(osx_output_domain, + "found matching device: ID=%u, name=%s", + (unsigned)deviceids[i], name); + break; + } + } + if (i == numdevices) { + FormatWarning(osx_output_domain, + "Found no audio device with name '%s' " + "(will use default audio device)", + oo->device_name); + goto done; + } + + status = AudioUnitSetProperty(oo->au, + kAudioOutputUnitProperty_CurrentDevice, + kAudioUnitScope_Global, + 0, + &(deviceids[i]), + sizeof(AudioDeviceID)); + if (status != noErr) { + error.Format(osx_output_domain, status, + "Unable to set OS X audio output device: %s", + GetMacOSStatusCommentString(status)); + ret = false; + goto done; + } + + FormatDebug(osx_output_domain, + "set OS X audio output device ID=%u, name=%s", + (unsigned)deviceids[i], name); + +done: + delete[] deviceids; + return ret; +} + +static OSStatus +osx_render(void *vdata, + gcc_unused AudioUnitRenderActionFlags *io_action_flags, + gcc_unused const AudioTimeStamp *in_timestamp, + gcc_unused UInt32 in_bus_number, + gcc_unused UInt32 in_number_frames, + AudioBufferList *buffer_list) +{ + OSXOutput *od = (OSXOutput *) vdata; + AudioBuffer *buffer = &buffer_list->mBuffers[0]; + size_t buffer_size = buffer->mDataByteSize; + + assert(od->buffer != NULL); + + od->mutex.lock(); + + auto src = od->buffer->Read(); + if (!src.IsEmpty()) { + if (src.size > buffer_size) + src.size = buffer_size; + + memcpy(buffer->mData, src.data, src.size); + od->buffer->Consume(src.size); + } + + od->condition.signal(); + od->mutex.unlock(); + + buffer->mDataByteSize = src.size; + + unsigned i; + for (i = 1; i < buffer_list->mNumberBuffers; ++i) { + buffer = &buffer_list->mBuffers[i]; + buffer->mDataByteSize = 0; + } + + return 0; +} + +static bool +osx_output_enable(struct audio_output *ao, Error &error) +{ + OSXOutput *oo = (OSXOutput *)ao; + + ComponentDescription desc; + desc.componentType = kAudioUnitType_Output; + desc.componentSubType = oo->component_subtype; + desc.componentManufacturer = kAudioUnitManufacturer_Apple; + desc.componentFlags = 0; + desc.componentFlagsMask = 0; + + Component comp = FindNextComponent(NULL, &desc); + if (comp == 0) { + error.Set(osx_output_domain, + "Error finding OS X component"); + return false; + } + + OSStatus status = OpenAComponent(comp, &oo->au); + if (status != noErr) { + error.Format(osx_output_domain, status, + "Unable to open OS X component: %s", + GetMacOSStatusCommentString(status)); + return false; + } + + if (!osx_output_set_device(oo, error)) { + CloseComponent(oo->au); + return false; + } + + AURenderCallbackStruct callback; + callback.inputProc = osx_render; + callback.inputProcRefCon = oo; + + ComponentResult result = + AudioUnitSetProperty(oo->au, + kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, 0, + &callback, sizeof(callback)); + if (result != noErr) { + CloseComponent(oo->au); + error.Set(osx_output_domain, result, + "unable to set callback for OS X audio unit"); + return false; + } + + return true; +} + +static void +osx_output_disable(struct audio_output *ao) +{ + OSXOutput *oo = (OSXOutput *)ao; + + CloseComponent(oo->au); +} + +static void +osx_output_cancel(struct audio_output *ao) +{ + OSXOutput *od = (OSXOutput *)ao; + + const ScopeLock protect(od->mutex); + od->buffer->Clear(); +} + +static void +osx_output_close(struct audio_output *ao) +{ + OSXOutput *od = (OSXOutput *)ao; + + AudioOutputUnitStop(od->au); + AudioUnitUninitialize(od->au); + + delete od->buffer; +} + +static bool +osx_output_open(struct audio_output *ao, AudioFormat &audio_format, + Error &error) +{ + OSXOutput *od = (OSXOutput *)ao; + + AudioStreamBasicDescription stream_description; + stream_description.mSampleRate = audio_format.sample_rate; + stream_description.mFormatID = kAudioFormatLinearPCM; + stream_description.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger; + + switch (audio_format.format) { + case SampleFormat::S8: + stream_description.mBitsPerChannel = 8; + break; + + case SampleFormat::S16: + stream_description.mBitsPerChannel = 16; + break; + + case SampleFormat::S32: + stream_description.mBitsPerChannel = 32; + break; + + default: + audio_format.format = SampleFormat::S32; + stream_description.mBitsPerChannel = 32; + break; + } + + if (IsBigEndian()) + stream_description.mFormatFlags |= kLinearPCMFormatFlagIsBigEndian; + + stream_description.mBytesPerPacket = audio_format.GetFrameSize(); + stream_description.mFramesPerPacket = 1; + stream_description.mBytesPerFrame = stream_description.mBytesPerPacket; + stream_description.mChannelsPerFrame = audio_format.channels; + + ComponentResult result = + AudioUnitSetProperty(od->au, kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, 0, + &stream_description, + sizeof(stream_description)); + if (result != noErr) { + error.Set(osx_output_domain, result, + "Unable to set format on OS X device"); + return false; + } + + OSStatus status = AudioUnitInitialize(od->au); + if (status != noErr) { + error.Format(osx_output_domain, status, + "Unable to initialize OS X audio unit: %s", + GetMacOSStatusCommentString(status)); + return false; + } + + /* create a buffer of 1s */ + od->buffer = new DynamicFifoBuffer(audio_format.sample_rate * + audio_format.GetFrameSize()); + + status = AudioOutputUnitStart(od->au); + if (status != 0) { + AudioUnitUninitialize(od->au); + error.Format(osx_output_domain, status, + "unable to start audio output: %s", + GetMacOSStatusCommentString(status)); + return false; + } + + return true; +} + +static size_t +osx_output_play(struct audio_output *ao, const void *chunk, size_t size, + gcc_unused Error &error) +{ + OSXOutput *od = (OSXOutput *)ao; + + const ScopeLock protect(od->mutex); + + DynamicFifoBuffer::Range dest; + while (true) { + dest = od->buffer->Write(); + if (!dest.IsEmpty()) + break; + + /* wait for some free space in the buffer */ + od->condition.wait(od->mutex); + } + + if (size > dest.size) + size = dest.size; + + memcpy(dest.data, chunk, size); + od->buffer->Append(size); + + return size; +} + +const struct audio_output_plugin osx_output_plugin = { + "osx", + osx_output_test_default_device, + osx_output_init, + osx_output_finish, + osx_output_enable, + osx_output_disable, + osx_output_open, + osx_output_close, + nullptr, + nullptr, + osx_output_play, + nullptr, + osx_output_cancel, + nullptr, + nullptr, +}; diff --git a/src/output/plugins/OSXOutputPlugin.hxx b/src/output/plugins/OSXOutputPlugin.hxx new file mode 100644 index 000000000..0de10f83e --- /dev/null +++ b/src/output/plugins/OSXOutputPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OSX_OUTPUT_PLUGIN_HXX +#define MPD_OSX_OUTPUT_PLUGIN_HXX + +extern const struct audio_output_plugin osx_output_plugin; + +#endif diff --git a/src/output/plugins/OpenALOutputPlugin.cxx b/src/output/plugins/OpenALOutputPlugin.cxx new file mode 100644 index 000000000..f590f0ea0 --- /dev/null +++ b/src/output/plugins/OpenALOutputPlugin.cxx @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "OpenALOutputPlugin.hxx" +#include "../OutputAPI.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" + +#include + +#ifndef __APPLE__ +#include +#include +#else +#include +#include +#endif + +/* should be enough for buffer size = 2048 */ +#define NUM_BUFFERS 16 + +struct OpenALOutput { + struct audio_output base; + + const char *device_name; + ALCdevice *device; + ALCcontext *context; + ALuint buffers[NUM_BUFFERS]; + unsigned filled; + ALuint source; + ALenum format; + ALuint frequency; + + bool Initialize(const config_param ¶m, Error &error_r) { + return ao_base_init(&base, &openal_output_plugin, param, + error_r); + } + + void Deinitialize() { + ao_base_finish(&base); + } +}; + +static constexpr Domain openal_output_domain("openal_output"); + +static ALenum +openal_audio_format(AudioFormat &audio_format) +{ + /* note: cannot map SampleFormat::S8 to AL_FORMAT_STEREO8 or + AL_FORMAT_MONO8 since OpenAL expects unsigned 8 bit + samples, while MPD uses signed samples */ + + switch (audio_format.format) { + case SampleFormat::S16: + if (audio_format.channels == 2) + return AL_FORMAT_STEREO16; + if (audio_format.channels == 1) + return AL_FORMAT_MONO16; + + /* fall back to mono */ + audio_format.channels = 1; + return openal_audio_format(audio_format); + + default: + /* fall back to 16 bit */ + audio_format.format = SampleFormat::S16; + return openal_audio_format(audio_format); + } +} + +gcc_pure +static inline ALint +openal_get_source_i(const OpenALOutput *od, ALenum param) +{ + ALint value; + alGetSourcei(od->source, param, &value); + return value; +} + +gcc_pure +static inline bool +openal_has_processed(const OpenALOutput *od) +{ + return openal_get_source_i(od, AL_BUFFERS_PROCESSED) > 0; +} + +gcc_pure +static inline ALint +openal_is_playing(const OpenALOutput *od) +{ + return openal_get_source_i(od, AL_SOURCE_STATE) == AL_PLAYING; +} + +static bool +openal_setup_context(OpenALOutput *od, Error &error) +{ + od->device = alcOpenDevice(od->device_name); + + if (od->device == nullptr) { + error.Format(openal_output_domain, + "Error opening OpenAL device \"%s\"", + od->device_name); + return false; + } + + od->context = alcCreateContext(od->device, nullptr); + + if (od->context == nullptr) { + error.Format(openal_output_domain, + "Error creating context for \"%s\"", + od->device_name); + alcCloseDevice(od->device); + return false; + } + + return true; +} + +static struct audio_output * +openal_init(const config_param ¶m, Error &error) +{ + const char *device_name = param.GetBlockValue("device"); + if (device_name == nullptr) { + device_name = alcGetString(nullptr, ALC_DEFAULT_DEVICE_SPECIFIER); + } + + OpenALOutput *od = new OpenALOutput(); + if (!od->Initialize(param, error)) { + delete od; + return nullptr; + } + + od->device_name = device_name; + + return &od->base; +} + +static void +openal_finish(struct audio_output *ao) +{ + OpenALOutput *od = (OpenALOutput *)ao; + + od->Deinitialize(); + delete od; +} + +static bool +openal_open(struct audio_output *ao, AudioFormat &audio_format, + Error &error) +{ + OpenALOutput *od = (OpenALOutput *)ao; + + od->format = openal_audio_format(audio_format); + + if (!openal_setup_context(od, error)) { + return false; + } + + alcMakeContextCurrent(od->context); + alGenBuffers(NUM_BUFFERS, od->buffers); + + if (alGetError() != AL_NO_ERROR) { + error.Set(openal_output_domain, "Failed to generate buffers"); + return false; + } + + alGenSources(1, &od->source); + + if (alGetError() != AL_NO_ERROR) { + error.Set(openal_output_domain, "Failed to generate source"); + alDeleteBuffers(NUM_BUFFERS, od->buffers); + return false; + } + + od->filled = 0; + od->frequency = audio_format.sample_rate; + + return true; +} + +static void +openal_close(struct audio_output *ao) +{ + OpenALOutput *od = (OpenALOutput *)ao; + + alcMakeContextCurrent(od->context); + alDeleteSources(1, &od->source); + alDeleteBuffers(NUM_BUFFERS, od->buffers); + alcDestroyContext(od->context); + alcCloseDevice(od->device); +} + +static unsigned +openal_delay(struct audio_output *ao) +{ + OpenALOutput *od = (OpenALOutput *)ao; + + return od->filled < NUM_BUFFERS || openal_has_processed(od) + ? 0 + /* we don't know exactly how long we must wait for the + next buffer to finish, so this is a random + guess: */ + : 50; +} + +static size_t +openal_play(struct audio_output *ao, const void *chunk, size_t size, + gcc_unused Error &error) +{ + OpenALOutput *od = (OpenALOutput *)ao; + ALuint buffer; + + if (alcGetCurrentContext() != od->context) { + alcMakeContextCurrent(od->context); + } + + if (od->filled < NUM_BUFFERS) { + /* fill all buffers */ + buffer = od->buffers[od->filled]; + od->filled++; + } else { + /* wait for processed buffer */ + while (!openal_has_processed(od)) + g_usleep(10); + + alSourceUnqueueBuffers(od->source, 1, &buffer); + } + + alBufferData(buffer, od->format, chunk, size, od->frequency); + alSourceQueueBuffers(od->source, 1, &buffer); + + if (!openal_is_playing(od)) + alSourcePlay(od->source); + + return size; +} + +static void +openal_cancel(struct audio_output *ao) +{ + OpenALOutput *od = (OpenALOutput *)ao; + + od->filled = 0; + alcMakeContextCurrent(od->context); + alSourceStop(od->source); + + /* force-unqueue all buffers */ + alSourcei(od->source, AL_BUFFER, 0); + od->filled = 0; +} + +const struct audio_output_plugin openal_output_plugin = { + "openal", + nullptr, + openal_init, + openal_finish, + nullptr, + nullptr, + openal_open, + openal_close, + openal_delay, + nullptr, + openal_play, + nullptr, + openal_cancel, + nullptr, + nullptr, +}; diff --git a/src/output/plugins/OpenALOutputPlugin.hxx b/src/output/plugins/OpenALOutputPlugin.hxx new file mode 100644 index 000000000..eb43d1aa5 --- /dev/null +++ b/src/output/plugins/OpenALOutputPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OPENAL_OUTPUT_PLUGIN_HXX +#define MPD_OPENAL_OUTPUT_PLUGIN_HXX + +extern const struct audio_output_plugin openal_output_plugin; + +#endif diff --git a/src/output/plugins/OssOutputPlugin.cxx b/src/output/plugins/OssOutputPlugin.cxx new file mode 100644 index 000000000..cdf055df9 --- /dev/null +++ b/src/output/plugins/OssOutputPlugin.cxx @@ -0,0 +1,776 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "OssOutputPlugin.hxx" +#include "../OutputAPI.hxx" +#include "MixerList.hxx" +#include "system/fd_util.h" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "util/Macros.hxx" +#include "system/ByteOrder.hxx" +#include "Log.hxx" + +#include +#include +#include +#include +#include +#include +#include + +#if defined(__OpenBSD__) || defined(__NetBSD__) +# include +#else /* !(defined(__OpenBSD__) || defined(__NetBSD__) */ +# include +#endif /* !(defined(__OpenBSD__) || defined(__NetBSD__) */ + +/* We got bug reports from FreeBSD users who said that the two 24 bit + formats generate white noise on FreeBSD, but 32 bit works. This is + a workaround until we know what exactly is expected by the kernel + audio drivers. */ +#ifndef __linux__ +#undef AFMT_S24_PACKED +#undef AFMT_S24_NE +#endif + +#ifdef AFMT_S24_PACKED +#include "pcm/PcmExport.hxx" +#include "util/Manual.hxx" +#endif + +struct OssOutput { + struct audio_output base; + +#ifdef AFMT_S24_PACKED + Manual pcm_export; +#endif + + int fd; + const char *device; + + /** + * The current input audio format. This is needed to reopen + * the device after cancel(). + */ + AudioFormat audio_format; + + /** + * The current OSS audio format. This is needed to reopen the + * device after cancel(). + */ + int oss_format; + + OssOutput():fd(-1), device(nullptr) {} + + bool Initialize(const config_param ¶m, Error &error_r) { + return ao_base_init(&base, &oss_output_plugin, param, + error_r); + } + + void Deinitialize() { + ao_base_finish(&base); + } +}; + +static constexpr Domain oss_output_domain("oss_output"); + +enum oss_stat { + OSS_STAT_NO_ERROR = 0, + OSS_STAT_NOT_CHAR_DEV = -1, + OSS_STAT_NO_PERMS = -2, + OSS_STAT_DOESN_T_EXIST = -3, + OSS_STAT_OTHER = -4, +}; + +static enum oss_stat +oss_stat_device(const char *device, int *errno_r) +{ + struct stat st; + + if (0 == stat(device, &st)) { + if (!S_ISCHR(st.st_mode)) { + return OSS_STAT_NOT_CHAR_DEV; + } + } else { + *errno_r = errno; + + switch (errno) { + case ENOENT: + case ENOTDIR: + return OSS_STAT_DOESN_T_EXIST; + case EACCES: + return OSS_STAT_NO_PERMS; + default: + return OSS_STAT_OTHER; + } + } + + return OSS_STAT_NO_ERROR; +} + +static const char *default_devices[] = { "/dev/sound/dsp", "/dev/dsp" }; + +static bool +oss_output_test_default_device(void) +{ + int fd, i; + + for (i = ARRAY_SIZE(default_devices); --i >= 0; ) { + fd = open_cloexec(default_devices[i], O_WRONLY, 0); + + if (fd >= 0) { + close(fd); + return true; + } + + FormatErrno(oss_output_domain, + "Error opening OSS device \"%s\"", + default_devices[i]); + } + + return false; +} + +static struct audio_output * +oss_open_default(Error &error) +{ + int err[ARRAY_SIZE(default_devices)]; + enum oss_stat ret[ARRAY_SIZE(default_devices)]; + + const config_param empty; + for (int i = ARRAY_SIZE(default_devices); --i >= 0; ) { + ret[i] = oss_stat_device(default_devices[i], &err[i]); + if (ret[i] == OSS_STAT_NO_ERROR) { + OssOutput *od = new OssOutput(); + if (!od->Initialize(empty, error)) { + delete od; + return NULL; + } + + od->device = default_devices[i]; + return &od->base; + } + } + + for (int i = ARRAY_SIZE(default_devices); --i >= 0; ) { + const char *dev = default_devices[i]; + switch(ret[i]) { + case OSS_STAT_NO_ERROR: + /* never reached */ + break; + case OSS_STAT_DOESN_T_EXIST: + FormatWarning(oss_output_domain, + "%s not found", dev); + break; + case OSS_STAT_NOT_CHAR_DEV: + FormatWarning(oss_output_domain, + "%s is not a character device", dev); + break; + case OSS_STAT_NO_PERMS: + FormatWarning(oss_output_domain, + "%s: permission denied", dev); + break; + case OSS_STAT_OTHER: + FormatErrno(oss_output_domain, err[i], + "Error accessing %s", dev); + } + } + + error.Set(oss_output_domain, + "error trying to open default OSS device"); + return NULL; +} + +static struct audio_output * +oss_output_init(const config_param ¶m, Error &error) +{ + const char *device = param.GetBlockValue("device"); + if (device != NULL) { + OssOutput *od = new OssOutput(); + if (!od->Initialize(param, error)) { + delete od; + return NULL; + } + + od->device = device; + return &od->base; + } + + return oss_open_default(error); +} + +static void +oss_output_finish(struct audio_output *ao) +{ + OssOutput *od = (OssOutput *)ao; + + ao_base_finish(&od->base); + delete od; +} + +#ifdef AFMT_S24_PACKED + +static bool +oss_output_enable(struct audio_output *ao, gcc_unused Error &error) +{ + OssOutput *od = (OssOutput *)ao; + + od->pcm_export.Construct(); + return true; +} + +static void +oss_output_disable(struct audio_output *ao) +{ + OssOutput *od = (OssOutput *)ao; + + od->pcm_export.Destruct(); +} + +#endif + +static void +oss_close(OssOutput *od) +{ + if (od->fd >= 0) + close(od->fd); + od->fd = -1; +} + +/** + * A tri-state type for oss_try_ioctl(). + */ +enum oss_setup_result { + SUCCESS, + ERROR, + UNSUPPORTED, +}; + +/** + * Invoke an ioctl on the OSS file descriptor. On success, SUCCESS is + * returned. If the parameter is not supported, UNSUPPORTED is + * returned. Any other failure returns ERROR and allocates an #Error. + */ +static enum oss_setup_result +oss_try_ioctl_r(int fd, unsigned long request, int *value_r, + const char *msg, Error &error) +{ + assert(fd >= 0); + assert(value_r != NULL); + assert(msg != NULL); + assert(!error.IsDefined()); + + int ret = ioctl(fd, request, value_r); + if (ret >= 0) + return SUCCESS; + + if (errno == EINVAL) + return UNSUPPORTED; + + error.SetErrno(msg); + return ERROR; +} + +/** + * Invoke an ioctl on the OSS file descriptor. On success, SUCCESS is + * returned. If the parameter is not supported, UNSUPPORTED is + * returned. Any other failure returns ERROR and allocates an #Error. + */ +static enum oss_setup_result +oss_try_ioctl(int fd, unsigned long request, int value, + const char *msg, Error &error_r) +{ + return oss_try_ioctl_r(fd, request, &value, msg, error_r); +} + +/** + * Set up the channel number, and attempts to find alternatives if the + * specified number is not supported. + */ +static bool +oss_setup_channels(int fd, AudioFormat &audio_format, Error &error) +{ + const char *const msg = "Failed to set channel count"; + int channels = audio_format.channels; + enum oss_setup_result result = + oss_try_ioctl_r(fd, SNDCTL_DSP_CHANNELS, &channels, msg, error); + switch (result) { + case SUCCESS: + if (!audio_valid_channel_count(channels)) + break; + + audio_format.channels = channels; + return true; + + case ERROR: + return false; + + case UNSUPPORTED: + break; + } + + for (unsigned i = 1; i < 2; ++i) { + if (i == audio_format.channels) + /* don't try that again */ + continue; + + channels = i; + result = oss_try_ioctl_r(fd, SNDCTL_DSP_CHANNELS, &channels, + msg, error); + switch (result) { + case SUCCESS: + if (!audio_valid_channel_count(channels)) + break; + + audio_format.channels = channels; + return true; + + case ERROR: + return false; + + case UNSUPPORTED: + break; + } + } + + error.Set(oss_output_domain, msg); + return false; +} + +/** + * Set up the sample rate, and attempts to find alternatives if the + * specified sample rate is not supported. + */ +static bool +oss_setup_sample_rate(int fd, AudioFormat &audio_format, + Error &error) +{ + const char *const msg = "Failed to set sample rate"; + int sample_rate = audio_format.sample_rate; + enum oss_setup_result result = + oss_try_ioctl_r(fd, SNDCTL_DSP_SPEED, &sample_rate, + msg, error); + switch (result) { + case SUCCESS: + if (!audio_valid_sample_rate(sample_rate)) + break; + + audio_format.sample_rate = sample_rate; + return true; + + case ERROR: + return false; + + case UNSUPPORTED: + break; + } + + static const int sample_rates[] = { 48000, 44100, 0 }; + for (unsigned i = 0; sample_rates[i] != 0; ++i) { + sample_rate = sample_rates[i]; + if (sample_rate == (int)audio_format.sample_rate) + continue; + + result = oss_try_ioctl_r(fd, SNDCTL_DSP_SPEED, &sample_rate, + msg, error); + switch (result) { + case SUCCESS: + if (!audio_valid_sample_rate(sample_rate)) + break; + + audio_format.sample_rate = sample_rate; + return true; + + case ERROR: + return false; + + case UNSUPPORTED: + break; + } + } + + error.Set(oss_output_domain, msg); + return false; +} + +/** + * Convert a MPD sample format to its OSS counterpart. Returns + * AFMT_QUERY if there is no direct counterpart. + */ +static int +sample_format_to_oss(SampleFormat format) +{ + switch (format) { + case SampleFormat::UNDEFINED: + case SampleFormat::FLOAT: + case SampleFormat::DSD: + return AFMT_QUERY; + + case SampleFormat::S8: + return AFMT_S8; + + case SampleFormat::S16: + return AFMT_S16_NE; + + case SampleFormat::S24_P32: +#ifdef AFMT_S24_NE + return AFMT_S24_NE; +#else + return AFMT_QUERY; +#endif + + case SampleFormat::S32: +#ifdef AFMT_S32_NE + return AFMT_S32_NE; +#else + return AFMT_QUERY; +#endif + } + + return AFMT_QUERY; +} + +/** + * Convert an OSS sample format to its MPD counterpart. Returns + * SampleFormat::UNDEFINED if there is no direct counterpart. + */ +static SampleFormat +sample_format_from_oss(int format) +{ + switch (format) { + case AFMT_S8: + return SampleFormat::S8; + + case AFMT_S16_NE: + return SampleFormat::S16; + +#ifdef AFMT_S24_PACKED + case AFMT_S24_PACKED: + return SampleFormat::S24_P32; +#endif + +#ifdef AFMT_S24_NE + case AFMT_S24_NE: + return SampleFormat::S24_P32; +#endif + +#ifdef AFMT_S32_NE + case AFMT_S32_NE: + return SampleFormat::S32; +#endif + + default: + return SampleFormat::UNDEFINED; + } +} + +/** + * Probe one sample format. + * + * @return the selected sample format or SampleFormat::UNDEFINED on + * error + */ +static enum oss_setup_result +oss_probe_sample_format(int fd, SampleFormat sample_format, + SampleFormat *sample_format_r, + int *oss_format_r, +#ifdef AFMT_S24_PACKED + PcmExport &pcm_export, +#endif + Error &error) +{ + int oss_format = sample_format_to_oss(sample_format); + if (oss_format == AFMT_QUERY) + return UNSUPPORTED; + + enum oss_setup_result result = + oss_try_ioctl_r(fd, SNDCTL_DSP_SAMPLESIZE, + &oss_format, + "Failed to set sample format", error); + +#ifdef AFMT_S24_PACKED + if (result == UNSUPPORTED && sample_format == SampleFormat::S24_P32) { + /* if the driver doesn't support padded 24 bit, try + packed 24 bit */ + oss_format = AFMT_S24_PACKED; + result = oss_try_ioctl_r(fd, SNDCTL_DSP_SAMPLESIZE, + &oss_format, + "Failed to set sample format", error); + } +#endif + + if (result != SUCCESS) + return result; + + sample_format = sample_format_from_oss(oss_format); + if (sample_format == SampleFormat::UNDEFINED) + return UNSUPPORTED; + + *sample_format_r = sample_format; + *oss_format_r = oss_format; + +#ifdef AFMT_S24_PACKED + pcm_export.Open(sample_format, 0, false, false, + oss_format == AFMT_S24_PACKED, + oss_format == AFMT_S24_PACKED && + !IsLittleEndian()); +#endif + + return SUCCESS; +} + +/** + * Set up the sample format, and attempts to find alternatives if the + * specified format is not supported. + */ +static bool +oss_setup_sample_format(int fd, AudioFormat &audio_format, + int *oss_format_r, +#ifdef AFMT_S24_PACKED + PcmExport &pcm_export, +#endif + Error &error) +{ + SampleFormat mpd_format; + enum oss_setup_result result = + oss_probe_sample_format(fd, audio_format.format, + &mpd_format, oss_format_r, +#ifdef AFMT_S24_PACKED + pcm_export, +#endif + error); + switch (result) { + case SUCCESS: + audio_format.format = mpd_format; + return true; + + case ERROR: + return false; + + case UNSUPPORTED: + break; + } + + if (result != UNSUPPORTED) + return result == SUCCESS; + + /* the requested sample format is not available - probe for + other formats supported by MPD */ + + static const SampleFormat sample_formats[] = { + SampleFormat::S24_P32, + SampleFormat::S32, + SampleFormat::S16, + SampleFormat::S8, + SampleFormat::UNDEFINED /* sentinel */ + }; + + for (unsigned i = 0; sample_formats[i] != SampleFormat::UNDEFINED; ++i) { + mpd_format = sample_formats[i]; + if (mpd_format == audio_format.format) + /* don't try that again */ + continue; + + result = oss_probe_sample_format(fd, mpd_format, + &mpd_format, oss_format_r, +#ifdef AFMT_S24_PACKED + pcm_export, +#endif + error); + switch (result) { + case SUCCESS: + audio_format.format = mpd_format; + return true; + + case ERROR: + return false; + + case UNSUPPORTED: + break; + } + } + + error.Set(oss_output_domain, "Failed to set sample format"); + return false; +} + +/** + * Sets up the OSS device which was opened before. + */ +static bool +oss_setup(OssOutput *od, AudioFormat &audio_format, + Error &error) +{ + return oss_setup_channels(od->fd, audio_format, error) && + oss_setup_sample_rate(od->fd, audio_format, error) && + oss_setup_sample_format(od->fd, audio_format, &od->oss_format, +#ifdef AFMT_S24_PACKED + od->pcm_export, +#endif + error); +} + +/** + * Reopen the device with the saved audio_format, without any probing. + */ +static bool +oss_reopen(OssOutput *od, Error &error) +{ + assert(od->fd < 0); + + od->fd = open_cloexec(od->device, O_WRONLY, 0); + if (od->fd < 0) { + error.FormatErrno("Error opening OSS device \"%s\"", + od->device); + return false; + } + + enum oss_setup_result result; + + const char *const msg1 = "Failed to set channel count"; + result = oss_try_ioctl(od->fd, SNDCTL_DSP_CHANNELS, + od->audio_format.channels, msg1, error); + if (result != SUCCESS) { + oss_close(od); + if (result == UNSUPPORTED) + error.Set(oss_output_domain, msg1); + return false; + } + + const char *const msg2 = "Failed to set sample rate"; + result = oss_try_ioctl(od->fd, SNDCTL_DSP_SPEED, + od->audio_format.sample_rate, msg2, error); + if (result != SUCCESS) { + oss_close(od); + if (result == UNSUPPORTED) + error.Set(oss_output_domain, msg2); + return false; + } + + const char *const msg3 = "Failed to set sample format"; + result = oss_try_ioctl(od->fd, SNDCTL_DSP_SAMPLESIZE, + od->oss_format, + msg3, error); + if (result != SUCCESS) { + oss_close(od); + if (result == UNSUPPORTED) + error.Set(oss_output_domain, msg3); + return false; + } + + return true; +} + +static bool +oss_output_open(struct audio_output *ao, AudioFormat &audio_format, + Error &error) +{ + OssOutput *od = (OssOutput *)ao; + + od->fd = open_cloexec(od->device, O_WRONLY, 0); + if (od->fd < 0) { + error.FormatErrno("Error opening OSS device \"%s\"", + od->device); + return false; + } + + if (!oss_setup(od, audio_format, error)) { + oss_close(od); + return false; + } + + od->audio_format = audio_format; + return true; +} + +static void +oss_output_close(struct audio_output *ao) +{ + OssOutput *od = (OssOutput *)ao; + + oss_close(od); +} + +static void +oss_output_cancel(struct audio_output *ao) +{ + OssOutput *od = (OssOutput *)ao; + + if (od->fd >= 0) { + ioctl(od->fd, SNDCTL_DSP_RESET, 0); + oss_close(od); + } +} + +static size_t +oss_output_play(struct audio_output *ao, const void *chunk, size_t size, + Error &error) +{ + OssOutput *od = (OssOutput *)ao; + ssize_t ret; + + /* reopen the device since it was closed by dropBufferedAudio */ + if (od->fd < 0 && !oss_reopen(od, error)) + return 0; + +#ifdef AFMT_S24_PACKED + chunk = od->pcm_export->Export(chunk, size, size); +#endif + + while (true) { + ret = write(od->fd, chunk, size); + if (ret > 0) { +#ifdef AFMT_S24_PACKED + ret = od->pcm_export->CalcSourceSize(ret); +#endif + return ret; + } + + if (ret < 0 && errno != EINTR) { + error.FormatErrno("Write error on %s", od->device); + return 0; + } + } +} + +const struct audio_output_plugin oss_output_plugin = { + "oss", + oss_output_test_default_device, + oss_output_init, + oss_output_finish, +#ifdef AFMT_S24_PACKED + oss_output_enable, + oss_output_disable, +#else + nullptr, + nullptr, +#endif + oss_output_open, + oss_output_close, + nullptr, + nullptr, + oss_output_play, + nullptr, + oss_output_cancel, + nullptr, + + &oss_mixer_plugin, +}; diff --git a/src/output/plugins/OssOutputPlugin.hxx b/src/output/plugins/OssOutputPlugin.hxx new file mode 100644 index 000000000..4762fa652 --- /dev/null +++ b/src/output/plugins/OssOutputPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OSS_OUTPUT_PLUGIN_HXX +#define MPD_OSS_OUTPUT_PLUGIN_HXX + +extern const struct audio_output_plugin oss_output_plugin; + +#endif diff --git a/src/output/plugins/PipeOutputPlugin.cxx b/src/output/plugins/PipeOutputPlugin.cxx new file mode 100644 index 000000000..802e1ba4d --- /dev/null +++ b/src/output/plugins/PipeOutputPlugin.cxx @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "PipeOutputPlugin.hxx" +#include "../OutputAPI.hxx" +#include "ConfigError.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" + +#include + +#include + +struct PipeOutput { + struct audio_output base; + + std::string cmd; + FILE *fh; + + bool Initialize(const config_param ¶m, Error &error) { + return ao_base_init(&base, &pipe_output_plugin, param, + error); + } + + void Deinitialize() { + ao_base_finish(&base); + } + + bool Configure(const config_param ¶m, Error &error); +}; + +static constexpr Domain pipe_output_domain("pipe_output"); + +inline bool +PipeOutput::Configure(const config_param ¶m, Error &error) +{ + cmd = param.GetBlockValue("command", ""); + if (cmd.empty()) { + error.Set(config_domain, + "No \"command\" parameter specified"); + return false; + } + + return true; +} + +static struct audio_output * +pipe_output_init(const config_param ¶m, Error &error) +{ + PipeOutput *pd = new PipeOutput(); + + if (!pd->Initialize(param, error)) { + delete pd; + return nullptr; + } + + if (!pd->Configure(param, error)) { + pd->Deinitialize(); + delete pd; + return nullptr; + } + + return &pd->base; +} + +static void +pipe_output_finish(struct audio_output *ao) +{ + PipeOutput *pd = (PipeOutput *)ao; + + pd->Deinitialize(); + delete pd; +} + +static bool +pipe_output_open(struct audio_output *ao, + gcc_unused AudioFormat &audio_format, + Error &error) +{ + PipeOutput *pd = (PipeOutput *)ao; + + pd->fh = popen(pd->cmd.c_str(), "w"); + if (pd->fh == nullptr) { + error.FormatErrno("Error opening pipe \"%s\"", + pd->cmd.c_str()); + return false; + } + + return true; +} + +static void +pipe_output_close(struct audio_output *ao) +{ + PipeOutput *pd = (PipeOutput *)ao; + + pclose(pd->fh); +} + +static size_t +pipe_output_play(struct audio_output *ao, const void *chunk, size_t size, + Error &error) +{ + PipeOutput *pd = (PipeOutput *)ao; + size_t ret; + + ret = fwrite(chunk, 1, size, pd->fh); + if (ret == 0) + error.SetErrno("Write error on pipe"); + + return ret; +} + +const struct audio_output_plugin pipe_output_plugin = { + "pipe", + nullptr, + pipe_output_init, + pipe_output_finish, + nullptr, + nullptr, + pipe_output_open, + pipe_output_close, + nullptr, + nullptr, + pipe_output_play, + nullptr, + nullptr, + nullptr, + nullptr, +}; diff --git a/src/output/plugins/PipeOutputPlugin.hxx b/src/output/plugins/PipeOutputPlugin.hxx new file mode 100644 index 000000000..42b01b9f7 --- /dev/null +++ b/src/output/plugins/PipeOutputPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_PIPE_OUTPUT_PLUGIN_HXX +#define MPD_PIPE_OUTPUT_PLUGIN_HXX + +extern const struct audio_output_plugin pipe_output_plugin; + +#endif diff --git a/src/output/plugins/PulseOutputPlugin.cxx b/src/output/plugins/PulseOutputPlugin.cxx new file mode 100644 index 000000000..c133d9796 --- /dev/null +++ b/src/output/plugins/PulseOutputPlugin.cxx @@ -0,0 +1,889 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "PulseOutputPlugin.hxx" +#include "../OutputAPI.hxx" +#include "MixerList.hxx" +#include "mixer/PulseMixerPlugin.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "Log.hxx" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define MPD_PULSE_NAME "Music Player Daemon" + +struct PulseOutput { + struct audio_output base; + + const char *name; + const char *server; + const char *sink; + + PulseMixer *mixer; + + struct pa_threaded_mainloop *mainloop; + struct pa_context *context; + struct pa_stream *stream; + + size_t writable; +}; + +static constexpr Domain pulse_output_domain("pulse_output"); + +static void +SetError(Error &error, pa_context *context, const char *msg) +{ + const int e = pa_context_errno(context); + error.Format(pulse_output_domain, e, "%s: %s", msg, pa_strerror(e)); +} + +void +pulse_output_lock(PulseOutput *po) +{ + pa_threaded_mainloop_lock(po->mainloop); +} + +void +pulse_output_unlock(PulseOutput *po) +{ + pa_threaded_mainloop_unlock(po->mainloop); +} + +void +pulse_output_set_mixer(PulseOutput *po, PulseMixer *pm) +{ + assert(po != nullptr); + assert(po->mixer == nullptr); + assert(pm != nullptr); + + po->mixer = pm; + + if (po->mainloop == nullptr) + return; + + pa_threaded_mainloop_lock(po->mainloop); + + if (po->context != nullptr && + pa_context_get_state(po->context) == PA_CONTEXT_READY) { + pulse_mixer_on_connect(pm, po->context); + + if (po->stream != nullptr && + pa_stream_get_state(po->stream) == PA_STREAM_READY) + pulse_mixer_on_change(pm, po->context, po->stream); + } + + pa_threaded_mainloop_unlock(po->mainloop); +} + +void +pulse_output_clear_mixer(PulseOutput *po, gcc_unused PulseMixer *pm) +{ + assert(po != nullptr); + assert(pm != nullptr); + assert(po->mixer == pm); + + po->mixer = nullptr; +} + +bool +pulse_output_set_volume(PulseOutput *po, const pa_cvolume *volume, + Error &error) +{ + pa_operation *o; + + if (po->context == nullptr || po->stream == nullptr || + pa_stream_get_state(po->stream) != PA_STREAM_READY) { + error.Set(pulse_output_domain, "disconnected"); + return false; + } + + o = pa_context_set_sink_input_volume(po->context, + pa_stream_get_index(po->stream), + volume, nullptr, nullptr); + if (o == nullptr) { + SetError(error, po->context, + "failed to set PulseAudio volume"); + return false; + } + + pa_operation_unref(o); + return true; +} + +/** + * \brief waits for a pulseaudio operation to finish, frees it and + * unlocks the mainloop + * \param operation the operation to wait for + * \return true if operation has finished normally (DONE state), + * false otherwise + */ +static bool +pulse_wait_for_operation(struct pa_threaded_mainloop *mainloop, + struct pa_operation *operation) +{ + pa_operation_state_t state; + + assert(mainloop != nullptr); + assert(operation != nullptr); + + 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(gcc_unused pa_stream *s, + gcc_unused int success, void *userdata) +{ + PulseOutput *po = (PulseOutput *)userdata; + + pa_threaded_mainloop_signal(po->mainloop, 0); +} + +static void +pulse_output_context_state_cb(struct pa_context *context, void *userdata) +{ + PulseOutput *po = (PulseOutput *)userdata; + + switch (pa_context_get_state(context)) { + case PA_CONTEXT_READY: + if (po->mixer != nullptr) + pulse_mixer_on_connect(po->mixer, context); + + pa_threaded_mainloop_signal(po->mainloop, 0); + break; + + case PA_CONTEXT_TERMINATED: + case PA_CONTEXT_FAILED: + if (po->mixer != nullptr) + pulse_mixer_on_disconnect(po->mixer); + + /* the caller thread might be waiting for these + states */ + pa_threaded_mainloop_signal(po->mainloop, 0); + break; + + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + break; + } +} + +static void +pulse_output_subscribe_cb(pa_context *context, + pa_subscription_event_type_t t, + uint32_t idx, void *userdata) +{ + PulseOutput *po = (PulseOutput *)userdata; + pa_subscription_event_type_t facility = + pa_subscription_event_type_t(t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK); + pa_subscription_event_type_t type = + pa_subscription_event_type_t(t & PA_SUBSCRIPTION_EVENT_TYPE_MASK); + + if (po->mixer != nullptr && + facility == PA_SUBSCRIPTION_EVENT_SINK_INPUT && + po->stream != nullptr && + pa_stream_get_state(po->stream) == PA_STREAM_READY && + idx == pa_stream_get_index(po->stream) && + (type == PA_SUBSCRIPTION_EVENT_NEW || + type == PA_SUBSCRIPTION_EVENT_CHANGE)) + pulse_mixer_on_change(po->mixer, context, po->stream); +} + +/** + * Attempt to connect asynchronously to the PulseAudio server. + * + * @return true on success, false on error + */ +static bool +pulse_output_connect(PulseOutput *po, Error &error) +{ + assert(po != nullptr); + assert(po->context != nullptr); + + if (pa_context_connect(po->context, po->server, + (pa_context_flags_t)0, nullptr) < 0) { + SetError(error, po->context, + "pa_context_connect() has failed"); + return false; + } + + return true; +} + +/** + * Frees and clears the stream. + */ +static void +pulse_output_delete_stream(PulseOutput *po) +{ + assert(po != nullptr); + assert(po->stream != nullptr); + + pa_stream_set_suspended_callback(po->stream, nullptr, nullptr); + + pa_stream_set_state_callback(po->stream, nullptr, nullptr); + pa_stream_set_write_callback(po->stream, nullptr, nullptr); + + pa_stream_disconnect(po->stream); + pa_stream_unref(po->stream); + po->stream = nullptr; +} + +/** + * Frees and clears the context. + * + * Caller must lock the main loop. + */ +static void +pulse_output_delete_context(PulseOutput *po) +{ + assert(po != nullptr); + assert(po->context != nullptr); + + pa_context_set_state_callback(po->context, nullptr, nullptr); + pa_context_set_subscribe_callback(po->context, nullptr, nullptr); + + pa_context_disconnect(po->context); + pa_context_unref(po->context); + po->context = nullptr; +} + +/** + * Create, set up and connect a context. + * + * Caller must lock the main loop. + * + * @return true on success, false on error + */ +static bool +pulse_output_setup_context(PulseOutput *po, Error &error) +{ + assert(po != nullptr); + assert(po->mainloop != nullptr); + + po->context = pa_context_new(pa_threaded_mainloop_get_api(po->mainloop), + MPD_PULSE_NAME); + if (po->context == nullptr) { + error.Set(pulse_output_domain, "pa_context_new() has failed"); + return false; + } + + pa_context_set_state_callback(po->context, + pulse_output_context_state_cb, po); + pa_context_set_subscribe_callback(po->context, + pulse_output_subscribe_cb, po); + + if (!pulse_output_connect(po, error)) { + pulse_output_delete_context(po); + return false; + } + + return true; +} + +static struct audio_output * +pulse_output_init(const config_param ¶m, Error &error) +{ + PulseOutput *po; + + g_setenv("PULSE_PROP_media.role", "music", true); + + po = new PulseOutput(); + if (!ao_base_init(&po->base, &pulse_output_plugin, param, error)) { + delete po; + return nullptr; + } + + po->name = param.GetBlockValue("name", "mpd_pulse"); + po->server = param.GetBlockValue("server"); + po->sink = param.GetBlockValue("sink"); + + po->mixer = nullptr; + po->mainloop = nullptr; + po->context = nullptr; + po->stream = nullptr; + + return &po->base; +} + +static void +pulse_output_finish(struct audio_output *ao) +{ + PulseOutput *po = (PulseOutput *)ao; + + ao_base_finish(&po->base); + delete po; +} + +static bool +pulse_output_enable(struct audio_output *ao, Error &error) +{ + PulseOutput *po = (PulseOutput *)ao; + + assert(po->mainloop == nullptr); + assert(po->context == nullptr); + + /* create the libpulse mainloop and start the thread */ + + po->mainloop = pa_threaded_mainloop_new(); + if (po->mainloop == nullptr) { + g_free(po); + + error.Set(pulse_output_domain, + "pa_threaded_mainloop_new() has failed"); + return false; + } + + pa_threaded_mainloop_lock(po->mainloop); + + if (pa_threaded_mainloop_start(po->mainloop) < 0) { + pa_threaded_mainloop_unlock(po->mainloop); + pa_threaded_mainloop_free(po->mainloop); + po->mainloop = nullptr; + + error.Set(pulse_output_domain, + "pa_threaded_mainloop_start() has failed"); + return false; + } + + /* create the libpulse context and connect it */ + + if (!pulse_output_setup_context(po, error)) { + pa_threaded_mainloop_unlock(po->mainloop); + pa_threaded_mainloop_stop(po->mainloop); + pa_threaded_mainloop_free(po->mainloop); + po->mainloop = nullptr; + return false; + } + + pa_threaded_mainloop_unlock(po->mainloop); + + return true; +} + +static void +pulse_output_disable(struct audio_output *ao) +{ + PulseOutput *po = (PulseOutput *)ao; + + assert(po->mainloop != nullptr); + + pa_threaded_mainloop_stop(po->mainloop); + if (po->context != nullptr) + pulse_output_delete_context(po); + pa_threaded_mainloop_free(po->mainloop); + po->mainloop = nullptr; +} + +/** + * Check if the context is (already) connected, and waits if not. If + * the context has been disconnected, retry to connect. + * + * Caller must lock the main loop. + * + * @return true on success, false on error + */ +static bool +pulse_output_wait_connection(PulseOutput *po, Error &error) +{ + assert(po->mainloop != nullptr); + + pa_context_state_t state; + + if (po->context == nullptr && !pulse_output_setup_context(po, error)) + return false; + + while (true) { + state = pa_context_get_state(po->context); + switch (state) { + case PA_CONTEXT_READY: + /* nothing to do */ + return true; + + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_TERMINATED: + case PA_CONTEXT_FAILED: + /* failure */ + SetError(error, po->context, "failed to connect"); + pulse_output_delete_context(po); + return false; + + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + /* wait some more */ + pa_threaded_mainloop_wait(po->mainloop); + break; + } + } +} + +static void +pulse_output_stream_suspended_cb(gcc_unused pa_stream *stream, void *userdata) +{ + PulseOutput *po = (PulseOutput *)userdata; + + assert(stream == po->stream || po->stream == nullptr); + assert(po->mainloop != nullptr); + + /* wake up the main loop to break out of the loop in + pulse_output_play() */ + pa_threaded_mainloop_signal(po->mainloop, 0); +} + +static void +pulse_output_stream_state_cb(pa_stream *stream, void *userdata) +{ + PulseOutput *po = (PulseOutput *)userdata; + + assert(stream == po->stream || po->stream == nullptr); + assert(po->mainloop != nullptr); + assert(po->context != nullptr); + + switch (pa_stream_get_state(stream)) { + case PA_STREAM_READY: + if (po->mixer != nullptr) + pulse_mixer_on_change(po->mixer, po->context, stream); + + pa_threaded_mainloop_signal(po->mainloop, 0); + break; + + case PA_STREAM_FAILED: + case PA_STREAM_TERMINATED: + if (po->mixer != nullptr) + pulse_mixer_on_disconnect(po->mixer); + + pa_threaded_mainloop_signal(po->mainloop, 0); + break; + + case PA_STREAM_UNCONNECTED: + case PA_STREAM_CREATING: + break; + } +} + +static void +pulse_output_stream_write_cb(gcc_unused pa_stream *stream, size_t nbytes, + void *userdata) +{ + PulseOutput *po = (PulseOutput *)userdata; + + assert(po->mainloop != nullptr); + + po->writable = nbytes; + pa_threaded_mainloop_signal(po->mainloop, 0); +} + +/** + * Create, set up and connect a context. + * + * Caller must lock the main loop. + * + * @return true on success, false on error + */ +static bool +pulse_output_setup_stream(PulseOutput *po, const pa_sample_spec *ss, + Error &error) +{ + assert(po != nullptr); + assert(po->context != nullptr); + + po->stream = pa_stream_new(po->context, po->name, ss, nullptr); + if (po->stream == nullptr) { + SetError(error, po->context, "pa_stream_new() has failed"); + return false; + } + + pa_stream_set_suspended_callback(po->stream, + pulse_output_stream_suspended_cb, po); + + pa_stream_set_state_callback(po->stream, + pulse_output_stream_state_cb, po); + pa_stream_set_write_callback(po->stream, + pulse_output_stream_write_cb, po); + + return true; +} + +static bool +pulse_output_open(struct audio_output *ao, AudioFormat &audio_format, + Error &error) +{ + PulseOutput *po = (PulseOutput *)ao; + pa_sample_spec ss; + + assert(po->mainloop != nullptr); + + pa_threaded_mainloop_lock(po->mainloop); + + if (po->context != nullptr) { + switch (pa_context_get_state(po->context)) { + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_TERMINATED: + case PA_CONTEXT_FAILED: + /* the connection was closed meanwhile; delete + it, and pulse_output_wait_connection() will + reopen it */ + pulse_output_delete_context(po); + break; + + case PA_CONTEXT_READY: + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + break; + } + } + + if (!pulse_output_wait_connection(po, error)) { + pa_threaded_mainloop_unlock(po->mainloop); + return false; + } + + /* MPD doesn't support the other pulseaudio sample formats, so + we just force MPD to send us everything as 16 bit */ + audio_format.format = SampleFormat::S16; + + ss.format = PA_SAMPLE_S16NE; + ss.rate = audio_format.sample_rate; + ss.channels = audio_format.channels; + + /* create a stream .. */ + + if (!pulse_output_setup_stream(po, &ss, error)) { + pa_threaded_mainloop_unlock(po->mainloop); + return false; + } + + /* .. and connect it (asynchronously) */ + + if (pa_stream_connect_playback(po->stream, po->sink, + nullptr, pa_stream_flags_t(0), + nullptr, nullptr) < 0) { + pulse_output_delete_stream(po); + + SetError(error, po->context, + "pa_stream_connect_playback() has failed"); + pa_threaded_mainloop_unlock(po->mainloop); + return false; + } + + pa_threaded_mainloop_unlock(po->mainloop); + + return true; +} + +static void +pulse_output_close(struct audio_output *ao) +{ + PulseOutput *po = (PulseOutput *)ao; + pa_operation *o; + + assert(po->mainloop != nullptr); + + pa_threaded_mainloop_lock(po->mainloop); + + if (pa_stream_get_state(po->stream) == PA_STREAM_READY) { + o = pa_stream_drain(po->stream, + pulse_output_stream_success_cb, po); + if (o == nullptr) { + FormatWarning(pulse_output_domain, + "pa_stream_drain() has failed: %s", + pa_strerror(pa_context_errno(po->context))); + } else + pulse_wait_for_operation(po->mainloop, o); + } + + pulse_output_delete_stream(po); + + if (po->context != nullptr && + pa_context_get_state(po->context) != PA_CONTEXT_READY) + pulse_output_delete_context(po); + + pa_threaded_mainloop_unlock(po->mainloop); +} + +/** + * Check if the stream is (already) connected, and waits if not. The + * mainloop must be locked before calling this function. + * + * @return true on success, false on error + */ +static bool +pulse_output_wait_stream(PulseOutput *po, Error &error) +{ + while (true) { + switch (pa_stream_get_state(po->stream)) { + case PA_STREAM_READY: + return true; + + case PA_STREAM_FAILED: + case PA_STREAM_TERMINATED: + case PA_STREAM_UNCONNECTED: + SetError(error, po->context, + "failed to connect the stream"); + return false; + + case PA_STREAM_CREATING: + pa_threaded_mainloop_wait(po->mainloop); + break; + } + } +} + +/** + * Sets cork mode on the stream. + */ +static bool +pulse_output_stream_pause(PulseOutput *po, bool pause, + Error &error) +{ + pa_operation *o; + + assert(po->mainloop != nullptr); + assert(po->context != nullptr); + assert(po->stream != nullptr); + + o = pa_stream_cork(po->stream, pause, + pulse_output_stream_success_cb, po); + if (o == nullptr) { + SetError(error, po->context, "pa_stream_cork() has failed"); + return false; + } + + if (!pulse_wait_for_operation(po->mainloop, o)) { + SetError(error, po->context, "pa_stream_cork() has failed"); + return false; + } + + return true; +} + +static unsigned +pulse_output_delay(struct audio_output *ao) +{ + PulseOutput *po = (PulseOutput *)ao; + unsigned result = 0; + + pa_threaded_mainloop_lock(po->mainloop); + + if (po->base.pause && pa_stream_is_corked(po->stream) && + pa_stream_get_state(po->stream) == PA_STREAM_READY) + /* idle while paused */ + result = 1000; + + pa_threaded_mainloop_unlock(po->mainloop); + + return result; +} + +static size_t +pulse_output_play(struct audio_output *ao, const void *chunk, size_t size, + Error &error) +{ + PulseOutput *po = (PulseOutput *)ao; + + assert(po->mainloop != nullptr); + assert(po->stream != nullptr); + + pa_threaded_mainloop_lock(po->mainloop); + + /* check if the stream is (already) connected */ + + if (!pulse_output_wait_stream(po, error)) { + pa_threaded_mainloop_unlock(po->mainloop); + return 0; + } + + assert(po->context != nullptr); + + /* unpause if previously paused */ + + if (pa_stream_is_corked(po->stream) && + !pulse_output_stream_pause(po, false, error)) { + pa_threaded_mainloop_unlock(po->mainloop); + return 0; + } + + /* wait until the server allows us to write */ + + while (po->writable == 0) { + if (pa_stream_is_suspended(po->stream)) { + pa_threaded_mainloop_unlock(po->mainloop); + error.Set(pulse_output_domain, "suspended"); + return 0; + } + + pa_threaded_mainloop_wait(po->mainloop); + + if (pa_stream_get_state(po->stream) != PA_STREAM_READY) { + pa_threaded_mainloop_unlock(po->mainloop); + error.Set(pulse_output_domain, "disconnected"); + return 0; + } + } + + /* now write */ + + if (size > po->writable) + /* don't send more than possible */ + size = po->writable; + + po->writable -= size; + + int result = pa_stream_write(po->stream, chunk, size, nullptr, + 0, PA_SEEK_RELATIVE); + pa_threaded_mainloop_unlock(po->mainloop); + if (result < 0) { + SetError(error, po->context, "pa_stream_write() failed"); + return 0; + } + + return size; +} + +static void +pulse_output_cancel(struct audio_output *ao) +{ + PulseOutput *po = (PulseOutput *)ao; + pa_operation *o; + + assert(po->mainloop != nullptr); + assert(po->stream != nullptr); + + pa_threaded_mainloop_lock(po->mainloop); + + if (pa_stream_get_state(po->stream) != PA_STREAM_READY) { + /* no need to flush when the stream isn't connected + yet */ + pa_threaded_mainloop_unlock(po->mainloop); + return; + } + + assert(po->context != nullptr); + + o = pa_stream_flush(po->stream, pulse_output_stream_success_cb, po); + if (o == nullptr) { + FormatWarning(pulse_output_domain, + "pa_stream_flush() has failed: %s", + pa_strerror(pa_context_errno(po->context))); + pa_threaded_mainloop_unlock(po->mainloop); + return; + } + + pulse_wait_for_operation(po->mainloop, o); + pa_threaded_mainloop_unlock(po->mainloop); +} + +static bool +pulse_output_pause(struct audio_output *ao) +{ + PulseOutput *po = (PulseOutput *)ao; + + assert(po->mainloop != nullptr); + assert(po->stream != nullptr); + + pa_threaded_mainloop_lock(po->mainloop); + + /* check if the stream is (already/still) connected */ + + Error error; + if (!pulse_output_wait_stream(po, error)) { + pa_threaded_mainloop_unlock(po->mainloop); + LogError(error); + return false; + } + + assert(po->context != nullptr); + + /* cork the stream */ + + if (!pa_stream_is_corked(po->stream) && + !pulse_output_stream_pause(po, true, error)) { + pa_threaded_mainloop_unlock(po->mainloop); + LogError(error); + return false; + } + + pa_threaded_mainloop_unlock(po->mainloop); + + return true; +} + +static bool +pulse_output_test_default_device(void) +{ + bool success; + + const config_param empty; + PulseOutput *po = (PulseOutput *) + pulse_output_init(empty, IgnoreError()); + if (po == nullptr) + return false; + + success = pulse_output_wait_connection(po, IgnoreError()); + pulse_output_finish(&po->base); + + return success; +} + +const struct audio_output_plugin pulse_output_plugin = { + "pulse", + pulse_output_test_default_device, + pulse_output_init, + pulse_output_finish, + pulse_output_enable, + pulse_output_disable, + pulse_output_open, + pulse_output_close, + pulse_output_delay, + nullptr, + pulse_output_play, + nullptr, + pulse_output_cancel, + pulse_output_pause, + + &pulse_mixer_plugin, +}; diff --git a/src/output/plugins/PulseOutputPlugin.hxx b/src/output/plugins/PulseOutputPlugin.hxx new file mode 100644 index 000000000..9df557282 --- /dev/null +++ b/src/output/plugins/PulseOutputPlugin.hxx @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_PULSE_OUTPUT_PLUGIN_HXX +#define MPD_PULSE_OUTPUT_PLUGIN_HXX + +struct PulseOutput; +struct PulseMixer; +struct pa_cvolume; +class Error; + +extern const struct audio_output_plugin pulse_output_plugin; + +void +pulse_output_lock(PulseOutput *po); + +void +pulse_output_unlock(PulseOutput *po); + +void +pulse_output_set_mixer(PulseOutput *po, PulseMixer *pm); + +void +pulse_output_clear_mixer(PulseOutput *po, PulseMixer *pm); + +bool +pulse_output_set_volume(PulseOutput *po, + const pa_cvolume *volume, Error &error); + +#endif diff --git a/src/output/plugins/RecorderOutputPlugin.cxx b/src/output/plugins/RecorderOutputPlugin.cxx new file mode 100644 index 000000000..16fe2c692 --- /dev/null +++ b/src/output/plugins/RecorderOutputPlugin.cxx @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "RecorderOutputPlugin.hxx" +#include "../OutputAPI.hxx" +#include "encoder/EncoderPlugin.hxx" +#include "encoder/EncoderList.hxx" +#include "ConfigError.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "system/fd_util.h" +#include "open.h" + +#include +#include +#include +#include +#include + +struct RecorderOutput { + struct audio_output base; + + /** + * The configured encoder plugin. + */ + Encoder *encoder; + + /** + * The destination file name. + */ + const char *path; + + /** + * The destination file descriptor. + */ + int fd; + + /** + * The buffer for encoder_read(). + */ + char buffer[32768]; + + bool Initialize(const config_param ¶m, Error &error_r) { + return ao_base_init(&base, &recorder_output_plugin, param, + error_r); + } + + void Deinitialize() { + ao_base_finish(&base); + } + + bool Configure(const config_param ¶m, Error &error); + + bool WriteToFile(const void *data, size_t length, Error &error); + + /** + * Writes pending data from the encoder to the output file. + */ + bool EncoderToFile(Error &error); +}; + +static constexpr Domain recorder_output_domain("recorder_output"); + +inline bool +RecorderOutput::Configure(const config_param ¶m, Error &error) +{ + /* read configuration */ + + const char *encoder_name = + param.GetBlockValue("encoder", "vorbis"); + const auto encoder_plugin = encoder_plugin_get(encoder_name); + if (encoder_plugin == nullptr) { + error.Format(config_domain, + "No such encoder: %s", encoder_name); + return false; + } + + path = param.GetBlockValue("path"); + if (path == nullptr) { + error.Set(config_domain, "'path' not configured"); + return false; + } + + /* initialize encoder */ + + encoder = encoder_init(*encoder_plugin, param, error); + if (encoder == nullptr) + return false; + + return true; +} + +static audio_output * +recorder_output_init(const config_param ¶m, Error &error) +{ + RecorderOutput *recorder = new RecorderOutput(); + + if (!recorder->Initialize(param, error)) { + delete recorder; + return nullptr; + } + + if (!recorder->Configure(param, error)) { + recorder->Deinitialize(); + delete recorder; + return nullptr; + } + + return &recorder->base; +} + +static void +recorder_output_finish(struct audio_output *ao) +{ + RecorderOutput *recorder = (RecorderOutput *)ao; + + encoder_finish(recorder->encoder); + recorder->Deinitialize(); + delete recorder; +} + +inline bool +RecorderOutput::WriteToFile(const void *_data, size_t length, Error &error) +{ + assert(length > 0); + + const uint8_t *data = (const uint8_t *)_data, *end = data + length; + + while (true) { + ssize_t nbytes = write(fd, data, end - data); + if (nbytes > 0) { + data += nbytes; + if (data == end) + return true; + } else if (nbytes == 0) { + /* shouldn't happen for files */ + error.Set(recorder_output_domain, + "write() returned 0"); + return false; + } else if (errno != EINTR) { + error.FormatErrno("Failed to write to '%s'", path); + return false; + } + } +} + +inline bool +RecorderOutput::EncoderToFile(Error &error) +{ + assert(fd >= 0); + + while (true) { + /* read from the encoder */ + + size_t size = encoder_read(encoder, buffer, sizeof(buffer)); + if (size == 0) + return true; + + /* write everything into the file */ + + if (!WriteToFile(buffer, size, error)) + return false; + } +} + +static bool +recorder_output_open(struct audio_output *ao, + AudioFormat &audio_format, + Error &error) +{ + RecorderOutput *recorder = (RecorderOutput *)ao; + + /* create the output file */ + + recorder->fd = open_cloexec(recorder->path, + O_CREAT|O_WRONLY|O_TRUNC|O_BINARY, + 0666); + if (recorder->fd < 0) { + error.FormatErrno("Failed to create '%s'", recorder->path); + return false; + } + + /* open the encoder */ + + if (!encoder_open(recorder->encoder, audio_format, error)) { + close(recorder->fd); + unlink(recorder->path); + return false; + } + + if (!recorder->EncoderToFile(error)) { + encoder_close(recorder->encoder); + close(recorder->fd); + unlink(recorder->path); + return false; + } + + return true; +} + +static void +recorder_output_close(struct audio_output *ao) +{ + RecorderOutput *recorder = (RecorderOutput *)ao; + + /* flush the encoder and write the rest to the file */ + + if (encoder_end(recorder->encoder, IgnoreError())) + recorder->EncoderToFile(IgnoreError()); + + /* now really close everything */ + + encoder_close(recorder->encoder); + + close(recorder->fd); +} + +static size_t +recorder_output_play(struct audio_output *ao, const void *chunk, size_t size, + Error &error) +{ + RecorderOutput *recorder = (RecorderOutput *)ao; + + return encoder_write(recorder->encoder, chunk, size, error) && + recorder->EncoderToFile(error) + ? size : 0; +} + +const struct audio_output_plugin recorder_output_plugin = { + "recorder", + nullptr, + recorder_output_init, + recorder_output_finish, + nullptr, + nullptr, + recorder_output_open, + recorder_output_close, + nullptr, + nullptr, + recorder_output_play, + nullptr, + nullptr, + nullptr, + nullptr, +}; diff --git a/src/output/plugins/RecorderOutputPlugin.hxx b/src/output/plugins/RecorderOutputPlugin.hxx new file mode 100644 index 000000000..4fac911a1 --- /dev/null +++ b/src/output/plugins/RecorderOutputPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_RECORDER_OUTPUT_PLUGIN_HXX +#define MPD_RECORDER_OUTPUT_PLUGIN_HXX + +extern const struct audio_output_plugin recorder_output_plugin; + +#endif diff --git a/src/output/plugins/RoarOutputPlugin.cxx b/src/output/plugins/RoarOutputPlugin.cxx new file mode 100644 index 000000000..7c1c41b47 --- /dev/null +++ b/src/output/plugins/RoarOutputPlugin.cxx @@ -0,0 +1,428 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * Copyright (C) 2010-2011 Philipp 'ph3-der-loewe' Schafft + * Copyright (C) 2010-2011 Hans-Kristian 'maister' Arntzen + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "RoarOutputPlugin.hxx" +#include "../OutputAPI.hxx" +#include "MixerList.hxx" +#include "thread/Mutex.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "Log.hxx" + +#include + +/* libroar/services.h declares roar_service_stream::new - work around + this C++ problem */ +#define new _new +#include +#undef new + +class RoarOutput { + struct audio_output base; + + std::string host, name; + + roar_vs_t * vss; + int err; + int role; + struct roar_connection con; + struct roar_audio_info info; + mutable Mutex mutex; + volatile bool alive; + +public: + RoarOutput() + :err(ROAR_ERROR_NONE) {} + + operator audio_output *() { + return &base; + } + + bool Initialize(const config_param ¶m, Error &error) { + return ao_base_init(&base, &roar_output_plugin, param, + error); + } + + void Deinitialize() { + ao_base_finish(&base); + } + + void Configure(const config_param ¶m); + + bool Open(AudioFormat &audio_format, Error &error); + void Close(); + + void SendTag(const Tag &tag); + size_t Play(const void *chunk, size_t size, Error &error); + void Cancel(); + + int GetVolume() const; + bool SetVolume(unsigned volume); +}; + +static constexpr Domain roar_output_domain("roar_output"); + +inline int +RoarOutput::GetVolume() const +{ + const ScopeLock protect(mutex); + + if (vss == nullptr || !alive) + return -1; + + float l, r; + int error; + if (roar_vs_volume_get(vss, &l, &r, &error) < 0) + return -1; + + return (l + r) * 50; +} + +int +roar_output_get_volume(RoarOutput *roar) +{ + return roar->GetVolume(); +} + +bool +RoarOutput::SetVolume(unsigned volume) +{ + assert(volume <= 100); + + const ScopeLock protect(mutex); + if (vss == nullptr || !alive) + return false; + + int error; + float level = volume / 100.0; + + roar_vs_volume_mono(vss, level, &error); + return true; +} + +bool +roar_output_set_volume(RoarOutput *roar, unsigned volume) +{ + return roar->SetVolume(volume); +} + +inline void +RoarOutput::Configure(const config_param ¶m) +{ + host = param.GetBlockValue("server", ""); + name = param.GetBlockValue("name", "MPD"); + + const char *_role = param.GetBlockValue("role", "music"); + role = _role != nullptr + ? roar_str2role(_role) + : ROAR_ROLE_MUSIC; +} + +static struct audio_output * +roar_init(const config_param ¶m, Error &error) +{ + RoarOutput *self = new RoarOutput(); + + if (!self->Initialize(param, error)) { + delete self; + return nullptr; + } + + self->Configure(param); + return *self; +} + +static void +roar_finish(struct audio_output *ao) +{ + RoarOutput *self = (RoarOutput *)ao; + + self->Deinitialize(); + delete self; +} + +static void +roar_use_audio_format(struct roar_audio_info *info, + AudioFormat &audio_format) +{ + info->rate = audio_format.sample_rate; + info->channels = audio_format.channels; + info->codec = ROAR_CODEC_PCM_S; + + switch (audio_format.format) { + case SampleFormat::UNDEFINED: + case SampleFormat::FLOAT: + case SampleFormat::DSD: + info->bits = 16; + audio_format.format = SampleFormat::S16; + break; + + case SampleFormat::S8: + info->bits = 8; + break; + + case SampleFormat::S16: + info->bits = 16; + break; + + case SampleFormat::S24_P32: + info->bits = 32; + audio_format.format = SampleFormat::S32; + break; + + case SampleFormat::S32: + info->bits = 32; + break; + } +} + +inline bool +RoarOutput::Open(AudioFormat &audio_format, Error &error) +{ + const ScopeLock protect(mutex); + + if (roar_simple_connect(&con, + host.empty() ? nullptr : host.c_str(), + name.c_str()) < 0) { + error.Set(roar_output_domain, + "Failed to connect to Roar server"); + return false; + } + + vss = roar_vs_new_from_con(&con, &err); + + if (vss == nullptr || err != ROAR_ERROR_NONE) { + error.Set(roar_output_domain, "Failed to connect to server"); + return false; + } + + roar_use_audio_format(&info, audio_format); + + if (roar_vs_stream(vss, &info, ROAR_DIR_PLAY, &err) < 0) { + error.Set(roar_output_domain, "Failed to start stream"); + return false; + } + + roar_vs_role(vss, role, &err); + alive = true; + return true; +} + +static bool +roar_open(struct audio_output *ao, AudioFormat &audio_format, Error &error) +{ + RoarOutput *self = (RoarOutput *)ao; + + return self->Open(audio_format, error); +} + +inline void +RoarOutput::Close() +{ + const ScopeLock protect(mutex); + + alive = false; + + if (vss != nullptr) + roar_vs_close(vss, ROAR_VS_TRUE, &err); + vss = nullptr; + roar_disconnect(&con); +} + +static void +roar_close(struct audio_output *ao) +{ + RoarOutput *self = (RoarOutput *)ao; + self->Close(); +} + +inline void +RoarOutput::Cancel() +{ + const ScopeLock protect(mutex); + + if (vss == nullptr) + return; + + roar_vs_t *_vss = vss; + vss = nullptr; + roar_vs_close(_vss, ROAR_VS_TRUE, &err); + alive = false; + + _vss = roar_vs_new_from_con(&con, &err); + if (_vss == nullptr) + return; + + if (roar_vs_stream(_vss, &info, ROAR_DIR_PLAY, &err) < 0) { + roar_vs_close(_vss, ROAR_VS_TRUE, &err); + LogError(roar_output_domain, "Failed to start stream"); + return; + } + + roar_vs_role(_vss, role, &err); + vss = _vss; + alive = true; +} + +static void +roar_cancel(struct audio_output *ao) +{ + RoarOutput *self = (RoarOutput *)ao; + + self->Cancel(); +} + +inline size_t +RoarOutput::Play(const void *chunk, size_t size, Error &error) +{ + if (vss == nullptr) { + error.Set(roar_output_domain, "Connection is invalid"); + return 0; + } + + ssize_t nbytes = roar_vs_write(vss, chunk, size, &err); + if (nbytes <= 0) { + error.Set(roar_output_domain, "Failed to play data"); + return 0; + } + + return nbytes; +} + +static size_t +roar_play(struct audio_output *ao, const void *chunk, size_t size, + Error &error) +{ + RoarOutput *self = (RoarOutput *)ao; + return self->Play(chunk, size, error); +} + +static const char* +roar_tag_convert(TagType type, bool *is_uuid) +{ + *is_uuid = false; + switch (type) + { + case TAG_ARTIST: + case TAG_ALBUM_ARTIST: + return "AUTHOR"; + case TAG_ALBUM: + return "ALBUM"; + case TAG_TITLE: + return "TITLE"; + case TAG_TRACK: + return "TRACK"; + case TAG_NAME: + return "NAME"; + case TAG_GENRE: + return "GENRE"; + case TAG_DATE: + return "DATE"; + case TAG_PERFORMER: + return "PERFORMER"; + case TAG_COMMENT: + return "COMMENT"; + case TAG_DISC: + return "DISCID"; + case TAG_COMPOSER: +#ifdef ROAR_META_TYPE_COMPOSER + return "COMPOSER"; +#else + return "AUTHOR"; +#endif + case TAG_MUSICBRAINZ_ARTISTID: + case TAG_MUSICBRAINZ_ALBUMID: + case TAG_MUSICBRAINZ_ALBUMARTISTID: + case TAG_MUSICBRAINZ_TRACKID: + *is_uuid = true; + return "HASH"; + + default: + return nullptr; + } +} + +inline void +RoarOutput::SendTag(const Tag &tag) +{ + if (vss == nullptr) + return; + + const ScopeLock protect(mutex); + + size_t cnt = 1; + struct roar_keyval vals[32]; + char uuid_buf[32][64]; + + char timebuf[16]; + snprintf(timebuf, sizeof(timebuf), "%02d:%02d:%02d", + tag.time / 3600, (tag.time % 3600) / 60, tag.time % 60); + + vals[0].key = const_cast("LENGTH"); + vals[0].value = timebuf; + + for (unsigned i = 0; i < tag.num_items && cnt < 32; i++) + { + bool is_uuid = false; + const char *key = roar_tag_convert(tag.items[i]->type, + &is_uuid); + if (key != nullptr) { + vals[cnt].key = const_cast(key); + + if (is_uuid) { + snprintf(uuid_buf[cnt], sizeof(uuid_buf[0]), "{UUID}%s", + tag.items[i]->value); + vals[cnt].value = uuid_buf[cnt]; + } else { + vals[cnt].value = tag.items[i]->value; + } + + cnt++; + } + } + + roar_vs_meta(vss, vals, cnt, &(err)); +} + +static void +roar_send_tag(struct audio_output *ao, const Tag *meta) +{ + RoarOutput *self = (RoarOutput *)ao; + self->SendTag(*meta); +} + +const struct audio_output_plugin roar_output_plugin = { + "roar", + nullptr, + roar_init, + roar_finish, + nullptr, + nullptr, + roar_open, + roar_close, + nullptr, + roar_send_tag, + roar_play, + nullptr, + roar_cancel, + nullptr, + &roar_mixer_plugin, +}; diff --git a/src/output/plugins/RoarOutputPlugin.hxx b/src/output/plugins/RoarOutputPlugin.hxx new file mode 100644 index 000000000..27c5dc420 --- /dev/null +++ b/src/output/plugins/RoarOutputPlugin.hxx @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_ROAR_OUTPUT_PLUGIN_H +#define MPD_ROAR_OUTPUT_PLUGIN_H + +class RoarOutput; + +extern const struct audio_output_plugin roar_output_plugin; + +int +roar_output_get_volume(RoarOutput *roar); + +bool +roar_output_set_volume(RoarOutput *roar, unsigned volume); + +#endif diff --git a/src/output/plugins/ShoutOutputPlugin.cxx b/src/output/plugins/ShoutOutputPlugin.cxx new file mode 100644 index 000000000..e0ec6ce3d --- /dev/null +++ b/src/output/plugins/ShoutOutputPlugin.cxx @@ -0,0 +1,544 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "ShoutOutputPlugin.hxx" +#include "../OutputAPI.hxx" +#include "encoder/EncoderPlugin.hxx" +#include "encoder/EncoderList.hxx" +#include "ConfigError.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "system/FatalError.hxx" +#include "Log.hxx" + +#include +#include + +#include +#include +#include +#include + +static constexpr unsigned DEFAULT_CONN_TIMEOUT = 2; + +struct ShoutOutput final { + struct audio_output base; + + shout_t *shout_conn; + shout_metadata_t *shout_meta; + + Encoder *encoder; + + float quality; + int bitrate; + + int timeout; + + uint8_t buffer[32768]; + + ShoutOutput() + :shout_conn(shout_new()), + shout_meta(shout_metadata_new()), + quality(-2.0), + bitrate(-1), + timeout(DEFAULT_CONN_TIMEOUT) {} + + ~ShoutOutput() { + if (shout_meta != nullptr) + shout_metadata_free(shout_meta); + if (shout_conn != nullptr) + shout_free(shout_conn); + } + + bool Initialize(const config_param ¶m, Error &error) { + return ao_base_init(&base, &shout_output_plugin, param, + error); + } + + void Deinitialize() { + ao_base_finish(&base); + } + + bool Configure(const config_param ¶m, Error &error); +}; + +static int shout_init_count; + +static constexpr Domain shout_output_domain("shout_output"); + +static const EncoderPlugin * +shout_encoder_plugin_get(const char *name) +{ + if (strcmp(name, "ogg") == 0) + name = "vorbis"; + else if (strcmp(name, "mp3") == 0) + name = "lame"; + + return encoder_plugin_get(name); +} + +gcc_pure +static const char * +require_block_string(const config_param ¶m, const char *name) +{ + const char *value = param.GetBlockValue(name); + if (value == nullptr) + FormatFatalError("no \"%s\" defined for shout device defined " + "at line %u\n", name, param.line); + + return value; +} + +inline bool +ShoutOutput::Configure(const config_param ¶m, Error &error) +{ + + const AudioFormat audio_format = base.config_audio_format; + if (!audio_format.IsFullyDefined()) { + error.Set(config_domain, + "Need full audio format specification"); + return nullptr; + } + + const char *host = require_block_string(param, "host"); + const char *mount = require_block_string(param, "mount"); + unsigned port = param.GetBlockValue("port", 0u); + if (port == 0) { + error.Set(config_domain, "shout port must be configured"); + return false; + } + + const char *passwd = require_block_string(param, "password"); + const char *name = require_block_string(param, "name"); + + bool is_public = param.GetBlockValue("public", false); + + const char *user = param.GetBlockValue("user", "source"); + + const char *value = param.GetBlockValue("quality"); + if (value != nullptr) { + char *test; + quality = strtod(value, &test); + + if (*test != '\0' || quality < -1.0 || quality > 10.0) { + error.Format(config_domain, + "shout quality \"%s\" is not a number in the " + "range -1 to 10", + value); + return false; + } + + if (param.GetBlockValue("bitrate") != nullptr) { + error.Set(config_domain, + "quality and bitrate are " + "both defined"); + return false; + } + } else { + value = param.GetBlockValue("bitrate"); + if (value == nullptr) { + error.Set(config_domain, + "neither bitrate nor quality defined"); + return false; + } + + char *test; + bitrate = strtol(value, &test, 10); + + if (*test != '\0' || bitrate <= 0) { + error.Set(config_domain, + "bitrate must be a positive integer"); + return false; + } + } + + const char *encoding = param.GetBlockValue("encoding", "ogg"); + const auto encoder_plugin = shout_encoder_plugin_get(encoding); + if (encoder_plugin == nullptr) { + error.Format(config_domain, + "couldn't find shout encoder plugin \"%s\"", + encoding); + return false; + } + + encoder = encoder_init(*encoder_plugin, param, error); + if (encoder == nullptr) + return false; + + unsigned shout_format; + if (strcmp(encoding, "mp3") == 0 || strcmp(encoding, "lame") == 0) + shout_format = SHOUT_FORMAT_MP3; + else + shout_format = SHOUT_FORMAT_OGG; + + unsigned protocol; + value = param.GetBlockValue("protocol"); + if (value != nullptr) { + if (0 == strcmp(value, "shoutcast") && + 0 != strcmp(encoding, "mp3")) { + error.Format(config_domain, + "you cannot stream \"%s\" to shoutcast, use mp3", + encoding); + return false; + } else if (0 == strcmp(value, "shoutcast")) + protocol = SHOUT_PROTOCOL_ICY; + else if (0 == strcmp(value, "icecast1")) + protocol = SHOUT_PROTOCOL_XAUDIOCAST; + else if (0 == strcmp(value, "icecast2")) + protocol = SHOUT_PROTOCOL_HTTP; + else { + error.Format(config_domain, + "shout protocol \"%s\" is not \"shoutcast\" or " + "\"icecast1\"or \"icecast2\"", + value); + return false; + } + } else { + protocol = SHOUT_PROTOCOL_HTTP; + } + + if (shout_set_host(shout_conn, host) != SHOUTERR_SUCCESS || + shout_set_port(shout_conn, port) != SHOUTERR_SUCCESS || + shout_set_password(shout_conn, passwd) != SHOUTERR_SUCCESS || + shout_set_mount(shout_conn, mount) != SHOUTERR_SUCCESS || + shout_set_name(shout_conn, name) != SHOUTERR_SUCCESS || + shout_set_user(shout_conn, user) != SHOUTERR_SUCCESS || + shout_set_public(shout_conn, is_public) != SHOUTERR_SUCCESS || + shout_set_format(shout_conn, shout_format) + != SHOUTERR_SUCCESS || + shout_set_protocol(shout_conn, protocol) != SHOUTERR_SUCCESS || + shout_set_agent(shout_conn, "MPD") != SHOUTERR_SUCCESS) { + error.Set(shout_output_domain, shout_get_error(shout_conn)); + return false; + } + + /* optional paramters */ + timeout = param.GetBlockValue("timeout", DEFAULT_CONN_TIMEOUT); + + value = param.GetBlockValue("genre"); + if (value != nullptr && shout_set_genre(shout_conn, value)) { + error.Set(shout_output_domain, shout_get_error(shout_conn)); + return false; + } + + value = param.GetBlockValue("description"); + if (value != nullptr && shout_set_description(shout_conn, value)) { + error.Set(shout_output_domain, shout_get_error(shout_conn)); + return false; + } + + value = param.GetBlockValue("url"); + if (value != nullptr && shout_set_url(shout_conn, value)) { + error.Set(shout_output_domain, shout_get_error(shout_conn)); + return false; + } + + { + char temp[11]; + memset(temp, 0, sizeof(temp)); + + snprintf(temp, sizeof(temp), "%u", audio_format.channels); + shout_set_audio_info(shout_conn, SHOUT_AI_CHANNELS, temp); + + snprintf(temp, sizeof(temp), "%u", audio_format.sample_rate); + + shout_set_audio_info(shout_conn, SHOUT_AI_SAMPLERATE, temp); + + if (quality >= -1.0) { + snprintf(temp, sizeof(temp), "%2.2f", quality); + shout_set_audio_info(shout_conn, SHOUT_AI_QUALITY, + temp); + } else { + snprintf(temp, sizeof(temp), "%d", bitrate); + shout_set_audio_info(shout_conn, SHOUT_AI_BITRATE, + temp); + } + } + + return true; +} + +static struct audio_output * +my_shout_init_driver(const config_param ¶m, Error &error) +{ + ShoutOutput *sd = new ShoutOutput(); + if (!sd->Initialize(param, error)) { + delete sd; + return nullptr; + } + + if (!sd->Configure(param, error)) { + sd->Deinitialize(); + delete sd; + return nullptr; + } + + if (shout_init_count == 0) + shout_init(); + + shout_init_count++; + + return &sd->base; +} + +static bool +handle_shout_error(ShoutOutput *sd, int err, Error &error) +{ + switch (err) { + case SHOUTERR_SUCCESS: + break; + + case SHOUTERR_UNCONNECTED: + case SHOUTERR_SOCKET: + error.Format(shout_output_domain, err, + "Lost shout connection to %s:%i: %s", + shout_get_host(sd->shout_conn), + shout_get_port(sd->shout_conn), + shout_get_error(sd->shout_conn)); + return false; + + default: + error.Format(shout_output_domain, err, + "connection to %s:%i error: %s", + shout_get_host(sd->shout_conn), + shout_get_port(sd->shout_conn), + shout_get_error(sd->shout_conn)); + return false; + } + + return true; +} + +static bool +write_page(ShoutOutput *sd, Error &error) +{ + assert(sd->encoder != nullptr); + + while (true) { + size_t nbytes = encoder_read(sd->encoder, + sd->buffer, sizeof(sd->buffer)); + if (nbytes == 0) + return true; + + int err = shout_send(sd->shout_conn, sd->buffer, nbytes); + if (!handle_shout_error(sd, err, error)) + return false; + } + + return true; +} + +static void close_shout_conn(ShoutOutput * sd) +{ + if (sd->encoder != nullptr) { + if (encoder_end(sd->encoder, IgnoreError())) + write_page(sd, IgnoreError()); + + encoder_close(sd->encoder); + } + + if (shout_get_connected(sd->shout_conn) != SHOUTERR_UNCONNECTED && + shout_close(sd->shout_conn) != SHOUTERR_SUCCESS) { + FormatWarning(shout_output_domain, + "problem closing connection to shout server: %s", + shout_get_error(sd->shout_conn)); + } +} + +static void +my_shout_finish_driver(struct audio_output *ao) +{ + ShoutOutput *sd = (ShoutOutput *)ao; + + encoder_finish(sd->encoder); + + sd->Deinitialize(); + delete sd; + + shout_init_count--; + + if (shout_init_count == 0) + shout_shutdown(); +} + +static void +my_shout_drop_buffered_audio(struct audio_output *ao) +{ + gcc_unused + ShoutOutput *sd = (ShoutOutput *)ao; + + /* needs to be implemented for shout */ +} + +static void +my_shout_close_device(struct audio_output *ao) +{ + ShoutOutput *sd = (ShoutOutput *)ao; + + close_shout_conn(sd); +} + +static bool +shout_connect(ShoutOutput *sd, Error &error) +{ + switch (shout_open(sd->shout_conn)) { + case SHOUTERR_SUCCESS: + case SHOUTERR_CONNECTED: + return true; + + default: + error.Format(shout_output_domain, + "problem opening connection to shout server %s:%i: %s", + shout_get_host(sd->shout_conn), + shout_get_port(sd->shout_conn), + shout_get_error(sd->shout_conn)); + return false; + } +} + +static bool +my_shout_open_device(struct audio_output *ao, AudioFormat &audio_format, + Error &error) +{ + ShoutOutput *sd = (ShoutOutput *)ao; + + if (!shout_connect(sd, error)) + return false; + + if (!encoder_open(sd->encoder, audio_format, error)) { + shout_close(sd->shout_conn); + return false; + } + + if (!write_page(sd, error)) { + encoder_close(sd->encoder); + shout_close(sd->shout_conn); + return false; + } + + return true; +} + +static unsigned +my_shout_delay(struct audio_output *ao) +{ + ShoutOutput *sd = (ShoutOutput *)ao; + + int delay = shout_delay(sd->shout_conn); + if (delay < 0) + delay = 0; + + return delay; +} + +static size_t +my_shout_play(struct audio_output *ao, const void *chunk, size_t size, + Error &error) +{ + ShoutOutput *sd = (ShoutOutput *)ao; + + return encoder_write(sd->encoder, chunk, size, error) && + write_page(sd, error) + ? size + : 0; +} + +static bool +my_shout_pause(struct audio_output *ao) +{ + static char silence[1020]; + + return my_shout_play(ao, silence, sizeof(silence), IgnoreError()); +} + +static void +shout_tag_to_metadata(const Tag *tag, char *dest, size_t size) +{ + char artist[size]; + char title[size]; + + artist[0] = 0; + title[0] = 0; + + for (unsigned i = 0; i < tag->num_items; i++) { + switch (tag->items[i]->type) { + case TAG_ARTIST: + strncpy(artist, tag->items[i]->value, size); + break; + case TAG_TITLE: + strncpy(title, tag->items[i]->value, size); + break; + + default: + break; + } + } + + snprintf(dest, size, "%s - %s", artist, title); +} + +static void my_shout_set_tag(struct audio_output *ao, + const Tag *tag) +{ + ShoutOutput *sd = (ShoutOutput *)ao; + + if (sd->encoder->plugin.tag != nullptr) { + /* encoder plugin supports stream tags */ + + Error error; + if (!encoder_pre_tag(sd->encoder, error) || + !write_page(sd, error) || + !encoder_tag(sd->encoder, tag, error)) { + LogError(error); + return; + } + } else { + /* no stream tag support: fall back to icy-metadata */ + char song[1024]; + shout_tag_to_metadata(tag, song, sizeof(song)); + + shout_metadata_add(sd->shout_meta, "song", song); + if (SHOUTERR_SUCCESS != shout_set_metadata(sd->shout_conn, + sd->shout_meta)) { + LogWarning(shout_output_domain, + "error setting shout metadata"); + } + } + + write_page(sd, IgnoreError()); +} + +const struct audio_output_plugin shout_output_plugin = { + "shout", + nullptr, + my_shout_init_driver, + my_shout_finish_driver, + nullptr, + nullptr, + my_shout_open_device, + my_shout_close_device, + my_shout_delay, + my_shout_set_tag, + my_shout_play, + nullptr, + my_shout_drop_buffered_audio, + my_shout_pause, + nullptr, +}; diff --git a/src/output/plugins/ShoutOutputPlugin.hxx b/src/output/plugins/ShoutOutputPlugin.hxx new file mode 100644 index 000000000..d437e0b0d --- /dev/null +++ b/src/output/plugins/ShoutOutputPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_SHOUT_OUTPUT_PLUGIN_HXX +#define MPD_SHOUT_OUTPUT_PLUGIN_HXX + +extern const struct audio_output_plugin shout_output_plugin; + +#endif diff --git a/src/output/plugins/SolarisOutputPlugin.cxx b/src/output/plugins/SolarisOutputPlugin.cxx new file mode 100644 index 000000000..38ed2e314 --- /dev/null +++ b/src/output/plugins/SolarisOutputPlugin.cxx @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "SolarisOutputPlugin.hxx" +#include "../OutputAPI.hxx" +#include "system/fd_util.h" +#include "util/Error.hxx" + +#include +#include +#include +#include +#include +#include + +#ifdef __sun +#include +#else + +/* some fake declarations that allow build this plugin on systems + other than Solaris, just to see if it compiles */ + +#define AUDIO_GETINFO 0 +#define AUDIO_SETINFO 0 +#define AUDIO_ENCODING_LINEAR 0 + +struct audio_info { + struct { + unsigned sample_rate, channels, precision, encoding; + } play; +}; + +#endif + +struct SolarisOutput { + struct audio_output base; + + /* configuration */ + const char *device; + + int fd; + + bool Initialize(const config_param ¶m, Error &error_r) { + return ao_base_init(&base, &solaris_output_plugin, param, + error_r); + } + + void Deinitialize() { + ao_base_finish(&base); + } +}; + +static bool +solaris_output_test_default_device(void) +{ + struct stat st; + + return stat("/dev/audio", &st) == 0 && S_ISCHR(st.st_mode) && + access("/dev/audio", W_OK) == 0; +} + +static struct audio_output * +solaris_output_init(const config_param ¶m, Error &error_r) +{ + SolarisOutput *so = new SolarisOutput(); + if (!so->Initialize(param, error_r)) { + delete so; + return nullptr; + } + + so->device = param.GetBlockValue("device", "/dev/audio"); + + return &so->base; +} + +static void +solaris_output_finish(struct audio_output *ao) +{ + SolarisOutput *so = (SolarisOutput *)ao; + + so->Deinitialize(); + delete so; +} + +static bool +solaris_output_open(struct audio_output *ao, AudioFormat &audio_format, + Error &error) +{ + SolarisOutput *so = (SolarisOutput *)ao; + struct audio_info info; + int ret, flags; + + /* support only 16 bit mono/stereo for now; nothing else has + been tested */ + audio_format.format = SampleFormat::S16; + + /* open the device in non-blocking mode */ + + so->fd = open_cloexec(so->device, O_WRONLY|O_NONBLOCK, 0); + if (so->fd < 0) { + error.FormatErrno("Failed to open %s", + so->device); + return false; + } + + /* restore blocking mode */ + + flags = fcntl(so->fd, F_GETFL); + if (flags > 0 && (flags & O_NONBLOCK) != 0) + fcntl(so->fd, F_SETFL, flags & ~O_NONBLOCK); + + /* configure the audio device */ + + ret = ioctl(so->fd, AUDIO_GETINFO, &info); + if (ret < 0) { + error.SetErrno("AUDIO_GETINFO failed"); + close(so->fd); + return false; + } + + info.play.sample_rate = audio_format.sample_rate; + info.play.channels = audio_format.channels; + info.play.precision = 16; + info.play.encoding = AUDIO_ENCODING_LINEAR; + + ret = ioctl(so->fd, AUDIO_SETINFO, &info); + if (ret < 0) { + error.SetErrno("AUDIO_SETINFO failed"); + close(so->fd); + return false; + } + + return true; +} + +static void +solaris_output_close(struct audio_output *ao) +{ + SolarisOutput *so = (SolarisOutput *)ao; + + close(so->fd); +} + +static size_t +solaris_output_play(struct audio_output *ao, const void *chunk, size_t size, + Error &error) +{ + SolarisOutput *so = (SolarisOutput *)ao; + ssize_t nbytes; + + nbytes = write(so->fd, chunk, size); + if (nbytes <= 0) { + error.SetErrno("Write failed"); + return 0; + } + + return nbytes; +} + +static void +solaris_output_cancel(struct audio_output *ao) +{ + SolarisOutput *so = (SolarisOutput *)ao; + + ioctl(so->fd, I_FLUSH); +} + +const struct audio_output_plugin solaris_output_plugin = { + "solaris", + solaris_output_test_default_device, + solaris_output_init, + solaris_output_finish, + nullptr, + nullptr, + solaris_output_open, + solaris_output_close, + nullptr, + nullptr, + solaris_output_play, + nullptr, + solaris_output_cancel, + nullptr, + nullptr, +}; diff --git a/src/output/plugins/SolarisOutputPlugin.hxx b/src/output/plugins/SolarisOutputPlugin.hxx new file mode 100644 index 000000000..9ce848a40 --- /dev/null +++ b/src/output/plugins/SolarisOutputPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_SOLARIS_OUTPUT_PLUGIN_HXX +#define MPD_SOLARIS_OUTPUT_PLUGIN_HXX + +extern const struct audio_output_plugin solaris_output_plugin; + +#endif diff --git a/src/output/plugins/WinmmOutputPlugin.cxx b/src/output/plugins/WinmmOutputPlugin.cxx new file mode 100644 index 000000000..87861180f --- /dev/null +++ b/src/output/plugins/WinmmOutputPlugin.cxx @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "WinmmOutputPlugin.hxx" +#include "../OutputAPI.hxx" +#include "pcm/PcmBuffer.hxx" +#include "MixerList.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "util/Macros.hxx" + +#include + +#include +#include + +struct WinmmBuffer { + PcmBuffer buffer; + + WAVEHDR hdr; +}; + +struct WinmmOutput { + struct audio_output base; + + UINT device_id; + HWAVEOUT handle; + + /** + * This event is triggered by Windows when a buffer is + * finished. + */ + HANDLE event; + + WinmmBuffer buffers[8]; + unsigned next_buffer; +}; + +static constexpr Domain winmm_output_domain("winmm_output"); + +HWAVEOUT +winmm_output_get_handle(WinmmOutput *output) +{ + return output->handle; +} + +static bool +winmm_output_test_default_device(void) +{ + return waveOutGetNumDevs() > 0; +} + +static bool +get_device_id(const char *device_name, UINT *device_id, Error &error) +{ + /* if device is not specified use wave mapper */ + if (device_name == nullptr) { + *device_id = WAVE_MAPPER; + return true; + } + + UINT numdevs = waveOutGetNumDevs(); + + /* check for device id */ + char *endptr; + UINT id = strtoul(device_name, &endptr, 0); + if (endptr > device_name && *endptr == 0) { + if (id >= numdevs) + goto fail; + *device_id = id; + return true; + } + + /* check for device name */ + for (UINT i = 0; i < numdevs; i++) { + WAVEOUTCAPS caps; + MMRESULT result = waveOutGetDevCaps(i, &caps, sizeof(caps)); + if (result != MMSYSERR_NOERROR) + continue; + /* szPname is only 32 chars long, so it is often truncated. + Use partial match to work around this. */ + if (strstr(device_name, caps.szPname) == device_name) { + *device_id = i; + return true; + } + } + +fail: + error.Format(winmm_output_domain, + "device \"%s\" is not found", device_name); + return false; +} + +static struct audio_output * +winmm_output_init(const config_param ¶m, Error &error) +{ + WinmmOutput *wo = new WinmmOutput(); + if (!ao_base_init(&wo->base, &winmm_output_plugin, param, error)) { + delete wo; + return nullptr; + } + + const char *device = param.GetBlockValue("device"); + if (!get_device_id(device, &wo->device_id, error)) { + ao_base_finish(&wo->base); + delete wo; + return nullptr; + } + + return &wo->base; +} + +static void +winmm_output_finish(struct audio_output *ao) +{ + WinmmOutput *wo = (WinmmOutput *)ao; + + ao_base_finish(&wo->base); + delete wo; +} + +static bool +winmm_output_open(struct audio_output *ao, AudioFormat &audio_format, + Error &error) +{ + WinmmOutput *wo = (WinmmOutput *)ao; + + wo->event = CreateEvent(nullptr, false, false, nullptr); + if (wo->event == nullptr) { + error.Set(winmm_output_domain, "CreateEvent() failed"); + return false; + } + + switch (audio_format.format) { + case SampleFormat::S8: + case SampleFormat::S16: + break; + + case SampleFormat::S24_P32: + case SampleFormat::S32: + case SampleFormat::FLOAT: + case SampleFormat::DSD: + case SampleFormat::UNDEFINED: + /* we havn't tested formats other than S16 */ + audio_format.format = SampleFormat::S16; + break; + } + + if (audio_format.channels > 2) + /* same here: more than stereo was not tested */ + audio_format.channels = 2; + + WAVEFORMATEX format; + format.wFormatTag = WAVE_FORMAT_PCM; + format.nChannels = audio_format.channels; + format.nSamplesPerSec = audio_format.sample_rate; + format.nBlockAlign = audio_format.GetFrameSize(); + format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign; + format.wBitsPerSample = audio_format.GetSampleSize() * 8; + format.cbSize = 0; + + MMRESULT result = waveOutOpen(&wo->handle, wo->device_id, &format, + (DWORD_PTR)wo->event, 0, CALLBACK_EVENT); + if (result != MMSYSERR_NOERROR) { + CloseHandle(wo->event); + error.Set(winmm_output_domain, "waveOutOpen() failed"); + return false; + } + + for (unsigned i = 0; i < ARRAY_SIZE(wo->buffers); ++i) { + memset(&wo->buffers[i].hdr, 0, sizeof(wo->buffers[i].hdr)); + } + + wo->next_buffer = 0; + + return true; +} + +static void +winmm_output_close(struct audio_output *ao) +{ + WinmmOutput *wo = (WinmmOutput *)ao; + + for (unsigned i = 0; i < ARRAY_SIZE(wo->buffers); ++i) + wo->buffers[i].buffer.Clear(); + + waveOutClose(wo->handle); + + CloseHandle(wo->event); +} + +/** + * Copy data into a buffer, and prepare the wave header. + */ +static bool +winmm_set_buffer(WinmmOutput *wo, WinmmBuffer *buffer, + const void *data, size_t size, + Error &error) +{ + void *dest = buffer->buffer.Get(size); + assert(dest != nullptr); + + memcpy(dest, data, size); + + memset(&buffer->hdr, 0, sizeof(buffer->hdr)); + buffer->hdr.lpData = (LPSTR)dest; + buffer->hdr.dwBufferLength = size; + + MMRESULT result = waveOutPrepareHeader(wo->handle, &buffer->hdr, + sizeof(buffer->hdr)); + if (result != MMSYSERR_NOERROR) { + error.Set(winmm_output_domain, result, + "waveOutPrepareHeader() failed"); + return false; + } + + return true; +} + +/** + * Wait until the buffer is finished. + */ +static bool +winmm_drain_buffer(WinmmOutput *wo, WinmmBuffer *buffer, + Error &error) +{ + if ((buffer->hdr.dwFlags & WHDR_DONE) == WHDR_DONE) + /* already finished */ + return true; + + while (true) { + MMRESULT result = waveOutUnprepareHeader(wo->handle, + &buffer->hdr, + sizeof(buffer->hdr)); + if (result == MMSYSERR_NOERROR) + return true; + else if (result != WAVERR_STILLPLAYING) { + error.Set(winmm_output_domain, result, + "waveOutUnprepareHeader() failed"); + return false; + } + + /* wait some more */ + WaitForSingleObject(wo->event, INFINITE); + } +} + +static size_t +winmm_output_play(struct audio_output *ao, const void *chunk, size_t size, Error &error) +{ + WinmmOutput *wo = (WinmmOutput *)ao; + + /* get the next buffer from the ring and prepare it */ + WinmmBuffer *buffer = &wo->buffers[wo->next_buffer]; + if (!winmm_drain_buffer(wo, buffer, error) || + !winmm_set_buffer(wo, buffer, chunk, size, error)) + return 0; + + /* enqueue the buffer */ + MMRESULT result = waveOutWrite(wo->handle, &buffer->hdr, + sizeof(buffer->hdr)); + if (result != MMSYSERR_NOERROR) { + waveOutUnprepareHeader(wo->handle, &buffer->hdr, + sizeof(buffer->hdr)); + error.Set(winmm_output_domain, result, + "waveOutWrite() failed"); + return 0; + } + + /* mark our buffer as "used" */ + wo->next_buffer = (wo->next_buffer + 1) % + ARRAY_SIZE(wo->buffers); + + return size; +} + +static bool +winmm_drain_all_buffers(WinmmOutput *wo, Error &error) +{ + for (unsigned i = wo->next_buffer; i < ARRAY_SIZE(wo->buffers); ++i) + if (!winmm_drain_buffer(wo, &wo->buffers[i], error)) + return false; + + for (unsigned i = 0; i < wo->next_buffer; ++i) + if (!winmm_drain_buffer(wo, &wo->buffers[i], error)) + return false; + + return true; +} + +static void +winmm_stop(WinmmOutput *wo) +{ + waveOutReset(wo->handle); + + for (unsigned i = 0; i < ARRAY_SIZE(wo->buffers); ++i) { + WinmmBuffer *buffer = &wo->buffers[i]; + waveOutUnprepareHeader(wo->handle, &buffer->hdr, + sizeof(buffer->hdr)); + } +} + +static void +winmm_output_drain(struct audio_output *ao) +{ + WinmmOutput *wo = (WinmmOutput *)ao; + + if (!winmm_drain_all_buffers(wo, IgnoreError())) + winmm_stop(wo); +} + +static void +winmm_output_cancel(struct audio_output *ao) +{ + WinmmOutput *wo = (WinmmOutput *)ao; + + winmm_stop(wo); +} + +const struct audio_output_plugin winmm_output_plugin = { + "winmm", + winmm_output_test_default_device, + winmm_output_init, + winmm_output_finish, + nullptr, + nullptr, + winmm_output_open, + winmm_output_close, + nullptr, + nullptr, + winmm_output_play, + winmm_output_drain, + winmm_output_cancel, + nullptr, + &winmm_mixer_plugin, +}; diff --git a/src/output/plugins/WinmmOutputPlugin.hxx b/src/output/plugins/WinmmOutputPlugin.hxx new file mode 100644 index 000000000..1409a2e8c --- /dev/null +++ b/src/output/plugins/WinmmOutputPlugin.hxx @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_WINMM_OUTPUT_PLUGIN_HXX +#define MPD_WINMM_OUTPUT_PLUGIN_HXX + +#include "check.h" + +#ifdef ENABLE_WINMM_OUTPUT + +#include "Compiler.h" + +#include +#include + +struct WinmmOutput; + +extern const struct audio_output_plugin winmm_output_plugin; + +gcc_pure +HWAVEOUT +winmm_output_get_handle(WinmmOutput *); + +#endif + +#endif diff --git a/test/read_mixer.cxx b/test/read_mixer.cxx index 0c1192587..41261a310 100644 --- a/test/read_mixer.cxx +++ b/test/read_mixer.cxx @@ -38,7 +38,7 @@ EventLoop *main_loop; #ifdef HAVE_PULSE -#include "output/PulseOutputPlugin.hxx" +#include "output/plugins/PulseOutputPlugin.hxx" void pulse_output_lock(gcc_unused PulseOutput *po) @@ -73,7 +73,7 @@ pulse_output_set_volume(gcc_unused PulseOutput *po, #endif #ifdef HAVE_ROAR -#include "output/RoarOutputPlugin.hxx" +#include "output/plugins/RoarOutputPlugin.hxx" int roar_output_get_volume(gcc_unused RoarOutput *roar) diff --git a/test/run_output.cxx b/test/run_output.cxx index 69dbbf7b3..c18451e10 100644 --- a/test/run_output.cxx +++ b/test/run_output.cxx @@ -18,9 +18,9 @@ */ #include "config.h" -#include "OutputControl.hxx" -#include "OutputInternal.hxx" -#include "OutputPlugin.hxx" +#include "output/OutputControl.hxx" +#include "output/OutputInternal.hxx" +#include "output/OutputPlugin.hxx" #include "ConfigData.hxx" #include "ConfigGlobal.hxx" #include "ConfigOption.hxx" -- cgit v1.2.3