diff --git a/src/mixer/plugins/AlsaMixerPlugin.cxx b/src/mixer/plugins/AlsaMixerPlugin.cxx deleted file mode 100644 index cc849ac..0000000 --- a/src/mixer/plugins/AlsaMixerPlugin.cxx +++ /dev/null @@ -1,336 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "AlsaMixerPlugin.hxx" -#include "VolumeMapping.hxx" -#include "lib/alsa/NonBlock.hxx" -#include "lib/alsa/Error.hxx" -#include "lib/fmt/RuntimeError.hxx" -#include "lib/fmt/ToBuffer.hxx" -#include "mixer/Mixer.hxx" -#include "mixer/Listener.hxx" -#include "output/OutputAPI.hxx" -#include "event/MultiSocketMonitor.hxx" -#include "event/InjectEvent.hxx" -#include "event/Call.hxx" -#include "util/ASCII.hxx" -#include "util/Domain.hxx" -#include "util/Math.hxx" -#include "Log.hxx" - -#include - -#define VOLUME_MIXER_ALSA_DEFAULT "default" -#define VOLUME_MIXER_ALSA_CONTROL_DEFAULT "PCM" -static constexpr unsigned VOLUME_MIXER_ALSA_INDEX_DEFAULT = 0; - -class AlsaMixerMonitor final : MultiSocketMonitor { - InjectEvent defer_invalidate_sockets; - - snd_mixer_t *mixer; - - Alsa::NonBlockMixer non_block; - -public: - AlsaMixerMonitor(EventLoop &_loop, snd_mixer_t *_mixer) noexcept - :MultiSocketMonitor(_loop), - defer_invalidate_sockets(_loop, - BIND_THIS_METHOD(InvalidateSockets)), - mixer(_mixer) { - defer_invalidate_sockets.Schedule(); - } - - ~AlsaMixerMonitor() noexcept { - BlockingCall(MultiSocketMonitor::GetEventLoop(), [this](){ - MultiSocketMonitor::Reset(); - defer_invalidate_sockets.Cancel(); - }); - } - - AlsaMixerMonitor(const AlsaMixerMonitor &) = delete; - AlsaMixerMonitor &operator=(const AlsaMixerMonitor &) = delete; - -private: - Event::Duration PrepareSockets() noexcept override; - void DispatchSockets() noexcept override; -}; - -class AlsaMixer final : public Mixer { - EventLoop &event_loop; - - const char *device; - const char *control; - unsigned int index; - - snd_mixer_t *handle; - snd_mixer_elem_t *elem; - - AlsaMixerMonitor *monitor; - - /** - * These fields are our workaround for rounding errors when - * the resolution of a mixer knob isn't fine enough to - * represent all 101 possible values (0..100). - * - * "desired_volume" is the percent value passed to - * SetVolume(), and "resulting_volume" is the volume which was - * actually set, and would be returned by the next - * GetPercentVolume() call. - * - * When GetVolume() is called, we compare the - * "resulting_volume" with the value returned by - * GetPercentVolume(), and if it's the same, we're still on - * the same value that was previously set (but may have been - * rounded down or up). - */ - int desired_volume, resulting_volume; - -public: - AlsaMixer(EventLoop &_event_loop, MixerListener &_listener) noexcept - :Mixer(alsa_mixer_plugin, _listener), - event_loop(_event_loop) {} - - ~AlsaMixer() noexcept override; - - AlsaMixer(const AlsaMixer &) = delete; - AlsaMixer &operator=(const AlsaMixer &) = delete; - - void Configure(const ConfigBlock &block); - void Setup(); - - /* virtual methods from class Mixer */ - void Open() override; - void Close() noexcept override; - int GetVolume() override; - void SetVolume(unsigned volume) override; - -private: - [[gnu::const]] - static unsigned NormalizedToPercent(double normalized) noexcept { - return lround(100 * normalized); - } - - [[gnu::pure]] - [[nodiscard]] double GetNormalizedVolume() const noexcept { - return get_normalized_playback_volume(elem, - SND_MIXER_SCHN_FRONT_LEFT); - } - - [[gnu::pure]] - [[nodiscard]] unsigned GetPercentVolume() const noexcept { - return NormalizedToPercent(GetNormalizedVolume()); - } - - static int ElemCallback(snd_mixer_elem_t *elem, - unsigned mask) noexcept; - -}; - -static constexpr Domain alsa_mixer_domain("alsa_mixer"); - -Event::Duration -AlsaMixerMonitor::PrepareSockets() noexcept -{ - if (mixer == nullptr) { - ClearSocketList(); - return Event::Duration(-1); - } - - return non_block.PrepareSockets(*this, mixer); -} - -void -AlsaMixerMonitor::DispatchSockets() noexcept -{ - assert(mixer != nullptr); - - non_block.DispatchSockets(*this, mixer); - - int err = snd_mixer_handle_events(mixer); - if (err < 0) { - FmtError(alsa_mixer_domain, - "snd_mixer_handle_events() failed: {}", - snd_strerror(err)); - - if (err == -ENODEV) { - /* the sound device was unplugged; disable - this GSource */ - mixer = nullptr; - InvalidateSockets(); - return; - } - } -} - -/* - * libasound callbacks - * - */ - -int -AlsaMixer::ElemCallback(snd_mixer_elem_t *elem, unsigned mask) noexcept -{ - AlsaMixer &mixer = *(AlsaMixer *) - snd_mixer_elem_get_callback_private(elem); - - if (mask & SND_CTL_EVENT_MASK_VALUE) { - int volume = mixer.GetPercentVolume(); - - if (mixer.resulting_volume >= 0 && - volume == mixer.resulting_volume) - /* still the same volume (this might be a - callback caused by SetVolume()) - switch to - desired_volume */ - volume = mixer.desired_volume; - else - /* flush */ - mixer.desired_volume = mixer.resulting_volume = -1; - - mixer.listener.OnMixerVolumeChanged(mixer, volume); - } - - return 0; -} - -/* - * mixer_plugin methods - * - */ - -inline void -AlsaMixer::Configure(const ConfigBlock &block) -{ - device = block.GetBlockValue("mixer_device", - VOLUME_MIXER_ALSA_DEFAULT); - control = block.GetBlockValue("mixer_control", - VOLUME_MIXER_ALSA_CONTROL_DEFAULT); - index = block.GetBlockValue("mixer_index", - VOLUME_MIXER_ALSA_INDEX_DEFAULT); -} - -static Mixer * -alsa_mixer_init(EventLoop &event_loop, [[maybe_unused]] AudioOutput &ao, - MixerListener &listener, - const ConfigBlock &block) -{ - auto *am = new AlsaMixer(event_loop, listener); - am->Configure(block); - - return am; -} - -AlsaMixer::~AlsaMixer() noexcept -{ - /* free libasound's config cache */ - snd_config_update_free_global(); -} - -[[gnu::pure]] -static snd_mixer_elem_t * -alsa_mixer_lookup_elem(snd_mixer_t *handle, - const char *name, unsigned idx) noexcept -{ - for (snd_mixer_elem_t *elem = snd_mixer_first_elem(handle); - elem != nullptr; elem = snd_mixer_elem_next(elem)) { - if (snd_mixer_elem_get_type(elem) == SND_MIXER_ELEM_SIMPLE && - StringEqualsCaseASCII(snd_mixer_selem_get_name(elem), - name) && - snd_mixer_selem_get_index(elem) == idx) - return elem; - } - - return nullptr; -} - -inline void -AlsaMixer::Setup() -{ - int err; - - if ((err = snd_mixer_attach(handle, device)) < 0) - throw Alsa::MakeError(err, - FmtBuffer<256>("failed to attach to {}", - device)); - - if ((err = snd_mixer_selem_register(handle, nullptr, nullptr)) < 0) - throw Alsa::MakeError(err, "snd_mixer_selem_register() failed"); - - if ((err = snd_mixer_load(handle)) < 0) - throw Alsa::MakeError(err, "snd_mixer_load() failed"); - - elem = alsa_mixer_lookup_elem(handle, control, index); - if (elem == nullptr) - throw FmtRuntimeError("no such mixer control: {}", control); - - snd_mixer_elem_set_callback_private(elem, this); - snd_mixer_elem_set_callback(elem, ElemCallback); - - monitor = new AlsaMixerMonitor(event_loop, handle); -} - -void -AlsaMixer::Open() -{ - desired_volume = resulting_volume = -1; - - int err; - - err = snd_mixer_open(&handle, 0); - if (err < 0) - throw Alsa::MakeError(err, "snd_mixer_open() failed"); - - try { - Setup(); - } catch (...) { - snd_mixer_close(handle); - throw; - } -} - -void -AlsaMixer::Close() noexcept -{ - assert(handle != nullptr); - - delete monitor; - - snd_mixer_elem_set_callback(elem, nullptr); - snd_mixer_close(handle); -} - -int -AlsaMixer::GetVolume() -{ - int err; - - assert(handle != nullptr); - - err = snd_mixer_handle_events(handle); - if (err < 0) - throw Alsa::MakeError(err, "snd_mixer_handle_events() failed"); - - int volume = GetPercentVolume(); - if (resulting_volume >= 0 && volume == resulting_volume) - /* we're still on the value passed to SetVolume() */ - volume = desired_volume; - - return volume; -} - -void -AlsaMixer::SetVolume(unsigned volume) -{ - assert(handle != nullptr); - - int err = set_normalized_playback_volume(elem, 0.01*volume, 1); - if (err < 0) - throw Alsa::MakeError(err, "failed to set ALSA volume"); - - desired_volume = volume; - resulting_volume = GetPercentVolume(); -} - -const MixerPlugin alsa_mixer_plugin = { - alsa_mixer_init, - true, -}; diff --git a/src/mixer/plugins/AlsaMixerPlugin.hxx b/src/mixer/plugins/AlsaMixerPlugin.hxx deleted file mode 100644 index 843f45f..0000000 --- a/src/mixer/plugins/AlsaMixerPlugin.hxx +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#pragma once - -struct MixerPlugin; - -extern const MixerPlugin alsa_mixer_plugin; diff --git a/src/mixer/plugins/AndroidMixerPlugin.cxx b/src/mixer/plugins/AndroidMixerPlugin.cxx deleted file mode 100644 index b2ccfc2..0000000 --- a/src/mixer/plugins/AndroidMixerPlugin.cxx +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "AndroidMixerPlugin.hxx" -#include "mixer/Mixer.hxx" -#include "filter/plugins/VolumeFilterPlugin.hxx" -#include "pcm/Volume.hxx" -#include "android/Context.hxx" -#include "android/AudioManager.hxx" - -#include "Main.hxx" - -#include -#include - -class AndroidMixer final : public Mixer { - AudioManager *audioManager; - int currentVolume; - int maxAndroidVolume; - int lastAndroidVolume; -public: - explicit AndroidMixer(MixerListener &_listener); - - ~AndroidMixer() override; - - /* virtual methods from class Mixer */ - void Open() override { - } - - void Close() noexcept override { - } - - int GetVolume() override; - - void SetVolume(unsigned volume) override; -}; - -static Mixer * -android_mixer_init([[maybe_unused]] EventLoop &event_loop, - [[maybe_unused]] AudioOutput &ao, - MixerListener &listener, - [[maybe_unused]] const ConfigBlock &block) -{ - return new AndroidMixer(listener); -} - -AndroidMixer::AndroidMixer(MixerListener &_listener) - :Mixer(android_mixer_plugin, _listener) -{ - JNIEnv *env = Java::GetEnv(); - audioManager = context->GetAudioManager(env); - - maxAndroidVolume = audioManager->GetMaxVolume(); - if (maxAndroidVolume != 0) - { - lastAndroidVolume = audioManager->GetVolume(env); - currentVolume = 100 * lastAndroidVolume / maxAndroidVolume; - } -} - -AndroidMixer::~AndroidMixer() -{ - delete audioManager; -} - -int -AndroidMixer::GetVolume() -{ - JNIEnv *env = Java::GetEnv(); - if (maxAndroidVolume == 0) - return -1; - - // The android volume index (or scale) is very likely inferior to the - // MPD one (100). The last volume set by MPD is saved into - // currentVolume, this volume is returned instead of the Android one - // when the Android mixer was not touched by an other application. This - // allows to fake a 0..100 scale from MPD. - - int volume = audioManager->GetVolume(env); - if (volume == lastAndroidVolume) - return currentVolume; - - return 100 * volume / maxAndroidVolume; -} - -void -AndroidMixer::SetVolume(unsigned newVolume) -{ - JNIEnv *env = Java::GetEnv(); - if (maxAndroidVolume == 0) - return; - currentVolume = newVolume; - lastAndroidVolume = currentVolume * maxAndroidVolume / 100; - audioManager->SetVolume(env, lastAndroidVolume); - -} - -const MixerPlugin android_mixer_plugin = { - android_mixer_init, - true, -}; diff --git a/src/mixer/plugins/AndroidMixerPlugin.hxx b/src/mixer/plugins/AndroidMixerPlugin.hxx deleted file mode 100644 index 5b72f09..0000000 --- a/src/mixer/plugins/AndroidMixerPlugin.hxx +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#pragma once - -struct MixerPlugin; - -extern const MixerPlugin android_mixer_plugin; diff --git a/src/mixer/plugins/OSXMixerPlugin.cxx b/src/mixer/plugins/OSXMixerPlugin.cxx deleted file mode 100644 index e5957df..0000000 --- a/src/mixer/plugins/OSXMixerPlugin.cxx +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "OSXMixerPlugin.hxx" -#include "mixer/Mixer.hxx" -#include "output/plugins/OSXOutputPlugin.hxx" - -class OSXMixer final : public Mixer { - OSXOutput &output; - -public: - OSXMixer(OSXOutput &_output, MixerListener &_listener) - :Mixer(osx_mixer_plugin, _listener), - output(_output) - { - } - - /* virtual methods from class Mixer */ - void Open() noexcept override { - } - - void Close() noexcept override { - } - - int GetVolume() override; - void SetVolume(unsigned volume) override; -}; - -int -OSXMixer::GetVolume() -{ - return osx_output_get_volume(output); -} - -void -OSXMixer::SetVolume(unsigned new_volume) -{ - osx_output_set_volume(output, new_volume); -} - -static Mixer * -osx_mixer_init([[maybe_unused]] EventLoop &event_loop, AudioOutput &ao, - MixerListener &listener, - [[maybe_unused]] const ConfigBlock &block) -{ - OSXOutput &osxo = (OSXOutput &)ao; - return new OSXMixer(osxo, listener); -} - -const MixerPlugin osx_mixer_plugin = { - osx_mixer_init, - true, -}; diff --git a/src/mixer/plugins/OSXMixerPlugin.hxx b/src/mixer/plugins/OSXMixerPlugin.hxx deleted file mode 100644 index 2dbfaa7..0000000 --- a/src/mixer/plugins/OSXMixerPlugin.hxx +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#pragma once - -struct MixerPlugin; - -extern const MixerPlugin osx_mixer_plugin; diff --git a/src/mixer/plugins/OssMixerPlugin.cxx b/src/mixer/plugins/OssMixerPlugin.cxx deleted file mode 100644 index 5e3f94b..0000000 --- a/src/mixer/plugins/OssMixerPlugin.cxx +++ /dev/null @@ -1,161 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "OssMixerPlugin.hxx" -#include "mixer/Mixer.hxx" -#include "config/Block.hxx" -#include "lib/fmt/RuntimeError.hxx" -#include "io/FileDescriptor.hxx" -#include "lib/fmt/SystemError.hxx" -#include "util/ASCII.hxx" -#include "util/Domain.hxx" -#include "Log.hxx" - -#include - -#include -#include -#include -#include -#include - -#include - -#define VOLUME_MIXER_OSS_DEFAULT "/dev/mixer" - -class OssMixer final : public Mixer { - const char *device; - const char *control; - - FileDescriptor device_fd; - int volume_control; - -public: - OssMixer(MixerListener &_listener, const ConfigBlock &block) - :Mixer(oss_mixer_plugin, _listener) { - Configure(block); - } - - void Configure(const ConfigBlock &block); - - /* virtual methods from class Mixer */ - void Open() override; - void Close() noexcept override; - int GetVolume() override; - void SetVolume(unsigned volume) override; -}; - -static constexpr Domain oss_mixer_domain("oss_mixer"); - -static int -oss_find_mixer(const char *name) -{ - const char *labels[SOUND_MIXER_NRDEVICES] = SOUND_DEVICE_LABELS; - size_t name_length = strlen(name); - - for (unsigned i = 0; i < SOUND_MIXER_NRDEVICES; i++) { - if (StringEqualsCaseASCII(name, labels[i], name_length) && - (labels[i][name_length] == 0 || - labels[i][name_length] == ' ')) - return i; - } - return -1; -} - -inline void -OssMixer::Configure(const ConfigBlock &block) -{ - device = block.GetBlockValue("mixer_device", VOLUME_MIXER_OSS_DEFAULT); - control = block.GetBlockValue("mixer_control"); - - if (control != NULL) { - volume_control = oss_find_mixer(control); - if (volume_control < 0) - throw FmtRuntimeError("no such mixer control: {}", - control); - } else - volume_control = SOUND_MIXER_PCM; -} - -static Mixer * -oss_mixer_init([[maybe_unused]] EventLoop &event_loop, - [[maybe_unused]] AudioOutput &ao, - MixerListener &listener, - const ConfigBlock &block) -{ - return new OssMixer(listener, block); -} - -void -OssMixer::Close() noexcept -{ - assert(device_fd.IsDefined()); - - device_fd.Close(); -} - -void -OssMixer::Open() -{ - if (!device_fd.OpenReadOnly(device)) - throw FmtErrno("failed to open {}", device); - - try { - if (control) { - int devmask = 0; - - if (ioctl(device_fd.Get(), SOUND_MIXER_READ_DEVMASK, &devmask) < 0) - throw MakeErrno("READ_DEVMASK failed"); - - if (((1 << volume_control) & devmask) == 0) - throw FmtErrno("mixer control {:?} not usable", - control); - } - } catch (...) { - Close(); - throw; - } -} - -int -OssMixer::GetVolume() -{ - int left, right, level; - int ret; - - assert(device_fd.IsDefined()); - - ret = ioctl(device_fd.Get(), MIXER_READ(volume_control), &level); - if (ret < 0) - throw MakeErrno("failed to read OSS volume"); - - left = level & 0xff; - right = (level & 0xff00) >> 8; - - if (left != right) { - FmtWarning(oss_mixer_domain, - "volume for left and right is not the same, {:?} and " - "{:?}\n", left, right); - } - - return left; -} - -void -OssMixer::SetVolume(unsigned volume) -{ - int level; - - assert(device_fd.IsDefined()); - assert(volume <= 100); - - level = (volume << 8) + volume; - - if (ioctl(device_fd.Get(), MIXER_WRITE(volume_control), &level) < 0) - throw MakeErrno("failed to set OSS volume"); -} - -constexpr MixerPlugin oss_mixer_plugin = { - oss_mixer_init, - true, -}; diff --git a/src/mixer/plugins/OssMixerPlugin.hxx b/src/mixer/plugins/OssMixerPlugin.hxx deleted file mode 100644 index e925345..0000000 --- a/src/mixer/plugins/OssMixerPlugin.hxx +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#pragma once - -struct MixerPlugin; - -extern const MixerPlugin oss_mixer_plugin; diff --git a/src/mixer/plugins/PipeWireMixerPlugin.cxx b/src/mixer/plugins/PipeWireMixerPlugin.cxx deleted file mode 100644 index 98ea62c..0000000 --- a/src/mixer/plugins/PipeWireMixerPlugin.cxx +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "PipeWireMixerPlugin.hxx" -#include "mixer/Mixer.hxx" -#include "mixer/Listener.hxx" -#include "output/plugins/PipeWireOutputPlugin.hxx" - -#include - -class PipeWireMixer final : public Mixer { - PipeWireOutput &output; - - int volume = 100; - -public: - PipeWireMixer(PipeWireOutput &_output, - MixerListener &_listener) noexcept - :Mixer(pipewire_mixer_plugin, _listener), - output(_output) - { - } - - ~PipeWireMixer() noexcept override; - - PipeWireMixer(const PipeWireMixer &) = delete; - PipeWireMixer &operator=(const PipeWireMixer &) = delete; - - void OnVolumeChanged(float new_volume) noexcept { - volume = std::lround(new_volume * 100.f); - - listener.OnMixerVolumeChanged(*this, volume); - } - - /* virtual methods from class Mixer */ - void Open() override { - } - - void Close() noexcept override { - } - - int GetVolume() override; - void SetVolume(unsigned volume) override; -}; - -void -pipewire_mixer_on_change(PipeWireMixer &pm, float new_volume) noexcept -{ - pm.OnVolumeChanged(new_volume); -} - -int -PipeWireMixer::GetVolume() -{ - return volume; -} - -void -PipeWireMixer::SetVolume(unsigned new_volume) -{ - pipewire_output_set_volume(output, float(new_volume) * 0.01f); - volume = new_volume; -} - -static Mixer * -pipewire_mixer_init([[maybe_unused]] EventLoop &event_loop, AudioOutput &ao, - MixerListener &listener, - const ConfigBlock &) -{ - auto &po = (PipeWireOutput &)ao; - auto *pm = new PipeWireMixer(po, listener); - pipewire_output_set_mixer(po, *pm); - return pm; -} - -PipeWireMixer::~PipeWireMixer() noexcept -{ - pipewire_output_clear_mixer(output, *this); -} - -const MixerPlugin pipewire_mixer_plugin = { - pipewire_mixer_init, - true, -}; diff --git a/src/mixer/plugins/PipeWireMixerPlugin.hxx b/src/mixer/plugins/PipeWireMixerPlugin.hxx deleted file mode 100644 index ffdad97..0000000 --- a/src/mixer/plugins/PipeWireMixerPlugin.hxx +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#ifndef MPD_PIPEWIRE_MIXER_PLUGIN_HXX -#define MPD_PIPEWIRE_MIXER_PLUGIN_HXX - -struct MixerPlugin; -class PipeWireMixer; - -extern const MixerPlugin pipewire_mixer_plugin; - -void -pipewire_mixer_on_change(PipeWireMixer &pm, float new_volume) noexcept; - -#endif diff --git a/src/mixer/plugins/PulseMixerPlugin.cxx b/src/mixer/plugins/PulseMixerPlugin.cxx deleted file mode 100644 index 5058a1f..0000000 --- a/src/mixer/plugins/PulseMixerPlugin.cxx +++ /dev/null @@ -1,230 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "PulseMixerPlugin.hxx" -#include "lib/fmt/RuntimeError.hxx" -#include "lib/pulse/LogError.hxx" -#include "lib/pulse/LockGuard.hxx" -#include "mixer/Mixer.hxx" -#include "mixer/Listener.hxx" -#include "output/plugins/PulseOutputPlugin.hxx" -#include "util/CNumberParser.hxx" -#include "config/Block.hxx" - -#include -#include -#include -#include - -#include -#include - -class PulseMixer final : public Mixer { - PulseOutput &output; - - const float volume_scale_factor; - - bool online = false; - - struct pa_cvolume volume; - -public: - PulseMixer(PulseOutput &_output, MixerListener &_listener, - double _volume_scale_factor) - :Mixer(pulse_mixer_plugin, _listener), - output(_output), - volume_scale_factor(float(_volume_scale_factor)) - { - } - - ~PulseMixer() override; - - PulseMixer(const PulseMixer &) = delete; - PulseMixer &operator=(const PulseMixer &) = delete; - - void Offline(); - void VolumeCallback(const pa_sink_input_info *i, int eol); - void Update(pa_context *context, pa_stream *stream); - int GetVolumeInternal(); - - /* virtual methods from class Mixer */ - void Open() override { - } - - void Close() noexcept override { - } - - int GetVolume() override; - void SetVolume(unsigned volume) override; -}; - -void -PulseMixer::Offline() -{ - if (!online) - return; - - online = false; - - listener.OnMixerVolumeChanged(*this, -1); -} - -inline void -PulseMixer::VolumeCallback(const pa_sink_input_info *i, int eol) -{ - if (eol) - return; - - if (i == nullptr) { - Offline(); - return; - } - - online = true; - volume = i->volume; - - listener.OnMixerVolumeChanged(*this, GetVolumeInternal()); -} - -/** - * Callback invoked by pulse_mixer_update(). Receives the new mixer - * value. - */ -static void -pulse_mixer_volume_cb([[maybe_unused]] pa_context *context, const pa_sink_input_info *i, - int eol, void *userdata) -{ - auto *pm = (PulseMixer *)userdata; - pm->VolumeCallback(i, eol); -} - -inline void -PulseMixer::Update(pa_context *context, pa_stream *stream) -{ - assert(context != nullptr); - assert(stream != nullptr); - assert(pa_stream_get_state(stream) == PA_STREAM_READY); - - pa_operation *o = - pa_context_get_sink_input_info(context, - pa_stream_get_index(stream), - pulse_mixer_volume_cb, this); - if (o == nullptr) { - LogPulseError(context, - "pa_context_get_sink_input_info() failed"); - Offline(); - return; - } - - pa_operation_unref(o); -} - -void -pulse_mixer_on_connect([[maybe_unused]] PulseMixer &pm, - struct pa_context *context) -{ - pa_operation *o; - - assert(context != nullptr); - - o = pa_context_subscribe(context, - (pa_subscription_mask_t)PA_SUBSCRIPTION_MASK_SINK_INPUT, - nullptr, nullptr); - if (o == nullptr) { - LogPulseError(context, - "pa_context_subscribe() failed"); - return; - } - - pa_operation_unref(o); -} - -void -pulse_mixer_on_disconnect(PulseMixer &pm) -{ - pm.Offline(); -} - -void -pulse_mixer_on_change(PulseMixer &pm, - struct pa_context *context, struct pa_stream *stream) -{ - pm.Update(context, stream); -} - -static float -parse_volume_scale_factor(const char *value) { - if (value == nullptr) - return 1.0; - - char *endptr; - float factor = ParseFloat(value, &endptr); - - if (endptr == value || *endptr != '\0' || factor < 0.5f || factor > 5.0f) - throw FmtRuntimeError("{:?} is not a number in the " - "range 0.5 to 5.0", - value); - - return factor; -} - -static Mixer * -pulse_mixer_init([[maybe_unused]] EventLoop &event_loop, AudioOutput &ao, - MixerListener &listener, - const ConfigBlock &block) -{ - auto &po = (PulseOutput &)ao; - float scale = parse_volume_scale_factor(block.GetBlockValue("scale_volume")); - auto *pm = new PulseMixer(po, listener, (double)scale); - - pulse_output_set_mixer(po, *pm); - - return pm; -} - -PulseMixer::~PulseMixer() -{ - pulse_output_clear_mixer(output, *this); -} - -int -PulseMixer::GetVolume() -{ - Pulse::LockGuard lock(pulse_output_get_mainloop(output)); - - return GetVolumeInternal(); -} - -/** - * Pulse mainloop lock must be held by caller - */ -int -PulseMixer::GetVolumeInternal() -{ - auto max_pa_volume = pa_volume_t(volume_scale_factor * PA_VOLUME_NORM); - return online ? - (int)((100 * (pa_cvolume_avg(&volume) + 1)) / max_pa_volume) - : -1; -} - -void -PulseMixer::SetVolume(unsigned new_volume) -{ - Pulse::LockGuard lock(pulse_output_get_mainloop(output)); - - if (!online) - throw std::runtime_error("disconnected"); - - auto max_pa_volume = pa_volume_t(volume_scale_factor * PA_VOLUME_NORM); - - struct pa_cvolume cvolume; - pa_cvolume_set(&cvolume, volume.channels, - (new_volume * max_pa_volume + 50) / 100); - pulse_output_set_volume(output, &cvolume); - volume = cvolume; -} - -const MixerPlugin pulse_mixer_plugin = { - pulse_mixer_init, - false, -}; diff --git a/src/mixer/plugins/PulseMixerPlugin.hxx b/src/mixer/plugins/PulseMixerPlugin.hxx deleted file mode 100644 index 17ebc46..0000000 --- a/src/mixer/plugins/PulseMixerPlugin.hxx +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#pragma once - -struct MixerPlugin; -class PulseMixer; -struct pa_context; -struct pa_stream; - -extern const MixerPlugin pulse_mixer_plugin; - -void -pulse_mixer_on_connect(PulseMixer &pm, pa_context *context); - -void -pulse_mixer_on_disconnect(PulseMixer &pm); - -void -pulse_mixer_on_change(PulseMixer &pm, pa_context *context, pa_stream *stream); diff --git a/src/mixer/plugins/SndioMixerPlugin.cxx b/src/mixer/plugins/SndioMixerPlugin.cxx deleted file mode 100644 index 7574b66..0000000 --- a/src/mixer/plugins/SndioMixerPlugin.cxx +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright Christopher Zimmermann - -#include "SndioMixerPlugin.hxx" -#include "mixer/Mixer.hxx" -#include "output/plugins/SndioOutputPlugin.hxx" - -class SndioMixer final : public Mixer { - SndioOutput &output; - -public: - SndioMixer(SndioOutput &_output, MixerListener &_listener) - :Mixer(sndio_mixer_plugin, _listener), output(_output) - { - output.RegisterMixerListener(this, &_listener); - } - - /* virtual methods from class Mixer */ - void Open() override {} - - void Close() noexcept override {} - - int GetVolume() override { - return output.GetVolume(); - } - - void SetVolume(unsigned volume) override { - output.SetVolume(volume); - } - -}; - -static Mixer * -sndio_mixer_init([[maybe_unused]] EventLoop &event_loop, - AudioOutput &ao, - MixerListener &listener, - [[maybe_unused]] const ConfigBlock &block) -{ - return new SndioMixer((SndioOutput &)ao, listener); -} - -constexpr MixerPlugin sndio_mixer_plugin = { - sndio_mixer_init, - false, -}; diff --git a/src/mixer/plugins/SndioMixerPlugin.hxx b/src/mixer/plugins/SndioMixerPlugin.hxx deleted file mode 100644 index 34940fe..0000000 --- a/src/mixer/plugins/SndioMixerPlugin.hxx +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#pragma once - -struct MixerPlugin; - -extern const MixerPlugin sndio_mixer_plugin; diff --git a/src/mixer/plugins/WasapiMixerPlugin.cxx b/src/mixer/plugins/WasapiMixerPlugin.cxx deleted file mode 100644 index 5ef8d71..0000000 --- a/src/mixer/plugins/WasapiMixerPlugin.cxx +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#undef NOUSER // COM needs the "MSG" typedef - -#include "WasapiMixerPlugin.hxx" -#include "output/plugins/wasapi/ForMixer.hxx" -#include "output/plugins/wasapi/AudioClient.hxx" -#include "output/plugins/wasapi/Device.hxx" -#include "mixer/Mixer.hxx" -#include "win32/ComPtr.hxx" -#include "win32/ComWorker.hxx" -#include "win32/HResult.hxx" - -#include -#include - -#include -#include -#include - -class WasapiMixer final : public Mixer { - WasapiOutput &output; - -public: - WasapiMixer(WasapiOutput &_output, MixerListener &_listener) - : Mixer(wasapi_mixer_plugin, _listener), output(_output) {} - - void Open() override {} - - void Close() noexcept override {} - - int GetVolume() override { - auto com_worker = wasapi_output_get_com_worker(output); - if (!com_worker) - return -1; - - auto future = com_worker->Async([&]() -> int { - HRESULT result; - float volume_level; - - if (wasapi_is_exclusive(output)) { - auto endpoint_volume = - Activate(*wasapi_output_get_device(output)); - - result = endpoint_volume->GetMasterVolumeLevelScalar( - &volume_level); - if (FAILED(result)) { - throw MakeHResultError(result, - "Unable to get master " - "volume level"); - } - } else { - auto session_volume = - GetService(*wasapi_output_get_client(output)); - - result = session_volume->GetMasterVolume(&volume_level); - if (FAILED(result)) { - throw MakeHResultError( - result, "Unable to get master volume"); - } - } - - return std::lround(volume_level * 100.0f); - }); - return future.get(); - } - - void SetVolume(unsigned volume) override { - auto com_worker = wasapi_output_get_com_worker(output); - if (!com_worker) - throw std::runtime_error("Cannot set WASAPI volume"); - - com_worker->Async([&]() { - HRESULT result; - const float volume_level = volume / 100.0f; - - if (wasapi_is_exclusive(output)) { - auto endpoint_volume = - Activate(*wasapi_output_get_device(output)); - - result = endpoint_volume->SetMasterVolumeLevelScalar( - volume_level, nullptr); - if (FAILED(result)) { - throw MakeHResultError( - result, - "Unable to set master volume level"); - } - } else { - auto session_volume = - GetService(*wasapi_output_get_client(output)); - - result = session_volume->SetMasterVolume(volume_level, - nullptr); - if (FAILED(result)) { - throw MakeHResultError( - result, "Unable to set master volume"); - } - } - }).get(); - } -}; - -static Mixer *wasapi_mixer_init(EventLoop &, AudioOutput &ao, MixerListener &listener, - const ConfigBlock &) { - return new WasapiMixer(wasapi_output_downcast(ao), listener); -} - -const MixerPlugin wasapi_mixer_plugin = { - wasapi_mixer_init, - false, -}; diff --git a/src/mixer/plugins/WasapiMixerPlugin.hxx b/src/mixer/plugins/WasapiMixerPlugin.hxx deleted file mode 100644 index 2577179..0000000 --- a/src/mixer/plugins/WasapiMixerPlugin.hxx +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#pragma once - -struct MixerPlugin; - -extern const MixerPlugin wasapi_mixer_plugin; diff --git a/src/mixer/plugins/WinmmMixerPlugin.cxx b/src/mixer/plugins/WinmmMixerPlugin.cxx deleted file mode 100644 index 08d5073..0000000 --- a/src/mixer/plugins/WinmmMixerPlugin.cxx +++ /dev/null @@ -1,86 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "WinmmMixerPlugin.hxx" -#include "mixer/Mixer.hxx" -#include "output/Features.h" -#include "output/OutputAPI.hxx" -#include "output/plugins/WinmmOutputPlugin.hxx" -#include "util/Math.hxx" - -#include - -#include -#include - -#include - -class WinmmMixer final : public Mixer { - WinmmOutput &output; - -public: - WinmmMixer(WinmmOutput &_output, MixerListener &_listener) - :Mixer(winmm_mixer_plugin, _listener), - output(_output) { - } - - /* virtual methods from class Mixer */ - void Open() override { - } - - void Close() noexcept override { - } - - int GetVolume() override; - void SetVolume(unsigned volume) override; -}; - -static inline int -winmm_volume_decode(DWORD volume) -{ - return lround((volume & 0xFFFF) / 655.35); -} - -static inline DWORD -winmm_volume_encode(int volume) -{ - int value = lround(volume * 655.35); - return MAKELONG(value, value); -} - -static Mixer * -winmm_mixer_init([[maybe_unused]] EventLoop &event_loop, AudioOutput &ao, - MixerListener &listener, - [[maybe_unused]] const ConfigBlock &block) -{ - return new WinmmMixer((WinmmOutput &)ao, listener); -} - -int -WinmmMixer::GetVolume() -{ - DWORD volume; - HWAVEOUT handle = winmm_output_get_handle(output); - MMRESULT result = waveOutGetVolume(handle, &volume); - - if (result != MMSYSERR_NOERROR) - throw std::runtime_error("Failed to get winmm volume"); - - return winmm_volume_decode(volume); -} - -void -WinmmMixer::SetVolume(unsigned volume) -{ - DWORD value = winmm_volume_encode(volume); - HWAVEOUT handle = winmm_output_get_handle(output); - MMRESULT result = waveOutSetVolume(handle, value); - - if (result != MMSYSERR_NOERROR) - throw std::runtime_error("Failed to set winmm volume"); -} - -const MixerPlugin winmm_mixer_plugin = { - winmm_mixer_init, - false, -}; diff --git a/src/mixer/plugins/WinmmMixerPlugin.hxx b/src/mixer/plugins/WinmmMixerPlugin.hxx deleted file mode 100644 index f6f303f..0000000 --- a/src/mixer/plugins/WinmmMixerPlugin.hxx +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#pragma once - -struct MixerPlugin; - -extern const MixerPlugin winmm_mixer_plugin; diff --git a/src/mixer/plugins/meson.build b/src/mixer/plugins/meson.build index 3c6efb2..c566a78 100644 --- a/src/mixer/plugins/meson.build +++ b/src/mixer/plugins/meson.build @@ -3,41 +3,7 @@ mixer_plugins_sources = [ 'SoftwareMixerPlugin.cxx', ] -if alsa_dep.found() - mixer_plugins_sources += [ - 'AlsaMixerPlugin.cxx', - 'VolumeMapping.cxx', - ] -endif - -if enable_oss - mixer_plugins_sources += 'OssMixerPlugin.cxx' -endif - -if is_darwin - mixer_plugins_sources += 'OSXMixerPlugin.cxx' -endif - -if pipewire_dep.found() - mixer_plugins_sources += 'PipeWireMixerPlugin.cxx' -endif - -if pulse_dep.found() - mixer_plugins_sources += 'PulseMixerPlugin.cxx' -endif - -if libsndio_dep.found() - mixer_plugins_sources += 'SndioMixerPlugin.cxx' -endif - -if is_windows - mixer_plugins_sources += [ - 'WinmmMixerPlugin.cxx', - 'WasapiMixerPlugin.cxx', - ] -endif -# Android support removed mixer_plugins = static_library( 'mixer_plugins', @@ -45,10 +11,6 @@ mixer_plugins = static_library( include_directories: inc, dependencies: [ mixer_api_dep, - alsa_dep, - pulse_dep, - libsndio_dep, - log_dep, ] ) diff --git a/src/output/plugins/AlsaOutputPlugin.cxx b/src/output/plugins/AlsaOutputPlugin.cxx deleted file mode 100644 index 7f2b16d..0000000 --- a/src/output/plugins/AlsaOutputPlugin.cxx +++ /dev/null @@ -1,1367 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "AlsaOutputPlugin.hxx" -#include "lib/alsa/AllowedFormat.hxx" -#include "lib/alsa/ChannelMap.hxx" -#include "lib/alsa/Error.hxx" -#include "lib/alsa/HwSetup.hxx" -#include "lib/alsa/NonBlock.hxx" -#include "lib/alsa/PeriodBuffer.hxx" -#include "lib/fmt/RuntimeError.hxx" -#include "lib/fmt/ToBuffer.hxx" -#include "../OutputAPI.hxx" -#include "../Error.hxx" -#include "mixer/plugins/AlsaMixerPlugin.hxx" -#include "pcm/Export.hxx" -#include "pcm/Features.h" // for ENABLE_DSD -#include "time/PeriodClock.hxx" -#include "thread/Mutex.hxx" -#include "thread/Cond.hxx" -#include "util/Manual.hxx" -#include "util/Domain.hxx" -#include "event/MultiSocketMonitor.hxx" -#include "event/InjectEvent.hxx" -#include "event/FineTimerEvent.hxx" -#include "event/Call.hxx" -#include "util/RingBuffer.hxx" -#include "Log.hxx" - -#ifdef ENABLE_DSD -#include "util/AllocatedArray.hxx" -#endif - -#include - -#include -#include -#include - -static const char default_device[] = "default"; - -static constexpr unsigned MPD_ALSA_BUFFER_TIME_US = 500000; - -class AlsaOutput final - : AudioOutput, MultiSocketMonitor -{ - InjectEvent defer_invalidate_sockets; - - /** - * This timer is used to re-schedule the #MultiSocketMonitor - * after it had been disabled to wait for the next Play() call - * to deliver more data. This timer is necessary to start - * generating silence if Play() doesn't get called soon enough - * to avoid the xrun. - */ - FineTimerEvent silence_timer; - - PeriodClock throttle_silence_log; - - Manual pcm_export; - - /** - * The configured name of the ALSA device; empty for the - * default device - */ - const std::string device; - - std::forward_list allowed_formats; - - /** - * Protects #dop_setting and #allowed_formats. - */ - mutable Mutex attributes_mutex; - - /** the libasound PCM device handle */ - snd_pcm_t *pcm; - - /** - * 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; - - Event::Duration effective_period_duration; - - /** - * This buffer gets allocated after opening the ALSA device. - * It contains silence samples, enough to fill one period (see - * #period_frames). - */ - std::byte *silence; - - Alsa::NonBlockPcm non_block; - - /** - * For copying data from OutputThread to IOThread. - */ - using RingBuffer = ::RingBuffer; - RingBuffer ring_buffer; - - Alsa::PeriodBuffer period_buffer; - - /** - * Protects #cond, #error, #active, #waiting, #drain. - */ - mutable Mutex mutex; - - /** - * Used to wait when #ring_buffer is full. It will be - * signalled each time data is popped from the #ring_buffer, - * making space for more data. - */ - Cond cond; - - std::exception_ptr error; - - /** - * The size of one period, in number of frames. - */ - snd_pcm_uframes_t period_frames; - - /** - * If snd_pcm_avail() goes above this value and no more data - * is available in the #ring_buffer, we need to play some - * silence. - */ - snd_pcm_sframes_t max_avail_frames; - - /** libasound's buffer_time setting (in microseconds) */ - const unsigned buffer_time; - - /** libasound's period_time setting (in microseconds) */ - const unsigned period_time; - - /** the mode flags passed to snd_pcm_open */ - const int mode; - -#ifdef ENABLE_DSD - /** - * Enable DSD over PCM according to the DoP standard? - * - * @see http://dsd-guide.com/dop-open-standard - */ - bool dop_setting; - - /** - * Are we currently playing DSD? (Native DSD or DoP) - */ - bool use_dsd; - - /** - * Play some silence before closing the output in DSD mode? - * This is a workaround for some DACs which emit noise when - * stopping DSD playback. - */ - const bool stop_dsd_silence; - - /** - * Are we currently draining with #stop_dsd_silence? - */ - bool in_stop_dsd_silence; - - /** - * Enable the DSD sync workaround for Thesycon USB audio - * receivers? On this device, playing DSD512 or PCM causes - * all subsequent attempts to play other DSD rates to fail, - * which can be fixed by briefly playing PCM at 44.1 kHz. - */ - const bool thesycon_dsd_workaround; - - bool need_thesycon_dsd_workaround = thesycon_dsd_workaround; -#endif - - /** - * After Open() or Cancel(), has this output been activated by - * a Play() command? - * - * Protected by #mutex. - */ - bool active; - - /** - * Is this output waiting for more data? - * - * Protected by #mutex. - */ - bool waiting; - - /** - * Do we need to call snd_pcm_prepare() before the next write? - * It means that we put the device to SND_PCM_STATE_SETUP by - * calling snd_pcm_drop(). - * - * Without this flag, we could easily recover after a failed - * optimistic write (returning -EBADFD), but the Raspberry Pi - * audio driver is infamous for generating ugly artefacts from - * this. - */ - bool must_prepare; - - /** - * Has snd_pcm_writei() been called successfully at least once - * since the PCM was prepared? - * - * This is necessary to work around a kernel bug which causes - * snd_pcm_drain() to return -EAGAIN forever in non-blocking - * mode if snd_pcm_writei() was never called. - */ - bool written; - - bool drain; - - /** - * Was Interrupt() called? This will unblock - * LockWaitWriteAvailable(). It will be reset by Cancel() and - * Pause(), as documented by the #AudioOutput interface. - * - * Only initialized while the output is open. - */ - bool interrupted; - - /** - * Close the ALSA PCM while playback is paused? This defaults - * to true because this allows other applications to use the - * PCM while MPD is paused. - */ - const bool close_on_pause; - - std::atomic_bool paused; - -public: - AlsaOutput(EventLoop &loop, const ConfigBlock &block); - - ~AlsaOutput() noexcept override { - /* free libasound's config cache */ - snd_config_update_free_global(); - } - - AlsaOutput(const AlsaOutput &) = delete; - AlsaOutput &operator=(const AlsaOutput &) = delete; - - using MultiSocketMonitor::GetEventLoop; - - [[gnu::pure]] - const char *GetDevice() const noexcept { - return device.empty() ? default_device : device.c_str(); - } - - static AudioOutput *Create(EventLoop &event_loop, - const ConfigBlock &block) { - return new AlsaOutput(event_loop, block); - } - -private: - void UnregisterSockets() noexcept { - MultiSocketMonitor::Reset(); - defer_invalidate_sockets.Cancel(); - } - - std::map> GetAttributes() const noexcept override; - void SetAttribute(std::string &&name, std::string &&value) override; - - void Enable() override; - void Disable() noexcept override; - - void Open(AudioFormat &audio_format) override; - void Close() noexcept override; - - void Interrupt() noexcept override; - std::chrono::steady_clock::duration Delay() const noexcept override; - - std::size_t Play(std::span src) override; - void Drain() override; - void Cancel() noexcept override; - bool Pause() noexcept override; - - /** - * Set up the snd_pcm_t object which was opened by the caller. - * Set up the configured settings and the audio format. - * - * Throws on error. - */ - void Setup(AudioFormat &audio_format, PcmExport::Params ¶ms); - -#ifdef ENABLE_DSD - void SetupDop(AudioFormat audio_format, - PcmExport::Params ¶ms); -#endif - - void SetupOrDop(AudioFormat &audio_format, PcmExport::Params ¶ms -#ifdef ENABLE_DSD - , bool dop -#endif - ); - - [[gnu::pure]] - bool LockIsActive() const noexcept { - const std::scoped_lock lock{mutex}; - return active; - } - - [[gnu::pure]] - bool LockIsActiveAndNotWaiting() const noexcept { - const std::scoped_lock lock{mutex}; - return active && !waiting; - } - - /** - * Activate the output by registering the sockets in the - * #EventLoop. Before calling this, filling the ring buffer - * has no effect; nothing will be played, and no code will be - * run on #EventLoop's thread. - * - * Caller must hold the mutex. - * - * @return true if Activate() was called, false if the mutex - * was never unlocked - */ - bool Activate() noexcept { - if (active && !waiting) - return false; - - active = true; - waiting = false; - - const ScopeUnlock unlock(mutex); - defer_invalidate_sockets.Schedule(); - return true; - } - - /** - * Wait until there is some space available in the ring buffer. - * - * Caller must not lock the mutex. - * - * Throws on error. - * - * @return the number of frames available for writing - */ - size_t LockWaitWriteAvailable(); - - int Recover(int err) noexcept; - - /** - * Drain all buffers. To be run in #EventLoop's thread. - * - * Throws on error. - * - * @return true if draining is complete, false if this method - * needs to be called again later - */ - bool DrainInternal(); - - /** - * Stop playback immediately, dropping all buffers. To be run - * in #EventLoop's thread. - */ - void CancelInternal() noexcept; - - /** - * @return false if no data was moved - */ - bool CopyRingToPeriodBuffer() noexcept; - - snd_pcm_sframes_t WriteFromPeriodBuffer() noexcept; - - void LockCaughtError() noexcept { - period_buffer.Clear(); - - const std::scoped_lock lock{mutex}; - error = std::current_exception(); - active = false; - waiting = false; -#ifdef ENABLE_DSD - in_stop_dsd_silence = false; -#endif - cond.notify_one(); - } - - /** - * Callback for @silence_timer - */ - void OnSilenceTimer() noexcept { - { - const std::scoped_lock lock{mutex}; - assert(active); - waiting = false; - } - - MultiSocketMonitor::InvalidateSockets(); - } - - /* virtual methods from class MultiSocketMonitor */ - Event::Duration PrepareSockets() noexcept override; - void DispatchSockets() noexcept override; -}; - -static constexpr Domain alsa_output_domain("alsa_output"); - -static int -GetAlsaOpenMode(const ConfigBlock &block) -{ - int mode = 0; - -#ifdef SND_PCM_NO_AUTO_RESAMPLE - if (!block.GetBlockValue("auto_resample", true)) - mode |= SND_PCM_NO_AUTO_RESAMPLE; -#endif - -#ifdef SND_PCM_NO_AUTO_CHANNELS - if (!block.GetBlockValue("auto_channels", true)) - mode |= SND_PCM_NO_AUTO_CHANNELS; -#endif - -#ifdef SND_PCM_NO_AUTO_FORMAT - if (!block.GetBlockValue("auto_format", true)) - mode |= SND_PCM_NO_AUTO_FORMAT; -#endif - - return mode; -} - -AlsaOutput::AlsaOutput(EventLoop &_loop, const ConfigBlock &block) - :AudioOutput(FLAG_ENABLE_DISABLE|FLAG_PAUSE), - MultiSocketMonitor(_loop), - defer_invalidate_sockets(_loop, BIND_THIS_METHOD(InvalidateSockets)), - silence_timer(_loop, BIND_THIS_METHOD(OnSilenceTimer)), - device(block.GetBlockValue("device", "")), - buffer_time(block.GetPositiveValue("buffer_time", - MPD_ALSA_BUFFER_TIME_US)), - period_time(block.GetPositiveValue("period_time", 0U)), - mode(GetAlsaOpenMode(block)), -#ifdef ENABLE_DSD - dop_setting(block.GetBlockValue("dop", false) || - /* legacy name from MPD 0.18 and older: */ - block.GetBlockValue("dsd_usb", false)), - stop_dsd_silence(block.GetBlockValue("stop_dsd_silence", false)), - thesycon_dsd_workaround(block.GetBlockValue("thesycon_dsd_workaround", - false)), -#endif - close_on_pause(block.GetBlockValue("close_on_pause", true)) -{ - const char *allowed_formats_string = - block.GetBlockValue("allowed_formats", nullptr); - if (allowed_formats_string != nullptr) - allowed_formats = Alsa::AllowedFormat::ParseList(allowed_formats_string); -} - -std::map> -AlsaOutput::GetAttributes() const noexcept -{ - const std::scoped_lock lock{attributes_mutex}; - - return { - {"allowed_formats", Alsa::ToString(allowed_formats)}, -#ifdef ENABLE_DSD - {"dop", dop_setting ? "1" : "0"}, -#endif - }; -} - -void -AlsaOutput::SetAttribute(std::string &&name, std::string &&value) -{ - if (name == "allowed_formats") { - const std::scoped_lock lock{attributes_mutex}; - allowed_formats = Alsa::AllowedFormat::ParseList(value); -#ifdef ENABLE_DSD - } else if (name == "dop") { - const std::scoped_lock lock{attributes_mutex}; - if (value == "0") - dop_setting = false; - else if (value == "1") - dop_setting = true; - else - throw std::invalid_argument("Bad 'dop' value"); -#endif - } else - AudioOutput::SetAttribute(std::move(name), std::move(value)); -} - -void -AlsaOutput::Enable() -{ - pcm_export.Construct(); -} - -void -AlsaOutput::Disable() noexcept -{ - pcm_export.Destruct(); -} - -static bool -alsa_test_default_device() -{ - snd_pcm_t *handle; - - int ret = snd_pcm_open(&handle, default_device, - SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); - if (ret) { - FmtError(alsa_output_domain, - "Error opening default ALSA device: {}", - snd_strerror(-ret)); - return false; - } else - snd_pcm_close(handle); - - return true; -} - -/** - * Wrapper for snd_pcm_sw_params(). - */ -static void -AlsaSetupSw(snd_pcm_t *pcm, snd_pcm_uframes_t start_threshold, - snd_pcm_uframes_t avail_min) -{ - snd_pcm_sw_params_t *swparams; - snd_pcm_sw_params_alloca(&swparams); - - int err = snd_pcm_sw_params_current(pcm, swparams); - if (err < 0) - throw Alsa::MakeError(err, "snd_pcm_sw_params_current() failed"); - - err = snd_pcm_sw_params_set_start_threshold(pcm, swparams, - start_threshold); - if (err < 0) - throw Alsa::MakeError(err, "snd_pcm_sw_params_set_start_threshold() failed"); - - err = snd_pcm_sw_params_set_avail_min(pcm, swparams, avail_min); - if (err < 0) - throw Alsa::MakeError(err, "snd_pcm_sw_params_set_avail_min() failed"); - - err = snd_pcm_sw_params(pcm, swparams); - if (err < 0) - throw Alsa::MakeError(err, "snd_pcm_sw_params() failed"); -} - -inline void -AlsaOutput::Setup(AudioFormat &audio_format, - PcmExport::Params ¶ms) -{ - const auto hw_result = Alsa::SetupHw(pcm, - buffer_time, period_time, - audio_format, params); - - FmtDebug(alsa_output_domain, "format={} ({})", - snd_pcm_format_name(hw_result.format), - snd_pcm_format_description(hw_result.format)); - - FmtDebug(alsa_output_domain, "buffer_size={} period_size={}", - hw_result.buffer_size, - hw_result.period_size); - - Alsa::SetupChannelMap(pcm, audio_format.channels, params); - - AlsaSetupSw(pcm, hw_result.buffer_size - hw_result.period_size, - hw_result.period_size); - - auto alsa_period_size = hw_result.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; - - period_frames = alsa_period_size; - effective_period_duration = audio_format.FramesToTime(period_frames); - - /* generate silence if there's less than one period of data - in the ALSA-PCM buffer */ - max_avail_frames = hw_result.buffer_size - hw_result.period_size; - - silence = new std::byte[snd_pcm_frames_to_bytes(pcm, alsa_period_size)]; - snd_pcm_format_set_silence(hw_result.format, silence, - alsa_period_size * audio_format.channels); - -} - -#ifdef ENABLE_DSD - -inline void -AlsaOutput::SetupDop(const AudioFormat audio_format, - PcmExport::Params ¶ms) -{ - assert(audio_format.format == SampleFormat::DSD); - - /* pass 24 bit to AlsaSetup() */ - - AudioFormat dop_format = audio_format; - dop_format.format = SampleFormat::S24_P32; - - const AudioFormat check = dop_format; - - Setup(dop_format, params); - - /* if the device allows only 32 bit, shift all DoP - samples left by 8 bit and leave the lower 8 bit cleared; - the DSD-over-USB documentation does not specify whether - this is legal, but there is anecdotical evidence that this - is possible (and the only option for some devices) */ - params.shift8 = dop_format.format == SampleFormat::S32; - if (dop_format.format == SampleFormat::S32) - dop_format.format = SampleFormat::S24_P32; - - if (dop_format != check) { - /* no bit-perfect playback, which is required - for DSD over USB */ - delete[] silence; - throw std::runtime_error("Failed to configure DSD-over-PCM"); - } -} - -#endif - -inline void -AlsaOutput::SetupOrDop(AudioFormat &audio_format, PcmExport::Params ¶ms -#ifdef ENABLE_DSD - , bool dop -#endif - ) -{ -#ifdef ENABLE_DSD - std::exception_ptr dop_error; - if (dop && audio_format.format == SampleFormat::DSD) { - try { - params.dsd_mode = PcmExport::DsdMode::DOP; - SetupDop(audio_format, params); - return; - } catch (...) { - dop_error = std::current_exception(); - params.dsd_mode = PcmExport::DsdMode::NONE; - } - } - - try { -#endif - Setup(audio_format, params); -#ifdef ENABLE_DSD - } catch (...) { - if (dop_error) - /* if DoP was attempted, prefer returning the - original DoP error instead of the fallback - error */ - std::rethrow_exception(dop_error); - else - throw; - } -#endif -} - -static const Alsa::AllowedFormat & -BestMatch(const std::forward_list &haystack, - const AudioFormat &needle) -{ - assert(!haystack.empty()); - - for (const auto &i : haystack) - if (needle.MatchMask(i.format)) - return i; - - return haystack.front(); -} - -#ifdef ENABLE_DSD - -static void -Play_44_1_Silence(snd_pcm_t *pcm) -{ - snd_pcm_hw_params_t *hw; - snd_pcm_hw_params_alloca(&hw); - - int err; - - err = snd_pcm_hw_params_any(pcm, hw); - if (err < 0) - throw Alsa::MakeError(err, "snd_pcm_hw_params_any() failed"); - - err = snd_pcm_hw_params_set_access(pcm, hw, - SND_PCM_ACCESS_RW_INTERLEAVED); - if (err < 0) - throw Alsa::MakeError(err, "snd_pcm_hw_params_set_access() failed"); - - err = snd_pcm_hw_params_set_format(pcm, hw, SND_PCM_FORMAT_S16); - if (err < 0) - throw Alsa::MakeError(err, "snd_pcm_hw_params_set_format() failed"); - - unsigned channels = 1; - err = snd_pcm_hw_params_set_channels_near(pcm, hw, &channels); - if (err < 0) - throw Alsa::MakeError(err, "snd_pcm_hw_params_set_channels_near() failed"); - - constexpr snd_pcm_uframes_t rate = 44100; - err = snd_pcm_hw_params_set_rate(pcm, hw, rate, 0); - if (err < 0) - throw Alsa::MakeError(err, "snd_pcm_hw_params_set_rate() failed"); - - snd_pcm_uframes_t buffer_size = 1; - err = snd_pcm_hw_params_set_buffer_size_near(pcm, hw, &buffer_size); - if (err < 0) - throw Alsa::MakeError(err, "snd_pcm_hw_params_set_buffer_size_near() failed"); - - snd_pcm_uframes_t period_size = 1; - int dir = 0; - err = snd_pcm_hw_params_set_period_size_near(pcm, hw, &period_size, - &dir); - if (err < 0) - throw Alsa::MakeError(err, "snd_pcm_hw_params_set_period_size_near() failed"); - - err = snd_pcm_hw_params(pcm, hw); - if (err < 0) - throw Alsa::MakeError(err, "snd_pcm_hw_params() failed"); - - snd_pcm_sw_params_t *sw; - snd_pcm_sw_params_alloca(&sw); - - err = snd_pcm_sw_params_current(pcm, sw); - if (err < 0) - throw Alsa::MakeError(err, "snd_pcm_sw_params_current() failed"); - - err = snd_pcm_sw_params_set_start_threshold(pcm, sw, period_size); - if (err < 0) - throw Alsa::MakeError(err, "snd_pcm_sw_params_set_start_threshold() failed"); - - err = snd_pcm_sw_params(pcm, sw); - if (err < 0) - throw Alsa::MakeError(err, "snd_pcm_sw_params() failed"); - - err = snd_pcm_prepare(pcm); - if (err < 0) - throw Alsa::MakeError(err, "snd_pcm_prepare() failed"); - - AllocatedArray buffer{channels * period_size}; - buffer = std::span{}; - - /* play at least 250ms of silence */ - for (snd_pcm_uframes_t remaining_frames = rate / 4;;) { - auto n = snd_pcm_writei(pcm, buffer.data(), - period_size); - if (n < 0) - throw Alsa::MakeError(err, "snd_pcm_writei() failed"); - - if (snd_pcm_uframes_t(n) >= remaining_frames) - break; - - remaining_frames -= snd_pcm_uframes_t(n); - } - - err = snd_pcm_drain(pcm); - if (err < 0) - throw Alsa::MakeError(err, "snd_pcm_drain() failed"); -} - -#endif - -void -AlsaOutput::Open(AudioFormat &audio_format) -{ - paused = false; - -#ifdef ENABLE_DSD - bool dop; -#endif - - { - const std::scoped_lock lock{attributes_mutex}; -#ifdef ENABLE_DSD - dop = dop_setting; -#endif - - if (!allowed_formats.empty()) { - const auto &a = BestMatch(allowed_formats, - audio_format); - audio_format.ApplyMask(a.format); -#ifdef ENABLE_DSD - dop = a.dop; -#endif - } - } - - int err = snd_pcm_open(&pcm, GetDevice(), - SND_PCM_STREAM_PLAYBACK, mode); - if (err < 0) - throw Alsa::MakeError(err, - FmtBuffer<256>("Failed to open ALSA device {:?}", - GetDevice())); - - const char *pcm_name = snd_pcm_name(pcm); - if (pcm_name == nullptr) - pcm_name = "?"; - - FmtDebug(alsa_output_domain, "opened {:?} type={}", - pcm_name, - snd_pcm_type_name(snd_pcm_type(pcm))); - -#ifdef ENABLE_DSD - if (need_thesycon_dsd_workaround && - audio_format.format == SampleFormat::DSD && - audio_format.sample_rate <= 256 * 44100 / 8) { - LogDebug(alsa_output_domain, "Playing some 44.1 kHz silence"); - - try { - Play_44_1_Silence(pcm); - } catch (...) { - LogError(std::current_exception()); - } - - need_thesycon_dsd_workaround = false; - } -#endif - - PcmExport::Params params; - - try { - SetupOrDop(audio_format, params -#ifdef ENABLE_DSD - , dop -#endif - ); - } catch (...) { - snd_pcm_close(pcm); - std::throw_with_nested(FmtRuntimeError("Error opening ALSA device {:?}", - GetDevice())); - } - - snd_pcm_nonblock(pcm, 1); - -#ifdef ENABLE_DSD - use_dsd = audio_format.format == SampleFormat::DSD; - in_stop_dsd_silence = false; - - if (thesycon_dsd_workaround && - (!use_dsd || - audio_format.sample_rate > 256 * 44100 / 8)) - need_thesycon_dsd_workaround = true; - - if (params.dsd_mode == PcmExport::DsdMode::DOP) - LogDebug(alsa_output_domain, "DoP (DSD over PCM) enabled"); -#endif - - pcm_export->Open(audio_format.format, - audio_format.channels, - params); - - in_frame_size = audio_format.GetFrameSize(); - out_frame_size = pcm_export->GetOutputFrameSize(); - - drain = false; - interrupted = false; - - size_t period_size = period_frames * out_frame_size; - ring_buffer = RingBuffer{period_size * 4}; - - period_buffer.Allocate(period_frames, out_frame_size); - - active = false; - waiting = false; - must_prepare = false; - written = false; - error = {}; -} - -void -AlsaOutput::Interrupt() noexcept -{ - std::scoped_lock lock{mutex}; - - /* the "interrupted" flag will prevent - LockWaitWriteAvailable() from actually waiting, and will - instead throw AudioOutputInterrupted */ - interrupted = true; - cond.notify_one(); -} - -std::chrono::steady_clock::duration -AlsaOutput::Delay() const noexcept -{ - if (paused) - return std::chrono::steady_clock::duration::max(); - - return AudioOutput::Delay(); -} - -inline int -AlsaOutput::Recover(int err) noexcept -{ - if (err == -EPIPE) { - FmtDebug(alsa_output_domain, - "Underrun on ALSA device {:?}", - GetDevice()); - } else if (err == -ESTRPIPE) { - FmtDebug(alsa_output_domain, - "ALSA device {:?} was suspended", - GetDevice()); - } - - switch (snd_pcm_state(pcm)) { - case SND_PCM_STATE_PAUSED: - err = snd_pcm_pause(pcm, /* disable */ 0); - break; - case SND_PCM_STATE_SUSPENDED: - err = snd_pcm_resume(pcm); - if (err == -EAGAIN) - return 0; - /* fall-through to snd_pcm_prepare: */ - [[fallthrough]]; - - case SND_PCM_STATE_SETUP: - case SND_PCM_STATE_XRUN: - period_buffer.Rewind(); - written = false; - err = snd_pcm_prepare(pcm); - break; - - case SND_PCM_STATE_OPEN: - case SND_PCM_STATE_DISCONNECTED: - case SND_PCM_STATE_DRAINING: - /* can't play in this state; throw the error */ - break; - - case SND_PCM_STATE_PREPARED: - case SND_PCM_STATE_RUNNING: - /* the state is ok, but the error was unexpected; - throw it */ - break; - - default: - /* this default case is just here to work around - -Wswitch due to SND_PCM_STATE_PRIVATE1 (libasound - 1.1.6) */ - break; - } - - return err; -} - -bool -AlsaOutput::CopyRingToPeriodBuffer() noexcept -{ - if (period_buffer.IsFull()) - return false; - - size_t nbytes = ring_buffer.ReadTo({period_buffer.GetTail(), period_buffer.GetSpaceBytes()}); - if (nbytes == 0) - return false; - - period_buffer.AppendBytes(nbytes); - - const std::scoped_lock lock{mutex}; - /* notify the OutputThread that there is now - room in ring_buffer */ - cond.notify_one(); - - return true; -} - -snd_pcm_sframes_t -AlsaOutput::WriteFromPeriodBuffer() noexcept -{ - assert(period_buffer.IsFull()); - assert(period_buffer.GetFrames(out_frame_size) > 0); - - auto frames_written = snd_pcm_writei(pcm, period_buffer.GetHead(), - period_buffer.GetFrames(out_frame_size)); - if (frames_written > 0) { - written = true; - period_buffer.ConsumeFrames(frames_written, - out_frame_size); - } - - return frames_written; -} - -inline bool -AlsaOutput::DrainInternal() -{ -#ifdef ENABLE_DSD - if (in_stop_dsd_silence) { - /* "stop_dsd_silence" is in progress: clear internal - buffers and instead, fill the period buffer with - silence */ - in_stop_dsd_silence = false; - ring_buffer.Clear(); - period_buffer.Clear(); - period_buffer.FillWithSilence(silence, out_frame_size); - } -#endif - - /* drain ring_buffer */ - CopyRingToPeriodBuffer(); - - /* drain period_buffer */ - if (!period_buffer.IsCleared()) { - if (!period_buffer.IsFull()) - /* generate some silence to finish the partial - period */ - period_buffer.FillWithSilence(silence, out_frame_size); - - /* drain period_buffer */ - unsigned int retry_count = 0; - while (!period_buffer.IsDrained() && retry_count <= 1) { - auto frames_written = WriteFromPeriodBuffer(); - if (frames_written < 0) { - if (frames_written == -EAGAIN || frames_written == -EINTR) - return false; - - if (Recover(frames_written) < 0) - throw Alsa::MakeError(frames_written, - "snd_pcm_writei() failed"); - - retry_count++; - continue; - } - - /* need to call CopyRingToPeriodBuffer() and - WriteFromPeriodBuffer() again in the next - iteration, so don't finish the drain just - yet */ - return false; - } - } - - if (!written) - /* if nothing has ever been written to the PCM, we - don't need to drain it */ - return true; - - switch (snd_pcm_state(pcm)) { - case SND_PCM_STATE_PREPARED: - case SND_PCM_STATE_RUNNING: - /* these states require a call to snd_pcm_drain() */ - break; - - case SND_PCM_STATE_DRAINING: - /* already draining, but not yet finished; this is - probably a spurious epoll event, and we should wait - for the next one */ - return false; - - default: - /* all other states cannot be drained, and we're - done */ - return true; - } - - /* .. and finally drain the ALSA hardware buffer */ - - const int result = snd_pcm_drain(pcm); - if (result == 0) - return true; - else if (result == -EAGAIN) - return false; - else - throw Alsa::MakeError(result, "snd_pcm_drain() failed"); -} - -void -AlsaOutput::Drain() -{ - std::unique_lock lock{mutex}; - - if (error) - std::rethrow_exception(error); - - drain = true; - - Activate(); - - cond.wait(lock, [this]{ return !drain || !active; }); - - if (error) - std::rethrow_exception(error); -} - -inline void -AlsaOutput::CancelInternal() noexcept -{ - /* this method doesn't need to lock the mutex because while it - runs, the calling thread is blocked inside Cancel() */ - - must_prepare = true; - - snd_pcm_drop(pcm); - - pcm_export->Reset(); - period_buffer.Clear(); - ring_buffer.Clear(); - - active = false; - waiting = false; - - UnregisterSockets(); - silence_timer.Cancel(); -} - -void -AlsaOutput::Cancel() noexcept -{ - { - std::lock_guard lock{mutex}; - interrupted = false; - } - - if (!LockIsActive()) { - /* early cancel, quick code path without thread - synchronization */ - - pcm_export->Reset(); - assert(period_buffer.IsCleared()); - ring_buffer.Clear(); - - return; - } - -#ifdef ENABLE_DSD - if (stop_dsd_silence && use_dsd) { - /* play some DSD silence instead of snd_pcm_drop() */ - std::unique_lock lock{mutex}; - in_stop_dsd_silence = true; - drain = true; - cond.wait(lock, [this]{ return !drain || !active; }); - return; - } -#endif - - BlockingCall(GetEventLoop(), [this](){ - CancelInternal(); - }); -} - -bool -AlsaOutput::Pause() noexcept -{ - std::lock_guard lock{mutex}; - interrupted = false; - - if (close_on_pause) - return false; - - // TODO use snd_pcm_pause()? - - paused = true; - return true; -} - -void -AlsaOutput::Close() noexcept -{ - /* make sure the I/O thread isn't inside DispatchSockets() */ - BlockingCall(GetEventLoop(), [this](){ - UnregisterSockets(); - silence_timer.Cancel(); - }); - - period_buffer.Free(); - ring_buffer = {}; - snd_pcm_close(pcm); - delete[] silence; -} - -size_t -AlsaOutput::LockWaitWriteAvailable() -{ - const size_t out_block_size = pcm_export->GetOutputBlockSize(); - const size_t min_available = 2 * out_block_size; - - std::unique_lock lock{mutex}; - - while (true) { - if (error) - std::rethrow_exception(error); - - if (interrupted) - /* a CANCEL command is in flight - don't block - here */ - throw AudioOutputInterrupted{}; - - size_t write_available = ring_buffer.WriteAvailable(); - if (write_available >= min_available) { - /* reserve room for one extra block, just in - case PcmExport::Export() has some partial - block data in its internal buffer */ - write_available -= out_block_size; - - return write_available / out_frame_size; - } - - /* now that the ring_buffer is full, we can activate - the socket handlers to trigger the first - snd_pcm_writei() */ - if (Activate()) - /* since everything may have changed while the - mutex was unlocked, we need to skip the - cond.wait() call below and check the new - status */ - continue; - - /* wait for the DispatchSockets() to make room in the - ring_buffer */ - cond.wait(lock); - } -} - -std::size_t -AlsaOutput::Play(std::span src) -{ - assert(!src.empty()); - assert(src.size() % in_frame_size == 0); - - paused = false; - - const size_t max_frames = LockWaitWriteAvailable(); - const size_t max_size = max_frames * in_frame_size; - if (src.size() > max_size) - src = src.first(max_size); - - const auto e = pcm_export->Export(src); - if (e.empty()) - return src.size(); - - size_t bytes_written = ring_buffer.WriteFrom(e); - assert(bytes_written == e.size()); - (void)bytes_written; - - return src.size(); -} - -Event::Duration -AlsaOutput::PrepareSockets() noexcept -{ - if (!LockIsActiveAndNotWaiting()) { - ClearSocketList(); - return Event::Duration(-1); - } - - try { - return non_block.PrepareSockets(*this, pcm); - } catch (...) { - ClearSocketList(); - LockCaughtError(); - return Event::Duration(-1); - } -} - -void -AlsaOutput::DispatchSockets() noexcept -try { - non_block.DispatchSockets(*this, pcm); - - if (must_prepare) { - must_prepare = false; - written = false; - - int err = snd_pcm_prepare(pcm); - if (err < 0) - throw Alsa::MakeError(err, "snd_pcm_prepare() failed"); - } - - { - const std::scoped_lock lock{mutex}; - - assert(active); - - if (drain) { - { - ScopeUnlock unlock(mutex); - if (!DrainInternal()) - return; - - MultiSocketMonitor::InvalidateSockets(); - } - - drain = false; - cond.notify_one(); - return; - } - } - - CopyRingToPeriodBuffer(); - - if (!period_buffer.IsFull()) { - if (snd_pcm_state(pcm) == SND_PCM_STATE_PREPARED || - snd_pcm_avail(pcm) <= max_avail_frames) { - /* at SND_PCM_STATE_PREPARED (not yet switched - to SND_PCM_STATE_RUNNING), we have no - pressure to fill the ALSA buffer, because - no xrun can possibly occur; and if no data - is available right now, we can easily wait - until some is available; so we just stop - monitoring the ALSA file descriptor, and - let it be reactivated by Play()/Activate() - whenever more data arrives */ - /* the same applies when there is still enough - data in the ALSA-PCM buffer (determined by - snd_pcm_avail()); this can happen at the - start of playback, when our ring_buffer is - smaller than the ALSA-PCM buffer */ - - { - const std::scoped_lock lock{mutex}; - waiting = true; - cond.notify_one(); - } - - /* avoid race condition: see if data has - arrived meanwhile before disabling the - event (but after setting the "waiting" - flag) */ - if (!CopyRingToPeriodBuffer()) { - UnregisterSockets(); - - /* just in case Play() doesn't get - called soon enough, schedule a - timer which generates silence - before the xrun occurs */ - /* the timer fires in half of a - period; this short duration may - produce a few more wakeups than - necessary, but should be small - enough to avoid the xrun */ - silence_timer.Schedule(effective_period_duration / 2); - } - - return; - } - - if (throttle_silence_log.CheckUpdate(std::chrono::seconds(5))) - LogWarning(alsa_output_domain, "Decoder is too slow; playing silence to avoid xrun"); - - /* insert some silence if the buffer has not enough - data yet, to avoid ALSA xrun */ - period_buffer.FillWithSilence(silence, out_frame_size); - } - - auto frames_written = WriteFromPeriodBuffer(); - if (frames_written < 0) { - if (frames_written == -EAGAIN || frames_written == -EINTR) - /* try again in the next DispatchSockets() - call which is still scheduled */ - return; - - if (Recover(frames_written) < 0) - throw Alsa::MakeError(frames_written, - "snd_pcm_writei() failed"); - - /* recovered; try again in the next DispatchSockets() - call */ - return; - } -} catch (...) { - MultiSocketMonitor::Reset(); - LockCaughtError(); -} - -constexpr struct AudioOutputPlugin alsa_output_plugin = { - "alsa", - alsa_test_default_device, - &AlsaOutput::Create, - &alsa_mixer_plugin, -}; diff --git a/src/output/plugins/AlsaOutputPlugin.hxx b/src/output/plugins/AlsaOutputPlugin.hxx deleted file mode 100644 index facd107..0000000 --- a/src/output/plugins/AlsaOutputPlugin.hxx +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#ifndef MPD_ALSA_OUTPUT_PLUGIN_HXX -#define MPD_ALSA_OUTPUT_PLUGIN_HXX - -extern const struct AudioOutputPlugin alsa_output_plugin; - -#endif diff --git a/src/output/plugins/AoOutputPlugin.cxx b/src/output/plugins/AoOutputPlugin.cxx deleted file mode 100644 index a98b625..0000000 --- a/src/output/plugins/AoOutputPlugin.cxx +++ /dev/null @@ -1,207 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "AoOutputPlugin.hxx" -#include "../OutputAPI.hxx" -#include "lib/fmt/RuntimeError.hxx" -#include "thread/SafeSingleton.hxx" -#include "system/Error.hxx" -#include "util/IterableSplitString.hxx" -#include "util/Domain.hxx" -#include "util/StringAPI.hxx" -#include "util/StringSplit.hxx" -#include "util/StringStrip.hxx" -#include "Log.hxx" - -#include - -#include - -/* An ao_sample_format, with all fields set to zero: */ -static ao_sample_format OUR_AO_FORMAT_INITIALIZER; - -class AoInit { -public: - AoInit() { - ao_initialize(); - } - - ~AoInit() noexcept { - ao_shutdown(); - } - - AoInit(const AoInit &) = delete; - AoInit &operator=(const AoInit &) = delete; -}; - -class AoOutput final : AudioOutput, SafeSingleton { - const size_t write_size; - int driver; - ao_option *options = nullptr; - ao_device *device; - - size_t frame_size; - - std::size_t max_size; - - explicit AoOutput(const ConfigBlock &block); - ~AoOutput() override; - - AoOutput(const AoOutput &) = delete; - AoOutput &operator=(const AoOutput &) = delete; - -public: - static AudioOutput *Create(EventLoop &, const ConfigBlock &block) { - return new AoOutput(block); - } - - void Open(AudioFormat &audio_format) override; - void Close() noexcept override; - - std::size_t Play(std::span src) override; -}; - -static constexpr Domain ao_output_domain("ao_output"); - - -static std::system_error -MakeAoError() -{ - const char *error = "Unknown libao failure"; - - 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; - } - - return MakeErrno(errno, error); -} - -AoOutput::AoOutput(const ConfigBlock &block) - :AudioOutput(0), - write_size(block.GetPositiveValue("write_size", 1024U)) -{ - const char *value = block.GetBlockValue("driver", "default"); - if (StringIsEqual(value, "default")) - driver = ao_default_driver_id(); - else - driver = ao_driver_id(value); - - if (driver < 0) - throw FmtRuntimeError("{:?} is not a valid ao driver", - value); - - ao_info *ai = ao_driver_info(driver); - if (ai == nullptr) - throw std::runtime_error("problems getting driver info"); - - FmtDebug(ao_output_domain, "using ao driver {:?} for {:?}\n", - ai->short_name, block.GetBlockValue("name", nullptr)); - - value = block.GetBlockValue("options", nullptr); - if (value != nullptr) { - for (const std::string_view i : IterableSplitString(value, ';')) { - const auto [n, v] = Split(Strip(i), '='); - if (n.empty() || v.data() == nullptr) - throw FmtRuntimeError("problems parsing option {:?}", - i); - - ao_append_option(&options, std::string{n}.c_str(), - std::string{v}.c_str()); - } - } -} - -AoOutput::~AoOutput() -{ - ao_free_options(options); -} - -void -AoOutput::Open(AudioFormat &audio_format) -{ - ao_sample_format format = OUR_AO_FORMAT_INITIALIZER; - - 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; - } - - frame_size = audio_format.GetFrameSize(); - - /* round down to a multiple of the frame size */ - /* no matter how small "write_size" was configured, we must - pass at least one frame to libao */ - max_size = std::max(write_size / frame_size, std::size_t{1}) * frame_size; - - format.rate = audio_format.sample_rate; - format.byte_format = AO_FMT_NATIVE; - format.channels = audio_format.channels; - - device = ao_open_live(driver, &format, options); - if (device == nullptr) - throw MakeAoError(); -} - -void -AoOutput::Close() noexcept -{ - ao_close(device); -} - -std::size_t -AoOutput::Play(std::span src) -{ - assert(src.size() % frame_size == 0); - - if (src.size() > max_size) - /* round down to a multiple of the frame size */ - src = src.first(max_size); - - /* 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. */ - char *data = const_cast((const char *)src.data()); - - if (ao_play(device, data, src.size()) == 0) - throw MakeAoError(); - - return src.size(); -} - -const struct AudioOutputPlugin ao_output_plugin = { - "ao", - nullptr, - &AoOutput::Create, - nullptr, -}; diff --git a/src/output/plugins/AoOutputPlugin.hxx b/src/output/plugins/AoOutputPlugin.hxx deleted file mode 100644 index 3d28315..0000000 --- a/src/output/plugins/AoOutputPlugin.hxx +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#ifndef MPD_AO_OUTPUT_PLUGIN_HXX -#define MPD_AO_OUTPUT_PLUGIN_HXX - -extern const struct AudioOutputPlugin ao_output_plugin; - -#endif diff --git a/src/output/plugins/FifoOutputPlugin.cxx b/src/output/plugins/FifoOutputPlugin.cxx deleted file mode 100644 index 671dc5b..0000000 --- a/src/output/plugins/FifoOutputPlugin.cxx +++ /dev/null @@ -1,223 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "FifoOutputPlugin.hxx" -#include "../OutputAPI.hxx" -#include "../Timer.hxx" -#include "lib/fmt/PathFormatter.hxx" -#include "lib/fmt/RuntimeError.hxx" -#include "fs/AllocatedPath.hxx" -#include "fs/FileSystem.hxx" -#include "fs/FileInfo.hxx" -#include "lib/fmt/SystemError.hxx" -#include "util/Domain.hxx" -#include "Log.hxx" -#include "open.h" - -#include - -#include -#include - -class FifoOutput final : AudioOutput { - const AllocatedPath path; - - int input = -1; - int output = -1; - bool created = false; - Timer *timer; - -public: - explicit FifoOutput(const ConfigBlock &block); - - ~FifoOutput() override { - CloseFifo(); - } - - FifoOutput(const FifoOutput &) = delete; - FifoOutput &operator=(const FifoOutput &) = delete; - - static AudioOutput *Create(EventLoop &, - const ConfigBlock &block) { - return new FifoOutput(block); - } - -private: - void Create(); - void Check(); - void Delete(); - - void OpenFifo(); - void CloseFifo(); - - void Open(AudioFormat &audio_format) override; - void Close() noexcept override; - - [[nodiscard]] std::chrono::steady_clock::duration Delay() const noexcept override; - std::size_t Play(std::span src) override; - void Cancel() noexcept override; -}; - -static constexpr Domain fifo_output_domain("fifo_output"); - -FifoOutput::FifoOutput(const ConfigBlock &block) - :AudioOutput(0), - path(block.GetPath("path")) -{ - if (path.IsNull()) - throw std::runtime_error("No \"path\" parameter specified"); - - OpenFifo(); -} - -inline void -FifoOutput::Delete() -{ - FmtDebug(fifo_output_domain, - "Removing FIFO {:?}", path); - - try { - RemoveFile(path); - } catch (...) { - LogError(std::current_exception(), "Could not remove FIFO"); - return; - } - - created = false; -} - -void -FifoOutput::CloseFifo() -{ - if (input >= 0) { - close(input); - input = -1; - } - - if (output >= 0) { - close(output); - output = -1; - } - - FileInfo fi; - if (created && GetFileInfo(path, fi)) - Delete(); -} - -inline void -FifoOutput::Create() -{ - if (!MakeFifo(path, 0666)) - throw FmtErrno("Couldn't create FIFO {:?}", path); - - created = true; -} - -inline void -FifoOutput::Check() -{ - struct stat st; - if (!StatFile(path, st)) { - if (errno == ENOENT) { - /* Path doesn't exist */ - Create(); - return; - } - - throw FmtErrno("Failed to stat FIFO {:?}", path); - } - - if (!S_ISFIFO(st.st_mode)) - throw FmtRuntimeError("{:?} already exists, but is not a FIFO", - path); -} - -inline void -FifoOutput::OpenFifo() -try { - Check(); - - input = OpenFile(path, O_RDONLY|O_NONBLOCK|O_BINARY, 0).Steal(); - if (input < 0) - throw FmtErrno("Could not open FIFO {:?} for reading", - path); - - output = OpenFile(path, O_WRONLY|O_NONBLOCK|O_BINARY, 0).Steal(); - if (output < 0) - throw FmtErrno("Could not open FIFO {:?} for writing"); -} catch (...) { - CloseFifo(); - throw; -} - -void -FifoOutput::Open(AudioFormat &audio_format) -{ - timer = new Timer(audio_format); -} - -void -FifoOutput::Close() noexcept -{ - delete timer; -} - -void -FifoOutput::Cancel() noexcept -{ - timer->Reset(); - - ssize_t bytes; - do { - char buffer[16384]; - bytes = read(input, buffer, sizeof(buffer)); - } while (bytes > 0 && errno != EINTR); - - if (bytes < 0 && errno != EAGAIN) { - FmtError(fifo_output_domain, - "Flush of FIFO {:?} failed: {}", - path, strerror(errno)); - } -} - -std::chrono::steady_clock::duration -FifoOutput::Delay() const noexcept -{ - return timer->IsStarted() - ? timer->GetDelay() - : std::chrono::steady_clock::duration::zero(); -} - -std::size_t -FifoOutput::Play(std::span src) -{ - if (!timer->IsStarted()) - timer->Start(); - timer->Add(src.size()); - - while (true) { - ssize_t bytes = write(output, src.data(), src.size()); - if (bytes > 0) - return (std::size_t)bytes; - - if (bytes < 0) { - switch (errno) { - case EAGAIN: - /* The pipe is full, so empty it */ - Cancel(); - continue; - case EINTR: - continue; - } - - throw FmtErrno("Failed to write to FIFO {}", path); - } - } -} - -const struct AudioOutputPlugin fifo_output_plugin = { - "fifo", - nullptr, - &FifoOutput::Create, - nullptr, -}; diff --git a/src/output/plugins/FifoOutputPlugin.hxx b/src/output/plugins/FifoOutputPlugin.hxx deleted file mode 100644 index a1ad1a8..0000000 --- a/src/output/plugins/FifoOutputPlugin.hxx +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#ifndef MPD_FIFO_OUTPUT_PLUGIN_HXX -#define MPD_FIFO_OUTPUT_PLUGIN_HXX - -extern const struct AudioOutputPlugin fifo_output_plugin; - -#endif diff --git a/src/output/plugins/JackOutputPlugin.cxx b/src/output/plugins/JackOutputPlugin.cxx deleted file mode 100644 index 8b88bdc..0000000 --- a/src/output/plugins/JackOutputPlugin.cxx +++ /dev/null @@ -1,729 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "config.h" -#include "JackOutputPlugin.hxx" -#include "../OutputAPI.hxx" -#include "../Error.hxx" -#include "output/Features.h" -#include "lib/fmt/RuntimeError.hxx" -#include "thread/Mutex.hxx" -#include "util/ScopeExit.hxx" -#include "util/IterableSplitString.hxx" -#include "util/SpanCast.hxx" -#include "util/Domain.hxx" -#include "Log.hxx" - -#include -#include -#include - -#include -#include -#include - -#include /* for usleep() */ -#include - -static constexpr unsigned MAX_PORTS = 16; - -static constexpr size_t jack_sample_size = sizeof(jack_default_audio_sample_t); - -class JackOutput final : public AudioOutput { - /** - * libjack options passed to jack_client_open(). - */ - jack_options_t options = JackNullOption; - - const char *name; - - const char *const server_name; - - /* configuration */ - - std::string source_ports[MAX_PORTS]; - unsigned num_source_ports; - - std::string destination_ports[MAX_PORTS]; - unsigned num_destination_ports; - /* overrides num_destination_ports*/ - bool auto_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]; - - /** - * While this flag is set, the "process" callback generates - * silence. - */ - std::atomic_bool pause; - - /** - * Was Interrupt() called? This will unblock Play(). It will - * be reset by Cancel() and Pause(), as documented by the - * #AudioOutput interface. - * - * Only initialized while the output is open. - */ - bool interrupted; - - /** - * Protects #error. - */ - mutable Mutex mutex; - - /** - * The error reported to the "on_info_shutdown" callback. - */ - std::exception_ptr error; - -public: - explicit JackOutput(const ConfigBlock &block); - -private: - /** - * Connect the JACK client and performs some basic setup - * (e.g. register callbacks). - * - * Throws on error. - */ - void Connect(); - - /** - * Disconnect the JACK client. - */ - void Disconnect() noexcept; - - void Shutdown(const char *reason) noexcept { - const std::scoped_lock lock{mutex}; - error = std::make_exception_ptr(FmtRuntimeError("JACK connection shutdown: {}", - reason)); - } - - static void OnShutdown(jack_status_t, const char *reason, - void *arg) noexcept { - auto &j = *(JackOutput *)arg; - j.Shutdown(reason); - } - - - /** - * Throws on error. - */ - void Start(); - void Stop() noexcept; - - /** - * Determine the number of frames guaranteed to be available - * on all channels. - */ - [[gnu::pure]] - jack_nframes_t GetAvailable() const noexcept; - - void Process(jack_nframes_t nframes); - static int Process(jack_nframes_t nframes, void *arg) noexcept { - auto &j = *(JackOutput *)arg; - j.Process(nframes); - return 0; - } - - /** - * @return the number of frames that were written - */ - size_t WriteSamples(const float *src, size_t n_frames); - -public: - /* virtual methods from class AudioOutput */ - - void Enable() override; - void Disable() noexcept override; - - void Open(AudioFormat &new_audio_format) override; - - void Close() noexcept override { - Stop(); - } - - void Interrupt() noexcept override; - - std::chrono::steady_clock::duration Delay() const noexcept override { - return pause && !LockWasShutdown() - ? std::chrono::steady_clock::duration::max() - : std::chrono::steady_clock::duration::zero(); - } - - std::size_t Play(std::span src) override; - - void Cancel() noexcept override; - bool Pause() override; - -private: - bool LockWasShutdown() const noexcept { - const std::scoped_lock lock{mutex}; - return !!error; - } -}; - -static constexpr Domain jack_output_domain("jack_output"); - -/** - * Throws on error. - */ -static unsigned -parse_port_list(const char *source, std::string dest[]) -{ - unsigned n = 0; - for (const std::string_view i : IterableSplitString(source, ',')) { - if (n >= MAX_PORTS) - throw std::runtime_error("too many port names"); - - dest[n++] = i; - } - - if (n == 0) - throw std::runtime_error("at least one port name expected"); - - return n; -} - -JackOutput::JackOutput(const ConfigBlock &block) - :AudioOutput(FLAG_ENABLE_DISABLE|FLAG_PAUSE), - name(block.GetBlockValue("client_name", nullptr)), - server_name(block.GetBlockValue("server_name", nullptr)) -{ - if (name != nullptr) - options = jack_options_t(options | JackUseExactName); - else - /* if there's a no configured client name, we don't - care about the JackUseExactName option */ - name = "Music Player Daemon"; - - if (server_name != nullptr) - options = jack_options_t(options | JackServerName); - - if (!block.GetBlockValue("autostart", false)) - options = jack_options_t(options | JackNoStartServer); - - /* configure the source ports */ - - const char *value = block.GetBlockValue("source_ports", "left,right"); - num_source_ports = parse_port_list(value, source_ports); - - /* configure the destination ports */ - - value = block.GetBlockValue("destination_ports", nullptr); - if (value == nullptr) { - /* compatibility with MPD < 0.16 */ - value = block.GetBlockValue("ports", nullptr); - if (value != nullptr) - FmtWarning(jack_output_domain, - "deprecated option 'ports' in line {}", - block.line); - } - - if (value != nullptr) { - num_destination_ports = - parse_port_list(value, destination_ports); - } else { - num_destination_ports = 0; - } - - auto_destination_ports = block.GetBlockValue("auto_destination_ports", true); - - if (num_destination_ports > 0 && - num_destination_ports != num_source_ports) - FmtWarning(jack_output_domain, - "number of source ports ({}) mismatches the " - "number of destination ports ({}) in line {}", - num_source_ports, num_destination_ports, - block.line); - - ringbuffer_size = block.GetPositiveValue("ringbuffer_size", 32768U); -} - -inline jack_nframes_t -JackOutput::GetAvailable() const noexcept -{ - size_t min = jack_ringbuffer_read_space(ringbuffer[0]); - - for (unsigned i = 1; i < audio_format.channels; ++i) { - size_t current = jack_ringbuffer_read_space(ringbuffer[i]); - if (current < min) - min = current; - } - - assert(min % jack_sample_size == 0); - - return min / jack_sample_size; -} - -/** - * Call jack_ringbuffer_read_advance() on all buffers in the list. - */ -static void -MultiReadAdvance(std::span buffers, - size_t size) -{ - for (auto *i : buffers) - jack_ringbuffer_read_advance(i, size); -} - -/** - * Write a specific amount of "silence" to the given port. - */ -static void -WriteSilence(jack_port_t &port, jack_nframes_t nframes) -{ - auto *out = - (jack_default_audio_sample_t *) - jack_port_get_buffer(&port, 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 */ - return; - - std::fill_n(out, nframes, 0.0); -} - -/** - * Write a specific amount of "silence" to all ports in the list. - */ -static void -MultiWriteSilence(std::span ports, jack_nframes_t nframes) -{ - for (auto *i : ports) - WriteSilence(*i, nframes); -} - -/** - * Copy data from the buffer to the port. If the buffer underruns, - * fill with silence. - */ -static void -Copy(jack_port_t &dest, jack_nframes_t nframes, - jack_ringbuffer_t &src, jack_nframes_t available) -{ - auto *out = - (jack_default_audio_sample_t *) - jack_port_get_buffer(&dest, 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 */ - return; - - /* copy from buffer to port */ - jack_ringbuffer_read(&src, (char *)out, - available * jack_sample_size); - - /* ringbuffer underrun, fill with silence */ - std::fill(out + available, out + nframes, 0.0); -} - -inline void -JackOutput::Process(jack_nframes_t nframes) -{ - if (nframes <= 0) - return; - - jack_nframes_t available = GetAvailable(); - - const unsigned n_channels = audio_format.channels; - - if (pause) { - /* empty the ring buffers */ - - MultiReadAdvance({ringbuffer, n_channels}, - available * jack_sample_size); - - /* generate silence while MPD is paused */ - - MultiWriteSilence({ports, n_channels}, nframes); - - return; - } - - if (available > nframes) - available = nframes; - - for (unsigned i = 0; i < n_channels; ++i) - Copy(*ports[i], nframes, *ringbuffer[i], available); - - /* generate silence for the unused source ports */ - - MultiWriteSilence({ports + n_channels, num_source_ports - n_channels}, - nframes); -} - -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) -{ - LogNotice(jack_output_domain, msg); -} -#endif - -void -JackOutput::Disconnect() noexcept -{ - assert(client != nullptr); - - jack_deactivate(client); - jack_client_close(client); - client = nullptr; -} - -void -JackOutput::Connect() -{ - error = {}; - - jack_status_t status; - client = jack_client_open(name, options, &status, server_name); - if (client == nullptr) - throw FmtRuntimeError("Failed to connect to JACK server, status={}", - (unsigned)status); - - jack_set_process_callback(client, Process, this); - jack_on_info_shutdown(client, OnShutdown, this); - - for (unsigned i = 0; i < num_source_ports; ++i) { - unsigned long portflags = JackPortIsOutput | JackPortIsTerminal; - ports[i] = jack_port_register(client, - source_ports[i].c_str(), - JACK_DEFAULT_AUDIO_TYPE, - portflags, 0); - if (ports[i] == nullptr) { - Disconnect(); - throw FmtRuntimeError("Cannot register output port {:?}", - source_ports[i]); - } - } -} - -static bool -mpd_jack_test_default_device() -{ - return true; -} - -inline void -JackOutput::Enable() -{ - for (unsigned i = 0; i < num_source_ports; ++i) - ringbuffer[i] = nullptr; - - Connect(); -} - -inline void -JackOutput::Disable() noexcept -{ - if (client != nullptr) - Disconnect(); - - for (unsigned i = 0; i < num_source_ports; ++i) { - if (ringbuffer[i] != nullptr) { - jack_ringbuffer_free(ringbuffer[i]); - ringbuffer[i] = nullptr; - } - } -} - -static AudioOutput * -mpd_jack_init(EventLoop &, const ConfigBlock &block) -{ - jack_set_error_function(mpd_jack_error); - -#ifdef HAVE_JACK_SET_INFO_FUNCTION - jack_set_info_function(mpd_jack_info); -#endif - - return new JackOutput(block); -} - -/** - * Stops the playback on the JACK connection. - */ -void -JackOutput::Stop() noexcept -{ - if (client == nullptr) - return; - - if (LockWasShutdown()) - /* the connection has failed; close it */ - Disconnect(); - else - /* the connection is alive: just stop playback */ - jack_deactivate(client); -} - -inline void -JackOutput::Start() -{ - assert(client != nullptr); - assert(audio_format.channels <= 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 < num_source_ports; ++i) { - if (ringbuffer[i] == nullptr) - ringbuffer[i] = - jack_ringbuffer_create(ringbuffer_size); - - /* clear the ring buffer to be sure that data from - previous playbacks are gone */ - jack_ringbuffer_reset(ringbuffer[i]); - } - - if ( jack_activate(client) ) { - Stop(); - throw std::runtime_error("cannot activate client"); - } - - const char *dports[MAX_PORTS], **jports; - unsigned num_dports; - if (num_destination_ports == 0) { - /* if user requests no auto connect, we are done */ - if (!auto_destination_ports) { - return; - } - /* no output ports were configured - ask libjack for - defaults */ - jports = jack_get_ports(client, nullptr, nullptr, - JackPortIsPhysical | JackPortIsInput); - if (jports == nullptr) { - Stop(); - throw std::runtime_error("no ports found"); - } - - assert(*jports != nullptr); - - for (num_dports = 0; num_dports < MAX_PORTS && - jports[num_dports] != nullptr; - ++num_dports) { - FmtDebug(jack_output_domain, - "destination_port[{}] = {:?}\n", - num_dports, jports[num_dports]); - dports[num_dports] = jports[num_dports]; - } - } else { - /* use the configured output ports */ - - num_dports = num_destination_ports; - for (unsigned i = 0; i < num_dports; ++i) - dports[i] = destination_ports[i].c_str(); - - jports = nullptr; - } - - AtScopeExit(jports) { - if (jports != nullptr) - jack_free(jports); - }; - - assert(num_dports > 0); - - const char *duplicate_port = nullptr; - if (audio_format.channels >= 2 && num_dports == 1) { - /* mix stereo signal on one speaker */ - - std::fill(dports + num_dports, dports + audio_format.channels, - dports[0]); - } else if (num_dports > audio_format.channels) { - if (audio_format.channels == 1 && num_dports >= 2) { - /* mono input file: connect the one source - channel to the both destination channels */ - duplicate_port = dports[1]; - num_dports = 1; - } else - /* connect only as many ports as we need */ - num_dports = audio_format.channels; - } - - assert(num_dports <= num_source_ports); - - for (unsigned i = 0; i < num_dports; ++i) { - int ret = jack_connect(client, jack_port_name(ports[i]), - dports[i]); - if (ret != 0) { - Stop(); - throw FmtRuntimeError("Not a valid JACK port: {}", - dports[i]); - } - } - - if (duplicate_port != nullptr) { - /* mono input file: connect the one source channel to - the both destination channels */ - int ret; - - ret = jack_connect(client, jack_port_name(ports[0]), - duplicate_port); - if (ret != 0) { - Stop(); - throw FmtRuntimeError("Not a valid JACK port: {}", - duplicate_port); - } - } -} - -inline void -JackOutput::Open(AudioFormat &new_audio_format) -{ - pause = false; - - if (client != nullptr && LockWasShutdown()) - Disconnect(); - - if (client == nullptr) - Connect(); - - new_audio_format.sample_rate = jack_get_sample_rate(client); - - if (num_source_ports == 1) - new_audio_format.channels = 1; - else if (new_audio_format.channels > num_source_ports) - new_audio_format.channels = 2; - - /* JACK uses 32 bit float in the range [-1 .. 1] - just like - MPD's SampleFormat::FLOAT*/ - static_assert(jack_sample_size == sizeof(float), "Expected float32"); - new_audio_format.format = SampleFormat::FLOAT; - audio_format = new_audio_format; - - interrupted = false; - - Start(); -} - -void -JackOutput::Interrupt() noexcept -{ - const std::lock_guard lock{mutex}; - - /* the "interrupted" flag will prevent Play() from waiting, - and will instead throw AudioOutputInterrupted */ - interrupted = true; -} - -inline size_t -JackOutput::WriteSamples(const float *src, size_t n_frames) -{ - assert(n_frames > 0); - - const unsigned n_channels = audio_format.channels; - - float *dest[MAX_CHANNELS]; - size_t space = SIZE_MAX; - for (unsigned i = 0; i < n_channels; ++i) { - jack_ringbuffer_data_t d[2]; - jack_ringbuffer_get_write_vector(ringbuffer[i], d); - - /* choose the first non-empty writable area */ - const jack_ringbuffer_data_t &e = d[d[0].len == 0]; - - if (e.len < space) - /* send data symmetrically */ - space = e.len; - - dest[i] = (float *)e.buf; - } - - space /= jack_sample_size; - if (space == 0) - return 0; - - const size_t result = n_frames = std::min(space, n_frames); - - while (n_frames-- > 0) - for (unsigned i = 0; i < n_channels; ++i) - *dest[i]++ = *src++; - - const size_t per_channel_advance = result * jack_sample_size; - for (unsigned i = 0; i < n_channels; ++i) - jack_ringbuffer_write_advance(ringbuffer[i], - per_channel_advance); - - return result; -} - -std::size_t -JackOutput::Play(std::span _src) -{ - const size_t frame_size = audio_format.GetFrameSize(); - assert(_src.size() % frame_size == 0); - - const auto src = FromBytesStrict(_src); - - pause = false; - - const std::size_t n_frames = src.size() / audio_format.channels; - - while (true) { - { - const std::scoped_lock lock{mutex}; - if (error) - std::rethrow_exception(error); - - if (interrupted) - throw AudioOutputInterrupted{}; - } - - size_t frames_written = - WriteSamples(src.data(), n_frames); - if (frames_written > 0) - return frames_written * frame_size; - - /* XXX do something more intelligent to - synchronize */ - usleep(1000); - } -} - -void -JackOutput::Cancel() noexcept -{ - const std::lock_guard lock{mutex}; - interrupted = false; -} - -inline bool -JackOutput::Pause() -{ - { - const std::scoped_lock lock{mutex}; - interrupted = false; - if (error) - std::rethrow_exception(error); - } - - pause = true; - - return true; -} - -const struct AudioOutputPlugin jack_output_plugin = { - "jack", - mpd_jack_test_default_device, - mpd_jack_init, - nullptr, -}; diff --git a/src/output/plugins/JackOutputPlugin.hxx b/src/output/plugins/JackOutputPlugin.hxx deleted file mode 100644 index d679186..0000000 --- a/src/output/plugins/JackOutputPlugin.hxx +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#ifndef MPD_JACK_OUTPUT_PLUGIN_HXX -#define MPD_JACK_OUTPUT_PLUGIN_HXX - -extern const struct AudioOutputPlugin jack_output_plugin; - -#endif diff --git a/src/output/plugins/OSXOutputPlugin.cxx b/src/output/plugins/OSXOutputPlugin.cxx deleted file mode 100644 index cf01702..0000000 --- a/src/output/plugins/OSXOutputPlugin.cxx +++ /dev/null @@ -1,851 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "OSXOutputPlugin.hxx" -#include "apple/AudioObject.hxx" -#include "apple/AudioUnit.hxx" -#include "apple/StringRef.hxx" -#include "apple/Throw.hxx" -#include "../OutputAPI.hxx" -#include "mixer/plugins/OSXMixerPlugin.hxx" -#include "lib/fmt/RuntimeError.hxx" -#include "lib/fmt/ToBuffer.hxx" -#include "util/Domain.hxx" -#include "util/Manual.hxx" -#include "pcm/Export.hxx" -#include "pcm/Features.h" // for ENABLE_DSD -#include "thread/Mutex.hxx" -#include "thread/Cond.hxx" -#include "util/ByteOrder.hxx" -#include "util/CharUtil.hxx" -#include "util/RingBuffer.hxx" -#include "util/StringAPI.hxx" -#include "util/StringBuffer.hxx" -#include "Log.hxx" - -#include -#include -#include -#include - -#include -#include - -// Backward compatibility from OSX 12.0 API change -#if (__MAC_OS_X_VERSION_MAX_ALLOWED >= 120000) - #define KAUDIO_OBJECT_PROPERTY_ELEMENT_MM kAudioObjectPropertyElementMain - #define KAUDIO_HARDWARE_SERVICE_DEVICE_PROPERTY_VV kAudioHardwareServiceDeviceProperty_VirtualMainVolume -#else - #define KAUDIO_OBJECT_PROPERTY_ELEMENT_MM kAudioObjectPropertyElementMaster - #define KAUDIO_HARDWARE_SERVICE_DEVICE_PROPERTY_VV kAudioHardwareServiceDeviceProperty_VirtualMasterVolume -#endif - -static constexpr unsigned MPD_OSX_BUFFER_TIME_MS = 100; - -static auto -StreamDescriptionToString(const AudioStreamBasicDescription desc) noexcept -{ - // Only convert the lpcm formats (nothing else supported / used by MPD) - assert(desc.mFormatID == kAudioFormatLinearPCM); - - return FmtBuffer<256>("{} channel {} {}interleaved {}-bit {} {} ({}Hz)", - desc.mChannelsPerFrame, - (desc.mFormatFlags & kAudioFormatFlagIsNonMixable) ? "" : "mixable", - (desc.mFormatFlags & kAudioFormatFlagIsNonInterleaved) ? "non-" : "", - desc.mBitsPerChannel, - (desc.mFormatFlags & kAudioFormatFlagIsFloat) ? "Float" : "SInt", - (desc.mFormatFlags & kAudioFormatFlagIsBigEndian) ? "BE" : "LE", - desc.mSampleRate); -} - - -struct OSXOutput final : AudioOutput { - /* configuration settings */ - OSType component_subtype; - /* only applicable with kAudioUnitSubType_HALOutput */ - const char *device_name; - const char *const channel_map; - const bool hog_device; - - bool pause; - - /** - * Is the audio unit "started", i.e. was AudioOutputUnitStart() called? - */ - bool started; - -#ifdef ENABLE_DSD - /** - * Enable DSD over PCM according to the DoP standard? - * - * @see http://dsd-guide.com/dop-open-standard - */ - const bool dop_setting; - bool dop_enabled; - Manual pcm_export; -#endif - - AudioDeviceID dev_id; - AudioComponentInstance au; - AudioStreamBasicDescription asbd; - - using RingBuffer = ::RingBuffer; - RingBuffer ring_buffer; - - OSXOutput(const ConfigBlock &block); - - static AudioOutput *Create(EventLoop &, const ConfigBlock &block); - int GetVolume(); - void SetVolume(unsigned new_volume); - -private: - void Enable() override; - void Disable() noexcept override; - - void Open(AudioFormat &audio_format) override; - void Close() noexcept override; - - std::chrono::steady_clock::duration Delay() const noexcept override; - std::size_t Play(std::span src) override; - bool Pause() override; - void Cancel() noexcept override; -}; - -static constexpr Domain osx_output_domain("osx_output"); - -static bool -osx_output_test_default_device() -{ - /* on a Mac, this is always the default plugin, if nothing - else is configured */ - return true; -} - -OSXOutput::OSXOutput(const ConfigBlock &block) - :AudioOutput(FLAG_ENABLE_DISABLE|FLAG_PAUSE), - channel_map(block.GetBlockValue("channel_map")), - hog_device(block.GetBlockValue("hog_device", false)) -#ifdef ENABLE_DSD - , dop_setting(block.GetBlockValue("dop", false)) -#endif -{ - const char *device = block.GetBlockValue("device"); - - if (device == nullptr || StringIsEqual(device, "default")) { - component_subtype = kAudioUnitSubType_DefaultOutput; - device_name = nullptr; - } - else if (StringIsEqual(device, "system")) { - component_subtype = kAudioUnitSubType_SystemOutput; - device_name = nullptr; - } - else { - component_subtype = kAudioUnitSubType_HALOutput; - /* XXX am I supposed to strdup() this? */ - device_name = device; - } -} - -AudioOutput * -OSXOutput::Create(EventLoop &, const ConfigBlock &block) -{ - OSXOutput *oo = new OSXOutput(block); - - static constexpr AudioObjectPropertyAddress default_system_output_device{ - kAudioHardwarePropertyDefaultSystemOutputDevice, - kAudioObjectPropertyScopeOutput, - KAUDIO_OBJECT_PROPERTY_ELEMENT_MM, - }; - - static constexpr AudioObjectPropertyAddress default_output_device{ - kAudioHardwarePropertyDefaultOutputDevice, - kAudioObjectPropertyScopeOutput, - KAUDIO_OBJECT_PROPERTY_ELEMENT_MM - }; - - const auto &aopa = - oo->component_subtype == kAudioUnitSubType_SystemOutput - // get system output dev_id if configured - ? default_system_output_device - /* fallback to default device initially (can still be - changed by osx_output_set_device) */ - : default_output_device; - - AudioDeviceID dev_id = kAudioDeviceUnknown; - UInt32 dev_id_size = sizeof(dev_id); - AudioObjectGetPropertyData(kAudioObjectSystemObject, - &aopa, - 0, - NULL, - &dev_id_size, - &dev_id); - oo->dev_id = dev_id; - - return oo; -} - - -int -OSXOutput::GetVolume() -{ - static constexpr AudioObjectPropertyAddress aopa = { - KAUDIO_HARDWARE_SERVICE_DEVICE_PROPERTY_VV, - kAudioObjectPropertyScopeOutput, - KAUDIO_OBJECT_PROPERTY_ELEMENT_MM, - }; - - const auto vol = AudioObjectGetPropertyDataT(dev_id, - aopa); - - return static_cast(vol * 100.0f); -} - -void -OSXOutput::SetVolume(unsigned new_volume) -{ - Float32 vol = new_volume / 100.0; - static constexpr AudioObjectPropertyAddress aopa = { - KAUDIO_HARDWARE_SERVICE_DEVICE_PROPERTY_VV, - kAudioObjectPropertyScopeOutput, - KAUDIO_OBJECT_PROPERTY_ELEMENT_MM - }; - UInt32 size = sizeof(vol); - OSStatus status = AudioObjectSetPropertyData(dev_id, - &aopa, - 0, - NULL, - size, - &vol); - - if (status != noErr) - Apple::ThrowOSStatus(status); -} - -static void -osx_output_parse_channel_map(const char *device_name, - const char *channel_map_str, - SInt32 channel_map[], - UInt32 num_channels) -{ - unsigned int inserted_channels = 0; - bool want_number = true; - - while (*channel_map_str) { - if (inserted_channels >= num_channels) - throw FmtRuntimeError("{}: channel map contains more than {} entries or trailing garbage", - device_name, num_channels); - - if (!want_number && *channel_map_str == ',') { - ++channel_map_str; - want_number = true; - continue; - } - - if (want_number && - (IsDigitASCII(*channel_map_str) || *channel_map_str == '-') - ) { - char *endptr; - channel_map[inserted_channels] = strtol(channel_map_str, &endptr, 10); - if (channel_map[inserted_channels] < -1) - throw FmtRuntimeError("{}: channel map value {} not allowed (must be -1 or greater)", - device_name, channel_map[inserted_channels]); - - channel_map_str = endptr; - want_number = false; - FmtDebug(osx_output_domain, - "{}: channel_map[{}] = {}", - device_name, inserted_channels, - channel_map[inserted_channels]); - ++inserted_channels; - continue; - } - - throw FmtRuntimeError("{}: invalid character {:?} in channel map", - device_name, *channel_map_str); - } - - if (inserted_channels < num_channels) - throw FmtRuntimeError("{}: channel map contains less than {} entries", - device_name, num_channels); -} - -static UInt32 -AudioUnitGetChannelsPerFrame(AudioUnit inUnit) -{ - const auto desc = AudioUnitGetPropertyT(inUnit, - kAudioUnitProperty_StreamFormat, - kAudioUnitScope_Output, - 0); - return desc.mChannelsPerFrame; -} - -static void -osx_output_set_channel_map(OSXOutput *oo) -{ - OSStatus status; - - const UInt32 num_channels = AudioUnitGetChannelsPerFrame(oo->au); - auto channel_map = std::make_unique(num_channels); - osx_output_parse_channel_map(oo->device_name, - oo->channel_map, - channel_map.get(), - num_channels); - - UInt32 size = num_channels * sizeof(SInt32); - status = AudioUnitSetProperty(oo->au, - kAudioOutputUnitProperty_ChannelMap, - kAudioUnitScope_Input, - 0, - channel_map.get(), - size); - if (status != noErr) - Apple::ThrowOSStatus(status, "unable to set channel map"); -} - - -static float -osx_output_score_sample_rate(Float64 destination_rate, unsigned source_rate) -{ - float score = 0; - double int_portion; - double frac_portion = modf(source_rate / destination_rate, &int_portion); - // prefer sample rates that are multiples of the source sample rate - if (frac_portion < 0.01 || frac_portion >= 0.99) - score += 1000; - // prefer exact matches over other multiples - score += (int_portion == 1.0) ? 500 : 0; - if (source_rate == destination_rate) - score += 1000; - else if (source_rate > destination_rate) - score += (int_portion > 1 && int_portion < 100) ? (100 - int_portion) / 100 * 100 : 0; - else - score += (int_portion > 1 && int_portion < 100) ? (100 + int_portion) / 100 * 100 : 0; - - return score; -} - -static float -osx_output_score_format(const AudioStreamBasicDescription &format_desc, - const AudioStreamBasicDescription &target_format) -{ - float score = 0; - // Score only linear PCM formats (everything else MPD cannot use) - if (format_desc.mFormatID == kAudioFormatLinearPCM) { - score += osx_output_score_sample_rate(format_desc.mSampleRate, - target_format.mSampleRate); - - // Just choose the stream / format with the highest number of output channels - score += format_desc.mChannelsPerFrame * 5; - - if (target_format.mFormatFlags == kLinearPCMFormatFlagIsFloat) { - // for float, prefer the highest bitdepth we have - if (format_desc.mBitsPerChannel >= 16) - score += (format_desc.mBitsPerChannel / 8); - } else { - if (format_desc.mBitsPerChannel == target_format.mBitsPerChannel) - score += 5; - else if (format_desc.mBitsPerChannel > target_format.mBitsPerChannel) - score += 1; - - } - } - - return score; -} - -static Float64 -osx_output_set_device_format(AudioDeviceID dev_id, - const AudioStreamBasicDescription &target_format) -{ - static constexpr AudioObjectPropertyAddress aopa_device_streams = { - kAudioDevicePropertyStreams, - kAudioObjectPropertyScopeOutput, - KAUDIO_OBJECT_PROPERTY_ELEMENT_MM - }; - - static constexpr AudioObjectPropertyAddress aopa_stream_direction = { - kAudioStreamPropertyDirection, - kAudioObjectPropertyScopeOutput, - KAUDIO_OBJECT_PROPERTY_ELEMENT_MM - }; - - static constexpr AudioObjectPropertyAddress aopa_stream_phys_formats = { - kAudioStreamPropertyAvailablePhysicalFormats, - kAudioObjectPropertyScopeOutput, - KAUDIO_OBJECT_PROPERTY_ELEMENT_MM - }; - - static constexpr AudioObjectPropertyAddress aopa_stream_phys_format = { - kAudioStreamPropertyPhysicalFormat, - kAudioObjectPropertyScopeOutput, - KAUDIO_OBJECT_PROPERTY_ELEMENT_MM - }; - - OSStatus err; - - const auto streams = - AudioObjectGetPropertyDataArray(dev_id, - aopa_device_streams); - - bool format_found = false; - int output_stream; - AudioStreamBasicDescription output_format; - - for (const auto stream : streams) { - const auto direction = - AudioObjectGetPropertyDataT(stream, - aopa_stream_direction); - if (direction != 0) - continue; - - const auto format_list = - AudioObjectGetPropertyDataArray(stream, - aopa_stream_phys_formats); - - float output_score = 0; - - for (const auto &format : format_list) { - AudioStreamBasicDescription format_desc = format.mFormat; - std::string format_string; - - // for devices with kAudioStreamAnyRate - // we use the requested samplerate here - if (format_desc.mSampleRate == kAudioStreamAnyRate) - format_desc.mSampleRate = target_format.mSampleRate; - float score = osx_output_score_format(format_desc, target_format); - - // print all (linear pcm) formats and their rating - if (score > 0.0f) - FmtDebug(osx_output_domain, - "Format: {} rated {}", - StreamDescriptionToString(format_desc).c_str(), - score); - - if (score > output_score) { - output_score = score; - output_format = format_desc; - output_stream = stream; // set the idx of the stream in the device - format_found = true; - } - } - } - - if (format_found) { - err = AudioObjectSetPropertyData(output_stream, - &aopa_stream_phys_format, - 0, - NULL, - sizeof(output_format), - &output_format); - if (err != noErr) - throw FmtRuntimeError("Failed to change the stream format: {}", - err); - } - - return output_format.mSampleRate; -} - -static UInt32 -osx_output_set_buffer_size(AudioUnit au, AudioStreamBasicDescription desc) -{ - const auto value_range = AudioUnitGetPropertyT(au, - kAudioDevicePropertyBufferFrameSizeRange, - kAudioUnitScope_Global, - 0); - - try { - AudioUnitSetBufferFrameSize(au, value_range.mMaximum); - } catch (...) { - LogError(std::current_exception(), - "Failed to set maximum buffer size"); - } - - auto buffer_frame_size = AudioUnitGetBufferFrameSize(au); - buffer_frame_size *= desc.mBytesPerFrame; - - // We set the frame size to a power of two integer that - // is larger than buffer_frame_size. - UInt32 frame_size = 1; - while (frame_size < buffer_frame_size + 1) - frame_size <<= 1; - - return frame_size; -} - -static void -osx_output_hog_device(AudioDeviceID dev_id, bool hog) noexcept -{ - static constexpr AudioObjectPropertyAddress aopa = { - kAudioDevicePropertyHogMode, - kAudioObjectPropertyScopeOutput, - KAUDIO_OBJECT_PROPERTY_ELEMENT_MM - }; - - pid_t hog_pid; - - try { - hog_pid = AudioObjectGetPropertyDataT(dev_id, aopa); - } catch (...) { - Log(LogLevel::DEBUG, std::current_exception(), - "Failed to query HogMode"); - return; - } - - if (hog) { - if (hog_pid != -1) { - LogDebug(osx_output_domain, - "Device is already hogged"); - return; - } - } else { - if (hog_pid != getpid()) { - FmtDebug(osx_output_domain, - "Device is not owned by this process"); - return; - } - } - - hog_pid = hog ? getpid() : -1; - UInt32 size = sizeof(hog_pid); - OSStatus err; - err = AudioObjectSetPropertyData(dev_id, - &aopa, - 0, - NULL, - size, - &hog_pid); - if (err != noErr) { - FmtDebug(osx_output_domain, - "Cannot hog the device: {}", err); - } else { - LogDebug(osx_output_domain, - hog_pid == -1 - ? "Device is unhogged" - : "Device is hogged"); - } -} - -[[gnu::pure]] -static bool -IsAudioDeviceName(AudioDeviceID id, const char *expected_name) noexcept -{ - static constexpr AudioObjectPropertyAddress aopa_name{ - kAudioObjectPropertyName, - kAudioObjectPropertyScopeGlobal, - KAUDIO_OBJECT_PROPERTY_ELEMENT_MM, - }; - - char actual_name[256]; - - try { - auto cfname = AudioObjectGetStringProperty(id, aopa_name); - if (!cfname.GetCString(actual_name, sizeof(actual_name))) - return false; - } catch (...) { - return false; - } - - return StringIsEqual(actual_name, expected_name); -} - -static AudioDeviceID -FindAudioDeviceByName(const char *name) -{ - /* what are the available audio device IDs? */ - static constexpr AudioObjectPropertyAddress aopa_hw_devices{ - kAudioHardwarePropertyDevices, - kAudioObjectPropertyScopeGlobal, - KAUDIO_OBJECT_PROPERTY_ELEMENT_MM, - }; - - const auto ids = - AudioObjectGetPropertyDataArray(kAudioObjectSystemObject, - aopa_hw_devices); - - for (const auto id : ids) { - if (IsAudioDeviceName(id, name)) - return id; - } - - throw FmtRuntimeError("Found no audio device names {:?}", name); -} - -static void -osx_output_set_device(OSXOutput *oo) -{ - if (oo->component_subtype != kAudioUnitSubType_HALOutput) - return; - - const auto id = FindAudioDeviceByName(oo->device_name); - - FmtDebug(osx_output_domain, - "found matching device: ID={}, name={}", - id, oo->device_name); - - AudioUnitSetCurrentDevice(oo->au, id); - - oo->dev_id = id; - FmtDebug(osx_output_domain, - "set OS X audio output device ID={}, name={}", - id, oo->device_name); - - if (oo->channel_map) - osx_output_set_channel_map(oo); -} - - -/** - * This function (the 'render callback' osx_render) is called by the - * OS X audio subsystem (CoreAudio) to request audio data that will be - * played by the audio hardware. This function has hard time - * constraints so it cannot do IO (debug statements) or memory - * allocations. - */ -static OSStatus -osx_render(void *vdata, - [[maybe_unused]] AudioUnitRenderActionFlags *io_action_flags, - [[maybe_unused]] const AudioTimeStamp *in_timestamp, - [[maybe_unused]] UInt32 in_bus_number, - UInt32 in_number_frames, - AudioBufferList *buffer_list) -{ - OSXOutput *od = (OSXOutput *) vdata; - - std::size_t count = in_number_frames * od->asbd.mBytesPerFrame; - buffer_list->mBuffers[0].mDataByteSize = - od->ring_buffer.ReadTo({(std::byte *)buffer_list->mBuffers[0].mData, count}); - return noErr; -} - -void -OSXOutput::Enable() -{ - AudioComponentDescription desc; - desc.componentType = kAudioUnitType_Output; - desc.componentSubType = component_subtype; - desc.componentManufacturer = kAudioUnitManufacturer_Apple; - desc.componentFlags = 0; - desc.componentFlagsMask = 0; - - AudioComponent comp = AudioComponentFindNext(nullptr, &desc); - if (comp == 0) - throw std::runtime_error("Error finding OS X component"); - - OSStatus status = AudioComponentInstanceNew(comp, &au); - if (status != noErr) - Apple::ThrowOSStatus(status, "Unable to open OS X component"); - -#ifdef ENABLE_DSD - pcm_export.Construct(); -#endif - - try { - osx_output_set_device(this); - } catch (...) { - AudioComponentInstanceDispose(au); -#ifdef ENABLE_DSD - pcm_export.Destruct(); -#endif - throw; - } - - if (hog_device) - osx_output_hog_device(dev_id, true); -} - -void -OSXOutput::Disable() noexcept -{ - AudioComponentInstanceDispose(au); -#ifdef ENABLE_DSD - pcm_export.Destruct(); -#endif - - if (hog_device) - osx_output_hog_device(dev_id, false); -} - -void -OSXOutput::Close() noexcept -{ - if (started) - AudioOutputUnitStop(au); - AudioUnitUninitialize(au); - ring_buffer = {}; -} - -void -OSXOutput::Open(AudioFormat &audio_format) -{ -#ifdef ENABLE_DSD - PcmExport::Params params; - params.alsa_channel_order = true; - bool dop = dop_setting; -#endif - - memset(&asbd, 0, sizeof(asbd)); - asbd.mFormatID = kAudioFormatLinearPCM; - if (audio_format.format == SampleFormat::FLOAT) { - asbd.mFormatFlags = kLinearPCMFormatFlagIsFloat; - } else { - asbd.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger; - } - - if (IsBigEndian()) - asbd.mFormatFlags |= kLinearPCMFormatFlagIsBigEndian; - - if (audio_format.format == SampleFormat::S24_P32) { - asbd.mBitsPerChannel = 24; - } else { - asbd.mBitsPerChannel = audio_format.GetSampleSize() * 8; - } - asbd.mBytesPerPacket = audio_format.GetFrameSize(); - asbd.mSampleRate = audio_format.sample_rate; - -#ifdef ENABLE_DSD - if (dop && audio_format.format == SampleFormat::DSD) { - asbd.mBitsPerChannel = 24; - params.dsd_mode = PcmExport::DsdMode::DOP; - asbd.mSampleRate = params.CalcOutputSampleRate(audio_format.sample_rate); - asbd.mBytesPerPacket = 4 * audio_format.channels; - - } -#endif - - asbd.mFramesPerPacket = 1; - asbd.mBytesPerFrame = asbd.mBytesPerPacket; - asbd.mChannelsPerFrame = audio_format.channels; - - Float64 sample_rate = osx_output_set_device_format(dev_id, asbd); - -#ifdef ENABLE_DSD - if (audio_format.format == SampleFormat::DSD && - sample_rate != asbd.mSampleRate) { - // fall back to PCM in case sample_rate cannot be synchronized - params.dsd_mode = PcmExport::DsdMode::NONE; - audio_format.format = SampleFormat::S32; - asbd.mBitsPerChannel = 32; - asbd.mBytesPerPacket = audio_format.GetFrameSize(); - asbd.mSampleRate = params.CalcOutputSampleRate(audio_format.sample_rate); - asbd.mBytesPerFrame = asbd.mBytesPerPacket; - } - dop_enabled = params.dsd_mode == PcmExport::DsdMode::DOP; -#endif - - AudioUnitSetInputStreamFormat(au, asbd); - - AURenderCallbackStruct callback; - callback.inputProc = osx_render; - callback.inputProcRefCon = this; - - AudioUnitSetInputRenderCallback(au, callback); - - OSStatus status = AudioUnitInitialize(au); - if (status != noErr) - Apple::ThrowOSStatus(status, "Unable to initialize OS X audio unit"); - - UInt32 buffer_frame_size = osx_output_set_buffer_size(au, asbd); - - size_t ring_buffer_size = std::max(buffer_frame_size, - MPD_OSX_BUFFER_TIME_MS * audio_format.GetFrameSize() * audio_format.sample_rate / 1000); - -#ifdef ENABLE_DSD - if (dop_enabled) { - pcm_export->Open(audio_format.format, audio_format.channels, params); - ring_buffer_size = std::max(buffer_frame_size, - MPD_OSX_BUFFER_TIME_MS * pcm_export->GetOutputFrameSize() * asbd.mSampleRate / 1000); - } -#endif - ring_buffer = RingBuffer{ring_buffer_size}; - - pause = false; - started = false; -} - -std::size_t -OSXOutput::Play(std::span input) -{ - assert(!input.empty()); - - pause = false; - -#ifdef ENABLE_DSD - if (dop_enabled) { - input = pcm_export->Export(input); - if (input.empty()) - return input.size(); - } -#endif - - size_t bytes_written = ring_buffer.WriteFrom(input); - - if (!started) { - OSStatus status = AudioOutputUnitStart(au); - if (status != noErr) - throw std::runtime_error("Unable to restart audio output after pause"); - - started = true; - } - -#ifdef ENABLE_DSD - if (dop_enabled) - bytes_written = pcm_export->CalcInputSize(bytes_written); -#endif - - return bytes_written; -} - -std::chrono::steady_clock::duration -OSXOutput::Delay() const noexcept -{ - return !ring_buffer.IsFull() && !pause - ? std::chrono::steady_clock::duration::zero() - : std::chrono::milliseconds(MPD_OSX_BUFFER_TIME_MS / 4); -} - -bool OSXOutput::Pause() -{ - pause = true; - - if (started) { - AudioOutputUnitStop(au); - started = false; - } - - return true; -} - -void -OSXOutput::Cancel() noexcept -{ - if (started) { - AudioOutputUnitStop(au); - started = false; - } - - ring_buffer.Clear(); -#ifdef ENABLE_DSD - pcm_export->Reset(); -#endif - - /* the AudioUnit will be restarted by the next Play() call */ -} - -int -osx_output_get_volume(OSXOutput &output) -{ - return output.GetVolume(); -} - -void -osx_output_set_volume(OSXOutput &output, unsigned new_volume) -{ - return output.SetVolume(new_volume); -} - -const struct AudioOutputPlugin osx_output_plugin = { - "osx", - osx_output_test_default_device, - &OSXOutput::Create, - &osx_mixer_plugin, -}; diff --git a/src/output/plugins/OSXOutputPlugin.hxx b/src/output/plugins/OSXOutputPlugin.hxx deleted file mode 100644 index 19376e4..0000000 --- a/src/output/plugins/OSXOutputPlugin.hxx +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#ifndef MPD_OSX_OUTPUT_PLUGIN_HXX -#define MPD_OSX_OUTPUT_PLUGIN_HXX - -struct OSXOutput; - -extern const struct AudioOutputPlugin osx_output_plugin; - -int -osx_output_get_volume(OSXOutput &output); - -void -osx_output_set_volume(OSXOutput &output, unsigned new_volume); - -#endif diff --git a/src/output/plugins/OpenALOutputPlugin.cxx b/src/output/plugins/OpenALOutputPlugin.cxx deleted file mode 100644 index 62d1143..0000000 --- a/src/output/plugins/OpenALOutputPlugin.cxx +++ /dev/null @@ -1,212 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "OpenALOutputPlugin.hxx" -#include "../OutputAPI.hxx" -#include "lib/fmt/RuntimeError.hxx" - -#include - -#ifndef __APPLE__ -#include -#include -#else -#include -#include -/* on macOS, OpenAL is deprecated, but since the user asked to enable - this plugin, let's ignore the compiler warnings */ -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" -#endif - -class OpenALOutput final : AudioOutput { - /* should be enough for buffer size = 2048 */ - static constexpr unsigned NUM_BUFFERS = 16; - - const char *device_name; - ALCdevice *device; - ALCcontext *context; - ALuint buffers[NUM_BUFFERS]; - unsigned filled; - ALuint source; - ALenum format; - ALuint frequency; - - explicit OpenALOutput(const ConfigBlock &block); - -public: - static AudioOutput *Create(EventLoop &, - const ConfigBlock &block) { - return new OpenALOutput(block); - } - -private: - void Open(AudioFormat &audio_format) override; - void Close() noexcept override; - - [[nodiscard]] [[gnu::pure]] - std::chrono::steady_clock::duration Delay() const noexcept override { - return filled < NUM_BUFFERS || HasProcessed() - ? std::chrono::steady_clock::duration::zero() - /* we don't know exactly how long we must wait - for the next buffer to finish, so this is a - random guess: */ - : std::chrono::milliseconds(50); - } - - std::size_t Play(std::span src) override; - - void Cancel() noexcept override; - - [[nodiscard]] [[gnu::pure]] - ALint GetSourceI(ALenum param) const noexcept { - ALint value; - alGetSourcei(source, param, &value); - return value; - } - - [[nodiscard]] [[gnu::pure]] - bool HasProcessed() const noexcept { - return GetSourceI(AL_BUFFERS_PROCESSED) > 0; - } - - [[nodiscard]] [[gnu::pure]] - bool IsPlaying() const noexcept { - return GetSourceI(AL_SOURCE_STATE) == AL_PLAYING; - } - - /** - * Throws on error. - */ - void SetupContext(); -}; - -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); - } -} - -inline void -OpenALOutput::SetupContext() -{ - device = alcOpenDevice(device_name); - if (device == nullptr) - throw FmtRuntimeError("Error opening OpenAL device {:?}", - device_name); - - context = alcCreateContext(device, nullptr); - if (context == nullptr) { - alcCloseDevice(device); - throw FmtRuntimeError("Error creating context for {:?}", - device_name); - } -} - -OpenALOutput::OpenALOutput(const ConfigBlock &block) - :AudioOutput(0), - device_name(block.GetBlockValue("device")) -{ - if (device_name == nullptr) - device_name = alcGetString(nullptr, - ALC_DEFAULT_DEVICE_SPECIFIER); -} - -void -OpenALOutput::Open(AudioFormat &audio_format) -{ - format = openal_audio_format(audio_format); - - SetupContext(); - - alcMakeContextCurrent(context); - alGenBuffers(NUM_BUFFERS, buffers); - - if (alGetError() != AL_NO_ERROR) - throw std::runtime_error("Failed to generate buffers"); - - alGenSources(1, &source); - - if (alGetError() != AL_NO_ERROR) { - alDeleteBuffers(NUM_BUFFERS, buffers); - throw std::runtime_error("Failed to generate source"); - } - - filled = 0; - frequency = audio_format.sample_rate; -} - -void -OpenALOutput::Close() noexcept -{ - alcMakeContextCurrent(context); - alDeleteSources(1, &source); - alDeleteBuffers(NUM_BUFFERS, buffers); - alcDestroyContext(context); - alcCloseDevice(device); -} - -std::size_t -OpenALOutput::Play(std::span src) -{ - if (alcGetCurrentContext() != context) - alcMakeContextCurrent(context); - - ALuint buffer; - if (filled < NUM_BUFFERS) { - /* fill all buffers */ - buffer = buffers[filled]; - filled++; - } else { - /* wait for processed buffer */ - while (!HasProcessed()) - usleep(10); - - alSourceUnqueueBuffers(source, 1, &buffer); - } - - alBufferData(buffer, format, src.data(), src.size(), frequency); - alSourceQueueBuffers(source, 1, &buffer); - - if (!IsPlaying()) - alSourcePlay(source); - - return src.size(); -} - -void -OpenALOutput::Cancel() noexcept -{ - filled = 0; - alcMakeContextCurrent(context); - alSourceStop(source); - - /* force-unqueue all buffers */ - alSourcei(source, AL_BUFFER, 0); - filled = 0; -} - -const struct AudioOutputPlugin openal_output_plugin = { - "openal", - nullptr, - OpenALOutput::Create, - nullptr, -}; diff --git a/src/output/plugins/OpenALOutputPlugin.hxx b/src/output/plugins/OpenALOutputPlugin.hxx deleted file mode 100644 index 56a9468..0000000 --- a/src/output/plugins/OpenALOutputPlugin.hxx +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#ifndef MPD_OPENAL_OUTPUT_PLUGIN_HXX -#define MPD_OPENAL_OUTPUT_PLUGIN_HXX - -extern const struct AudioOutputPlugin openal_output_plugin; - -#endif diff --git a/src/output/plugins/OssOutputPlugin.cxx b/src/output/plugins/OssOutputPlugin.cxx deleted file mode 100644 index e900a3d..0000000 --- a/src/output/plugins/OssOutputPlugin.cxx +++ /dev/null @@ -1,742 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "OssOutputPlugin.hxx" -#include "../OutputAPI.hxx" -#include "mixer/plugins/OssMixerPlugin.hxx" -#include "pcm/Export.hxx" -#include "io/UniqueFileDescriptor.hxx" -#include "lib/fmt/SystemError.hxx" -#include "util/Domain.hxx" -#include "util/ByteOrder.hxx" -#include "util/Manual.hxx" -#include "Log.hxx" - -#include -#include -#include -#include -#include // for std::unreachable() - -#include -#include -#include -#include -#include - -#include - -/* 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 - -#if defined(ENABLE_DSD) && defined(AFMT_S32_NE) -#define ENABLE_OSS_DSD -#endif - -class OssOutput final : AudioOutput { - Manual pcm_export; - - const char *const device; - - FileDescriptor fd = FileDescriptor::Undefined(); - - /** - * The effective audio format settings of the OSS device. - * This is needed by Reopen() after Cancel(). - */ - int effective_channels, effective_speed, effective_samplesize; - -#ifdef ENABLE_OSS_DSD - /** - * Enable DSD over PCM according to the DoP standard? - * - * @see http://dsd-guide.com/dop-open-standard - * - * this is default in oss as no other dsd-method is known to man - */ - const bool dop_setting; -#endif - - /** - * Has Drain() been called? If not, then Close() will use - * SNDCTL_DSP_RESET to omit the implicit sync on close(). - */ - bool drain = false; - - static constexpr unsigned oss_flags = FLAG_ENABLE_DISABLE; - -public: - explicit OssOutput(const char *_device=nullptr -#ifdef ENABLE_OSS_DSD - , bool dop = false -#endif - ) - :AudioOutput(oss_flags), - device(_device) -#ifdef ENABLE_OSS_DSD - , dop_setting(dop) -#endif - { - } - - static AudioOutput *Create(EventLoop &event_loop, - const ConfigBlock &block); - - // virtual methods from class AudioOutput - void Enable() override { - pcm_export.Construct(); - } - - void Disable() noexcept override { - pcm_export.Destruct(); - } - - void Open(AudioFormat &audio_format) override; - void Close() noexcept override; - - std::size_t Play(std::span src) override; - void Drain() noexcept override; - void Cancel() noexcept override; - -private: - /** - * Sets up the OSS device which was opened before. - */ - void Setup(AudioFormat &audio_format); - -#ifdef ENABLE_OSS_DSD - void SetupDop(const AudioFormat &audio_format); -#endif - - void SetupOrDop(AudioFormat &audio_format); - - /** - * Reopen the device with the saved audio_format, without any probing. - * - * Throws on error. - */ - void Reopen(); - - void DoClose() noexcept; -}; - -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) noexcept -{ - 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 *const default_devices[] = { "/dev/sound/dsp", "/dev/dsp" }; - -static bool -oss_output_test_default_device() noexcept -{ - for (int i = std::size(default_devices); --i >= 0; ) { - UniqueFileDescriptor fd; - if (fd.Open(default_devices[i], O_WRONLY, 0)) - return true; - - FmtError(oss_output_domain, - "Error opening OSS device {:?}: {}", - default_devices[i], strerror(errno)); - } - - return false; -} - -static OssOutput * -oss_open_default( -#ifdef ENABLE_OSS_DSD - bool dop -#endif - ) -{ - int err[std::size(default_devices)]; - enum oss_stat ret[std::size(default_devices)]; - - for (int i = std::size(default_devices); --i >= 0; ) { - ret[i] = oss_stat_device(default_devices[i], &err[i]); - if (ret[i] == OSS_STAT_NO_ERROR) - return new OssOutput(default_devices[i] -#ifdef ENABLE_OSS_DSD - , dop -#endif - ); - } - - for (int i = std::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: - FmtWarning(oss_output_domain, - "{} not found", dev); - break; - case OSS_STAT_NOT_CHAR_DEV: - FmtWarning(oss_output_domain, - "{} is not a character device", dev); - break; - case OSS_STAT_NO_PERMS: - FmtWarning(oss_output_domain, - "{}: permission denied", dev); - break; - case OSS_STAT_OTHER: - FmtError(oss_output_domain, "Error accessing {}: {}", - dev, strerror(err[i])); - } - } - - throw std::runtime_error("error trying to open default OSS device"); -} - -AudioOutput * -OssOutput::Create(EventLoop &, const ConfigBlock &block) -{ -#ifdef ENABLE_OSS_DSD - bool dop = block.GetBlockValue("dop", false); -#endif - - const char *device = block.GetBlockValue("device"); - if (device != nullptr) - return new OssOutput(device -#ifdef ENABLE_OSS_DSD - , dop -#endif - ); - - return oss_open_default( -#ifdef ENABLE_OSS_DSD - dop -#endif - ); -} - -void -OssOutput::DoClose() noexcept -{ - if (fd.IsDefined()) - fd.Close(); -} - -/** - * Invoke an ioctl on the OSS file descriptor. - * - * Throws on error. - * - * @return true success, false if the parameter is not supported - */ -static bool -oss_try_ioctl_r(FileDescriptor fd, unsigned long request, int *value_r, - const char *msg) -{ - assert(fd.IsDefined()); - assert(value_r != nullptr); - assert(msg != nullptr); - - int ret = ioctl(fd.Get(), request, value_r); - if (ret >= 0) - return true; - - const int err = errno; - if (err == EINVAL) - return false; - - throw MakeErrno(err, msg); -} - -/** - * Invoke an ioctl on the OSS file descriptor, and expect an - * unmodified effective value. - * - * Throws on error. - */ -static void -OssIoctlExact(FileDescriptor fd, unsigned long request, int requested_value, - const char *msg) -{ - assert(fd.IsDefined()); - assert(msg != nullptr); - - int effective_value = requested_value; - if (ioctl(fd.Get(), request, &effective_value) < 0) - throw MakeErrno(msg); - - if (effective_value != requested_value) - throw std::runtime_error(msg); -} - -/** - * Set up the channel number, and attempts to find alternatives if the - * specified number is not supported. - * - * Throws on error. - */ -static void -oss_setup_channels(FileDescriptor fd, AudioFormat &audio_format, - int &effective_channels) -{ - const char *const msg = "Failed to set channel count"; - - effective_channels = audio_format.channels; - - if (oss_try_ioctl_r(fd, SNDCTL_DSP_CHANNELS, - &effective_channels, msg) && - audio_valid_channel_count(effective_channels)) { - audio_format.channels = effective_channels; - return; - } - - for (unsigned i = 1; i < 2; ++i) { - if (i == audio_format.channels) - /* don't try that again */ - continue; - - effective_channels = i; - if (oss_try_ioctl_r(fd, SNDCTL_DSP_CHANNELS, - &effective_channels, msg) && - audio_valid_channel_count(effective_channels)) { - audio_format.channels = effective_channels; - return; - } - } - - throw std::runtime_error(msg); -} - -/** - * Set up the sample rate, and attempts to find alternatives if the - * specified sample rate is not supported. - * - * Throws on error. - */ -static void -oss_setup_sample_rate(FileDescriptor fd, AudioFormat &audio_format, - int &effective_speed) -{ - const char *const msg = "Failed to set sample rate"; - - effective_speed = audio_format.sample_rate; - if (oss_try_ioctl_r(fd, SNDCTL_DSP_SPEED, &effective_speed, msg) && - audio_valid_sample_rate(effective_speed)) { - audio_format.sample_rate = effective_speed; - return; - } - - static constexpr int sample_rates[] = { 48000, 44100, 0 }; - for (unsigned i = 0; sample_rates[i] != 0; ++i) { - effective_speed = sample_rates[i]; - if (effective_speed == (int)audio_format.sample_rate) - continue; - - if (oss_try_ioctl_r(fd, SNDCTL_DSP_SPEED, &effective_speed, msg) && - audio_valid_sample_rate(effective_speed)) { - audio_format.sample_rate = effective_speed; - return; - } - } - - throw std::runtime_error(msg); -} - -/** - * Convert a MPD sample format to its OSS counterpart. Returns - * AFMT_QUERY if there is no direct counterpart. - */ -static constexpr int -sample_format_to_oss(SampleFormat format) noexcept -{ - 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 - } - - std::unreachable(); -} - -/** - * Convert an OSS sample format to its MPD counterpart. Returns - * SampleFormat::UNDEFINED if there is no direct counterpart. - */ -static constexpr SampleFormat -sample_format_from_oss(int format) noexcept -{ - 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. - * - * Throws on error. - * - * @return true success, false if the parameter is not supported - */ -static bool -oss_probe_sample_format(FileDescriptor fd, SampleFormat sample_format, - SampleFormat *sample_format_r, - int *oss_format_r, - PcmExport &pcm_export) -{ - int oss_format = sample_format_to_oss(sample_format); - if (oss_format == AFMT_QUERY) - return false; - - bool success = - oss_try_ioctl_r(fd, SNDCTL_DSP_SAMPLESIZE, - &oss_format, - "Failed to set sample format"); - -#ifdef AFMT_S24_PACKED - if (!success && sample_format == SampleFormat::S24_P32) { - /* if the driver doesn't support padded 24 bit, try - packed 24 bit */ - oss_format = AFMT_S24_PACKED; - success = oss_try_ioctl_r(fd, SNDCTL_DSP_SAMPLESIZE, - &oss_format, - "Failed to set sample format"); - } -#endif - - if (!success) - return false; - - sample_format = sample_format_from_oss(oss_format); - - if (sample_format == SampleFormat::UNDEFINED) - return false; - - *sample_format_r = sample_format; - *oss_format_r = oss_format; - - PcmExport::Params params; - params.alsa_channel_order = true; -#ifdef AFMT_S24_PACKED - params.pack24 = oss_format == AFMT_S24_PACKED; - params.reverse_endian = oss_format == AFMT_S24_PACKED && - !IsLittleEndian(); -#endif - - pcm_export.Open(sample_format, 0, params); - - return true; -} - -/** - * Set up the sample format, and attempts to find alternatives if the - * specified format is not supported. - */ -static void -oss_setup_sample_format(FileDescriptor fd, AudioFormat &audio_format, - int *oss_format_r, - PcmExport &pcm_export) -{ - SampleFormat mpd_format; - if (oss_probe_sample_format(fd, audio_format.format, - &mpd_format, oss_format_r, - pcm_export)) { - audio_format.format = mpd_format; - return; - } - - /* the requested sample format is not available - probe for - other formats supported by MPD */ - - static constexpr 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; - - if (oss_probe_sample_format(fd, mpd_format, - &mpd_format, oss_format_r, - pcm_export)) { - audio_format.format = mpd_format; - return; - } - } - - throw std::runtime_error("Failed to set sample format"); -} - -inline void -OssOutput::Setup(AudioFormat &_audio_format) -{ - oss_setup_channels(fd, _audio_format, effective_channels); - oss_setup_sample_rate(fd, _audio_format, effective_speed); - oss_setup_sample_format(fd, _audio_format, &effective_samplesize, - pcm_export); -} - -#ifdef ENABLE_OSS_DSD - -void -OssOutput::SetupDop(const AudioFormat &audio_format) -{ - assert(audio_format.format == SampleFormat::DSD); - - effective_channels = audio_format.channels; - - /* DoP packs two 8-bit "samples" in one 24-bit "sample" */ - effective_speed = audio_format.sample_rate / 2; - - effective_samplesize = AFMT_S32_NE; - - OssIoctlExact(fd, SNDCTL_DSP_CHANNELS, effective_channels, - "Failed to set channel count"); - OssIoctlExact(fd, SNDCTL_DSP_SPEED, effective_speed, - "Failed to set sample rate"); - OssIoctlExact(fd, SNDCTL_DSP_SAMPLESIZE, effective_samplesize, - "Failed to set sample format"); - - PcmExport::Params params; - params.alsa_channel_order = true; - params.dsd_mode = PcmExport::DsdMode::DOP; - params.shift8 = true; - - pcm_export->Open(audio_format.format, audio_format.channels, params); -} - -#endif - -void -OssOutput::SetupOrDop(AudioFormat &audio_format) -{ -#ifdef ENABLE_OSS_DSD - std::exception_ptr dop_error; - if (dop_setting && audio_format.format == SampleFormat::DSD) { - try { - SetupDop(audio_format); - return; - } catch (...) { - dop_error = std::current_exception(); - } - } - - try { -#endif - Setup(audio_format); -#ifdef ENABLE_OSS_DSD - } catch (...) { - if (dop_error) - /* if DoP was attempted, prefer returning the - original DoP error instead of the fallback - error */ - std::rethrow_exception(dop_error); - else - throw; - } -#endif -} - -/** - * Reopen the device with the saved audio_format, without any probing. - */ -inline void -OssOutput::Reopen() -try { - assert(!fd.IsDefined()); - - if (!fd.Open(device, O_WRONLY)) - throw FmtErrno("Error opening OSS device {:?}", device); - - OssIoctlExact(fd, SNDCTL_DSP_CHANNELS, effective_channels, - "Failed to set channel count"); - OssIoctlExact(fd, SNDCTL_DSP_SPEED, effective_speed, - "Failed to set sample rate"); - OssIoctlExact(fd, SNDCTL_DSP_SAMPLESIZE, effective_samplesize, - "Failed to set sample format"); -} catch (...) { - DoClose(); - throw; -} - -void -OssOutput::Open(AudioFormat &_audio_format) -try { - if (!fd.Open(device, O_WRONLY)) - throw FmtErrno("Error opening OSS device {:?}", device); - - SetupOrDop(_audio_format); - - drain = false; -} catch (...) { - DoClose(); - throw; -} - -void -OssOutput::Close() noexcept -{ - if (!fd.IsDefined()) - return; - - if (!drain) - /* if Drain() has not been called, then the caller - wishes to close as quickly as possible, so let's - skip the implicit sync on close */ - ioctl(fd.Get(), SNDCTL_DSP_RESET, 0); - - fd.Close(); -} - -void -OssOutput::Drain() noexcept -{ - /* enable the "drain" flag; the actual sync happens later in - Close() */ - drain = true; -} - -void -OssOutput::Cancel() noexcept -{ - drain = false; - - if (fd.IsDefined()) { - ioctl(fd.Get(), SNDCTL_DSP_RESET, 0); - - /* after SNDCTL_DSP_RESET, we can't use the file - handle anymore; closing it here, to be reopened by - the next Play() call */ - DoClose(); - } - - pcm_export->Reset(); -} - -std::size_t -OssOutput::Play(std::span src) -{ - assert(!src.empty()); - - /* reopen the device since it was closed by Cancel() */ - if (!fd.IsDefined()) - Reopen(); - - const auto e = pcm_export->Export(src); - if (e.empty()) - return src.size(); - - while (true) { - const ssize_t ret = fd.Write(e); - if (ret > 0) [[likely]] - return pcm_export->CalcInputSize(ret); - - if (ret == 0) [[unlikely]] - // can this ever happen? What now? - continue; - - const int err = errno; - if (err == EINTR) - /* interrupted by a signal - try again */ - continue; - - if (err == EAGAIN) { - /* we opened the device in non-blocking mode - and the OSS FIFO is full */ - const int w = fd.WaitWritable(1000); - if (w >= 0) - continue; - } - - throw FmtErrno(err, "Write error on {:?}", device); - } -} - -constexpr struct AudioOutputPlugin oss_output_plugin = { - "oss", - oss_output_test_default_device, - OssOutput::Create, - &oss_mixer_plugin, -}; diff --git a/src/output/plugins/OssOutputPlugin.hxx b/src/output/plugins/OssOutputPlugin.hxx deleted file mode 100644 index e1fcb1d..0000000 --- a/src/output/plugins/OssOutputPlugin.hxx +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#ifndef MPD_OSS_OUTPUT_PLUGIN_HXX -#define MPD_OSS_OUTPUT_PLUGIN_HXX - -extern const struct AudioOutputPlugin oss_output_plugin; - -#endif diff --git a/src/output/plugins/PipeOutputPlugin.cxx b/src/output/plugins/PipeOutputPlugin.cxx deleted file mode 100644 index 1a45c80..0000000 --- a/src/output/plugins/PipeOutputPlugin.cxx +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "PipeOutputPlugin.hxx" -#include "../OutputAPI.hxx" -#include "lib/fmt/SystemError.hxx" - -#include -#include - -#include - -class PipeOutput final : AudioOutput { - const std::string cmd; - FILE *fh; - - explicit PipeOutput(const ConfigBlock &block); - -public: - static AudioOutput *Create(EventLoop &, - const ConfigBlock &block) { - return new PipeOutput(block); - } - -private: - void Open(AudioFormat &audio_format) override; - - void Close() noexcept override { - pclose(fh); - } - - std::size_t Play(std::span src) override; -}; - -PipeOutput::PipeOutput(const ConfigBlock &block) - :AudioOutput(0), - cmd(block.GetBlockValue("command", "")) -{ - if (cmd.empty()) - throw std::runtime_error("No \"command\" parameter specified"); -} - -inline void -PipeOutput::Open([[maybe_unused]] AudioFormat &audio_format) -{ - fh = popen(cmd.c_str(), "w"); - if (fh == nullptr) - throw FmtErrno("Error opening pipe {:?}", cmd); -} - -std::size_t -PipeOutput::Play(std::span src) -{ - size_t nbytes = fwrite(src.data(), 1, src.size(), fh); - if (nbytes == 0) - throw MakeErrno("Write error on pipe"); - - return nbytes; -} - -const struct AudioOutputPlugin pipe_output_plugin = { - "pipe", - nullptr, - &PipeOutput::Create, - nullptr, -}; diff --git a/src/output/plugins/PipeOutputPlugin.hxx b/src/output/plugins/PipeOutputPlugin.hxx deleted file mode 100644 index e693860..0000000 --- a/src/output/plugins/PipeOutputPlugin.hxx +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#ifndef MPD_PIPE_OUTPUT_PLUGIN_HXX -#define MPD_PIPE_OUTPUT_PLUGIN_HXX - -extern const struct AudioOutputPlugin pipe_output_plugin; - -#endif diff --git a/src/output/plugins/PipeWireOutputPlugin.cxx b/src/output/plugins/PipeWireOutputPlugin.cxx deleted file mode 100644 index 70348f1..0000000 --- a/src/output/plugins/PipeWireOutputPlugin.cxx +++ /dev/null @@ -1,980 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "PipeWireOutputPlugin.hxx" -#include "lib/pipewire/Error.hxx" -#include "lib/pipewire/ThreadLoop.hxx" -#include "../OutputAPI.hxx" -#include "../Error.hxx" -#include "mixer/plugins/PipeWireMixerPlugin.hxx" -#include "pcm/Features.h" // for ENABLE_DSD -#include "pcm/Silence.hxx" -#include "lib/fmt/ExceptionFormatter.hxx" -#include "system/Error.hxx" -#include "util/BitReverse.hxx" -#include "util/Domain.hxx" -#include "util/RingBuffer.hxx" -#include "util/ScopeExit.hxx" -#include "util/StaticVector.hxx" -#include "util/StringCompare.hxx" -#include "Log.hxx" -#include "tag/Format.hxx" - -#ifdef __GNUC__ -#pragma GCC diagnostic push -/* oh no, libspa likes to cast away "const"! */ -#pragma GCC diagnostic ignored "-Wcast-qual" -#endif - -#include -#include -#include - -#include - -#ifdef __GNUC__ -#pragma GCC diagnostic pop -#endif - -#include -#include -#include -#include -#include - -static constexpr Domain pipewire_output_domain("pipewire_output"); - -class PipeWireOutput final : AudioOutput { - const char *const name; - - const char *const remote; - const char *const target; - - struct pw_thread_loop *thread_loop = nullptr; - struct pw_stream *stream; - - std::string error_message; - - std::byte pod_buffer[1024]; - struct spa_pod_builder pod_builder; - - std::size_t frame_size; - - /** - * This buffer passes PCM data from Play() to Process(). - */ - using RingBuffer = ::RingBuffer; - RingBuffer ring_buffer; - - uint32_t target_id = PW_ID_ANY; - - /** - * The current volume level (0.0 .. 1.0). - * - * This get initialized to -1 which means "unknown", so - * restore_volume will not attempt to override PipeWire's - * initial volume level. - */ - float volume = -1; - - PipeWireMixer *mixer = nullptr; - unsigned channels; - - /** - * The active sample format, needed for PcmSilence(). - */ - SampleFormat sample_format; - -#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE) - /** - * Is the "dsd" setting enabled, i.e. is DSD playback allowed? - */ - const bool enable_dsd; - - /** - * Are we currently playing in native DSD mode? - */ - bool use_dsd; - - /** - * Reverse the 8 bits in each DSD byte? This is necessary if - * PipeWire wants LSB (because MPD uses MSB internally). - */ - bool dsd_reverse_bits; - - /** - * Pack this many bytes of each frame together. MPD uses 1 - * internally, and if PipeWire wants more than one - * (e.g. because it uses DSD_U32), we need to reorder bytes. - */ - uint_least8_t dsd_interleave; -#endif - - /** - * Configuration setting for #PW_STREAM_FLAG_DONT_RECONNECT - * (negated). - */ - const bool reconnect_stream; - - bool disconnected; - - /** - * Shall the previously known volume be restored as soon as - * PW_STREAM_STATE_STREAMING is reached? This needs to be - * done each time after the pw_stream got created, thus this - * flag gets set by Open(). - */ - bool restore_volume; - - bool interrupted; - bool paused; - - /** - * Is the PipeWire stream active, i.e. has - * pw_stream_set_active() been called successfully? - */ - bool active; - - /** - * Has Drain() been called? This causes Process() to invoke - * pw_stream_flush() to drain PipeWire as soon as the - * #ring_buffer has been drained. - */ - bool drain_requested; - - bool drained; - - explicit PipeWireOutput(const ConfigBlock &block); - -public: - static AudioOutput *Create(EventLoop &, - const ConfigBlock &block) { - pw_init(nullptr, nullptr); - - return new PipeWireOutput(block); - } - - static constexpr struct pw_stream_events MakeStreamEvents() noexcept { - struct pw_stream_events events{}; - events.version = PW_VERSION_STREAM_EVENTS; - events.state_changed = StateChanged; - events.process = Process; - events.drained = Drained; - events.control_info = ControlInfo; - events.param_changed = ParamChanged; - return events; - } - - void SetVolume(float volume); - - void SetMixer(PipeWireMixer &_mixer) noexcept; - - void ClearMixer([[maybe_unused]] PipeWireMixer &old_mixer) noexcept { - assert(mixer == &old_mixer); - - mixer = nullptr; - } - -private: - void CheckThrowError() { - if (disconnected) { - if (error_message.empty()) - throw std::runtime_error("Disconnected from PipeWire"); - else - throw std::runtime_error(error_message); - } - } - - void StateChanged(enum pw_stream_state state, - const char *error) noexcept; - - static void StateChanged(void *data, - [[maybe_unused]] enum pw_stream_state old, - enum pw_stream_state state, - const char *error) noexcept { - auto &o = *(PipeWireOutput *)data; - o.StateChanged(state, error); - } - - void Process() noexcept; - - static void Process(void *data) noexcept { - auto &o = *(PipeWireOutput *)data; - o.Process(); - } - - void Drained() noexcept { - drained = true; - pw_thread_loop_signal(thread_loop, false); - } - - static void Drained(void *data) noexcept { - auto &o = *(PipeWireOutput *)data; - o.Drained(); - } - - void OnChannelVolumes(const struct pw_stream_control &control) noexcept { - if (control.n_values < 1) - return; - - float sum = std::accumulate(control.values, - control.values + control.n_values, - 0.0f); - volume = std::cbrt(sum / control.n_values); - - if (mixer != nullptr) - pipewire_mixer_on_change(*mixer, volume); - - pw_thread_loop_signal(thread_loop, false); - } - - void ControlInfo([[maybe_unused]] uint32_t id, - const struct pw_stream_control &control) noexcept { - switch (id) { - case SPA_PROP_channelVolumes: - OnChannelVolumes(control); - break; - } - } - - static void ControlInfo(void *data, uint32_t id, - const struct pw_stream_control *control) noexcept { - auto &o = *(PipeWireOutput *)data; - o.ControlInfo(id, *control); - } - -#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE) - void DsdFormatChanged(const struct spa_audio_info_dsd &dsd) noexcept; - void DsdFormatChanged(const struct spa_pod ¶m) noexcept; -#endif - - void ParamChanged(uint32_t id, const struct spa_pod *param) noexcept; - - static void ParamChanged(void *data, - uint32_t id, - const struct spa_pod *param) noexcept - { - if (id != SPA_PARAM_Format || param == nullptr) - return; - - auto &o = *(PipeWireOutput *)data; - o.ParamChanged(id, param); - } - - /* virtual methods from class AudioOutput */ - void Enable() override; - void Disable() noexcept override; - - void Open(AudioFormat &audio_format) override; - void Close() noexcept override; - - void Interrupt() noexcept override { - if (thread_loop == nullptr) - return; - - const PipeWire::ThreadLoopLock lock(thread_loop); - interrupted = true; - pw_thread_loop_signal(thread_loop, false); - } - - [[nodiscard]] std::chrono::steady_clock::duration Delay() const noexcept override; - std::size_t Play(std::span src) override; - - void Drain() override; - void Cancel() noexcept override; - bool Pause() noexcept override; - - void SendTag(const Tag &tag) override; -}; - -static constexpr auto stream_events = PipeWireOutput::MakeStreamEvents(); - -inline -PipeWireOutput::PipeWireOutput(const ConfigBlock &block) - :AudioOutput(FLAG_ENABLE_DISABLE), - name(block.GetBlockValue("name", "pipewire")), - remote(block.GetBlockValue("remote", nullptr)), - target(block.GetBlockValue("target", nullptr)), -#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE) - enable_dsd(block.GetBlockValue("dsd", false)), -#endif - reconnect_stream(block.GetBlockValue("reconnect_stream", true)) -{ - if (target != nullptr) { - if (StringIsEmpty(target)) - throw std::runtime_error("target must not be empty"); - - char *endptr; - const auto _target_id = strtoul(target, &endptr, 10); - if (endptr > target && *endptr == 0) - /* numeric value means target_id, not target - name */ - target_id = (uint32_t)_target_id; - } -} - -/** - * Throws on error. - * - * @param volume a volume level between 0.0 and 1.0 - */ -static void -SetVolume(struct pw_stream &stream, unsigned channels, float volume) -{ - float value[MAX_CHANNELS]; - std::fill_n(value, channels, volume * volume * volume); - - if (pw_stream_set_control(&stream, - SPA_PROP_channelVolumes, channels, value, - 0) != 0) - throw std::runtime_error("pw_stream_set_control() failed"); -} - -void -PipeWireOutput::SetVolume(float _volume) -{ - if (thread_loop == nullptr) { - /* the mixer is open (because it is a "global" mixer), - but Enable() on this output has not yet been - called */ - volume = _volume; - return; - } - - const PipeWire::ThreadLoopLock lock(thread_loop); - - if (stream != nullptr && !restore_volume) - ::SetVolume(*stream, channels, _volume); - - volume = _volume; -} - -void -PipeWireOutput::Enable() -{ - thread_loop = pw_thread_loop_new(name, nullptr); - if (thread_loop == nullptr) - throw MakeErrno("pw_thread_loop_new() failed"); - - pw_thread_loop_start(thread_loop); - - stream = nullptr; -} - -void -PipeWireOutput::Disable() noexcept -{ - pw_thread_loop_destroy(thread_loop); - thread_loop = nullptr; -} - -static constexpr enum spa_audio_format -ToPipeWireSampleFormat(SampleFormat format) noexcept -{ - switch (format) { - case SampleFormat::UNDEFINED: - break; - - case SampleFormat::S8: - return SPA_AUDIO_FORMAT_S8; - - case SampleFormat::S16: - return SPA_AUDIO_FORMAT_S16; - - case SampleFormat::S24_P32: - return SPA_AUDIO_FORMAT_S24_32; - - case SampleFormat::S32: - return SPA_AUDIO_FORMAT_S32; - - case SampleFormat::FLOAT: - return SPA_AUDIO_FORMAT_F32; - - case SampleFormat::DSD: - break; - } - - return SPA_AUDIO_FORMAT_UNKNOWN; -} - -static struct spa_audio_info_raw -ToPipeWireAudioFormat(AudioFormat &audio_format) noexcept -{ - struct spa_audio_info_raw raw{}; - - raw.format = ToPipeWireSampleFormat(audio_format.format); - if (raw.format == SPA_AUDIO_FORMAT_UNKNOWN) { - raw.format = SPA_AUDIO_FORMAT_S16; - audio_format.format = SampleFormat::S16; - } - - raw.flags = SPA_AUDIO_FLAG_NONE; - raw.rate = audio_format.sample_rate; - raw.channels = audio_format.channels; - - /* MPD uses the FLAC channel assignment - (https://xiph.org/flac/format.html) */ - switch (audio_format.channels) { - case 1: - raw.position[0] = SPA_AUDIO_CHANNEL_MONO; - break; - - case 2: - raw.position[0] = SPA_AUDIO_CHANNEL_FL; - raw.position[1] = SPA_AUDIO_CHANNEL_FR; - break; - - case 3: - raw.position[0] = SPA_AUDIO_CHANNEL_FL; - raw.position[1] = SPA_AUDIO_CHANNEL_FR; - raw.position[2] = SPA_AUDIO_CHANNEL_FC; - break; - - case 4: - raw.position[0] = SPA_AUDIO_CHANNEL_FL; - raw.position[1] = SPA_AUDIO_CHANNEL_FR; - raw.position[2] = SPA_AUDIO_CHANNEL_RL; - raw.position[3] = SPA_AUDIO_CHANNEL_RR; - break; - - case 5: - raw.position[0] = SPA_AUDIO_CHANNEL_FL; - raw.position[1] = SPA_AUDIO_CHANNEL_FR; - raw.position[2] = SPA_AUDIO_CHANNEL_FC; - raw.position[3] = SPA_AUDIO_CHANNEL_RL; - raw.position[4] = SPA_AUDIO_CHANNEL_RR; - break; - - case 6: - raw.position[0] = SPA_AUDIO_CHANNEL_FL; - raw.position[1] = SPA_AUDIO_CHANNEL_FR; - raw.position[2] = SPA_AUDIO_CHANNEL_FC; - raw.position[3] = SPA_AUDIO_CHANNEL_LFE; - raw.position[4] = SPA_AUDIO_CHANNEL_RL; - raw.position[5] = SPA_AUDIO_CHANNEL_RR; - break; - - case 7: - raw.position[0] = SPA_AUDIO_CHANNEL_FL; - raw.position[1] = SPA_AUDIO_CHANNEL_FR; - raw.position[2] = SPA_AUDIO_CHANNEL_FC; - raw.position[3] = SPA_AUDIO_CHANNEL_LFE; - raw.position[4] = SPA_AUDIO_CHANNEL_RC; - raw.position[5] = SPA_AUDIO_CHANNEL_SL; - raw.position[6] = SPA_AUDIO_CHANNEL_SR; - break; - - case 8: - raw.position[0] = SPA_AUDIO_CHANNEL_FL; - raw.position[1] = SPA_AUDIO_CHANNEL_FR; - raw.position[2] = SPA_AUDIO_CHANNEL_FC; - raw.position[3] = SPA_AUDIO_CHANNEL_LFE; - raw.position[4] = SPA_AUDIO_CHANNEL_RL; - raw.position[5] = SPA_AUDIO_CHANNEL_RR; - raw.position[6] = SPA_AUDIO_CHANNEL_SL; - raw.position[7] = SPA_AUDIO_CHANNEL_SR; - break; - - default: - raw.flags |= SPA_AUDIO_FLAG_UNPOSITIONED; - } - - return raw; -} - -void -PipeWireOutput::Open(AudioFormat &audio_format) -{ - error_message.clear(); - disconnected = false; - restore_volume = true; - - paused = false; - - /* stay inactive (PW_STREAM_FLAG_INACTIVE) until the ring - buffer has been filled */ - active = false; - - drain_requested = false; - drained = true; - - auto props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio", - PW_KEY_MEDIA_CATEGORY, "Playback", - PW_KEY_MEDIA_ROLE, "Music", - PW_KEY_APP_NAME, "Music Player Daemon", - PW_KEY_APP_ICON_NAME, "mpd", - nullptr); - - pw_properties_setf(props, PW_KEY_NODE_NAME, "mpd.%s", name); - - if (remote != nullptr && target_id == PW_ID_ANY) - pw_properties_setf(props, PW_KEY_REMOTE_NAME, "%s", remote); - - if (target != nullptr && target_id == PW_ID_ANY) - pw_properties_setf(props, - PW_KEY_TARGET_OBJECT, - "%s", target); - -#ifdef PW_KEY_NODE_RATE - /* ask PipeWire to change the graph sample rate to ours - (requires PipeWire 0.3.32) */ - pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u", - audio_format.sample_rate); -#endif - - const PipeWire::ThreadLoopLock lock(thread_loop); - - stream = pw_stream_new_simple(pw_thread_loop_get_loop(thread_loop), - "mpd", - props, - &stream_events, - this); - if (stream == nullptr) - throw MakeErrno("pw_stream_new_simple() failed"); - -#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE) - /* this needs to be determined before ToPipeWireAudioFormat() - switches DSD to S16 */ - use_dsd = enable_dsd && - audio_format.format == SampleFormat::DSD; - dsd_reverse_bits = false; - dsd_interleave = 0; -#endif - - auto raw = ToPipeWireAudioFormat(audio_format); - -#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE) - if (use_dsd) - /* restore the DSD format which was overwritten by - ToPipeWireAudioFormat(), because DSD is a special - case in PipeWire */ - audio_format.format = SampleFormat::DSD; -#endif - - frame_size = audio_format.GetFrameSize(); - sample_format = audio_format.format; - channels = audio_format.channels; - interrupted = false; - - /* allocate a ring buffer of 0.5 seconds */ - ring_buffer = RingBuffer{frame_size * (audio_format.sample_rate / 2)}; - - const struct spa_pod *params[1]; - - pod_builder = {}; - pod_builder.data = pod_buffer; - pod_builder.size = sizeof(pod_buffer); - -#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE) - struct spa_audio_info_dsd dsd; - if (use_dsd) { - dsd = {}; - - /* copy all relevant settings from the - ToPipeWireAudioFormat() return value */ - dsd.flags = raw.flags; - dsd.rate = raw.rate; - dsd.channels = raw.channels; - if ((dsd.flags & SPA_AUDIO_FLAG_UNPOSITIONED) == 0) - std::copy_n(raw.position, dsd.channels, dsd.position); - - params[0] = spa_format_audio_dsd_build(&pod_builder, - SPA_PARAM_EnumFormat, - &dsd); - } else -#endif - params[0] = spa_format_audio_raw_build(&pod_builder, - SPA_PARAM_EnumFormat, - &raw); - - unsigned stream_flags = PW_STREAM_FLAG_AUTOCONNECT | - PW_STREAM_FLAG_INACTIVE | - PW_STREAM_FLAG_MAP_BUFFERS | - PW_STREAM_FLAG_RT_PROCESS; - - if (!reconnect_stream) - stream_flags |= PW_STREAM_FLAG_DONT_RECONNECT; - - int error = - pw_stream_connect(stream, - PW_DIRECTION_OUTPUT, - target_id, - static_cast(stream_flags), - params, 1); - if (error < 0) - throw PipeWire::MakeError(error, "Failed to connect stream"); -} - -void -PipeWireOutput::Close() noexcept -{ - { - const PipeWire::ThreadLoopLock lock(thread_loop); - pw_stream_destroy(stream); - stream = nullptr; - } - - ring_buffer = {}; -} - -inline void -PipeWireOutput::StateChanged(enum pw_stream_state state, - const char *error) noexcept -{ - const bool was_disconnected = disconnected; - disconnected = state == PW_STREAM_STATE_ERROR || - state == PW_STREAM_STATE_UNCONNECTED; - if (!was_disconnected && disconnected) { - if (error != nullptr) - error_message = error; - - pw_thread_loop_signal(thread_loop, false); - } - -} - -#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE) - -inline void -PipeWireOutput::DsdFormatChanged(const struct spa_audio_info_dsd &dsd) noexcept -{ - /* MPD uses MSB internally, which means if PipeWire asks LSB - from us, we need to reverse the bits in each DSD byte */ - dsd_reverse_bits = dsd.bitorder == SPA_PARAM_BITORDER_lsb; - - dsd_interleave = dsd.interleave; -} - -inline void -PipeWireOutput::DsdFormatChanged(const struct spa_pod ¶m) noexcept -{ - uint32_t media_type, media_subtype; - struct spa_audio_info_dsd dsd; - - if (spa_format_parse(¶m, &media_type, &media_subtype) >= 0 && - media_type == SPA_MEDIA_TYPE_audio && - media_subtype == SPA_MEDIA_SUBTYPE_dsd && - spa_format_audio_dsd_parse(¶m, &dsd) >= 0) - DsdFormatChanged(dsd); -} - -#endif - -inline void -PipeWireOutput::ParamChanged([[maybe_unused]] uint32_t id, - [[maybe_unused]] const struct spa_pod *param) noexcept -{ - if (restore_volume) { - restore_volume = false; - - if (volume >= 0) { - try { - ::SetVolume(*stream, channels, volume); - } catch (...) { - FmtError(pipewire_output_domain, - "Failed to restore volume: {}", - std::current_exception()); - } - } - } - -#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE) - if (use_dsd && id == SPA_PARAM_Format && param != nullptr) - DsdFormatChanged(*param); -#endif -} - -#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE) - -static void -Interleave(std::byte *data, std::byte *end, - std::size_t channels, std::size_t interleave) noexcept -{ - assert(channels > 1); - assert(channels <= MAX_CHANNELS); - - constexpr std::size_t MAX_INTERLEAVE = 8; - assert(interleave > 1); - assert(interleave <= MAX_INTERLEAVE); - - std::array buffer; - std::size_t buffer_size = channels * interleave; - - while (data < end) { - std::copy_n(data, buffer_size, buffer.data()); - - const std::byte *src0 = buffer.data(); - for (std::size_t channel = 0; channel < channels; - ++channel, ++src0) { - const std::byte *src = src0; - for (std::size_t i = 0; i < interleave; - ++i, src += channels) - *data++ = *src; - } - } -} - -static void -BitReverse(std::byte *data, std::size_t n) noexcept -{ - while (n-- > 0) - *data = BitReverse(*data); -} - -static void -PostProcessDsd(std::byte *data, struct spa_chunk &chunk, unsigned channels, - bool reverse_bits, unsigned interleave) noexcept -{ - assert(chunk.size % channels == 0); - - if (interleave > 1 && channels > 1) { - assert(chunk.size % (channels * interleave) == 0); - - Interleave(data, data + chunk.size, channels, interleave); - chunk.stride *= interleave; - } - - if (reverse_bits) - BitReverse(data, chunk.size); -} - -#endif - -inline void -PipeWireOutput::Process() noexcept -{ - auto *b = pw_stream_dequeue_buffer(stream); - if (b == nullptr) { - pw_log_warn("out of buffers: %m"); - return; - } - - auto &buffer = *b->buffer; - auto &d = buffer.datas[0]; - - auto dest = (std::byte *)d.data; - if (dest == nullptr) - return; - - std::size_t chunk_size = frame_size; - -#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE) - if (use_dsd && dsd_interleave > 1) { - /* make sure we don't get partial interleave frames */ - chunk_size *= dsd_interleave; - } -#endif - - size_t nbytes = ring_buffer.ReadFramesTo({dest, d.maxsize}, chunk_size); - assert(nbytes % chunk_size == 0); - if (nbytes == 0) { - if (drain_requested) { - pw_stream_flush(stream, true); - return; - } - - /* buffer underrun: generate some silence */ - std::size_t max_chunks = d.maxsize / chunk_size; - nbytes = max_chunks * chunk_size; - PcmSilence({dest, nbytes}, sample_format); - - LogWarning(pipewire_output_domain, "Decoder is too slow; playing silence to avoid xrun"); - } - - auto &chunk = *d.chunk; - chunk.offset = 0; - chunk.stride = frame_size; - chunk.size = nbytes; - -#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE) - if (use_dsd) - PostProcessDsd(dest, chunk, channels, - dsd_reverse_bits, dsd_interleave); -#endif - - pw_stream_queue_buffer(stream, b); - - pw_thread_loop_signal(thread_loop, false); -} - -std::chrono::steady_clock::duration -PipeWireOutput::Delay() const noexcept -{ - const PipeWire::ThreadLoopLock lock(thread_loop); - - auto result = std::chrono::steady_clock::duration::zero(); - if (paused) - /* idle while paused */ - result = std::chrono::seconds(1); - - return result; -} - -std::size_t -PipeWireOutput::Play(std::span src) -{ - const PipeWire::ThreadLoopLock lock(thread_loop); - - paused = false; - - while (true) { - CheckThrowError(); - - std::size_t bytes_written = - ring_buffer.WriteFrom(src); - if (bytes_written > 0) { - drained = false; - return bytes_written; - } - - if (!active) { - /* now that the ring_buffer is full, there is - enough data for Process(), so let's resume - the stream now */ - active = true; - pw_stream_set_active(stream, true); - } - - if (interrupted) - throw AudioOutputInterrupted{}; - - pw_thread_loop_wait(thread_loop); - } -} - -void -PipeWireOutput::Drain() -{ - const PipeWire::ThreadLoopLock lock(thread_loop); - - if (drained) - return; - - if (!active) { - /* there is data in the ring_buffer, but the stream is - not yet active; activate it now to ensure it is - played before this method returns */ - active = true; - pw_stream_set_active(stream, true); - } - - drain_requested = true; - AtScopeExit(this) { drain_requested = false; }; - - while (!drained && !interrupted) { - CheckThrowError(); - pw_thread_loop_wait(thread_loop); - } -} - -void -PipeWireOutput::Cancel() noexcept -{ - const PipeWire::ThreadLoopLock lock(thread_loop); - interrupted = false; - - if (drained) - return; - - /* clear MPD's ring buffer */ - ring_buffer.Clear(); - - /* clear libpipewire's buffer */ - pw_stream_flush(stream, false); - drained = true; - - /* pause the PipeWire stream so libpipewire ceases invoking - the "process" callback (we have no data until our Play() - method gets called again); the stream will be resume by - Play() after the ring_buffer has been refilled */ - if (active) { - active = false; - pw_stream_set_active(stream, false); - } -} - -bool -PipeWireOutput::Pause() noexcept -{ - const PipeWire::ThreadLoopLock lock(thread_loop); - interrupted = false; - - paused = true; - - if (active) { - active = false; - pw_stream_set_active(stream, false); - } - - return true; -} - -inline void -PipeWireOutput::SetMixer(PipeWireMixer &_mixer) noexcept -{ - assert(mixer == nullptr); - - mixer = &_mixer; - - // TODO: Check if context and stream is ready and trigger a volume update... -} - -void -PipeWireOutput::SendTag(const Tag &tag) -{ - CheckThrowError(); - - static constexpr struct { - TagType mpd; - const char *pipewire; - } tag_map[] = { - { TAG_ARTIST, PW_KEY_MEDIA_ARTIST }, - { TAG_TITLE, PW_KEY_MEDIA_TITLE }, - { TAG_DATE, PW_KEY_MEDIA_DATE }, - { TAG_COMMENT, PW_KEY_MEDIA_COMMENT }, - }; - - StaticVector items; - - char *medianame = FormatTag(tag, "%artist% - %title%"); - AtScopeExit(medianame) { free(medianame); }; - - items.push_back(SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_NAME, medianame)); - - for (const auto &i : tag_map) - if (const char *value = tag.GetValue(i.mpd)) - items.push_back(SPA_DICT_ITEM_INIT(i.pipewire, value)); - - struct spa_dict dict = SPA_DICT_INIT(items.data(), (uint32_t)items.size()); - - const PipeWire::ThreadLoopLock lock(thread_loop); - - auto rc = pw_stream_update_properties(stream, &dict); - if (rc < 0) - LogWarning(pipewire_output_domain, "Error updating properties"); -} - -void -pipewire_output_set_mixer(PipeWireOutput &po, PipeWireMixer &pm) noexcept -{ - po.SetMixer(pm); -} - -void -pipewire_output_clear_mixer(PipeWireOutput &po, PipeWireMixer &pm) noexcept -{ - po.ClearMixer(pm); -} - -const struct AudioOutputPlugin pipewire_output_plugin = { - "pipewire", - nullptr, - &PipeWireOutput::Create, - &pipewire_mixer_plugin, -}; - -void -pipewire_output_set_volume(PipeWireOutput &output, float volume) -{ - output.SetVolume(volume); -} diff --git a/src/output/plugins/PipeWireOutputPlugin.hxx b/src/output/plugins/PipeWireOutputPlugin.hxx deleted file mode 100644 index 99e487c..0000000 --- a/src/output/plugins/PipeWireOutputPlugin.hxx +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#ifndef MPD_PIPEWIRE_OUTPUT_PLUGIN_HXX -#define MPD_PIPEWIRE_OUTPUT_PLUGIN_HXX - -class PipeWireOutput; -class PipeWireMixer; - -extern const struct AudioOutputPlugin pipewire_output_plugin; - -void -pipewire_output_set_mixer(PipeWireOutput &po, PipeWireMixer &pm) noexcept; - -void -pipewire_output_clear_mixer(PipeWireOutput &po, PipeWireMixer &pm) noexcept; - -void -pipewire_output_set_volume(PipeWireOutput &output, float volume); - -#endif diff --git a/src/output/plugins/PulseOutputPlugin.cxx b/src/output/plugins/PulseOutputPlugin.cxx deleted file mode 100644 index 67e9058..0000000 --- a/src/output/plugins/PulseOutputPlugin.cxx +++ /dev/null @@ -1,916 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "PulseOutputPlugin.hxx" -#include "lib/pulse/Error.hxx" -#include "lib/pulse/LogError.hxx" -#include "lib/pulse/LockGuard.hxx" -#include "../OutputAPI.hxx" -#include "../Error.hxx" -#include "mixer/plugins/PulseMixerPlugin.hxx" -#include "util/ScopeExit.hxx" - -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include - -#ifdef _WIN32 -#include -#endif - -#define MPD_PULSE_NAME "Music Player Daemon" - -class PulseOutput final : AudioOutput { - const char *name; - const char *server; - const char *sink; - const char *const media_role; - - PulseMixer *mixer = nullptr; - - struct pa_threaded_mainloop *mainloop = nullptr; - struct pa_context *context; - struct pa_stream *stream = nullptr; - - size_t writable; - - /** - * Was Interrupt() called? This will unblock Play(). It will - * be reset by Cancel() and Pause(), as documented by the - * #AudioOutput interface. - * - * Only initialized while the output is open. - */ - bool interrupted; - - explicit PulseOutput(const ConfigBlock &block); - -public: - void SetMixer(PulseMixer &_mixer); - - void ClearMixer([[maybe_unused]] PulseMixer &old_mixer) { - assert(mixer == &old_mixer); - - mixer = nullptr; - } - - void SetVolume(const pa_cvolume &volume); - - struct pa_threaded_mainloop *GetMainloop() { - return mainloop; - } - - void OnContextStateChanged(pa_context_state_t new_state); - void OnServerLayoutChanged(pa_subscription_event_type_t t, - uint32_t idx); - void OnStreamSuspended(pa_stream *_stream); - void OnStreamStateChanged(pa_stream *_stream, - pa_stream_state_t new_state); - void OnStreamWrite(size_t nbytes); - - void OnStreamSuccess() { - Signal(); - } - - static bool TestDefaultDevice(); - - static AudioOutput *Create(EventLoop &, - const ConfigBlock &block) { - return new PulseOutput(block); - } - - void Enable() override; - void Disable() noexcept override; - - void Open(AudioFormat &audio_format) override; - void Close() noexcept override; - - void Interrupt() noexcept override; - - [[nodiscard]] std::chrono::steady_clock::duration Delay() const noexcept override; - std::size_t Play(std::span src) override; - void Drain() override; - void Cancel() noexcept override; - bool Pause() override; - -private: - /** - * Attempt to connect asynchronously to the PulseAudio server. - * - * Throws on error. - */ - void Connect(); - - /** - * Create, set up and connect a context. - * - * Caller must lock the main loop. - * - * Throws on error. - */ - void SetupContext(); - - /** - * Frees and clears the context. - * - * Caller must lock the main loop. - */ - void DeleteContext(); - - void Signal() { - pa_threaded_mainloop_signal(mainloop, 0); - } - - /** - * 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. - * - * Throws on error. - */ - void WaitConnection(); - - /** - * Create, set up and connect a context. - * - * Caller must lock the main loop. - * - * Throws on error. - */ - void SetupStream(const pa_sample_spec &ss); - - /** - * Frees and clears the stream. - */ - void DeleteStream(); - - /** - * Check if the stream is (already) connected, and waits if - * not. The mainloop must be locked before calling this - * function. - * - * Throws on error. - */ - void WaitStream(); - - /** - * Sets cork mode on the stream. - * - * Throws on error. - */ - void StreamPause(bool pause); -}; - -PulseOutput::PulseOutput(const ConfigBlock &block) - :AudioOutput(FLAG_ENABLE_DISABLE|FLAG_PAUSE), - name(block.GetBlockValue("name", "mpd_pulse")), - server(block.GetBlockValue("server")), - sink(block.GetBlockValue("sink")), - media_role(block.GetBlockValue("media_role")) -{ -#ifdef _WIN32 - SetEnvironmentVariableA("PULSE_PROP_media.role", "music"); - SetEnvironmentVariableA("PULSE_PROP_application.icon_name", "mpd"); -#else - setenv("PULSE_PROP_media.role", "music", true); - setenv("PULSE_PROP_application.icon_name", "mpd", true); -#endif -} - -struct pa_threaded_mainloop * -pulse_output_get_mainloop(PulseOutput &po) -{ - return po.GetMainloop(); -} - -inline void -PulseOutput::SetMixer(PulseMixer &_mixer) -{ - assert(mixer == nullptr); - - mixer = &_mixer; - - if (mainloop == nullptr) - return; - - Pulse::LockGuard lock(mainloop); - - if (context != nullptr && - pa_context_get_state(context) == PA_CONTEXT_READY) { - pulse_mixer_on_connect(_mixer, context); - - if (stream != nullptr && - pa_stream_get_state(stream) == PA_STREAM_READY) - pulse_mixer_on_change(_mixer, context, stream); - } -} - -void -pulse_output_set_mixer(PulseOutput &po, PulseMixer &pm) -{ - po.SetMixer(pm); -} - -void -pulse_output_clear_mixer(PulseOutput &po, PulseMixer &pm) -{ - po.ClearMixer(pm); -} - -inline void -PulseOutput::SetVolume(const pa_cvolume &volume) -{ - if (context == nullptr || stream == nullptr || - pa_stream_get_state(stream) != PA_STREAM_READY) - throw std::runtime_error("disconnected"); - - pa_operation *o = - pa_context_set_sink_input_volume(context, - pa_stream_get_index(stream), - &volume, nullptr, nullptr); - if (o == nullptr) - throw std::runtime_error("failed to set PulseAudio volume"); - - pa_operation_unref(o); -} - -void -pulse_output_set_volume(PulseOutput &po, const pa_cvolume *volume) -{ - return po.SetVolume(*volume); -} - -/** - * \brief waits for a pulseaudio operation to finish, frees it and - * unlocks the mainloop - * \param operation the operation to wait for - * \return true if operation has finished normally (DONE state), - * false otherwise - */ -static bool -pulse_wait_for_operation(struct pa_threaded_mainloop *mainloop, - struct pa_operation *operation) -{ - assert(mainloop != nullptr); - assert(operation != nullptr); - - pa_operation_state_t state; - while ((state = pa_operation_get_state(operation)) - == PA_OPERATION_RUNNING) - pa_threaded_mainloop_wait(mainloop); - - pa_operation_unref(operation); - - return state == PA_OPERATION_DONE; -} - -/** - * Callback function for stream operation. It just sends a signal to - * the caller thread, to wake pulse_wait_for_operation() up. - */ -static void -pulse_output_stream_success_cb([[maybe_unused]] pa_stream *s, - [[maybe_unused]] int success, void *userdata) -{ - PulseOutput &po = *(PulseOutput *)userdata; - - po.OnStreamSuccess(); -} - -inline void -PulseOutput::OnContextStateChanged(pa_context_state_t new_state) -{ - switch (new_state) { - case PA_CONTEXT_READY: - if (mixer != nullptr) - pulse_mixer_on_connect(*mixer, context); - - Signal(); - break; - - case PA_CONTEXT_TERMINATED: - case PA_CONTEXT_FAILED: - if (mixer != nullptr) - pulse_mixer_on_disconnect(*mixer); - - /* the caller thread might be waiting for these - states */ - Signal(); - break; - - case PA_CONTEXT_UNCONNECTED: - case PA_CONTEXT_CONNECTING: - case PA_CONTEXT_AUTHORIZING: - case PA_CONTEXT_SETTING_NAME: - break; - } -} - -static void -pulse_output_context_state_cb(struct pa_context *context, void *userdata) -{ - PulseOutput &po = *(PulseOutput *)userdata; - - po.OnContextStateChanged(pa_context_get_state(context)); -} - -inline void -PulseOutput::OnServerLayoutChanged(pa_subscription_event_type_t t, - uint32_t idx) -{ - auto facility = - pa_subscription_event_type_t(t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK); - auto type = - pa_subscription_event_type_t(t & PA_SUBSCRIPTION_EVENT_TYPE_MASK); - - if (mixer != nullptr && - facility == PA_SUBSCRIPTION_EVENT_SINK_INPUT && - stream != nullptr && - pa_stream_get_state(stream) == PA_STREAM_READY && - idx == pa_stream_get_index(stream) && - (type == PA_SUBSCRIPTION_EVENT_NEW || - type == PA_SUBSCRIPTION_EVENT_CHANGE)) - pulse_mixer_on_change(*mixer, context, stream); -} - -static void -pulse_output_subscribe_cb([[maybe_unused]] pa_context *context, - pa_subscription_event_type_t t, - uint32_t idx, void *userdata) -{ - PulseOutput &po = *(PulseOutput *)userdata; - - po.OnServerLayoutChanged(t, idx); -} - -inline void -PulseOutput::Connect() -{ - assert(context != nullptr); - - if (pa_context_connect(context, server, - (pa_context_flags_t)0, nullptr) < 0) - throw Pulse::MakeError(context, - "pa_context_connect() has failed"); -} - -void -PulseOutput::DeleteStream() -{ - assert(stream != nullptr); - - pa_stream_set_suspended_callback(stream, nullptr, nullptr); - - pa_stream_set_state_callback(stream, nullptr, nullptr); - pa_stream_set_write_callback(stream, nullptr, nullptr); - - pa_stream_disconnect(stream); - pa_stream_unref(stream); - stream = nullptr; -} - -void -PulseOutput::DeleteContext() -{ - assert(context != nullptr); - - pa_context_set_state_callback(context, nullptr, nullptr); - pa_context_set_subscribe_callback(context, nullptr, nullptr); - - pa_context_disconnect(context); - pa_context_unref(context); - context = nullptr; -} - -void -PulseOutput::SetupContext() -{ - assert(mainloop != nullptr); - - pa_proplist *proplist = pa_proplist_new(); - if (media_role) - pa_proplist_sets(proplist, PA_PROP_MEDIA_ROLE, media_role); - - context = pa_context_new_with_proplist(pa_threaded_mainloop_get_api(mainloop), - MPD_PULSE_NAME, - proplist); - - pa_proplist_free(proplist); - - if (context == nullptr) - throw std::runtime_error("pa_context_new() has failed"); - - pa_context_set_state_callback(context, - pulse_output_context_state_cb, this); - pa_context_set_subscribe_callback(context, - pulse_output_subscribe_cb, this); - - try { - Connect(); - } catch (...) { - DeleteContext(); - throw; - } -} - -void -PulseOutput::Enable() -{ - assert(mainloop == nullptr); - - /* create the libpulse mainloop and start the thread */ - - mainloop = pa_threaded_mainloop_new(); - if (mainloop == nullptr) - throw std::runtime_error("pa_threaded_mainloop_new() has failed"); - - pa_threaded_mainloop_lock(mainloop); - - if (pa_threaded_mainloop_start(mainloop) < 0) { - pa_threaded_mainloop_unlock(mainloop); - pa_threaded_mainloop_free(mainloop); - mainloop = nullptr; - - throw std::runtime_error("pa_threaded_mainloop_start() has failed"); - } - - /* create the libpulse context and connect it */ - - try { - SetupContext(); - } catch (...) { - pa_threaded_mainloop_unlock(mainloop); - pa_threaded_mainloop_stop(mainloop); - pa_threaded_mainloop_free(mainloop); - mainloop = nullptr; - throw; - } - - pa_threaded_mainloop_unlock(mainloop); -} - -void -PulseOutput::Disable() noexcept -{ - assert(mainloop != nullptr); - - pa_threaded_mainloop_stop(mainloop); - if (context != nullptr) - DeleteContext(); - pa_threaded_mainloop_free(mainloop); - mainloop = nullptr; -} - -void -PulseOutput::WaitConnection() -{ - assert(mainloop != nullptr); - - pa_context_state_t state; - - if (context == nullptr) - SetupContext(); - - while (true) { - state = pa_context_get_state(context); - switch (state) { - case PA_CONTEXT_READY: - /* nothing to do */ - return; - - case PA_CONTEXT_UNCONNECTED: - case PA_CONTEXT_TERMINATED: - case PA_CONTEXT_FAILED: - /* failure */ - { - auto e = Pulse::MakeError(context, - "failed to connect"); - DeleteContext(); - throw e; - } - - case PA_CONTEXT_CONNECTING: - case PA_CONTEXT_AUTHORIZING: - case PA_CONTEXT_SETTING_NAME: - /* wait some more */ - pa_threaded_mainloop_wait(mainloop); - break; - } - } -} - -inline void -PulseOutput::OnStreamSuspended([[maybe_unused]] pa_stream *_stream) -{ - assert(_stream == stream || stream == nullptr); - assert(mainloop != nullptr); - - /* wake up the main loop to break out of the loop in - pulse_output_play() */ - Signal(); -} - -static void -pulse_output_stream_suspended_cb(pa_stream *stream, void *userdata) -{ - PulseOutput &po = *(PulseOutput *)userdata; - - po.OnStreamSuspended(stream); -} - -inline void -PulseOutput::OnStreamStateChanged(pa_stream *_stream, - pa_stream_state_t new_state) -{ - assert(_stream == stream || stream == nullptr); - assert(mainloop != nullptr); - assert(context != nullptr); - - switch (new_state) { - case PA_STREAM_READY: - if (mixer != nullptr) - pulse_mixer_on_change(*mixer, context, _stream); - - Signal(); - break; - - case PA_STREAM_FAILED: - case PA_STREAM_TERMINATED: - if (mixer != nullptr) - pulse_mixer_on_disconnect(*mixer); - - Signal(); - break; - - case PA_STREAM_UNCONNECTED: - case PA_STREAM_CREATING: - break; - } -} - -static void -pulse_output_stream_state_cb(pa_stream *stream, void *userdata) -{ - PulseOutput &po = *(PulseOutput *)userdata; - - return po.OnStreamStateChanged(stream, pa_stream_get_state(stream)); -} - -inline void -PulseOutput::OnStreamWrite(size_t nbytes) -{ - assert(mainloop != nullptr); - - writable = nbytes; - Signal(); -} - -static void -pulse_output_stream_write_cb([[maybe_unused]] pa_stream *stream, size_t nbytes, - void *userdata) -{ - PulseOutput &po = *(PulseOutput *)userdata; - - return po.OnStreamWrite(nbytes); -} - -inline void -PulseOutput::SetupStream(const pa_sample_spec &ss) -{ - assert(context != nullptr); - - /* WAVE-EX is been adopted as the speaker map for most media files */ - pa_channel_map chan_map; - pa_channel_map_init_extend(&chan_map, ss.channels, - PA_CHANNEL_MAP_WAVEEX); - stream = pa_stream_new(context, name, &ss, &chan_map); - if (stream == nullptr) - throw Pulse::MakeError(context, - "pa_stream_new() has failed"); - - pa_stream_set_suspended_callback(stream, - pulse_output_stream_suspended_cb, - this); - - pa_stream_set_state_callback(stream, - pulse_output_stream_state_cb, this); - pa_stream_set_write_callback(stream, - pulse_output_stream_write_cb, this); -} - -void -PulseOutput::Open(AudioFormat &audio_format) -{ - assert(mainloop != nullptr); - - Pulse::LockGuard lock(mainloop); - - if (context != nullptr) { - switch (pa_context_get_state(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 */ - DeleteContext(); - break; - - case PA_CONTEXT_READY: - case PA_CONTEXT_CONNECTING: - case PA_CONTEXT_AUTHORIZING: - case PA_CONTEXT_SETTING_NAME: - break; - } - } - - WaitConnection(); - - /* Use the sample formats that our version of PulseAudio and MPD - have in common, otherwise force MPD to send 16 bit */ - - pa_sample_spec ss; - - switch (audio_format.format) { - case SampleFormat::FLOAT: - ss.format = PA_SAMPLE_FLOAT32NE; - break; - case SampleFormat::S32: - ss.format = PA_SAMPLE_S32NE; - break; - case SampleFormat::S24_P32: - ss.format = PA_SAMPLE_S24_32NE; - break; - case SampleFormat::S16: - ss.format = PA_SAMPLE_S16NE; - break; - default: - audio_format.format = SampleFormat::S16; - ss.format = PA_SAMPLE_S16NE; - break; - } - - ss.rate = std::min(audio_format.sample_rate, PA_RATE_MAX); - ss.channels = audio_format.channels; - - /* create a stream .. */ - - SetupStream(ss); - - /* .. and connect it (asynchronously) */ - - if (pa_stream_connect_playback(stream, sink, - nullptr, pa_stream_flags_t(0), - nullptr, nullptr) < 0) { - DeleteStream(); - - throw Pulse::MakeError(context, - "pa_stream_connect_playback() has failed"); - } - - interrupted = false; -} - -void -PulseOutput::Close() noexcept -{ - assert(mainloop != nullptr); - - Pulse::LockGuard lock(mainloop); - - DeleteStream(); - - if (context != nullptr && - pa_context_get_state(context) != PA_CONTEXT_READY) - DeleteContext(); -} - -void -PulseOutput::Interrupt() noexcept -{ - if (mainloop == nullptr) - return; - - const Pulse::LockGuard lock(mainloop); - - /* the "interrupted" flag will prevent Play() from blocking, - and will instead throw AudioOutputInterrupted */ - interrupted = true; - - Signal(); -} - -void -PulseOutput::WaitStream() -{ - while (true) { - switch (pa_stream_get_state(stream)) { - case PA_STREAM_READY: - return; - - case PA_STREAM_FAILED: - case PA_STREAM_TERMINATED: - case PA_STREAM_UNCONNECTED: - throw Pulse::MakeError(context, - "failed to connect the stream"); - - case PA_STREAM_CREATING: - if (interrupted) - throw AudioOutputInterrupted{}; - - pa_threaded_mainloop_wait(mainloop); - break; - } - } -} - -void -PulseOutput::StreamPause(bool _pause) -{ - assert(mainloop != nullptr); - assert(context != nullptr); - assert(stream != nullptr); - - pa_operation *o = pa_stream_cork(stream, _pause, - pulse_output_stream_success_cb, this); - if (o == nullptr) - throw Pulse::MakeError(context, - "pa_stream_cork() has failed"); - - if (!pulse_wait_for_operation(mainloop, o)) - throw Pulse::MakeError(context, - "pa_stream_cork() has failed"); -} - -std::chrono::steady_clock::duration -PulseOutput::Delay() const noexcept -{ - Pulse::LockGuard lock(mainloop); - - auto result = std::chrono::steady_clock::duration::zero(); - if (pa_stream_is_corked(stream) && - pa_stream_get_state(stream) == PA_STREAM_READY) - /* idle while paused */ - result = std::chrono::steady_clock::duration::max(); - - return result; -} - -std::size_t -PulseOutput::Play(std::span src) -{ - assert(mainloop != nullptr); - assert(stream != nullptr); - - Pulse::LockGuard lock(mainloop); - - /* check if the stream is (already) connected */ - - WaitStream(); - - assert(context != nullptr); - - /* unpause if previously paused */ - - if (pa_stream_is_corked(stream)) - StreamPause(false); - - /* wait until the server allows us to write */ - - while (writable == 0) { - if (pa_stream_is_suspended(stream)) - throw std::runtime_error("suspended"); - - if (interrupted) - throw AudioOutputInterrupted{}; - - pa_threaded_mainloop_wait(mainloop); - - if (pa_stream_get_state(stream) != PA_STREAM_READY) - throw std::runtime_error("disconnected"); - } - - /* now write */ - - if (src.size() > writable) - /* don't send more than possible */ - src = src.first(writable); - - writable -= src.size(); - - int result = pa_stream_write(stream, src.data(), src.size(), nullptr, - 0, PA_SEEK_RELATIVE); - if (result < 0) - throw Pulse::MakeError(context, "pa_stream_write() failed"); - - return src.size(); -} - -void -PulseOutput::Drain() -{ - Pulse::LockGuard lock(mainloop); - - if (pa_stream_get_state(stream) != PA_STREAM_READY || - pa_stream_is_suspended(stream) || - pa_stream_is_corked(stream)) - return; - - pa_operation *o = - pa_stream_drain(stream, - pulse_output_stream_success_cb, this); - if (o == nullptr) - throw Pulse::MakeError(context, "pa_stream_drain() failed"); - - pulse_wait_for_operation(mainloop, o); -} - -void -PulseOutput::Cancel() noexcept -{ - assert(mainloop != nullptr); - assert(stream != nullptr); - - Pulse::LockGuard lock(mainloop); - interrupted = false; - - if (pa_stream_get_state(stream) != PA_STREAM_READY) { - /* no need to flush when the stream isn't connected - yet */ - return; - } - - assert(context != nullptr); - - pa_operation *o = pa_stream_flush(stream, - pulse_output_stream_success_cb, - this); - if (o == nullptr) { - LogPulseError(context, "pa_stream_flush() has failed"); - return; - } - - pulse_wait_for_operation(mainloop, o); -} - -bool -PulseOutput::Pause() -{ - assert(mainloop != nullptr); - assert(stream != nullptr); - - Pulse::LockGuard lock(mainloop); - - interrupted = false; - - /* check if the stream is (already/still) connected */ - - WaitStream(); - - assert(context != nullptr); - - /* cork the stream */ - - if (!pa_stream_is_corked(stream)) - StreamPause(true); - - return true; -} - -inline bool -PulseOutput::TestDefaultDevice() -try { - const ConfigBlock empty; - PulseOutput po(empty); - po.Enable(); - AtScopeExit(&po) { po.Disable(); }; - po.WaitConnection(); - - return true; -} catch (...) { - return false; -} - -static bool -pulse_output_test_default_device() -{ - return PulseOutput::TestDefaultDevice(); -} - -constexpr struct AudioOutputPlugin pulse_output_plugin = { - "pulse", - pulse_output_test_default_device, - PulseOutput::Create, - &pulse_mixer_plugin, -}; diff --git a/src/output/plugins/PulseOutputPlugin.hxx b/src/output/plugins/PulseOutputPlugin.hxx deleted file mode 100644 index 7de7d22..0000000 --- a/src/output/plugins/PulseOutputPlugin.hxx +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#ifndef MPD_PULSE_OUTPUT_PLUGIN_HXX -#define MPD_PULSE_OUTPUT_PLUGIN_HXX - -class PulseOutput; -class PulseMixer; -struct pa_cvolume; - -extern const struct AudioOutputPlugin pulse_output_plugin; - -struct pa_threaded_mainloop * -pulse_output_get_mainloop(PulseOutput &po); - -void -pulse_output_set_mixer(PulseOutput &po, PulseMixer &pm); - -void -pulse_output_clear_mixer(PulseOutput &po, PulseMixer &pm); - -void -pulse_output_set_volume(PulseOutput &po, const pa_cvolume *volume); - -#endif diff --git a/src/output/plugins/RecorderOutputPlugin.cxx b/src/output/plugins/RecorderOutputPlugin.cxx deleted file mode 100644 index 7fc0862..0000000 --- a/src/output/plugins/RecorderOutputPlugin.cxx +++ /dev/null @@ -1,332 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "RecorderOutputPlugin.hxx" -#include "../OutputAPI.hxx" -#include "lib/fmt/PathFormatter.hxx" -#include "tag/Format.hxx" -#include "encoder/ToOutputStream.hxx" -#include "encoder/EncoderInterface.hxx" -#include "encoder/Configured.hxx" -#include "config/Path.hxx" -#include "Log.hxx" -#include "fs/AllocatedPath.hxx" -#include "io/FileOutputStream.hxx" -#include "util/Domain.hxx" -#include "util/ScopeExit.hxx" - -#include -#include -#include - -#include - -static constexpr Domain recorder_domain("recorder"); - -class RecorderOutput final : AudioOutput { - /** - * The configured encoder plugin. - */ - std::unique_ptr prepared_encoder; - Encoder *encoder; - - /** - * The destination file name. - */ - AllocatedPath path = nullptr; - - /** - * A string that will be used with FormatTag() to build the - * destination path. - */ - std::string format_path; - - /** - * The #AudioFormat that is currently active. This is used - * for switching to another file. - */ - AudioFormat effective_audio_format; - - /** - * The destination file. - */ - FileOutputStream *file; - - explicit RecorderOutput(const ConfigBlock &block); - -public: - static AudioOutput *Create(EventLoop &, const ConfigBlock &block) { - return new RecorderOutput(block); - } - -private: - void Open(AudioFormat &audio_format) override; - void Close() noexcept override; - - /** - * Writes pending data from the encoder to the output file. - */ - void EncoderToFile(); - - void SendTag(const Tag &tag) override; - - std::size_t Play(std::span src) override; - - [[nodiscard]] [[gnu::pure]] - bool HasDynamicPath() const noexcept { - return !format_path.empty(); - } - - /** - * Finish the encoder and commit the file. - * - * Throws on error. - */ - void Commit(); - - void FinishFormat(); - void ReopenFormat(AllocatedPath &&new_path); -}; - -RecorderOutput::RecorderOutput(const ConfigBlock &block) - :AudioOutput(0), - prepared_encoder(CreateConfiguredEncoder(block)) -{ - /* read configuration */ - - path = block.GetPath("path"); - - const char *fmt = block.GetBlockValue("format_path", nullptr); - if (fmt != nullptr) - format_path = fmt; - - if (path.IsNull() && fmt == nullptr) - throw std::runtime_error("'path' not configured"); - - if (!path.IsNull() && fmt != nullptr) - throw std::runtime_error("Cannot have both 'path' and 'format_path'"); -} - -inline void -RecorderOutput::EncoderToFile() -{ - assert(file != nullptr); - - EncoderToOutputStream(*file, *encoder); -} - -void -RecorderOutput::Open(AudioFormat &audio_format) -{ - /* create the output file */ - - if (!HasDynamicPath()) { - assert(!path.IsNull()); - - file = new FileOutputStream(path); - } else { - /* don't open the file just yet; wait until we have - a tag that we can use to build the path */ - assert(path.IsNull()); - - file = nullptr; - } - - /* open the encoder */ - - try { - encoder = prepared_encoder->Open(audio_format); - } catch (...) { - delete file; - throw; - } - - if (!HasDynamicPath()) { - try { - EncoderToFile(); - } catch (...) { - delete encoder; - throw; - } - } else { - /* remember the AudioFormat for ReopenFormat() */ - effective_audio_format = audio_format; - - /* close the encoder for now; it will be opened as - soon as we have received a tag */ - delete encoder; - } -} - -inline void -RecorderOutput::Commit() -{ - assert(!path.IsNull()); - - /* flush the encoder and write the rest to the file */ - - try { - encoder->End(); - EncoderToFile(); - } catch (...) { - delete encoder; - throw; - } - - /* now really close everything */ - - delete encoder; - - try { - file->Commit(); - } catch (...) { - delete file; - throw; - } - - delete file; -} - -void -RecorderOutput::Close() noexcept -{ - if (file == nullptr) { - /* not currently encoding to a file; nothing needs to - be done now */ - assert(HasDynamicPath()); - assert(path.IsNull()); - return; - } - - try { - Commit(); - } catch (...) { - LogError(std::current_exception()); - } - - if (HasDynamicPath()) { - assert(!path.IsNull()); - path.SetNull(); - } -} - -void -RecorderOutput::FinishFormat() -{ - assert(HasDynamicPath()); - - if (file == nullptr) - return; - - try { - Commit(); - } catch (...) { - LogError(std::current_exception()); - } - - file = nullptr; - path.SetNull(); -} - -inline void -RecorderOutput::ReopenFormat(AllocatedPath &&new_path) -{ - assert(HasDynamicPath()); - assert(path.IsNull()); - assert(file == nullptr); - - auto *new_file = new FileOutputStream(new_path); - - AudioFormat new_audio_format = effective_audio_format; - - try { - encoder = prepared_encoder->Open(new_audio_format); - } catch (...) { - delete new_file; - throw; - } - - /* reopening the encoder must always result in the same - AudioFormat as before */ - assert(new_audio_format == effective_audio_format); - - try { - EncoderToOutputStream(*new_file, *encoder); - } catch (...) { - delete encoder; - delete new_file; - throw; - } - - path = std::move(new_path); - file = new_file; - - FmtDebug(recorder_domain, "Recording to {:?}", path); -} - -void -RecorderOutput::SendTag(const Tag &tag) -{ - if (HasDynamicPath()) { - char *p = FormatTag(tag, format_path.c_str()); - if (p == nullptr || *p == 0) { - /* no path could be composed with this tag: - don't write a file */ - free(p); - FinishFormat(); - return; - } - - AtScopeExit(p) { free(p); }; - - AllocatedPath new_path = nullptr; - - try { - new_path = ParsePath(p); - } catch (...) { - LogError(std::current_exception()); - FinishFormat(); - return; - } - - if (new_path != path) { - FinishFormat(); - - try { - ReopenFormat(std::move(new_path)); - } catch (...) { - LogError(std::current_exception()); - return; - } - } - } - - encoder->PreTag(); - EncoderToFile(); - encoder->SendTag(tag); -} - -std::size_t -RecorderOutput::Play(std::span src) -{ - if (file == nullptr) { - /* not currently encoding to a file; discard incoming - data */ - assert(HasDynamicPath()); - assert(path.IsNull()); - return src.size(); - } - - encoder->Write(src); - - EncoderToFile(); - - return src.size(); -} - -const struct AudioOutputPlugin recorder_output_plugin = { - "recorder", - nullptr, - &RecorderOutput::Create, - nullptr, -}; diff --git a/src/output/plugins/RecorderOutputPlugin.hxx b/src/output/plugins/RecorderOutputPlugin.hxx deleted file mode 100644 index a035265..0000000 --- a/src/output/plugins/RecorderOutputPlugin.hxx +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#ifndef MPD_RECORDER_OUTPUT_PLUGIN_HXX -#define MPD_RECORDER_OUTPUT_PLUGIN_HXX - -extern const struct AudioOutputPlugin recorder_output_plugin; - -#endif diff --git a/src/output/plugins/ShoutOutputPlugin.cxx b/src/output/plugins/ShoutOutputPlugin.cxx deleted file mode 100644 index 8389138..0000000 --- a/src/output/plugins/ShoutOutputPlugin.cxx +++ /dev/null @@ -1,468 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "ShoutOutputPlugin.hxx" -#include "../OutputAPI.hxx" -#include "encoder/EncoderInterface.hxx" -#include "encoder/Configured.hxx" -#include "lib/fmt/RuntimeError.hxx" -#include "util/Domain.hxx" -#include "util/ScopeExit.hxx" -#include "util/StringAPI.hxx" -#include "Log.hxx" - -#include - -#include - -#include -#include -#include - -class ShoutConfig { - const char *const host; - const char *const mount; - const char *const user, *const passwd; - const char *const name; - const char *const genre, *const description; - const char *const url; - const char *const quality, *const bitrate; - - const unsigned port; - - const unsigned format; - const unsigned protocol; - -#ifdef SHOUT_TLS - const int tls; -#endif - - const bool is_public; - -public: - ShoutConfig(const ConfigBlock &block, const char *mime_type); - - void Setup(shout_t &connection) const; -}; - -struct ShoutOutput final : AudioOutput { - shout_t *shout_conn; - - std::unique_ptr prepared_encoder; - - const ShoutConfig config; - - Encoder *encoder; - - explicit ShoutOutput(const ConfigBlock &block); - ~ShoutOutput() override; - - ShoutOutput(const ShoutOutput &) = delete; - ShoutOutput &operator=(const ShoutOutput &) = delete; - - static AudioOutput *Create(EventLoop &event_loop, - const ConfigBlock &block); - - void Enable() override; - void Disable() noexcept override; - - void Open(AudioFormat &audio_format) override; - void Close() noexcept override; - - [[nodiscard]] std::chrono::steady_clock::duration Delay() const noexcept override; - void SendTag(const Tag &tag) override; - std::size_t Play(std::span src) override; - void Cancel() noexcept override; - bool Pause() override; - -private: - void WritePage(); -}; - -static int shout_init_count; - -static constexpr Domain shout_output_domain("shout_output"); - -static const char * -require_block_string(const ConfigBlock &block, const char *name) -{ - const char *value = block.GetBlockValue(name); - if (value == nullptr) - throw FmtRuntimeError("no {:?} defined for shout device defined " - "at line {}\n", name, block.line); - - return value; -} - -static void -ShoutSetAudioInfo(shout_t *shout_conn, const AudioFormat &audio_format) -{ - shout_set_audio_info(shout_conn, SHOUT_AI_CHANNELS, - fmt::format_int{static_cast(audio_format.channels)}.c_str()); - - shout_set_audio_info(shout_conn, SHOUT_AI_SAMPLERATE, - fmt::format_int{audio_format.sample_rate}.c_str()); -} - -#ifdef SHOUT_TLS - -static int -ParseShoutTls(const char *value) -{ - if (value == nullptr) - return SHOUT_TLS_DISABLED; - - if (StringIsEqual(value, "disabled")) - return SHOUT_TLS_DISABLED; - else if (StringIsEqual(value, "auto")) - return SHOUT_TLS_AUTO; - else if (StringIsEqual(value, "auto_no_plain")) - return SHOUT_TLS_AUTO_NO_PLAIN; - else if (StringIsEqual(value, "rfc2818")) - return SHOUT_TLS_RFC2818; - else if (StringIsEqual(value, "rfc2817")) - return SHOUT_TLS_RFC2817; - else - throw FmtRuntimeError("invalid shout TLS option {:?}", - value); -} - -#endif - -static unsigned -ParseShoutFormat(const char *mime_type) -{ - if (StringIsEqual(mime_type, "audio/mpeg")) - return SHOUT_FORMAT_MP3; - else - return SHOUT_FORMAT_OGG; -} - -static unsigned -ParseShoutProtocol(const char *value, const char *mime_type) -{ - if (value == nullptr) - return SHOUT_PROTOCOL_HTTP; - - if (StringIsEqual(value, "shoutcast")) { - if (!StringIsEqual(mime_type, "audio/mpeg")) - throw FmtRuntimeError("you cannot stream {:?} to shoutcast, use mp3", - mime_type); - return SHOUT_PROTOCOL_ICY; - } else if (StringIsEqual(value, "icecast1")) - return SHOUT_PROTOCOL_XAUDIOCAST; - else if (StringIsEqual(value, "icecast2")) - return SHOUT_PROTOCOL_HTTP; - else - throw FmtRuntimeError("shout protocol {:?} is not \"shoutcast\" or " - "\"icecast1\"or \"icecast2\"", - value); -} - -inline -ShoutConfig::ShoutConfig(const ConfigBlock &block, const char *mime_type) - :host(require_block_string(block, "host")), - mount(require_block_string(block, "mount")), - user(block.GetBlockValue("user", "source")), - passwd(require_block_string(block, "password")), - name(require_block_string(block, "name")), - genre(block.GetBlockValue("genre")), - description(block.GetBlockValue("description")), - url(block.GetBlockValue("url")), - quality(block.GetBlockValue("quality")), - bitrate(block.GetBlockValue("bitrate")), - port(block.GetBlockValue("port", 0U)), - format(ParseShoutFormat(mime_type)), - protocol(ParseShoutProtocol(block.GetBlockValue("protocol"), - mime_type)), -#ifdef SHOUT_TLS - tls(ParseShoutTls(block.GetBlockValue("tls"))), -#endif - is_public(block.GetBlockValue("public", false)) -{ - if (port == 0) - throw std::runtime_error("shout port must be configured"); -} - -ShoutOutput::ShoutOutput(const ConfigBlock &block) - :AudioOutput(FLAG_PAUSE|FLAG_NEED_FULLY_DEFINED_AUDIO_FORMAT| - FLAG_ENABLE_DISABLE), - prepared_encoder(CreateConfiguredEncoder(block, true)), - config(block, prepared_encoder->GetMimeType()) -{ -} - -ShoutOutput::~ShoutOutput() -{ - shout_init_count--; - if (shout_init_count == 0) - shout_shutdown(); -} - -AudioOutput * -ShoutOutput::Create(EventLoop &, const ConfigBlock &block) -{ - if (shout_init_count == 0) - shout_init(); - - shout_init_count++; - - return new ShoutOutput(block); -} - -static void -SetMeta(shout_t &connection, const char *name, const char *value) -{ - if (shout_set_meta(&connection, name, value) != SHOUTERR_SUCCESS) - throw std::runtime_error(shout_get_error(&connection)); -} - -static void -SetOptionalMeta(shout_t &connection, const char *name, const char *value) -{ - if (value != nullptr) - SetMeta(connection, name, value); -} - -inline void -ShoutConfig::Setup(shout_t &connection) const -{ - if (shout_set_host(&connection, host) != SHOUTERR_SUCCESS || - shout_set_port(&connection, port) != SHOUTERR_SUCCESS || - shout_set_password(&connection, passwd) != SHOUTERR_SUCCESS || - shout_set_mount(&connection, mount) != SHOUTERR_SUCCESS || - shout_set_user(&connection, user) != SHOUTERR_SUCCESS || - shout_set_public(&connection, is_public) != SHOUTERR_SUCCESS || -#ifdef SHOUT_USAGE_AUDIO - /* since libshout 2.4.3 */ - shout_set_content_format(&connection, format, SHOUT_USAGE_AUDIO, - nullptr) != SHOUTERR_SUCCESS || -#else - shout_set_format(&connection, format) != SHOUTERR_SUCCESS || -#endif - shout_set_protocol(&connection, protocol) != SHOUTERR_SUCCESS || -#ifdef SHOUT_TLS - shout_set_tls(&connection, tls) != SHOUTERR_SUCCESS || -#endif - shout_set_agent(&connection, "MPD") != SHOUTERR_SUCCESS) - throw std::runtime_error(shout_get_error(&connection)); - - SetMeta(connection, SHOUT_META_NAME, name); - - /* optional paramters */ - - SetOptionalMeta(connection, SHOUT_META_GENRE, genre); - SetOptionalMeta(connection, SHOUT_META_DESCRIPTION, description); - SetOptionalMeta(connection, SHOUT_META_URL, url); - - if (quality != nullptr) - shout_set_audio_info(&connection, SHOUT_AI_QUALITY, quality); - - if (bitrate != nullptr) - shout_set_audio_info(&connection, SHOUT_AI_BITRATE, bitrate); -} - -void -ShoutOutput::Enable() -{ - shout_conn = shout_new(); - if (shout_conn == nullptr) - throw std::bad_alloc{}; - - try { - config.Setup(*shout_conn); - } catch (...) { - shout_free(shout_conn); - throw; - } -} - -void -ShoutOutput::Disable() noexcept -{ - shout_free(shout_conn); -} - -static void -HandleShoutError(shout_t *shout_conn, int err) -{ - switch (err) { - case SHOUTERR_SUCCESS: - break; - - case SHOUTERR_UNCONNECTED: - case SHOUTERR_SOCKET: - throw FmtRuntimeError("Lost shout connection to {}:{}: {}", - shout_get_host(shout_conn), - shout_get_port(shout_conn), - shout_get_error(shout_conn)); - - default: - throw FmtRuntimeError("connection to {}:{} error: {}", - shout_get_host(shout_conn), - shout_get_port(shout_conn), - shout_get_error(shout_conn)); - } -} - -static void -EncoderToShout(shout_t *shout_conn, Encoder &encoder) -{ - while (true) { - std::byte buffer[32768]; - const auto e = encoder.Read(std::span{buffer}); - if (e.empty()) - return; - - int err = shout_send(shout_conn, - (const unsigned char *)e.data(), - e.size()); - HandleShoutError(shout_conn, err); - } -} - -void -ShoutOutput::WritePage() -{ - assert(encoder != nullptr); - - EncoderToShout(shout_conn, *encoder); -} - -void -ShoutOutput::Close() noexcept -{ - try { - encoder->End(); - WritePage(); - } catch (...) { - /* ignore */ - } - - delete encoder; - - if (shout_get_connected(shout_conn) != SHOUTERR_UNCONNECTED && - shout_close(shout_conn) != SHOUTERR_SUCCESS) { - FmtWarning(shout_output_domain, - "problem closing connection to shout server: {}", - shout_get_error(shout_conn)); - } -} - -void -ShoutOutput::Cancel() noexcept -{ - /* needs to be implemented for shout */ -} - -static void -ShoutOpen(shout_t *shout_conn) -{ - switch (shout_open(shout_conn)) { - case SHOUTERR_SUCCESS: - case SHOUTERR_CONNECTED: - break; - - default: - throw FmtRuntimeError("problem opening connection to shout server {}:{}: {}", - shout_get_host(shout_conn), - shout_get_port(shout_conn), - shout_get_error(shout_conn)); - } -} - -void -ShoutOutput::Open(AudioFormat &audio_format) -{ - encoder = prepared_encoder->Open(audio_format); - - try { - ShoutSetAudioInfo(shout_conn, audio_format); - ShoutOpen(shout_conn); - WritePage(); - } catch (...) { - delete encoder; - throw; - } -} - -std::chrono::steady_clock::duration -ShoutOutput::Delay() const noexcept -{ - int delay = shout_delay(shout_conn); - if (delay < 0) - delay = 0; - - return std::chrono::milliseconds(delay); -} - -std::size_t -ShoutOutput::Play(std::span src) -{ - encoder->Write(src); - WritePage(); - return src.size(); -} - -bool -ShoutOutput::Pause() -{ - static std::byte silence[1020]; - - encoder->Write(std::span{silence}); - WritePage(); - - return true; -} - -static std::string -shout_tag_to_metadata(const Tag &tag) noexcept -{ - const char *artist = tag.GetValue(TAG_ARTIST); - const char *title = tag.GetValue(TAG_TITLE); - - return fmt::format("{} - {}", - artist != nullptr ? artist : "", - title != nullptr ? title : ""); -} - -void -ShoutOutput::SendTag(const Tag &tag) -{ - if (encoder->ImplementsTag()) { - /* encoder plugin supports stream tags */ - - encoder->PreTag(); - WritePage(); - encoder->SendTag(tag); - } else { - /* no stream tag support: fall back to icy-metadata */ - - const auto meta = shout_metadata_new(); - AtScopeExit(meta) { shout_metadata_free(meta); }; - - const auto song = shout_tag_to_metadata(tag); - - if (SHOUTERR_SUCCESS != shout_metadata_add(meta, "song", song.c_str()) || -#ifdef SHOUT_FORMAT_TEXT - /* since libshout 2.4.6 */ - SHOUTERR_SUCCESS != shout_set_metadata_utf8(shout_conn, meta) -#else - SHOUTERR_SUCCESS != shout_metadata_add(meta, "charset", "UTF-8") || - SHOUTERR_SUCCESS != shout_set_metadata(shout_conn, meta) -#endif - ) { - LogWarning(shout_output_domain, - "error setting shout metadata"); - } - } - - WritePage(); -} - -const struct AudioOutputPlugin shout_output_plugin = { - "shout", - nullptr, - &ShoutOutput::Create, - nullptr, -}; diff --git a/src/output/plugins/ShoutOutputPlugin.hxx b/src/output/plugins/ShoutOutputPlugin.hxx deleted file mode 100644 index 06636e2..0000000 --- a/src/output/plugins/ShoutOutputPlugin.hxx +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#ifndef MPD_SHOUT_OUTPUT_PLUGIN_HXX -#define MPD_SHOUT_OUTPUT_PLUGIN_HXX - -extern const struct AudioOutputPlugin shout_output_plugin; - -#endif diff --git a/src/output/plugins/SndioOutputPlugin.cxx b/src/output/plugins/SndioOutputPlugin.cxx deleted file mode 100644 index b8e76f4..0000000 --- a/src/output/plugins/SndioOutputPlugin.cxx +++ /dev/null @@ -1,175 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "SndioOutputPlugin.hxx" -#include "mixer/Listener.hxx" -#include "mixer/plugins/SndioMixerPlugin.hxx" -#include "util/Domain.hxx" -#include "Log.hxx" - -#include - -#include - -#ifndef SIO_DEVANY -/* this macro is missing in libroar-dev 1.0~beta2-3 (Debian Wheezy) */ -#define SIO_DEVANY "default" -#endif - -static constexpr unsigned MPD_SNDIO_BUFFER_TIME_MS = 250; - -static constexpr Domain sndio_output_domain("sndio_output"); - -SndioOutput::SndioOutput(const ConfigBlock &block) - :AudioOutput(0), - device(block.GetBlockValue("device", SIO_DEVANY)), - buffer_time(block.GetBlockValue("buffer_time", - MPD_SNDIO_BUFFER_TIME_MS)), - raw_volume(SIO_MAXVOL) -{ -} - -static void -VolumeCallback(void *arg, unsigned int volume) { - ((SndioOutput *)arg)->VolumeChanged(volume); -} - -AudioOutput * -SndioOutput::Create(EventLoop &, const ConfigBlock &block) { - return new SndioOutput(block); -} - -static bool -sndio_test_default_device() -{ - auto *hdl = sio_open(SIO_DEVANY, SIO_PLAY, 0); - if (!hdl) { - LogError(sndio_output_domain, - "Error opening default sndio device"); - return false; - } - - sio_close(hdl); - return true; -} - -void -SndioOutput::Open(AudioFormat &audio_format) -{ - struct sio_par par; - unsigned bits, rate, chans; - - hdl = sio_open(device, SIO_PLAY, 0); - if (!hdl) - throw std::runtime_error("Failed to open default sndio device"); - - switch (audio_format.format) { - case SampleFormat::S16: - bits = 16; - break; - case SampleFormat::S24_P32: - bits = 24; - break; - case SampleFormat::S32: - bits = 32; - break; - default: - audio_format.format = SampleFormat::S16; - bits = 16; - break; - } - - rate = audio_format.sample_rate; - chans = audio_format.channels; - - sio_initpar(&par); - par.bits = bits; - par.rate = rate; - par.pchan = chans; - par.sig = 1; - par.le = SIO_LE_NATIVE; - par.appbufsz = rate * buffer_time / 1000; - - if (!sio_setpar(hdl, &par) || - !sio_getpar(hdl, &par)) { - sio_close(hdl); - throw std::runtime_error("Failed to set/get audio params"); - } - - if (par.bits != bits || - par.rate < rate * 995 / 1000 || - par.rate > rate * 1005 / 1000 || - par.pchan != chans || - par.sig != 1 || - par.le != SIO_LE_NATIVE) { - sio_close(hdl); - throw std::runtime_error("Requested audio params cannot be satisfied"); - } - - // Set volume after opening fresh audio stream which does - // know nothing about previous audio streams. - sio_setvol(hdl, raw_volume); - // sio_onvol returns 0 if no volume knob is available. - // This is the case on raw audio devices rather than - // the sndiod audio server. - if (sio_onvol(hdl, VolumeCallback, this) == 0) - raw_volume = -1; - - if (!sio_start(hdl)) { - sio_close(hdl); - throw std::runtime_error("Failed to start audio device"); - } -} - -void -SndioOutput::Close() noexcept -{ - sio_close(hdl); -} - -size_t -SndioOutput::Play(std::span src) -{ - const std::size_t n = sio_write(hdl, src.data(), src.size()); - if (n == 0 && sio_eof(hdl) != 0) - throw std::runtime_error("sndio write failed"); - return n; -} - -void -SndioOutput::SetVolume(unsigned int volume) -{ - sio_setvol(hdl, (volume * SIO_MAXVOL + 50) / 100); -} - -static inline unsigned int -RawToPercent(int raw_volume) { - return raw_volume < 0 ? 100 : (raw_volume * 100 + SIO_MAXVOL / 2) / SIO_MAXVOL; -} - -void -SndioOutput::VolumeChanged(int _raw_volume) { - if (raw_volume >= 0 && listener != nullptr && mixer != nullptr) { - raw_volume = _raw_volume; - listener->OnMixerVolumeChanged(*mixer, - RawToPercent(raw_volume)); - } -} - -unsigned int -SndioOutput::GetVolume() { - return RawToPercent(raw_volume); -} - -void -SndioOutput::RegisterMixerListener(Mixer *_mixer, MixerListener *_listener) { - mixer = _mixer; - listener = _listener; -} - -constexpr struct AudioOutputPlugin sndio_output_plugin = { - "sndio", - sndio_test_default_device, - SndioOutput::Create, - &sndio_mixer_plugin, -}; diff --git a/src/output/plugins/SndioOutputPlugin.hxx b/src/output/plugins/SndioOutputPlugin.hxx deleted file mode 100644 index b9bfc6f..0000000 --- a/src/output/plugins/SndioOutputPlugin.hxx +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#ifndef MPD_SNDIO_OUTPUT_PLUGIN_HXX -#define MPD_SNDIO_OUTPUT_PLUGIN_HXX - -#include "../OutputAPI.hxx" - -class Mixer; -class MixerListener; - -extern const struct AudioOutputPlugin sndio_output_plugin; - -class SndioOutput final : AudioOutput { - Mixer *mixer = nullptr; - MixerListener *listener = nullptr; - const char *const device; - const unsigned buffer_time; /* in ms */ - struct sio_hdl *hdl; - int raw_volume; - -public: - SndioOutput(const ConfigBlock &block); - - static AudioOutput *Create(EventLoop &, - const ConfigBlock &block); - - void SetVolume(unsigned int _volume); - unsigned int GetVolume(); - void VolumeChanged(int _volume); - void RegisterMixerListener(Mixer *_mixer, MixerListener *_listener); - -private: - void Open(AudioFormat &audio_format) override; - void Close() noexcept override; - size_t Play(std::span src) override; -}; - -#endif diff --git a/src/output/plugins/SolarisOutputPlugin.cxx b/src/output/plugins/SolarisOutputPlugin.cxx deleted file mode 100644 index 5572b18..0000000 --- a/src/output/plugins/SolarisOutputPlugin.cxx +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "SolarisOutputPlugin.hxx" -#include "../OutputAPI.hxx" -#include "io/FileDescriptor.hxx" -#include "lib/fmt/SystemError.hxx" - -#include - -#include -#include -#include -#include -#include - -#if defined(__sun) -#include -#include -#elif defined(__NetBSD__) -#include -#else - -/* some fake declarations that allow build this plugin on systems - other than Solaris, just to see if it compiles */ - -#ifndef I_FLUSH -#define I_FLUSH 0 -#endif - -#define AUDIO_INITINFO(v) -#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 - -class SolarisOutput final : AudioOutput { - /* configuration */ - const char *const device; - - FileDescriptor fd; - - explicit SolarisOutput(const ConfigBlock &block) - :AudioOutput(0), - device(block.GetBlockValue("device", "/dev/audio")) {} - -public: - static AudioOutput *Create(EventLoop &, const ConfigBlock &block) { - return new SolarisOutput(block); - } - -private: - void Open(AudioFormat &audio_format) override; - void Close() noexcept override; - - std::size_t Play(std::span src) override; - void Cancel() noexcept override; -}; - -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; -} - -void -SolarisOutput::Open(AudioFormat &audio_format) -{ - struct audio_info info; - int ret; - - AUDIO_INITINFO(&info); - - /* open the device in non-blocking mode */ - - if (!fd.Open(device, O_WRONLY|O_NONBLOCK)) - throw FmtErrno("Failed to open {}", device); - - /* restore blocking mode */ - - fd.SetBlocking(); - - /* configure the audio device */ - - info.play.sample_rate = audio_format.sample_rate; - info.play.channels = audio_format.channels; - info.play.encoding = AUDIO_ENCODING_LINEAR; - switch (audio_format.format) { - case SampleFormat::S8: - info.play.precision = 8; - break; - case SampleFormat::S16: - info.play.precision = 16; - break; - default: - info.play.precision = 32; - audio_format.format = SampleFormat::S32; - break; - } - - ret = ioctl(fd.Get(), AUDIO_SETINFO, &info); - if (ret < 0) { - const int e = errno; - fd.Close(); - throw MakeErrno(e, "AUDIO_SETINFO failed"); - } -} - -void -SolarisOutput::Close() noexcept -{ - fd.Close(); -} - -std::size_t -SolarisOutput::Play(std::span src) -{ - ssize_t nbytes = fd.Write(src); - if (nbytes <= 0) - throw MakeErrno("Write failed"); - - return nbytes; -} - -void -SolarisOutput::Cancel() noexcept -{ -#if defined(AUDIO_FLUSH) - ioctl(fd.Get(), AUDIO_FLUSH); -#elif defined(I_FLUSH) - ioctl(fd.Get(), I_FLUSH); -#endif -} - -const struct AudioOutputPlugin solaris_output_plugin = { - "solaris", - solaris_output_test_default_device, - &SolarisOutput::Create, - nullptr, -}; diff --git a/src/output/plugins/SolarisOutputPlugin.hxx b/src/output/plugins/SolarisOutputPlugin.hxx deleted file mode 100644 index d99a691..0000000 --- a/src/output/plugins/SolarisOutputPlugin.hxx +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#ifndef MPD_SOLARIS_OUTPUT_PLUGIN_HXX -#define MPD_SOLARIS_OUTPUT_PLUGIN_HXX - -extern const struct AudioOutputPlugin solaris_output_plugin; - -#endif diff --git a/src/output/plugins/WinmmOutputPlugin.cxx b/src/output/plugins/WinmmOutputPlugin.cxx deleted file mode 100644 index df2eeb1..0000000 --- a/src/output/plugins/WinmmOutputPlugin.cxx +++ /dev/null @@ -1,307 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#include "WinmmOutputPlugin.hxx" -#include "../OutputAPI.hxx" -#include "pcm/Buffer.hxx" -#include "mixer/plugins/WinmmMixerPlugin.hxx" -#include "lib/fmt/RuntimeError.hxx" -#include "fs/AllocatedPath.hxx" -#include "util/StringCompare.hxx" - -#include -#include - -#include -#include -#include // for INFINITE - -#include -#include - -struct WinmmBuffer { - PcmBuffer buffer; - - WAVEHDR hdr; -}; - -class WinmmOutput final : AudioOutput { - const UINT device_id; - HWAVEOUT handle; - - /** - * This event is triggered by Windows when a buffer is - * finished. - */ - HANDLE event; - - std::array buffers; - unsigned next_buffer; - -public: - WinmmOutput(const ConfigBlock &block); - - HWAVEOUT GetHandle() { - return handle; - } - - static AudioOutput *Create(EventLoop &, const ConfigBlock &block) { - return new WinmmOutput(block); - } - -private: - void Open(AudioFormat &audio_format) override; - void Close() noexcept override; - - std::size_t Play(std::span src) override; - void Drain() override; - void Cancel() noexcept override; - -private: - /** - * Wait until the buffer is finished. - */ - void DrainBuffer(WinmmBuffer &buffer); - - void DrainAllBuffers(); - - void Stop() noexcept; - -}; - -static std::runtime_error -MakeWaveOutError(MMRESULT result, const char *prefix) -{ - char buffer[256]; - if (waveOutGetErrorTextA(result, buffer, - std::size(buffer)) == MMSYSERR_NOERROR) - return FmtRuntimeError("{}: {}", prefix, buffer); - else - return std::runtime_error(prefix); -} - -HWAVEOUT -winmm_output_get_handle(WinmmOutput &output) -{ - return output.GetHandle(); -} - -static bool -winmm_output_test_default_device(void) -{ - return waveOutGetNumDevs() > 0; -} - -static UINT -get_device_id(const char *device_name) -{ - /* if device is not specified use wave mapper */ - if (device_name == nullptr) - return WAVE_MAPPER; - - 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) - throw FmtRuntimeError("device {:?} is not found", - device_name); - - return id; - } - - /* check for device name */ - const AllocatedPath device_name_fs = - AllocatedPath::FromUTF8Throw(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 (StringStartsWith(device_name_fs.c_str(), caps.szPname)) - return i; - } - - throw FmtRuntimeError("device {:?} is not found", device_name); -} - -WinmmOutput::WinmmOutput(const ConfigBlock &block) - :AudioOutput(0), - device_id(get_device_id(block.GetBlockValue("device"))) -{ -} - -void -WinmmOutput::Open(AudioFormat &audio_format) -{ - event = CreateEvent(nullptr, false, false, nullptr); - if (event == nullptr) - throw std::runtime_error("CreateEvent() failed"); - - switch (audio_format.format) { - case SampleFormat::S16: - break; - - case SampleFormat::S8: - 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(&handle, device_id, &format, - (DWORD_PTR)event, 0, CALLBACK_EVENT); - if (result != MMSYSERR_NOERROR) { - CloseHandle(event); - throw MakeWaveOutError(result, "waveOutOpen() failed"); - } - - for (auto &i : buffers) - memset(&i.hdr, 0, sizeof(i.hdr)); - - next_buffer = 0; -} - -void -WinmmOutput::Close() noexcept -{ - for (auto &i : buffers) - i.buffer.Clear(); - - waveOutClose(handle); - - CloseHandle(event); -} - -/** - * Copy data into a buffer, and prepare the wave header. - */ -static void -winmm_set_buffer(HWAVEOUT handle, WinmmBuffer *buffer, - const void *data, size_t size) -{ - 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(handle, &buffer->hdr, - sizeof(buffer->hdr)); - if (result != MMSYSERR_NOERROR) - throw MakeWaveOutError(result, - "waveOutPrepareHeader() failed"); -} - -void -WinmmOutput::DrainBuffer(WinmmBuffer &buffer) -{ - if ((buffer.hdr.dwFlags & WHDR_DONE) == WHDR_DONE) - /* already finished */ - return; - - while (true) { - MMRESULT result = waveOutUnprepareHeader(handle, - &buffer.hdr, - sizeof(buffer.hdr)); - if (result == MMSYSERR_NOERROR) - return; - else if (result != WAVERR_STILLPLAYING) - throw MakeWaveOutError(result, - "waveOutUnprepareHeader() failed"); - - /* wait some more */ - WaitForSingleObject(event, INFINITE); - } -} - -std::size_t -WinmmOutput::Play(std::span src) -{ - /* get the next buffer from the ring and prepare it */ - WinmmBuffer *buffer = &buffers[next_buffer]; - DrainBuffer(*buffer); - winmm_set_buffer(handle, buffer, src.data(), src.size()); - - /* enqueue the buffer */ - MMRESULT result = waveOutWrite(handle, &buffer->hdr, - sizeof(buffer->hdr)); - if (result != MMSYSERR_NOERROR) { - waveOutUnprepareHeader(handle, &buffer->hdr, - sizeof(buffer->hdr)); - throw MakeWaveOutError(result, "waveOutWrite() failed"); - } - - /* mark our buffer as "used" */ - next_buffer = (next_buffer + 1) % buffers.size(); - - return src.size(); -} - -void -WinmmOutput::DrainAllBuffers() -{ - for (unsigned i = next_buffer; i < buffers.size(); ++i) - DrainBuffer(buffers[i]); - - for (unsigned i = 0; i < next_buffer; ++i) - DrainBuffer(buffers[i]); -} - -void -WinmmOutput::Stop() noexcept -{ - waveOutReset(handle); - - for (auto &i : buffers) - waveOutUnprepareHeader(handle, &i.hdr, sizeof(i.hdr)); -} - -void -WinmmOutput::Drain() -{ - try { - DrainAllBuffers(); - } catch (...) { - Stop(); - throw; - } -} - -void -WinmmOutput::Cancel() noexcept -{ - Stop(); -} - -const struct AudioOutputPlugin winmm_output_plugin = { - "winmm", - winmm_output_test_default_device, - WinmmOutput::Create, - &winmm_mixer_plugin, -}; diff --git a/src/output/plugins/WinmmOutputPlugin.hxx b/src/output/plugins/WinmmOutputPlugin.hxx deleted file mode 100644 index adc4b37..0000000 --- a/src/output/plugins/WinmmOutputPlugin.hxx +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -#ifndef MPD_WINMM_OUTPUT_PLUGIN_HXX -#define MPD_WINMM_OUTPUT_PLUGIN_HXX - -#include "output/Features.h" - -#ifdef ENABLE_WINMM_OUTPUT - -#include -#include - -class WinmmOutput; - -extern const struct AudioOutputPlugin winmm_output_plugin; - -[[gnu::pure]] -HWAVEOUT -winmm_output_get_handle(WinmmOutput &output); - -#endif - -#endif diff --git a/src/output/plugins/meson.build b/src/output/plugins/meson.build index 935e1de..54ab170 100644 --- a/src/output/plugins/meson.build +++ b/src/output/plugins/meson.build @@ -12,30 +12,10 @@ output_plugins_deps = [ need_encoder = false need_wave_encoder = false -# All output plugins disabled for mpd-dbcreate - only NullOutputPlugin needed -# Set all feature flags to false -output_features.set('ENABLE_AO', false) -output_features.set('HAVE_FIFO', false) -output_features.set('ENABLE_HTTPD_OUTPUT', false) -output_features.set('ENABLE_JACK', false) -output_features.set('HAVE_OPENAL', false) -output_features.set('HAVE_OSX', false) -output_features.set('ENABLE_PIPE_OUTPUT', false) -output_features.set('ENABLE_RECORDER_OUTPUT', false) -output_features.set('HAVE_SHOUT', false) -output_features.set('ENABLE_SNAPCAST_OUTPUT', false) -output_features.set('ENABLE_SOLARIS_OUTPUT', false) -output_features.set('ENABLE_WINMM_OUTPUT', false) -output_features.set('ENABLE_WASAPI_OUTPUT', false) + # Define empty dependencies to avoid build errors -libao_dep = dependency('', required: false) -libjack_dep = dependency('', required: false) -openal_dep = dependency('', required: false) -libshout_dep = dependency('', required: false) -sles_dep = dependency('', required: false) -winmm_dep = dependency('', required: false) -wasapi_dep = dependency('', required: false) + output_plugins = static_library( 'output_plugins',