diff --git a/CMakeLists.txt b/CMakeLists.txt index ec61f9f..86d7ef1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,6 +51,12 @@ if (PROJECT_IS_TOP_LEVEL) target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic -Werror -pedantic -pedantic-errors -Wfatal-errors) endif() +if (CMAKE_CXX_COMPILER_ID MATCHES "GNU") + target_compile_options(${PROJECT_NAME} PUBLIC -Wno-attributes=vc::) +else() + target_compile_options(${PROJECT_NAME} PUBLIC -Wno-unknown-attributes) +endif() + target_compile_options(${PROJECT_NAME} PRIVATE -Wno-missing-field-initializers -Wno-cast-function-type) # -------------------------------------------------------------------------------------------------------- @@ -81,7 +87,7 @@ include("cmake/cpm.cmake") CPMFindPackage( NAME rohrkabel - VERSION 4.1 + VERSION 5.0 GIT_REPOSITORY "https://github.com/Curve/rohrkabel" ) diff --git a/private/patchbay.impl.hpp b/private/patchbay.impl.hpp index 69065ee..6105c6d 100644 --- a/private/patchbay.impl.hpp +++ b/private/patchbay.impl.hpp @@ -22,6 +22,13 @@ namespace vencord { + /** + * Used by @see map_ports: + * + * @returns port map in form of [mic-port -> target-port] + */ + using port_map = std::vector>; + struct node_with_ports { pw::node_info info; @@ -46,25 +53,56 @@ namespace vencord link_options options; private: + /** + * ╔══════════════════╗ + * ║ Metadata related ║ + * ╚══════════════════╝ + * + * 1. The *default* metadata is bound to @see metadata + * 2. A listener is installed to check for default speaker updates, @see meta_listener + * 3. Speaker info is saved to @see speaker, the name is parsed from the metadata, the id is set in @see on_node + * 4. The @see lettuce_target is used by the workaround, a redirect is installed in the metadata (see venmic#15) + */ + + std::unique_ptr metadata; + std::unique_ptr meta_listener; + std::optional speaker; - std::unique_ptr listener; + std::optional lettuce_target; private: + /** + * ╔══════════════════╗ + * ║ Virt mic related ║ + * ╚══════════════════╝ + * + * 1. The @see virt_mic is created by @see create_mic + * 2. All created links, @see relink, are bound in @see created + * └┬─────────┘ + * key: related node + * value: bound link + */ + std::unique_ptr virt_mic; - std::multimap created; + [[vc::check_erase]] std::multimap created; private: - std::map links; - std::map nodes; + /** + * ╔═══════════════╗ + * ║ Logic related ║ + * ╚═══════════════╝ + * + * 1. All links we encounter are saved in @see links + * 2. All nodes we encounter are saved in @see nodes + */ + + [[vc::check_erase]] std::map links; + [[vc::check_erase]] std::map nodes; private: std::shared_ptr core; std::shared_ptr registry; - private: - std::unique_ptr metadata; - std::optional lettuce_target; // https://github.com/Vencord/venmic/issues/15 - private: std::atomic_bool should_exit{false}; @@ -79,28 +117,30 @@ namespace vencord void cleanup(bool); private: - void relink_all(); - void relink(std::uint32_t); + void reload(); + + private: + void link(std::uint32_t); + port_map map_ports(node_with_ports &); private: void meta_update(std::string_view, pw::metadata_property); + private: + void on_link(std::uint32_t); + void on_node(std::uint32_t); + private: template void bind(pw::global &); public: - void add_global(pw::global &); - template - void add_global(T &, pw::global &); + void handle(T &, pw::global &); private: - void rem_global(std::uint32_t); - - private: - void on_link(std::uint32_t); - void on_node(std::uint32_t); + void add_global(pw::global &); + void del_global(std::uint32_t); private: template diff --git a/src/patchbay.impl.cpp b/src/patchbay.impl.cpp index 32df6e3..3be62ee 100644 --- a/src/patchbay.impl.cpp +++ b/src/patchbay.impl.cpp @@ -4,11 +4,10 @@ #include -#include - #include #include +#include #include namespace vencord @@ -21,10 +20,9 @@ namespace vencord patchbay::impl::~impl() { cleanup(true); - should_exit = true; + should_exit = true; sender->send(quit{}); - thread.join(); } patchbay::impl::impl() @@ -53,12 +51,10 @@ namespace vencord void patchbay::impl::create_mic() { - auto node = core->create({"adapter", - {{"audio.channels", "2"}, - {"audio.position", "FL,FR"}, - {"node.name", "vencord-screen-share"}, - {"media.class", "Audio/Source/Virtual"}, - {"factory.name", "support.null-audio-sink"}}}) + auto node = core->create(pw::null_sink_factory{ + .name = "vencord-screen-share", + .positions = {"FL", "FR"}, + }) .get(); while (nodes[node->id()].ports.size() < 4) @@ -67,6 +63,8 @@ namespace vencord } virt_mic = std::make_unique(std::move(*node)); + + logger::get()->info("[patchbay] (create_mic) created: {}", virt_mic->id()); } void patchbay::impl::cleanup(bool mic) @@ -87,7 +85,7 @@ namespace vencord virt_mic.reset(); } - void patchbay::impl::relink_all() + void patchbay::impl::reload() { cleanup(false); @@ -101,26 +99,27 @@ namespace vencord on_link(id); } - logger::get()->debug("[patchbay] (relink_all) relinked all targets"); + logger::get()->debug("[patchbay] (reload) finished"); } - void patchbay::impl::relink(std::uint32_t id) + void patchbay::impl::link(std::uint32_t id) { - created.erase(id); - if (!virt_mic) { return; } - if (!nodes.contains(id)) + const auto mic_id = virt_mic->id(); + + if (id == mic_id) { + logger::get()->warn("[patchbay] (link) prevented link to self", id, mic_id); return; } - if (id == virt_mic->id()) + if (!nodes.contains(id)) { - logger::get()->warn("[patchbay] (relink) prevented link to self", id, virt_mic->id()); + logger::get()->warn("[patchbay] (link) called with bad node: {}", id); return; } @@ -128,13 +127,52 @@ namespace vencord if (options.ignore_devices && !target.info.props["device.id"].empty()) { - logger::get()->warn("[patchbay] (relink) prevented link to device", id, virt_mic->id()); + logger::get()->warn("[patchbay] (link) prevented link to device", id, mic_id); return; } - logger::get()->debug("[patchbay] (relink) linking {} [with mic = {}]", id, virt_mic->id()); + logger::get()->debug("[patchbay] (link) linking {}", id); + + auto mapping = map_ports(target); + + for (auto [it, end] = created.equal_range(id); it != end; ++it) + { + auto equal = [info = it->second.info()](const auto &map) + { + return map.first.id == info.input.port && map.second.id == info.output.port; + }; + + std::erase_if(mapping, equal); + } + + for (auto [mic_port, target_port] : mapping) + { + auto link = core->create(pw::link_factory{ + mic_port.id, + target_port.id, + }) + .get(); + + if (!link.has_value()) + { + logger::get()->warn("[patchbay] (link) failed to link {} (mic) -> {} (node): {}", mic_port.id, + target_port.id, link.error().message); + + return; + } + + logger::get()->debug("[patchbay] (link) created {}: {} (mic) -> {} (node) (channel: {})", link->id(), + mic_port.id, target_port.id, target_port.props["audio.channel"]); + + created.emplace(id, std::move(*link)); + } + + logger::get()->debug("[patchbay] (link) linked all ports of {}", id); + } - auto &source = nodes[virt_mic->id()]; + port_map patchbay::impl::map_ports(node_with_ports &target) + { + port_map rtn; auto is_output = [](const auto &item) { @@ -145,26 +183,34 @@ namespace vencord return item.direction == pw::port_direction::input; }; - auto target_ports = target.ports | ranges::views::filter(is_output) | ranges::to; - auto source_ports = source.ports | ranges::views::filter(is_input); + auto &mic = nodes[virt_mic->id()]; + auto mic_inputs = mic.ports | ranges::views::filter(is_input); + + const auto id = target.info.id; + auto target_outputs = target.ports | ranges::views::filter(is_output) | ranges::to; + + if (target_outputs.empty()) + { + logger::get()->warn("[patchbay] (map_ports) {} has no ports", id); + return rtn; + } + + const auto is_mono = target_outputs.size() == 1; - logger::get()->debug("[patchbay] (relink) {} has {} port(s)", id, target_ports.size()); + if (is_mono) + { + logger::get()->debug("[patchbay] (map_ports) {} is mono", id); + } - for (auto &port : target_ports) + for (auto &port : target_outputs) { - auto matching_channel = [&](auto &item) + auto matching_channel = [is_mono, &port](auto &item) { - if (target_ports.size() == 1) + if (is_mono) { - logger::get()->debug("[patchbay] (relink) {} is mono", item.id); return true; } - /* - * Sometimes, for whatever reason, the "audio.channel" of a venmic port may be "UNK", to circumvent any - * issues regarding this we'll fallback to "port.id" - */ - if (item.props["audio.channel"] == "UNK" || port.props["audio.channel"] == "UNK") { return item.props["port.id"] == port.props["port.id"]; @@ -173,25 +219,18 @@ namespace vencord return item.props["audio.channel"] == port.props["audio.channel"]; }; - auto others = source_ports | ranges::views::filter(matching_channel) | ranges::to; - logger::get()->debug("[patchbay] (relink) {} maps to {} other(s)", port.id, others.size()); + auto mapping = + mic_inputs // + | ranges::views::filter(matching_channel) // + | ranges::views::transform([port](const auto &mic_port) { return std::make_pair(mic_port, port); }) // + | ranges::to; - for (auto &other : others) - { - auto link = core->create({other.id, port.id}).get(); + rtn.insert(rtn.end(), mapping.begin(), mapping.end()); - if (!link.has_value()) - { - logger::get()->warn("[patchbay] (relink) failed to link: {}", link.error().message); - return; - } - - logger::get()->debug("[patchbay] (relink) created {}, which maps {}->{}", link->id(), other.id, - port.props["audio.channel"]); - - created.emplace(id, std::move(*link)); - } + logger::get()->debug("[patchbay] (map_ports) {} maps to {} mic port(s)", port.id, mapping.size()); } + + return rtn; } void patchbay::impl::meta_update(std::string_view key, pw::metadata_property prop) @@ -213,29 +252,115 @@ namespace vencord } speaker.emplace(parsed->name); + logger::get()->info(R"([patchbay] (meta_update) speaker name: "{}")", speaker->name); - relink_all(); + reload(); + } + + void patchbay::impl::on_link(std::uint32_t id) + { + if (!speaker || !options.include.empty()) + { + return; + } + + auto &info = links[id]; + + if (info.input.node != speaker->id) + { + logger::get()->trace("[patchbay] (on_link) {} is not connected to speaker but with {}", id, + info.input.node); + return; + } + + const auto node = info.output.node; + auto &output = nodes[node]; // The node emitting sound + + auto match = [&output](const auto &prop) + { + return output.info.props[prop.key] == prop.value; + }; + + if (ranges::any_of(options.exclude, match)) + { + return; + } + + core->update(); + + link(node); + } + + void patchbay::impl::on_node(std::uint32_t id) + { + auto &[info, ports] = nodes[id]; + + if (speaker && info.props["node.name"] == speaker->name) + { + logger::get()->debug("[patchbay] (on_node) speakers are {}", id); + speaker->id = id; + + return; + } + + if (options.include.empty()) + { + return; + } + + if (ports.empty()) + { + logger::get()->debug("[patchbay] (on_node) {} has no ports", id); + return; + } + + auto match = [&info](const auto &prop) + { + return info.props[prop.key] == prop.value; + }; + + if (ranges::any_of(options.exclude, match)) + { + logger::get()->debug("[patchbay] (on_node) {} is excluded", id); + return; + } + + if (!ranges::any_of(options.include, match)) + { + logger::get()->debug("[patchbay] (on_node) {} is not included", id); + return; + } + + created.erase(id); + core->update(); + + link(id); } template <> - void patchbay::impl::add_global(pw::node &node, pw::global &) + void patchbay::impl::handle(pw::node &node, pw::global &global) { - auto id = node.id(); + auto id = global.id; auto props = node.info().props; nodes[id].info = node.info(); - logger::get()->trace(R"([patchbay] (add_global) new node: {} (name: "{}", app: "{}"))", id, props["node.name"], + logger::get()->trace(R"([patchbay] (handle) new node: {} (name: "{}", app: "{}"))", id, props["node.name"], props["application.name"]); - if (options.workaround.empty() || !metadata || !virt_mic) + if (!virt_mic) + { + return; + } + + if (!metadata || options.workaround.empty()) { return on_node(id); } - logger::get()->trace("[patchbay] (add_global) workaround is active ({})", glz::write_json(options.workaround)); + logger::get()->trace("[patchbay] (handle) workaround is active ({})", glz::write_json(options.workaround)); - auto match = [&](const auto &prop) + auto match = [&props](const auto &prop) { return props[prop.key] == prop.value; }; @@ -246,14 +371,12 @@ namespace vencord } const auto serial = virt_mic->info().props["object.serial"]; - - logger::get()->debug("[patchbay] (add_global) applying workaround to {} (mic = {}, serial = {})", id, - virt_mic->id(), serial); + logger::get()->debug("[patchbay] (handle) applying workaround to {} (serial = {})", id, serial); // https://github.com/Vencord/venmic/issues/13#issuecomment-1884975782 metadata->set_property(id, "target.object", "Spa:Id", serial); - metadata->set_property(id, "target.node", "Spa:Id", fmt::format("{}", virt_mic->id())); + metadata->set_property(id, "target.node", "Spa:Id", std::to_string(virt_mic->id())); lettuce_target.emplace(id); options.workaround.clear(); @@ -262,83 +385,67 @@ namespace vencord } template <> - void patchbay::impl::add_global(pw::link &link, pw::global &) + void patchbay::impl::handle(pw::link &link, pw::global &global) { - auto id = link.id(); + auto id = global.id; auto info = link.info(); links[id] = info; logger::get()->trace( - "[patchbay] (add_global) new link: {} (input-node: {}, output-node: {}, input-port: {}, output-port: {})", - link.id(), info.input.node, info.output.node, info.input.port, info.output.port); + "[patchbay] (handle) new link: {} (input-node: {}, output-node: {}, input-port: {}, output-port: {})", id, + info.input.node, info.output.node, info.input.port, info.output.port); on_link(id); } template <> - void patchbay::impl::add_global(pw::port &port, pw::global &) + void patchbay::impl::handle(pw::port &port, pw::global &global) { - auto props = port.info().props; + auto info = port.info(); + auto props = info.props; if (!props.contains("node.id")) { - logger::get()->warn("[patchbay] (add_global) {} has no parent", port.id()); + logger::get()->warn("[patchbay] (handle) {} has no parent", global.id); return; } auto parent = std::stoull(props["node.id"]); - nodes[parent].ports.emplace_back(port.info()); + nodes[parent].ports.emplace_back(std::move(info)); - logger::get()->trace("[patchbay] (add_global) new port: {} with parent {}", port.id(), parent); - - /* - ? Yes. This belongs here, as the node is created **before** the ports. - */ + logger::get()->trace("[patchbay] (handle) new port: {} with parent {}", global.id, parent); + // Check the parent again on_node(parent); } template <> - void patchbay::impl::add_global(pw::metadata &data, pw::global &global) + void patchbay::impl::handle(pw::metadata &data, pw::global &global) { auto props = data.properties(); const auto name = global.props["metadata.name"]; - logger::get()->trace(R"([patchbay] (add_global) new metadata: {} (name: "{}"))", data.id(), name); + logger::get()->trace(R"([patchbay] (handle) new metadata: {} (name: "{}"))", global.id, name); if (name != "default") { return; } - metadata = std::make_unique(std::move(data)); - logger::get()->info("[patchbay] (add_global) found default metadata: {}", metadata->id()); - - auto parsed = glz::read_json(props["default.audio.sink"].value); + metadata = std::make_unique(std::move(data)); + meta_listener = std::make_unique(metadata->listen()); - if (!parsed.has_value()) - { - logger::get()->warn("[patchbay] (add_global) failed to parse speaker"); - return; - } - - listener = std::make_unique(metadata->listen()); - logger::get()->debug("[patchbay] (add_global) speaker name: \"{}\"", parsed->name); - - speaker.emplace(parsed->name); + logger::get()->info("[patchbay] (handle) found default metadata: {}", global.id); - listener->on( - [this](auto... args) + meta_listener->on( + [this](T &&...args) { - meta_update(args...); + meta_update(std::forward(args)...); return 0; }); - for (const auto &[id, info] : nodes) - { - on_node(id); - } + meta_update("default.audio.sink", props["default.audio.sink"]); } template @@ -353,7 +460,7 @@ namespace vencord return; } - add_global(bound.value(), global); + handle(bound.value(), global); } void patchbay::impl::add_global(pw::global &global) @@ -378,96 +485,20 @@ namespace vencord } } - void patchbay::impl::rem_global(std::uint32_t id) + void patchbay::impl::del_global(std::uint32_t id) { nodes.erase(id); + links.erase(id); created.erase(id); } - void patchbay::impl::on_link(std::uint32_t id) - { - if (!options.include.empty() || !speaker) - { - return; - } - - auto &info = links[id]; - - if (info.input.node != speaker->id) - { - logger::get()->trace("[patchbay] (on_link) {} is not connected to speaker but with {}", id, - info.input.node); - return; - } - - // "Output" = the node that is emitting sound - auto &output = nodes[info.output.node]; - - auto match = [&](const auto &prop) - { - return output.info.props[prop.key] == prop.value; - }; - - if (ranges::any_of(options.exclude, match)) - { - return; - } - - core->update(); - - relink(info.output.node); - } - - void patchbay::impl::on_node(std::uint32_t id) - { - auto &[info, ports] = nodes[id]; - - if (speaker && info.props["node.name"] == speaker->name) - { - logger::get()->debug("[patchbay] (on_node) speakers are {}", id); - speaker->id = id; - } - - if (options.include.empty()) - { - return; - } - - auto match = [&](const auto &prop) - { - return info.props[prop.key] == prop.value; - }; - - if (ranges::any_of(options.exclude, match)) - { - logger::get()->debug("[patchbay] (on_node) {} is excluded", id); - return; - } - - if (!ranges::any_of(options.include, match)) - { - logger::get()->debug("[patchbay] (on_node) {} is not included", id); - return; - } - - if (ports.empty()) - { - logger::get()->debug("[patchbay] (on_node) {} has no ports", id); - return; - } - - core->update(); - - relink(id); - } - template <> void patchbay::impl::receive(cr_recipe::sender &sender, list_nodes &req) { static const std::vector required{"application.name", "node.name"}; const auto &props = req.props.empty() ? required : req.props; - auto desireable = [&](auto &item) + auto desireable = [&props](auto &item) { return ranges::all_of(props, [&](const auto &key) { return !item.second.info.props[key].empty(); }); }; @@ -475,7 +506,7 @@ namespace vencord { return item.second.info.output.max > 0; }; - auto to_node = [&](auto &item) + auto to_node = [](auto &item) { return node{item.second.info.props}; }; @@ -488,12 +519,21 @@ namespace vencord /* * Some nodes update their props (metadata) over time, and to avoid binding the node constantly, - * we simply re-bind it to fetch the updates only when needed. + * we simply rebind it to fetch the updates only when needed. */ for (auto &[id, node] : filtered) { - auto updated = registry->bind(id).get(); + auto updated = registry->bind(id).get(); + + if (!updated.has_value()) + { + const auto error = updated.error(); + logger::get()->warn(R"([patchbay] (receive) failed to rebind {}: "{}")", id, error.message); + + continue; + } + node.info.props = updated->info().props; } @@ -514,7 +554,7 @@ namespace vencord options = std::move(req); - relink_all(); + reload(); } template <> @@ -548,7 +588,7 @@ namespace vencord auto listener = registry->listen(); - listener.on([this](std::uint32_t id) { rem_global(id); }); + listener.on([this](std::uint32_t id) { del_global(id); }); listener.on([this](auto global) { add_global(global); }); sender.send(ready{});