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/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/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', 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(); }