293 lines
8.8 KiB
C++
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;
|
|
}
|