From d2e9b3715b7cc07b690386c6b096488639dad547 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Sat, 18 Jul 2026 08:08:00 +0200 Subject: [PATCH 1/2] output/Client: add `noexcept` --- src/output/Client.hxx | 9 +++------ src/player/Control.hxx | 4 ++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/output/Client.hxx b/src/output/Client.hxx index 67c57f4893..89a2c141cb 100644 --- a/src/output/Client.hxx +++ b/src/output/Client.hxx @@ -1,8 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later // Copyright The Music Player Daemon Project -#ifndef MPD_OUTPUT_CLIENT_HXX -#define MPD_OUTPUT_CLIENT_HXX +#pragma once /** * An interface between the #AudioOutput and the #Player. @@ -14,7 +13,7 @@ public: * is called from within the output thread. The client may * perform actions to refill the #MusicPipe. */ - virtual void ChunksConsumed() = 0; + virtual void ChunksConsumed() noexcept = 0; /** * The #AudioOutput has modified the "enabled" flag, and the @@ -23,7 +22,5 @@ public: * AudioOutput::Command to the output thread; only the client * can do that safely. */ - virtual void ApplyEnabled() = 0; + virtual void ApplyEnabled() noexcept = 0; }; - -#endif diff --git a/src/player/Control.hxx b/src/player/Control.hxx index 09ec0dc4ce..f2b3dc85dc 100644 --- a/src/player/Control.hxx +++ b/src/player/Control.hxx @@ -553,11 +553,11 @@ private: const AudioFormat &format); /* virtual methods from AudioOutputClient */ - void ChunksConsumed() override { + void ChunksConsumed() noexcept override { LockSignal(); } - void ApplyEnabled() override { + void ApplyEnabled() noexcept override { LockUpdateAudio(); } From ff22bd411bedbda508eb9406e9fe77f1116a2d11 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Wed, 15 Jul 2026 18:14:48 +0200 Subject: [PATCH 2/2] output/MultipleOutputs: move ownership to new class AllOutputs There is now a global list of outputs owned by AllOutputs, and MultipleOutputs contains just pointers to the AudioOutputControl instances. This way, it will be easier to move audio outputs across partitions. And it makes the weird "dummy" objects go away. --- src/Instance.cxx | 16 -- src/Instance.hxx | 14 +- src/Main.cxx | 11 +- src/Partition.cxx | 2 +- src/command/PartitionCommands.cxx | 47 +++--- src/mixer/All.cxx | 18 ++- src/output/AllOutputs.cxx | 95 +++++++++++ src/output/AllOutputs.hxx | 70 ++++++++ src/output/Control.cxx | 55 ++----- src/output/Control.hxx | 46 +++--- src/output/MultipleOutputs.cxx | 257 +++++++++--------------------- src/output/MultipleOutputs.hxx | 111 +++++-------- src/output/OutputCommand.cxx | 12 +- src/output/Print.cxx | 8 +- src/output/State.cxx | 3 +- src/output/meson.build | 1 + 16 files changed, 380 insertions(+), 386 deletions(-) create mode 100644 src/output/AllOutputs.cxx create mode 100644 src/output/AllOutputs.hxx diff --git a/src/Instance.cxx b/src/Instance.cxx index 68c0e154a2..f064e09cb1 100644 --- a/src/Instance.cxx +++ b/src/Instance.cxx @@ -91,22 +91,6 @@ Instance::DeletePartition(Partition &partition) noexcept } } -std::pair -Instance::FindOutput(std::string_view name, - Partition &excluding_partition) noexcept -{ - for (auto &partition : partitions) { - if (&partition == &excluding_partition) - continue; - - const auto idx = partition.outputs.FindIndexByName(name); - if (idx >= 0 && !partition.outputs.Get(idx).IsDummy()) - return {&partition, idx}; - } - - return {}; -} - #ifdef ENABLE_DATABASE const Database & diff --git a/src/Instance.hxx b/src/Instance.hxx index bbc8e2fdb2..c7a3003329 100644 --- a/src/Instance.hxx +++ b/src/Instance.hxx @@ -5,6 +5,7 @@ #define MPD_INSTANCE_HXX #include "config.h" +#include "output/AllOutputs.hxx" #include "db/Features.hxx" // for ENABLE_DATABASE #include "event/Loop.hxx" #include "event/Thread.hxx" @@ -123,6 +124,8 @@ struct Instance final std::unique_ptr client_list; + AllOutputs outputs; + std::list partitions; std::unique_ptr state_file; @@ -177,17 +180,6 @@ struct Instance final void BeginShutdownPartitions() noexcept; - /** - * Returns the (non-dummy) audio output device with the - * specified name. Returns nullptr if the name does not - * exist. - * - * @param excluding_partition ignore this partition - */ - [[gnu::pure]] - std::pair FindOutput(std::string_view name, - Partition &excluding_partition) noexcept; - #ifdef ENABLE_DATABASE /** * Returns the global #Database instance. May return nullptr diff --git a/src/Main.cxx b/src/Main.cxx index 19dcfeab27..73ace04f1c 100644 --- a/src/Main.cxx +++ b/src/Main.cxx @@ -395,11 +395,14 @@ MainConfigured(const CommandLineOptions &options, command_init(); + instance.outputs.Configure(instance.io_thread.GetEventLoop(), + instance.rtio_thread.GetEventLoop(), + raw_config, + partition_config.player.replay_gain); + + instance.partitions.front().outputs.AcquireAll(instance.partitions.front().replay_gain_mode); + for (auto &partition : instance.partitions) { - partition.outputs.Configure(instance.io_thread.GetEventLoop(), - instance.rtio_thread.GetEventLoop(), - raw_config, - partition_config.player.replay_gain); partition.UpdateEffectiveReplayGainMode(); } diff --git a/src/Partition.cxx b/src/Partition.cxx index 24f6c5bd7e..e34b141450 100644 --- a/src/Partition.cxx +++ b/src/Partition.cxx @@ -35,7 +35,7 @@ Partition::Partition(Instance &_instance, idle_monitor(instance.event_loop, BIND_THIS_METHOD(OnIdleMonitor)), global_events(instance.event_loop, BIND_THIS_METHOD(OnGlobalEvent)), playlist(config.queue.max_length, *this), - outputs(pc, *this), + outputs(instance.outputs, pc, *this), pc(*this, outputs, instance.input_cache.get(), config.player) diff --git a/src/command/PartitionCommands.cxx b/src/command/PartitionCommands.cxx index 1e48b79fd1..f4a0b2cb83 100644 --- a/src/command/PartitionCommands.cxx +++ b/src/command/PartitionCommands.cxx @@ -118,7 +118,7 @@ handle_delpartition(Client &client, Request request, Response &response) return CommandResult::ERROR; } - if (!partition->outputs.IsDummy()) { + if (!partition->outputs.empty()) { response.Error(ACK_ERROR_UNKNOWN, "partition still has outputs"); return CommandResult::ERROR; @@ -132,41 +132,40 @@ handle_delpartition(Client &client, Request request, Response &response) return CommandResult::OK; } +static Partition * +FindOwningPartition(AudioOutputControl &ao) noexcept +{ + return static_cast(ao.GetMixerListener()); +} + CommandResult handle_moveoutput(Client &client, Request request, Response &response) { const std::string_view output_name = request[0]; - auto &dest_partition = client.GetPartition(); - const auto existing_output_index = dest_partition.outputs.FindIndexByName(output_name); - if (existing_output_index >= 0 && !dest_partition.outputs.Get(existing_output_index).IsDummy()) - /* this output is already in the specified partition, - so nothing needs to be done */ - return CommandResult::OK; - - /* find the partition which owns this output currently */ auto &instance = client.GetInstance(); - - const auto [src_partition, src_index] = instance.FindOutput(output_name, dest_partition); - if (src_partition == nullptr) { + auto *ao = instance.outputs.FindByName(output_name); + if (ao == nullptr) { response.Error(ACK_ERROR_NO_EXIST, "No such output"); return CommandResult::ERROR; } - const bool was_enabled = src_partition->outputs.Get(src_index).IsEnabled(); + auto &dest_partition = client.GetPartition(); + + /* find the partition which owns this output currently */ + auto *src_partition = FindOwningPartition(*ao); + if (src_partition == &dest_partition) + /* this output is already in the specified partition, + so nothing needs to be done */ + return CommandResult::OK; + + const bool was_enabled = ao->IsEnabled(); - auto output = src_partition->outputs.ReplaceWithDummy(src_index); + if (src_partition != nullptr) + src_partition->outputs.ReleaseOwnership(*ao); - if (existing_output_index >= 0) - /* move the output back where it once was */ - dest_partition.outputs.ReplaceDummy(existing_output_index, std::move(output), - was_enabled, - dest_partition.replay_gain_mode); - else - /* copy the AudioOutputControl and add it to the output list */ - dest_partition.outputs.Add(std::move(output), - was_enabled, - dest_partition.replay_gain_mode); + dest_partition.outputs.AcquireOwnership(*ao, was_enabled, + dest_partition.replay_gain_mode); instance.EmitIdle(IDLE_OUTPUT); return CommandResult::OK; diff --git a/src/mixer/All.cxx b/src/mixer/All.cxx index 516ef5c5e5..b4ebaad959 100644 --- a/src/mixer/All.cxx +++ b/src/mixer/All.cxx @@ -43,11 +43,13 @@ output_mixer_get_volume(const AudioOutputControl &ao) noexcept int MultipleOutputs::GetVolume() const noexcept { + const std::lock_guard lock{mutex}; + unsigned ok = 0; int total = 0; for (const auto &ao : outputs) { - int volume = output_mixer_get_volume(*ao); + int volume = output_mixer_get_volume(ao); if (volume >= 0) { total += volume; ++ok; @@ -104,9 +106,10 @@ MultipleOutputs::SetVolume(unsigned volume) SetVolumeResult result = SetVolumeResult::NO_MIXER; std::exception_ptr error; - for (const auto &ao : outputs) { + const std::lock_guard lock{mutex}; + for (auto &ao : outputs) { try { - auto r = output_mixer_set_volume(*ao, volume); + auto r = output_mixer_set_volume(ao, volume); if (r > result) result = r; } catch (...) { @@ -149,11 +152,13 @@ output_mixer_get_software_volume(const AudioOutputControl &ao) noexcept int MultipleOutputs::GetSoftwareVolume() const noexcept { + const std::lock_guard lock{mutex}; + unsigned ok = 0; int total = 0; - for (const auto &ao : outputs) { - int volume = output_mixer_get_software_volume(*ao); + for (auto &ao : outputs) { + int volume = output_mixer_get_software_volume(ao); if (volume >= 0) { total += volume; ++ok; @@ -171,8 +176,9 @@ MultipleOutputs::SetSoftwareVolume(unsigned volume) noexcept { assert(volume <= PCM_VOLUME_1); + const std::lock_guard lock{mutex}; for (const auto &ao : outputs) { - auto *mixer = ao->GetMixer(); + auto *mixer = ao.GetMixer(); if (mixer != nullptr && (mixer->IsPlugin(software_mixer_plugin) || diff --git a/src/output/AllOutputs.cxx b/src/output/AllOutputs.cxx new file mode 100644 index 0000000000..1e786dee6a --- /dev/null +++ b/src/output/AllOutputs.cxx @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright The Music Player Daemon Project + +#include "AllOutputs.hxx" +#include "Control.hxx" +#include "Defaults.hxx" +#include "Filtered.hxx" +#include "filter/Factory.hxx" +#include "config/Block.hxx" +#include "config/Data.hxx" +#include "config/Option.hxx" +#include "lib/fmt/RuntimeError.hxx" + +AllOutputs::AllOutputs() noexcept = default; + +AllOutputs::~AllOutputs() noexcept +{ + /* parallel destruction */ + for (const auto &i : outputs) + i->BeginDestroy(); +} + +static std::unique_ptr +LoadOutput(EventLoop &event_loop, EventLoop &rt_event_loop, + const ReplayGainConfig &replay_gain_config, + const ConfigBlock &block, + const AudioOutputDefaults &defaults, + FilterFactory *filter_factory) +try { + return audio_output_new(event_loop, rt_event_loop, replay_gain_config, block, + defaults, + filter_factory); +} catch (...) { + if (block.line > 0) + std::throw_with_nested(FmtRuntimeError("Failed to configure output in line {}", + block.line)); + else + throw; +} + +static std::unique_ptr +LoadOutputControl(EventLoop &event_loop, EventLoop &rt_event_loop, + const ReplayGainConfig &replay_gain_config, + const ConfigBlock &block, + const AudioOutputDefaults &defaults, + FilterFactory *filter_factory) +{ + auto output = LoadOutput(event_loop, rt_event_loop, + replay_gain_config, + block, defaults, filter_factory); + return std::make_unique(std::move(output), + block); +} + +void +AllOutputs::Configure(EventLoop &event_loop, EventLoop &rt_event_loop, + const ConfigData &config, + const ReplayGainConfig &replay_gain_config) +{ + const AudioOutputDefaults defaults(config); + FilterFactory filter_factory(config); + + config.WithEach(ConfigBlockOption::AUDIO_OUTPUT, [&, this](const auto &block){ + auto output = LoadOutputControl(event_loop, rt_event_loop, + replay_gain_config, + block, defaults, + &filter_factory); + if (HasName(output->GetName())) + throw FmtRuntimeError("output devices with identical " + "names: {}", + output->GetName()); + + outputs.emplace_back(std::move(output)); + }); + + if (outputs.empty()) { + /* auto-detect device */ + const ConfigBlock empty; + outputs.emplace_back(LoadOutputControl(event_loop, + rt_event_loop, + replay_gain_config, + empty, defaults, + nullptr)); + } +} + +AudioOutputControl * +AllOutputs::FindByName(const std::string_view name) noexcept +{ + for (const auto &i : outputs) + if (name == i->GetName()) + return i.get(); + + return nullptr; +} diff --git a/src/output/AllOutputs.hxx b/src/output/AllOutputs.hxx new file mode 100644 index 0000000000..1ccf8cbb40 --- /dev/null +++ b/src/output/AllOutputs.hxx @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright The Music Player Daemon Project + +#pragma once + +#include +#include +#include + +struct ConfigData; +struct ReplayGainConfig; +class EventLoop; +class AudioOutputControl; + +/** + * Manages the global list of #AudioOutputControl instances. + * + * This class only owns those objects, but #MultipleOutputs is the one + * that will drive them. + */ +class AllOutputs final { + std::vector> outputs; + +public: + AllOutputs() noexcept; + ~AllOutputs() noexcept; + + void Configure(EventLoop &event_loop, EventLoop &rt_event_loop, + const ConfigData &config, + const ReplayGainConfig &replay_gain_config); + + /** + * Returns the total number of audio output devices, including + * those which are disabled right now. + */ + [[gnu::pure]] + std::size_t Size() const noexcept { + return outputs.size(); + } + + /** + * Returns the "i"th audio output device. + */ + const AudioOutputControl &Get(std::size_t i) const noexcept { + assert(i < Size()); + + return *outputs[i]; + } + + AudioOutputControl &Get(std::size_t i) noexcept { + assert(i < Size()); + + return *outputs[i]; + } + + /** + * Returns the audio output device with the specified name. + * Returns nullptr if the name does not exist. + */ + [[gnu::pure]] + AudioOutputControl *FindByName(std::string_view name) noexcept; + + /** + * Does an audio output device with this name exist? + */ + [[gnu::pure]] + bool HasName(std::string_view name) noexcept { + return FindByName(name) != nullptr; + } +}; diff --git a/src/output/Control.cxx b/src/output/Control.cxx index 7efea004a0..67cf34f744 100644 --- a/src/output/Control.cxx +++ b/src/output/Control.cxx @@ -16,16 +16,9 @@ automatically reopening the device */ static constexpr PeriodClock::Duration REOPEN_AFTER = std::chrono::seconds(10); -AudioOutputControl::AudioOutputControl(Dummy, std::string_view _name) noexcept - :name(_name), - tags(), always_on(), always_off() {} - AudioOutputControl::AudioOutputControl(std::unique_ptr _output, - AudioOutputClient &_client, const ConfigBlock &block) :output(std::move(_output)), - name(output->GetName()), - client(&_client), tags(block.GetBlockValue("tags", true)), always_on(block.GetBlockValue("always_on", false)), always_off(block.GetBlockValue("always_off", false)), @@ -40,41 +33,40 @@ AudioOutputControl::~AudioOutputControl() noexcept StopThread(); } +const char * +AudioOutputControl::GetName() const noexcept +{ + return output->GetName(); +} + const char * AudioOutputControl::GetPluginName() const noexcept { - return output ? output->GetPluginName() : "dummy"; + return output->GetPluginName(); } const char * AudioOutputControl::GetLogName() const noexcept { - assert(!IsDummy()); - - return output ? output->GetLogName() : name.c_str(); + return output->GetLogName(); } Mixer * AudioOutputControl::GetMixer() const noexcept { - return output ? output->mixer : nullptr; + return output->mixer; } std::map> AudioOutputControl::GetAttributes() const noexcept { - return output - ? output->GetAttributes() - : std::map>{}; + return output->GetAttributes(); } void AudioOutputControl::SetAttribute(std::string &&attribute_name, std::string &&value) { - if (!output) - throw std::runtime_error("Cannot set attribute on dummy output"); - output->SetAttribute(std::move(attribute_name), std::move(value)); } @@ -132,9 +124,6 @@ AudioOutputControl::LockDisable() noexcept { std::unique_lock lock{mutex}; - if (!output) - return; - if (really_enabled && output->SupportsEnableDisable()) CommandWait(lock, Command::DISABLE); @@ -144,9 +133,6 @@ AudioOutputControl::LockDisable() noexcept void AudioOutputControl::EnableAsync() { - if (!output) - return; - if (always_off) return; @@ -168,9 +154,6 @@ AudioOutputControl::EnableAsync() void AudioOutputControl::DisableAsync() noexcept { - if (!output) - return; - if (!thread.IsDefined()) { if (!output->SupportsEnableDisable()) really_enabled = false; @@ -251,9 +234,6 @@ AudioOutputControl::CloseWait(std::unique_lock &lock) noexcept { assert(allow_play); - if (IsDummy()) - return; - if (output->mixer != nullptr) output->mixer->LockAutoClose(); @@ -321,8 +301,7 @@ AudioOutputControl::LockPauseAsync() noexcept Mixer::LockAutoClose()) */ output->mixer->LockAutoClose(); - if (output) - output->Interrupt(); + output->Interrupt(); const std::lock_guard protect{mutex}; @@ -344,8 +323,7 @@ AudioOutputControl::LockDrainAsync() noexcept void AudioOutputControl::LockCancelAsync() noexcept { - if (output) - output->Interrupt(); + output->Interrupt(); const std::lock_guard protect{mutex}; @@ -368,9 +346,6 @@ AudioOutputControl::LockAllowPlay() noexcept void AudioOutputControl::LockRelease() noexcept { - if (!output) - return; - output->Interrupt(); if (output->mixer != nullptr && @@ -396,8 +371,7 @@ AudioOutputControl::LockCloseWait() noexcept { assert(!open || !fail_timer.IsDefined()); - if (output) - output->Interrupt(); + output->Interrupt(); std::unique_lock lock{mutex}; CloseWait(lock); @@ -407,8 +381,7 @@ void AudioOutputControl::BeginDestroy() noexcept { if (thread.IsDefined()) { - if (output) - output->Interrupt(); + output->Interrupt(); const std::lock_guard protect{mutex}; if (!killed) { diff --git a/src/output/Control.hxx b/src/output/Control.hxx index 7c5dc97bfd..d14f33661b 100644 --- a/src/output/Control.hxx +++ b/src/output/Control.hxx @@ -10,6 +10,7 @@ #include "thread/Mutex.hxx" #include "thread/Cond.hxx" #include "time/PeriodClock.hxx" +#include "util/IntrusiveList.hxx" #include #include @@ -29,21 +30,20 @@ class AudioOutputClient; /** * Controller for an #AudioOutput and its output thread. */ -class AudioOutputControl final : MixerListener { +class AudioOutputControl final : public IntrusiveListHook<>, MixerListener { const std::unique_ptr output; - /** - * A copy of FilteredAudioOutput::name which we need just in - * case this is a "dummy" output (output==nullptr) because - * this output has been moved to another partition. - */ - const std::string name; - MixerListener *mixer_listener = nullptr; /** * The PlayerControl object which "owns" this output. This * object is needed to signal command completion. + * + * This field is left uninitialized by the constructor; it + * will be initialized as soon as the output gets acquired by + * the default partition. + * + * Protected by #mutex. */ AudioOutputClient *client; @@ -276,18 +276,10 @@ public: */ mutable Mutex mutex; - struct Dummy{}; - - /** - * Construct a "dummy" instance. - */ - explicit AudioOutputControl(Dummy, std::string_view _name) noexcept; - /** * Throws on error. */ AudioOutputControl(std::unique_ptr _output, - AudioOutputClient &_client, const ConfigBlock &block); ~AudioOutputControl() noexcept; @@ -296,9 +288,7 @@ public: AudioOutputControl &operator=(const AudioOutputControl &) = delete; [[gnu::pure]] - const auto &GetName() const noexcept { - return name; - } + const char *GetName() const noexcept; [[gnu::pure]] const char *GetPluginName() const noexcept; @@ -312,11 +302,25 @@ public: return *client; } + /** + * May only be called from the main thread. + */ void LockSetMixerListener(MixerListener &_mixer_listener) noexcept { const std::lock_guard lock{mutex}; mixer_listener = &_mixer_listener; } + /** + * Return the current #MixerListener. This method exists only + * as an efficient kludge to determine which #Partition this + * object is currently assigned to. + * + * May only be called from the main thread. + */ + MixerListener *GetMixerListener() const noexcept { + return mixer_listener; + } + void SetClient(AudioOutputClient &_client) noexcept { assert(source_state == SourceState::CLOSED); @@ -326,10 +330,6 @@ public: [[gnu::pure]] Mixer *GetMixer() const noexcept; - bool IsDummy() const noexcept { - return !output; - } - bool AlwaysOff() const noexcept { return always_off; } diff --git a/src/output/MultipleOutputs.cxx b/src/output/MultipleOutputs.cxx index 1753b11634..e05882394f 100644 --- a/src/output/MultipleOutputs.cxx +++ b/src/output/MultipleOutputs.cxx @@ -2,17 +2,12 @@ // Copyright The Music Player Daemon Project #include "MultipleOutputs.hxx" +#include "AllOutputs.hxx" #include "Client.hxx" #include "Control.hxx" #include "Filtered.hxx" -#include "Defaults.hxx" #include "MusicPipe.hxx" #include "MusicChunk.hxx" -#include "filter/Factory.hxx" -#include "config/Block.hxx" -#include "config/Data.hxx" -#include "config/Option.hxx" -#include "lib/fmt/RuntimeError.hxx" #include "util/StringAPI.hxx" #include // for std::all_of() @@ -21,175 +16,77 @@ #include -MultipleOutputs::MultipleOutputs(AudioOutputClient &_client, +MultipleOutputs::MultipleOutputs(AllOutputs &_all_outputs, + AudioOutputClient &_client, MixerListener &_mixer_listener) noexcept - :client(_client), mixer_listener(_mixer_listener) + :all_outputs(_all_outputs), + client(_client), + mixer_listener(_mixer_listener) { } -MultipleOutputs::~MultipleOutputs() noexcept -{ - /* parallel destruction */ - for (const auto &i : outputs) - i->BeginDestroy(); -} +MultipleOutputs::~MultipleOutputs() noexcept = default; -static std::unique_ptr -LoadOutput(EventLoop &event_loop, EventLoop &rt_event_loop, - const ReplayGainConfig &replay_gain_config, - const ConfigBlock &block, - const AudioOutputDefaults &defaults, - FilterFactory *filter_factory) -try { - return audio_output_new(event_loop, rt_event_loop, replay_gain_config, block, - defaults, - filter_factory); -} catch (...) { - if (block.line > 0) - std::throw_with_nested(FmtRuntimeError("Failed to configure output in line {}", - block.line)); - else - throw; -} - -static std::unique_ptr -LoadOutputControl(EventLoop &event_loop, EventLoop &rt_event_loop, - const ReplayGainConfig &replay_gain_config, - AudioOutputClient &client, const ConfigBlock &block, - const AudioOutputDefaults &defaults, - FilterFactory *filter_factory) +AudioOutputControl * +MultipleOutputs::FindByName(const std::string_view name) noexcept { - auto output = LoadOutput(event_loop, rt_event_loop, - replay_gain_config, - block, defaults, filter_factory); - return std::make_unique(std::move(output), - client, block); -} + for (auto &ao : outputs) + if (name == ao.GetName()) + return &ao; -void -MultipleOutputs::Configure(EventLoop &event_loop, EventLoop &rt_event_loop, - const ConfigData &config, - const ReplayGainConfig &replay_gain_config) -{ - const AudioOutputDefaults defaults(config); - FilterFactory filter_factory(config); - - config.WithEach(ConfigBlockOption::AUDIO_OUTPUT, [&, this](const auto &block){ - auto output = LoadOutputControl(event_loop, rt_event_loop, - replay_gain_config, - client, block, defaults, - &filter_factory); - if (HasName(output->GetName())) - throw FmtRuntimeError("output devices with identical " - "names: {}", - output->GetName()); - - outputs.emplace_back(std::move(output)); - outputs.back()->LockSetMixerListener(mixer_listener); - }); - - if (outputs.empty()) { - /* auto-detect device */ - const ConfigBlock empty; - outputs.emplace_back(LoadOutputControl(event_loop, - rt_event_loop, - replay_gain_config, - client, empty, defaults, - nullptr)); - outputs.back()->LockSetMixerListener(mixer_listener); - } + return nullptr; } bool -MultipleOutputs::IsDummy() const noexcept +MultipleOutputs::Owns(const AudioOutputControl &ao) const noexcept { - return std::all_of(outputs.begin(), outputs.end(), [](const auto &i) { return i->IsDummy(); }); + return ao.GetMixerListener() == &mixer_listener; } -int -MultipleOutputs::FindIndexByName(const std::string_view name) const noexcept +void +MultipleOutputs::AcquireAll(ReplayGainMode replay_gain_mode) noexcept { - for (int idx = 0; const auto &i : outputs) { - if (name == i->GetName()) - return idx; - ++idx; + assert(outputs.empty()); + + for (unsigned i = 0, n = all_outputs.Size(); i != n; ++i) { + auto &ao = all_outputs.Get(i); + if (ao.GetMixerListener() == nullptr) { + outputs.push_back(ao); + ao.LockSetMixerListener(mixer_listener); + ao.SetClient(client); + ao.SetReplayGainMode(replay_gain_mode); + } } - - return -1; -} - -AudioOutputControl * -MultipleOutputs::FindByName(const std::string_view name) noexcept -{ - const std::lock_guard lock{mutex}; - - for (const auto &i : outputs) - if (name == i->GetName()) - return i.get(); - - return nullptr; -} - -std::unique_ptr -MultipleOutputs::ReplaceWithDummy(std::size_t idx) noexcept -{ - assert(idx < outputs.size()); - - auto &slot = outputs[idx]; - slot->LockDisable(); - - const std::lock_guard lock{mutex}; - - auto old = std::move(slot); - old->LockDisable(); - - slot = std::make_unique(AudioOutputControl::Dummy{}, old->GetName()); - - return old; } void -MultipleOutputs::ReplaceDummy(std::size_t idx, - std::unique_ptr &&src, - bool enable, - ReplayGainMode replay_gain_mode) noexcept +MultipleOutputs::AcquireOwnership(AudioOutputControl &ao, bool enable, + ReplayGainMode replay_gain_mode) noexcept { - assert(idx < outputs.size()); - assert(!src->IsEnabled()); - assert(!src->IsReallyEnabled()); - - auto &output = *src; + assert(!Owns(ao)); - auto &slot = outputs[idx]; - assert(slot->IsDummy()); + { + const std::lock_guard lock{mutex}; + outputs.push_back(ao); + } - const std::lock_guard lock{mutex}; - slot = std::move(src); - output.LockSetMixerListener(mixer_listener); - output.SetClient(client); - output.LockSetEnabled(enable); - output.SetReplayGainMode(replay_gain_mode); + ao.LockSetMixerListener(mixer_listener); + ao.SetClient(client); + ao.LockSetEnabled(enable); + ao.SetReplayGainMode(replay_gain_mode); client.ApplyEnabled(); } void -MultipleOutputs::Add(std::unique_ptr &&src, - bool enable, - ReplayGainMode replay_gain_mode) noexcept +MultipleOutputs::ReleaseOwnership(AudioOutputControl &ao) noexcept { - { - const std::lock_guard lock{mutex}; - outputs.push_back(std::move(src)); - } + assert(Owns(ao)); - auto &output = *outputs.back(); - output.LockSetMixerListener(mixer_listener); - output.SetClient(client); - output.LockSetEnabled(enable); - output.SetReplayGainMode(replay_gain_mode); + ao.LockDisable(); - client.ApplyEnabled(); + const std::lock_guard lock{mutex}; + outputs.erase(outputs.iterator_to(ao)); } void @@ -197,8 +94,8 @@ MultipleOutputs::_EnableDisable() { /* parallel execution */ - for (const auto &ao : outputs) - ao->LockEnableDisableAsync(); + for (auto &ao : outputs) + ao.LockEnableDisableAsync(); WaitAll(); } @@ -213,15 +110,15 @@ MultipleOutputs::EnableDisable() void MultipleOutputs::WaitAll() noexcept { - for (const auto &ao : outputs) - ao->LockWaitForCommand(); + for (auto &ao : outputs) + ao.LockWaitForCommand(); } inline void MultipleOutputs::AllowPlay() noexcept { - for (const auto &ao : outputs) - ao->LockAllowPlay(); + for (auto &ao : outputs) + ao.LockAllowPlay(); } bool @@ -232,8 +129,8 @@ MultipleOutputs::_Update(bool force) noexcept if (!IsOpen()) return false; - for (const auto &ao : outputs) - ret = ao->LockUpdate(input_audio_format, *pipe, force) + for (auto &ao : outputs) + ret = ao.LockUpdate(input_audio_format, *pipe, force) || ret; return ret; @@ -250,9 +147,8 @@ void MultipleOutputs::SetReplayGainMode(ReplayGainMode mode) noexcept { const std::lock_guard lock{mutex}; - - for (const auto &ao : outputs) - ao->SetReplayGainMode(mode); + for (auto &ao : outputs) + ao.SetReplayGainMode(mode); } void @@ -269,8 +165,8 @@ MultipleOutputs::Play(MusicChunkPtr chunk) pipe->Push(std::move(chunk)); const std::lock_guard lock{mutex}; - for (const auto &ao : outputs) - ao->LockPlay(); + for (auto &ao : outputs) + ao.LockPlay(); } void @@ -298,20 +194,20 @@ MultipleOutputs::Open(const AudioFormat audio_format) std::exception_ptr first_error; - for (const auto &ao : outputs) { - const std::lock_guard lock{ao->mutex}; + for (auto &ao : outputs) { + const std::lock_guard lock{ao.mutex}; /* can't play on this device even if it's enabled */ - if (ao->AlwaysOff()) + if (ao.AlwaysOff()) continue; - if (ao->IsEnabled()) + if (ao.IsEnabled()) enabled = true; - if (ao->IsOpen()) + if (ao.IsOpen()) ret = true; else if (!first_error) - first_error = ao->GetLastError(); + first_error = ao.GetLastError(); } if (!enabled) { @@ -336,7 +232,7 @@ MultipleOutputs::IsChunkConsumed(const MusicChunk *chunk) const noexcept const std::lock_guard lock{mutex}; return std::all_of(outputs.begin(), outputs.end(), [chunk](const auto &ao) { - return ao->LockIsChunkConsumed(*chunk); }); + return ao.LockIsChunkConsumed(*chunk); }); } unsigned @@ -364,8 +260,8 @@ MultipleOutputs::CheckPipe() noexcept /* this is the tail of the pipe - clear the chunk reference in all outputs */ const std::lock_guard lock{mutex}; - for (const auto &ao : outputs) - ao->LockClearTailChunk(*chunk); + for (auto &ao : outputs) + ao.LockClearTailChunk(*chunk); } /* remove the chunk from the pipe */ @@ -393,8 +289,8 @@ MultipleOutputs::Pause() noexcept _Update(false); - for (const auto &ao : outputs) - ao->LockPauseAsync(); + for (auto &ao : outputs) + ao.LockPauseAsync(); WaitAll(); } @@ -404,8 +300,8 @@ MultipleOutputs::Drain() noexcept { const std::lock_guard lock{mutex}; - for (const auto &ao : outputs) - ao->LockDrainAsync(); + for (auto &ao : outputs) + ao.LockDrainAsync(); WaitAll(); } @@ -417,8 +313,8 @@ MultipleOutputs::Cancel() noexcept /* send the cancel() command to all audio outputs */ - for (const auto &ao : outputs) - ao->LockCancelAsync(); + for (auto &ao : outputs) + ao.LockCancelAsync(); WaitAll(); @@ -440,8 +336,8 @@ MultipleOutputs::Cancel() noexcept void MultipleOutputs::_Close() noexcept { - for (const auto &ao : outputs) - ao->LockCloseWait(); + for (auto &ao : outputs) + ao.LockCloseWait(); pipe.reset(); @@ -460,10 +356,11 @@ MultipleOutputs::Close() noexcept void MultipleOutputs::Release() noexcept { - const std::lock_guard lock{mutex}; - - for (const auto &ao : outputs) - ao->LockRelease(); + { + const std::lock_guard lock{mutex}; + for (auto &ao : outputs) + ao.LockRelease(); + } pipe.reset(); diff --git a/src/output/MultipleOutputs.hxx b/src/output/MultipleOutputs.hxx index d279df8933..67dba8796c 100644 --- a/src/output/MultipleOutputs.hxx +++ b/src/output/MultipleOutputs.hxx @@ -1,48 +1,52 @@ // SPDX-License-Identifier: GPL-2.0-or-later // Copyright The Music Player Daemon Project -#ifndef OUTPUT_ALL_H -#define OUTPUT_ALL_H +#pragma once #include "MusicChunkPtr.hxx" #include "player/Outputs.hxx" #include "pcm/AudioFormat.hxx" #include "thread/Mutex.hxx" +#include "util/IntrusiveList.hxx" #include "Chrono.hxx" #include +#include #include #include #include enum class ReplayGainMode : uint8_t; class MusicPipe; -class EventLoop; class MixerListener; +class AllOutputs; class AudioOutputClient; class AudioOutputControl; -struct ConfigData; -struct ReplayGainConfig; /* * Wrap multiple #AudioOutputControl objects a single interface which * keeps them synchronized. */ class MultipleOutputs final : public PlayerOutputs { + AllOutputs &all_outputs; + AudioOutputClient &client; MixerListener &mixer_listener; /** - * Protects #outputs. + * Protects #output. * - * The main thread is allowed to read #outputs without holding - * the lock, because only the main thread is allowed to modify - * it. + * The main thread is allowed to read #per_output without + * holding the lock, because only the main thread is allowed + * to modify it. */ mutable Mutex mutex; - std::vector> outputs; + /** + * A doubly-linked list of outputs owned by this object. + */ + IntrusiveList outputs; AudioFormat input_audio_format = AudioFormat::Undefined(); @@ -63,57 +67,25 @@ public: * Load audio outputs from the configuration file and * initialize them. */ - MultipleOutputs(AudioOutputClient &_client, + MultipleOutputs(AllOutputs &_all_outputs, AudioOutputClient &_client, MixerListener &_mixer_listener) noexcept; ~MultipleOutputs() noexcept; - void Configure(EventLoop &event_loop, EventLoop &rt_event_loop, - const ConfigData &config, - const ReplayGainConfig &replay_gain_config); - - /** - * Returns the total number of audio output devices, including - * those which are disabled right now. - */ - [[gnu::pure]] - std::size_t Size() const noexcept { - return outputs.size(); + const auto &GetAllOutputs() const noexcept { + return all_outputs; } - /** - * Returns the "i"th audio output device. - * - * Since this returns an unprotected reference, it may only be - * called by the main thread (i.e. the only thread that is - * allowed to modify #outputs). - */ - const AudioOutputControl &Get(std::size_t i) const noexcept { - assert(i < Size()); - - return *outputs[i]; + bool empty() const noexcept { + return outputs.empty(); } - AudioOutputControl &Get(std::size_t i) noexcept { - assert(i < Size()); - - return *outputs[i]; + auto begin() const noexcept { + return outputs.begin(); } - /** - * Are all outputs dummy? - * - * May only be called by the main thread (i.e. the only thread - * that is allowed to modify #outputs). - */ - [[gnu::pure]] - bool IsDummy() const noexcept; - - /** - * Returns the index of the audio output device with the specified name. - * Returns -1 if the name does not exist. - */ - [[gnu::pure]] - int FindIndexByName(std::string_view name) const noexcept; + auto end() const noexcept { + return outputs.end(); + } /** * Returns the audio output device with the specified name. @@ -123,31 +95,26 @@ public: AudioOutputControl *FindByName(std::string_view name) noexcept; /** - * Does an audio output device with this name exist? + * Does this object own the specified #AudioOutputControl instance? + * + * May only be called from the main thread. */ [[gnu::pure]] - bool HasName(std::string_view name) noexcept { - return FindByName(name) != nullptr; - } + bool Owns(const AudioOutputControl &ao) const noexcept; /** - * Replace the output at the specified index with a dummy - * output and return the original output to the caller. - */ - [[nodiscard]] - std::unique_ptr ReplaceWithDummy(std::size_t idx) noexcept; - - /** - * Replace the dummy output at the specified index with #src. + * Acquire ownership of all (orphan) outputs in #all_outputs + * (but do not enable/disable them). This is what we do with + * the default partitions on startup. + * + * This method is unsafe for later use because it does not + * care for mutex locking. */ - void ReplaceDummy(std::size_t idx, - std::unique_ptr &&src, - bool enable, - ReplayGainMode replay_gain_mode) noexcept; + void AcquireAll(ReplayGainMode replay_gain_mode) noexcept; - void Add(std::unique_ptr &&src, - bool enable, - ReplayGainMode replay_gain_mode) noexcept; + void AcquireOwnership(AudioOutputControl &ao, bool enable, + ReplayGainMode replay_gain_mode) noexcept; + void ReleaseOwnership(AudioOutputControl &ao) noexcept; void SetReplayGainMode(ReplayGainMode mode) noexcept; @@ -254,5 +221,3 @@ private: return elapsed_time; } }; - -#endif diff --git a/src/output/OutputCommand.cxx b/src/output/OutputCommand.cxx index c8a08443c1..e2c9a497b6 100644 --- a/src/output/OutputCommand.cxx +++ b/src/output/OutputCommand.cxx @@ -9,6 +9,7 @@ */ #include "OutputCommand.hxx" +#include "AllOutputs.hxx" #include "Client.hxx" #include "Control.hxx" #include "mixer/Mixer.hxx" @@ -17,17 +18,22 @@ #include "protocol/Ack.hxx" #include "protocol/IdleFlags.hxx" #include "Partition.hxx" +#include "Instance.hxx" extern unsigned audio_output_state_version; AudioOutputControl & CheckPartitionOutput(Partition &partition, unsigned idx) { - auto &outputs = partition.outputs; - if (idx >= outputs.Size()) + auto &all_outputs = partition.instance.outputs; + if (idx >= all_outputs.Size()) throw ProtocolError(ACK_ERROR_NO_EXIST, "No such audio output"); - return outputs.Get(idx); + auto &ao = all_outputs.Get(idx); + if (!partition.outputs.Owns(ao)) + throw ProtocolError{ACK_ERROR_NO_EXIST, "Audio output not in this partition"}; + + return ao; } void diff --git a/src/output/Print.cxx b/src/output/Print.cxx index 70d45361ba..1c4458c96b 100644 --- a/src/output/Print.cxx +++ b/src/output/Print.cxx @@ -9,6 +9,7 @@ #include "Print.hxx" #include "Control.hxx" #include "MultipleOutputs.hxx" +#include "AllOutputs.hxx" #include "client/Response.hxx" #include @@ -16,8 +17,11 @@ void printAudioDevices(Response &r, const MultipleOutputs &outputs) { - for (unsigned i = 0, n = outputs.Size(); i != n; ++i) { - const auto &ao = outputs.Get(i); + const auto &all_outputs = outputs.GetAllOutputs(); + for (unsigned i = 0, n = all_outputs.Size(); i != n; ++i) { + const auto &ao = all_outputs.Get(i); + if (!outputs.Owns(ao)) + continue; r.Fmt("outputid: {}\n" "outputname: {}\n" diff --git a/src/output/State.cxx b/src/output/State.cxx index 0b44f84c95..c99b8b9842 100644 --- a/src/output/State.cxx +++ b/src/output/State.cxx @@ -26,8 +26,7 @@ void audio_output_state_save(BufferedOutputStream &os, const MultipleOutputs &outputs) { - for (unsigned i = 0, n = outputs.Size(); i != n; ++i) { - const auto &ao = outputs.Get(i); + for (const auto &ao : outputs) { const std::lock_guard lock{ao.mutex}; os.Fmt(AUDIO_DEVICE_STATE "{}:{}\n", diff --git a/src/output/meson.build b/src/output/meson.build index 53bc526eb5..7a41ddbca5 100644 --- a/src/output/meson.build +++ b/src/output/meson.build @@ -38,6 +38,7 @@ output_glue = static_library( 'output_glue', 'Defaults.cxx', 'Filtered.cxx', + 'AllOutputs.cxx', 'MultipleOutputs.cxx', 'SharedPipeConsumer.cxx', 'Source.cxx',