diff --git a/NEWS b/NEWS index efb10b604..e2fab2390 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,7 @@ ver 0.22 (not yet released) - "findadd"/"searchadd"/"searchaddpl" support the "sort" and "window" parameters - add command "readpicture" to download embedded pictures + - command "moveoutput" moves an output between partitions * tags - new tags "Grouping" (for ID3 "TIT1"), "Work" and "Conductor" * input diff --git a/doc/protocol.rst b/doc/protocol.rst index f0d1a1071..36622b574 100644 --- a/doc/protocol.rst +++ b/doc/protocol.rst @@ -1258,6 +1258,9 @@ client is assigned to one partition at a time. :command:`newpartition {NAME}` Create a new partition. +:command:`moveoutput {OUTPUTNAME}` + Move an output to the current partition. + Audio output devices ==================== diff --git a/src/command/AllCommands.cxx b/src/command/AllCommands.cxx index 774b1b3d3..73ae9b4e7 100644 --- a/src/command/AllCommands.cxx +++ b/src/command/AllCommands.cxx @@ -139,6 +139,7 @@ static constexpr struct command commands[] = { #endif { "move", PERMISSION_CONTROL, 2, 2, handle_move }, { "moveid", PERMISSION_CONTROL, 2, 2, handle_moveid }, + { "moveoutput", PERMISSION_ADMIN, 1, 1, handle_moveoutput }, { "newpartition", PERMISSION_ADMIN, 1, 1, handle_newpartition }, { "next", PERMISSION_CONTROL, 0, 0, handle_next }, { "notcommands", PERMISSION_NONE, 0, 0, handle_not_commands }, diff --git a/src/command/PartitionCommands.cxx b/src/command/PartitionCommands.cxx index 86c0deaee..46d39db32 100644 --- a/src/command/PartitionCommands.cxx +++ b/src/command/PartitionCommands.cxx @@ -22,6 +22,7 @@ #include "Instance.hxx" #include "Partition.hxx" #include "IdleFlags.hxx" +#include "output/Filtered.hxx" #include "client/Client.hxx" #include "client/Response.hxx" #include "util/CharUtil.hxx" @@ -113,3 +114,44 @@ handle_newpartition(Client &client, Request request, Response &response) return CommandResult::OK; } + +CommandResult +handle_moveoutput(Client &client, Request request, Response &response) +{ + const char *output_name = request[0]; + + auto &dest_partition = client.GetPartition(); + auto *existing_output = dest_partition.outputs.FindByName(output_name); + if (existing_output != nullptr && !existing_output->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(); + for (auto &partition : instance.partitions) { + if (&partition == &dest_partition) + continue; + + auto *output = partition.outputs.FindByName(output_name); + if (output == nullptr || output->IsDummy()) + continue; + + const bool was_enabled = output->IsEnabled(); + + if (existing_output != nullptr) + /* move the output back where it once was */ + existing_output->ReplaceDummy(output->Steal(), + was_enabled); + else + /* add it to the output list */ + dest_partition.outputs.Add(output->Steal(), + was_enabled); + + instance.EmitIdle(IDLE_OUTPUT); + return CommandResult::OK; + } + + response.Error(ACK_ERROR_NO_EXIST, "No such output"); + return CommandResult::ERROR; +} diff --git a/src/command/PartitionCommands.hxx b/src/command/PartitionCommands.hxx index 08fd98dea..c840c0a82 100644 --- a/src/command/PartitionCommands.hxx +++ b/src/command/PartitionCommands.hxx @@ -35,4 +35,7 @@ handle_listpartitions(Client &client, Request request, Response &response); CommandResult handle_newpartition(Client &client, Request request, Response &response); +CommandResult +handle_moveoutput(Client &client, Request request, Response &response); + #endif diff --git a/src/output/Control.cxx b/src/output/Control.cxx index b4168514f..bf29fb28f 100644 --- a/src/output/Control.cxx +++ b/src/output/Control.cxx @@ -19,6 +19,7 @@ #include "Control.hxx" #include "Filtered.hxx" +#include "Client.hxx" #include "mixer/MixerControl.hxx" #include "config/Block.hxx" #include "Log.hxx" @@ -31,7 +32,9 @@ static constexpr PeriodClock::Duration REOPEN_AFTER = std::chrono::seconds(10); AudioOutputControl::AudioOutputControl(std::unique_ptr _output, AudioOutputClient &_client) noexcept - :output(std::move(_output)), client(_client), + :output(std::move(_output)), + name(output->GetName()), + client(_client), thread(BIND_THIS_METHOD(Task)) { } @@ -49,40 +52,86 @@ AudioOutputControl::Configure(const ConfigBlock &block) enabled = block.GetBlockValue("enabled", true); } +std::unique_ptr +AudioOutputControl::Steal() noexcept +{ + assert(!IsDummy()); + + /* close and disable the output */ + { + std::unique_lock lock(mutex); + if (really_enabled && output->SupportsEnableDisable()) + CommandWait(lock, Command::DISABLE); + + enabled = really_enabled = false; + } + + /* stop the thread */ + StopThread(); + + /* now we can finally remove it */ + const std::lock_guard protect(mutex); + return std::exchange(output, nullptr); +} + +void +AudioOutputControl::ReplaceDummy(std::unique_ptr new_output, + bool _enabled) noexcept +{ + assert(IsDummy()); + assert(new_output); + + { + const std::lock_guard protect(mutex); + output = std::move(new_output); + enabled = _enabled; + } + + client.ApplyEnabled(); +} + const char * AudioOutputControl::GetName() const noexcept { - return output->GetName(); + return name.c_str(); } const char * AudioOutputControl::GetPluginName() const noexcept { - return output->GetPluginName(); + return output ? output->GetPluginName() : "dummy"; } const char * AudioOutputControl::GetLogName() const noexcept { + assert(!IsDummy()); + return output->GetLogName(); } Mixer * AudioOutputControl::GetMixer() const noexcept { - return output->mixer; + return output ? output->mixer : nullptr; } const std::map AudioOutputControl::GetAttributes() const noexcept { - return output->GetAttributes(); + return output + ? output->GetAttributes() + : std::map{}; } void -AudioOutputControl::SetAttribute(std::string &&name, std::string &&value) +AudioOutputControl::SetAttribute(std::string &&attribute_name, + std::string &&value) { - output->SetAttribute(std::move(name), std::move(value)); + if (!output) + throw std::runtime_error("Cannot set attribute on dummy output"); + + output->SetAttribute(std::move(attribute_name), std::move(value)); } bool @@ -137,6 +186,9 @@ AudioOutputControl::LockCommandWait(Command cmd) noexcept void AudioOutputControl::EnableAsync() { + if (!output) + return; + if (!thread.IsDefined()) { if (!output->SupportsEnableDisable()) { /* don't bother to start the thread now if the @@ -155,6 +207,9 @@ AudioOutputControl::EnableAsync() void AudioOutputControl::DisableAsync() noexcept { + if (!output) + return; + if (!thread.IsDefined()) { if (!output->SupportsEnableDisable()) really_enabled = false; @@ -234,6 +289,9 @@ AudioOutputControl::CloseWait(std::unique_lock &lock) noexcept { assert(allow_play); + if (IsDummy()) + return; + if (output->mixer != nullptr) mixer_auto_close(output->mixer); diff --git a/src/output/Control.hxx b/src/output/Control.hxx index 26a1bb3dc..ef9cf5f2b 100644 --- a/src/output/Control.hxx +++ b/src/output/Control.hxx @@ -49,6 +49,13 @@ class AudioOutputClient; class AudioOutputControl { 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 partitioncommands. + */ + const std::string name; + /** * The PlayerControl object which "owns" this output. This * object is needed to signal command completion. @@ -256,6 +263,10 @@ public: gcc_pure Mixer *GetMixer() const noexcept; + bool IsDummy() const noexcept { + return !output; + } + /** * Caller must lock the mutex. */ @@ -294,6 +305,14 @@ public: return last_error; } + /** + * Detach and return the #FilteredAudioOutput instance and, + * replacing it here with a "dummy" object. + */ + std::unique_ptr Steal() noexcept; + void ReplaceDummy(std::unique_ptr new_output, + bool _enabled) noexcept; + void StartThread(); /** diff --git a/src/output/MultipleOutputs.cxx b/src/output/MultipleOutputs.cxx index cde7089a7..6cc7ac4e4 100644 --- a/src/output/MultipleOutputs.cxx +++ b/src/output/MultipleOutputs.cxx @@ -18,6 +18,7 @@ */ #include "MultipleOutputs.hxx" +#include "Client.hxx" #include "Filtered.hxx" #include "Defaults.hxx" #include "MusicPipe.hxx" @@ -142,6 +143,21 @@ MultipleOutputs::FindByName(const char *name) noexcept return nullptr; } +void +MultipleOutputs::Add(std::unique_ptr output, + bool enable) noexcept +{ + auto &client = GetAnyClient(); + + // TODO: this operation needs to be protected with a mutex + outputs.emplace_back(std::make_unique(std::move(output), + client)); + + outputs.back()->LockSetEnabled(enable); + + client.ApplyEnabled(); +} + void MultipleOutputs::EnableDisable() { diff --git a/src/output/MultipleOutputs.hxx b/src/output/MultipleOutputs.hxx index 33c581617..f918ab660 100644 --- a/src/output/MultipleOutputs.hxx +++ b/src/output/MultipleOutputs.hxx @@ -119,6 +119,9 @@ public: return FindByName(name) != nullptr; } + void Add(std::unique_ptr output, + bool enable) noexcept; + void SetReplayGainMode(ReplayGainMode mode) noexcept; /** @@ -153,6 +156,10 @@ public: void SetSoftwareVolume(unsigned volume) noexcept; private: + AudioOutputClient &GetAnyClient() noexcept { + return outputs.front()->GetClient(); + } + /** * Was Open() called successfully? *