potato/src/presets/PresetManager.cpp
2026-01-27 21:11:20 -07:00

293 lines
8.8 KiB
C++

#include "presets/PresetManager.h"
#include "pipewire/pipewirecontroller.h"
#include "gui/PipeWireGraphModel.h"
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QVector>
#include <algorithm>
PresetManager::PresetManager(Potato::PipeWireController *controller,
PipeWireGraphModel *model,
QObject *parent)
: QObject(parent)
, m_controller(controller)
, m_model(model)
{
}
bool PresetManager::savePreset(const QString &path) const
{
if (!m_controller || !m_model || path.isEmpty()) {
return false;
}
const QJsonObject root = buildPreset();
const QJsonDocument doc(root);
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
return false;
}
file.write(doc.toJson(QJsonDocument::Indented));
return true;
}
bool PresetManager::loadPreset(const QString &path)
{
if (!m_controller || !m_model || path.isEmpty()) {
return false;
}
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
return false;
}
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
if (!doc.isObject()) {
return false;
}
return applyPreset(doc.object());
}
QJsonObject PresetManager::buildPreset() const
{
QJsonObject root;
root["version"] = QString("1.0");
const QVector<Potato::NodeInfo> nodes = m_controller->nodes();
QHash<uint32_t, Potato::NodeInfo> nodesById;
for (const auto &node : nodes) {
nodesById.insert(node.id, node);
}
QJsonArray virtualDevices;
for (const auto &node : nodes) {
if (node.type != Potato::NodeType::Virtual) {
continue;
}
QJsonObject device;
device["name"] = node.name;
device["description"] = node.description;
device["stable_id"] = node.stableId;
const int channels = std::max(node.inputPorts.size(), node.outputPorts.size());
device["channels"] = channels > 0 ? channels : 2;
device["rate"] = 48000;
device["media_class"] = mediaClassToString(node.mediaClass);
virtualDevices.append(device);
}
root["virtual_devices"] = virtualDevices;
QJsonArray routing;
const QVector<Potato::LinkInfo> links = m_controller->links();
for (const auto &link : links) {
if (!nodesById.contains(link.outputNodeId) || !nodesById.contains(link.inputNodeId)) {
continue;
}
const Potato::NodeInfo &outNode = nodesById.value(link.outputNodeId);
const Potato::NodeInfo &inNode = nodesById.value(link.inputNodeId);
QString outPortName;
QString inPortName;
if (!findPortName(outNode, link.outputPortId, outPortName)) {
continue;
}
if (!findPortName(inNode, link.inputPortId, inPortName)) {
continue;
}
QJsonObject route;
route["source"] = QString("%1:%2").arg(outNode.stableId, outPortName);
route["target"] = QString("%1:%2").arg(inNode.stableId, inPortName);
route["volume"] = 1.0;
route["muted"] = false;
routing.append(route);
}
root["routing"] = routing;
QJsonObject volumes;
QJsonObject mutes;
const QHash<QString, NodeVolumeState> volumeStates = m_model->volumeStates();
for (auto it = volumeStates.cbegin(); it != volumeStates.cend(); ++it) {
volumes[it.key()] = it.value().volume;
mutes[it.key()] = it.value().mute;
}
root["persistent_volumes"] = volumes;
root["persistent_mutes"] = mutes;
root["ui_layout"] = m_model->layoutJson();
return root;
}
bool PresetManager::applyPreset(const QJsonObject &root)
{
const QVector<Potato::NodeInfo> nodes = m_controller->nodes();
QHash<QString, Potato::NodeInfo> nodesByStableId;
for (const auto &node : nodes) {
if (!node.stableId.isEmpty()) {
nodesByStableId.insert(node.stableId, node);
}
}
const QJsonArray virtualDevices = root.value("virtual_devices").toArray();
for (const auto &entry : virtualDevices) {
const QJsonObject device = entry.toObject();
const QString stableId = device.value("stable_id").toString();
const QString name = device.value("name").toString();
const QString description = device.value("description").toString();
const QString mediaClassValue = device.value("media_class").toString();
const int channels = device.value("channels").toInt(2);
const int rate = device.value("rate").toInt(48000);
if (!stableId.isEmpty() && nodesByStableId.contains(stableId)) {
continue;
}
const Potato::MediaClass mediaClass = mediaClassFromString(mediaClassValue);
if (mediaClass == Potato::MediaClass::AudioSource) {
m_controller->createVirtualSource(name, description, channels, rate);
} else {
m_controller->createVirtualSink(name, description, channels, rate);
}
}
const QJsonArray routing = root.value("routing").toArray();
for (const auto &entry : routing) {
const QJsonObject route = entry.toObject();
const QString source = route.value("source").toString();
const QString target = route.value("target").toString();
uint32_t outNodeId = 0;
uint32_t outPortId = 0;
uint32_t inNodeId = 0;
uint32_t inPortId = 0;
if (!parsePortKey(source, outNodeId, outPortId)) {
continue;
}
if (!parsePortKey(target, inNodeId, inPortId)) {
continue;
}
m_controller->createLink(outNodeId, outPortId, inNodeId, inPortId);
}
QHash<QString, NodeVolumeState> volumeStates;
const QJsonObject volumes = root.value("persistent_volumes").toObject();
const QJsonObject mutes = root.value("persistent_mutes").toObject();
for (auto it = volumes.begin(); it != volumes.end(); ++it) {
NodeVolumeState state;
state.volume = static_cast<float>(it.value().toDouble(1.0));
state.mute = mutes.value(it.key()).toBool(false);
volumeStates.insert(it.key(), state);
}
if (!volumeStates.isEmpty()) {
m_model->applyVolumeStates(volumeStates);
}
const QJsonObject layout = root.value("ui_layout").toObject();
if (!layout.isEmpty()) {
m_model->applyLayoutJson(layout);
}
return true;
}
bool PresetManager::findPortName(const Potato::NodeInfo &node, uint32_t portId, QString &portName) const
{
for (const auto &port : node.inputPorts) {
if (port.id == portId) {
portName = port.name;
return true;
}
}
for (const auto &port : node.outputPorts) {
if (port.id == portId) {
portName = port.name;
return true;
}
}
return false;
}
bool PresetManager::parsePortKey(const QString &key, uint32_t &nodeId, uint32_t &portId) const
{
const int split = key.lastIndexOf(':');
if (split <= 0) {
return false;
}
const QString stableId = key.left(split);
const QString portName = key.mid(split + 1);
if (stableId.isEmpty() || portName.isEmpty()) {
return false;
}
const QVector<Potato::NodeInfo> nodes = m_controller->nodes();
for (const auto &node : nodes) {
if (node.stableId != stableId) {
continue;
}
for (const auto &port : node.inputPorts) {
if (port.name == portName) {
nodeId = node.id;
portId = port.id;
return true;
}
}
for (const auto &port : node.outputPorts) {
if (port.name == portName) {
nodeId = node.id;
portId = port.id;
return true;
}
}
}
return false;
}
QString PresetManager::portKey(const Potato::NodeInfo &node, const Potato::PortInfo &port) const
{
return QString("%1:%2").arg(node.stableId, port.name);
}
QString PresetManager::mediaClassToString(Potato::MediaClass mediaClass) const
{
switch (mediaClass) {
case Potato::MediaClass::AudioSink:
return QString("Audio/Sink");
case Potato::MediaClass::AudioSource:
return QString("Audio/Source");
case Potato::MediaClass::AudioDuplex:
return QString("Audio/Duplex");
case Potato::MediaClass::Stream:
return QString("Stream");
default:
return QString();
}
}
Potato::MediaClass PresetManager::mediaClassFromString(const QString &value) const
{
if (value.contains("Audio/Source")) {
return Potato::MediaClass::AudioSource;
}
if (value.contains("Audio/Duplex")) {
return Potato::MediaClass::AudioDuplex;
}
if (value.contains("Audio/Sink")) {
return Potato::MediaClass::AudioSink;
}
if (value.contains("Stream")) {
return Potato::MediaClass::Stream;
}
return Potato::MediaClass::Unknown;
}