#include "pipewire.hpp" #include #include #include #include namespace Pipewire { QString Node::get_display_name() const { if (!application_name.isEmpty()) return application_name; if (!node_name.isEmpty()) return node_name; return "Unknown App"; } Graph::Graph() { refresh(); } bool Graph::refresh() { QProcess process; process.start("pw-dump"); if (!process.waitForFinished()) { return false; } QJsonParseError error; QJsonDocument json = QJsonDocument::fromJson(process.readAllStandardOutput(), &error); if (error.error != QJsonParseError::NoError) { return false; } m_nodes.clear(); m_ports.clear(); m_links.clear(); for (const QJsonValue& value : json.array()) { QJsonObject obj = value.toObject(); QString type = obj["type"].toString(); int id = obj["id"].toInt(); QJsonObject props = obj["info"].toObject()["props"].toObject(); if (type == "PipeWire:Interface:Node") { m_nodes[id] = { id, props["node.name"].toString(), props["application.name"].toString(), props["media.class"].toString() }; } else if (type == "PipeWire:Interface:Port") { m_ports[id] = { id, props["node.id"].toInt(), props["port.direction"].toString(), props["audio.channel"].toString() }; } else if (type == "PipeWire:Interface:Link") { m_links[id] = { id, props["link.output.node"].toInt(), props["link.input.node"].toInt() }; } } return true; } std::vector Graph::get_input_apps() const { std::vector apps; for (const auto& [id, node] : m_nodes) { if (node.media_class == "Stream/Input/Audio") { apps.push_back(node); } } return apps; } std::optional Graph::get_node_by_name(const QString& name) const { for (const auto& [id, node] : m_nodes) { if (node.node_name == name) return node; } return std::nullopt; } std::vector Graph::get_ports_for_node(int node_id, const QString& direction) const { std::vector ports; for (const auto& [id, port] : m_ports) { if (port.node_id == node_id && port.direction == direction) { ports.push_back(port); } } return ports; } std::vector Graph::get_links_from_node(int output_node_id) const { std::vector links; for (const auto& [id, link] : m_links) { if (link.output_node_id == output_node_id) { links.push_back(link); } } return links; } std::vector Graph::get_all_sinks() const { std::vector sinks; for (const auto& [id, node] : m_nodes) { if (node.media_class == "Audio/Sink") { sinks.push_back(node); } } return sinks; } Player::~Player() { stop_all(); } void Player::play_file(const QString& file_path, const QString& target_sink/* = "auto" */, float speed/* = 1.0f */) { QString target = target_sink; if (target_sink != "auto") { target = "pipewire/" + target_sink; } QStringList args; args << "--vo=null" << "--no-video" << "--audio-pitch-correction=no" << "--audio-device=" + target << "--speed=" + QString::number(speed, 'f', 2) << file_path; auto process = std::make_unique(); process->start("mpv", args); m_playing_processes.push_back(std::move(process)); } void Player::stop_all() { for (const auto& process : m_playing_processes) { if (process->state() == QProcess::Running) { process->terminate(); process->waitForFinished(); } } m_playing_processes.clear(); } std::optional Sink::create(const QString& name, Graph& graph) { QProcess process; process.start("pactl", { "load-module", "module-null-sink", "sink_name=" + name, "sink_properties=device.description=" + name, }); if (!process.waitForFinished()) { return std::nullopt; } QByteArray output = process.readAllStandardOutput().trimmed(); bool ok = false; int pulse_audio_id = output.toInt(&ok); if (!ok) { return std::nullopt; } if (!graph.refresh()) { destroy(pulse_audio_id); return std::nullopt; } auto node = graph.get_node_by_name(name); if (!node) { destroy(pulse_audio_id); return std::nullopt; } Sink sink; sink.m_name = name; sink.m_pulse_audio_id = pulse_audio_id; sink.m_pipewire_id = node->id; return sink; } Sink::~Sink() { destroy(m_pulse_audio_id); } Sink::Sink(Sink&& other) : m_name(std::move(other.m_name)), m_pulse_audio_id(std::exchange(other.m_pulse_audio_id, -1)), m_pipewire_id(std::exchange(other.m_pipewire_id, -1)) { } Sink& Sink::operator=(Sink&& other) { if (this == &other) { return *this; } destroy(m_pulse_audio_id); m_name = std::move(other.m_name); m_pulse_audio_id = std::exchange(other.m_pulse_audio_id, -1); m_pipewire_id = std::exchange(other.m_pipewire_id, -1); return *this; } int Sink::get_id() const { return m_pipewire_id; } QString Sink::get_name() const { return m_name; } void Sink::destroy(int pulse_audio_id) { if (pulse_audio_id == -1) { return; } QProcess process; process.start("pactl", { "unload-module", QString::number(pulse_audio_id) }); process.waitForFinished(); } bool link_ports(int out_port_id, int in_port_id) { QProcess process; process.start("pw-link", { QString::number(out_port_id), QString::number(in_port_id) }); return process.waitForFinished() && process.exitCode() == 0; } bool link_nodes(int out_node_id, int in_node_id, const Graph& graph) { auto out_ports = graph.get_ports_for_node(out_node_id, "out"); auto in_ports = graph.get_ports_for_node(in_node_id, "in"); for (const Port& out_port : out_ports) { for (const Port& in_port : in_ports) { if (out_port.channel != in_port.channel) { continue; } if (!link_ports(out_port.id, in_port.id)) { return false; } } } return true; } bool destroy_link(int link_id) { QProcess process; process.start("pw-cli", { "destroy", QString::number(link_id) }); return process.waitForFinished() && process.exitCode() == 0; } void set_volume(const QString& target_node, int volume_percent) { volume_percent = std::max(0, std::min(100, volume_percent)); QProcess process; process.startDetached("pactl", { "set-sink-volume", target_node, QString::number(volume_percent) + "%" }); } } // namespace Pipewire