#include "presets/PresetManager.h" #include "pipewire/pipewirecontroller.h" #include "gui/PipeWireGraphModel.h" #include #include #include #include #include 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 nodes = m_controller->nodes(); QHash 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 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 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 nodes = m_controller->nodes(); QHash 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 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(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 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; }