Files
soundboard/pipewire.cpp

296 lines
6.9 KiB
C++

#include "pipewire.hpp"
#include <algorithm>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
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<Node> Graph::get_input_apps() const
{
std::vector<Node> apps;
for (const auto& [id, node] : m_nodes) {
if (node.media_class == "Stream/Input/Audio") {
apps.push_back(node);
}
}
return apps;
}
std::optional<Node> 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<Port> Graph::get_ports_for_node(int node_id, const QString& direction) const
{
std::vector<Port> 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<Link> Graph::get_links_from_node(int output_node_id) const
{
std::vector<Link> links;
for (const auto& [id, link] : m_links) {
if (link.output_node_id == output_node_id) {
links.push_back(link);
}
}
return links;
}
std::vector<Node> Graph::get_all_sinks() const
{
std::vector<Node> 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<QProcess>();
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> 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