xybrid/xybrid/fileops.cpp

254 lines
9.3 KiB
C++

#include "fileops.h"
#include "uisocket.h"
#include "data/project.h"
#include "data/graph.h"
#include <QDebug>
#include <QFile>
#include <QCborMap>
#include <QCborArray>
#include <QCborStreamReader>
#include <QCborStreamWriter>
#include <QUndoStack>
using Xybrid::Data::Project;
using Xybrid::Data::Pattern;
using Xybrid::Data::Graph;
using Xybrid::Data::Node;
namespace {
constexpr uint32_t packedVersion(uint8_t major = 0, uint8_t minor = 0, uint8_t revision = 0, uint8_t wip = 0) {
return + (static_cast<uint32_t>(wip))
+ (static_cast<uint32_t>(revision)<<8)
+ (static_cast<uint32_t>(minor)<<16)
+ (static_cast<uint32_t>(major)<<24);
}
constexpr const uint32_t XYBRID_VERSION = packedVersion(0,0,0,1);
}
bool Xybrid::FileOps::saveProject(std::shared_ptr<Project> project, QString fileName) {
if (!fileName.isEmpty()) project->fileName = fileName;
else fileName = project->fileName;
if (fileName.isEmpty()) return false; // fail
QFile file(fileName);
if (!file.open(QFile::WriteOnly)) return false;
// header
QCborArray root;
root << "xybrid:project" << XYBRID_VERSION;
{ /* Main body */ } {
QCborMap main;
{ /* Project metadata */ } {
QCborMap meta;
meta.insert(QString("artist"), QString::fromStdString(project->artist));
meta.insert(QString("title"), QString::fromStdString(project->title));
main.insert(QString("meta"), meta);
}
{ /* Sequence */ } {
QCborArray seq;
for (auto s : project->sequence) {
if (!s) seq << -1;
else seq << static_cast<int>(s->index);
}
main.insert(QString("sequence"), seq);
}
{ /* Patterns */ } {
QCborArray ptns;
for (auto p : project->patterns) {
QCborMap pm;
pm.insert(QString("name"), QString::fromStdString(p->name));
{
QCborArray ts;
ts << p->time.beatsPerMeasure << p->time.rowsPerBeat << p->time.ticksPerRow;
pm.insert(QString("time"), ts);
}
QCborArray chns;
bool needsCount = true;
for (auto& ch : p->channels) {
QCborMap chm;
chm.insert(QString("name"), QString::fromStdString(ch.name));
QCborArray rows;
int skipped = 0;
for (auto& r : ch.rows) {
if (r.isEmpty()) {
skipped++; // compact empty rows into a simple number
continue;
} else if (skipped > 0) { // ...and insert said number only if there's data after
rows << skipped;
skipped = 0;
}
QCborArray row;
row << r.port << r.note;
if (r.params) for (auto p : *r.params) row << p[0] << p[1];
rows << row;
}
if (!ch.rows.back().isEmpty()) needsCount = false; // omit extra count if any channel has data on the last row
chm.insert(QString("rows"), rows);
chns << chm;
}
if (needsCount) chns << p->rows; // if no pattern data reaches the last row, store a manual count
if (chns.size() != 0) pm.insert(QString("channels"), chns);
ptns << pm;
}
main.insert(QString("patterns"), ptns);
}
{ /* Graph */ } {
QCborMap g;
project->rootGraph->saveData(g);
main.insert(QString("graph"), g);
}
root << main;
}
// write out
QCborStreamWriter w(&file);
root.toCborValue().toCbor(w);
file.close();
if (project->socket && project->socket->undoStack) project->socket->undoStack->setClean();
return true;
}
std::shared_ptr<Xybrid::Data::Project> Xybrid::FileOps::loadProject(QString fileName) {
QCborArray root;
{
QFile file(fileName);
if (!file.open(QFile::ReadOnly)) return nullptr;
QCborStreamReader read(&file);
// if it's not an array it'll just return an empty which will fail the header check anyway
root = QCborValue::fromCbor(read).toArray();
file.close(); // don't want to leave this hanging any longer than necessary
}
// header and sanity checks
if (root.at(0) != QString("xybrid:project")) return nullptr; // not a project
if (auto v = root.at(1); !v.isInteger() || v.toInteger() > XYBRID_VERSION) return nullptr; // invalid version or too new
if (!root.at(2).isMap()) return nullptr; // so close, but... nope
// intentionally allocate project and control block separately
std::shared_ptr<Project> project(new Project());
project->fileName = fileName;
QCborMap main = root.at(2).toMap();
{ /* Project metadata */ } {
QCborMap meta = main.value("meta").toMap();
project->artist = meta.value("artist").toString().toStdString();
project->title = meta.value("title").toString().toStdString();
}
{ /* Patterns */ } {
QCborArray ptns = main.value("patterns").toArray();
project->patterns.reserve(static_cast<size_t>(ptns.size()));
for (auto pm_ : ptns) {
auto pm = pm_.toMap();
auto p = std::make_shared<Pattern>(pm.value("rows").toInteger(1));
p->project = project.get();
project->patterns.push_back(p);
p->name = pm.value("name").toString().toStdString();
if (auto ts = pm.value("time").toArray(); !ts.empty())
p->time = Data::TimeSignature(static_cast<int>(ts[0].toInteger()), static_cast<int>(ts[1].toInteger()), static_cast<int>(ts[2].toInteger()));
auto chns = pm.value("channels").toArray();
for (auto chm_ : chns) {
if (chm_.isInteger()) { // can have a number of rows encoded as part of the channels map
p->rows = std::max(p->rows, static_cast<int>(chm_.toInteger()));
continue;
}
auto chm = chm_.toMap();
auto& ch = p->channels.emplace_back();
ch.name = chm.value("name").toString().toStdString();
auto rows = chm.value("rows").toArray();
ch.rows.reserve(static_cast<size_t>(rows.size()));
for (auto row_ : rows) {
if (row_.isInteger()) { // empty space encoded as a number of rows to skip
for (auto i = row_.toInteger(); i > 0; i--) ch.rows.emplace_back();
continue;
}
auto& r = ch.rows.emplace_back();
if (row_.isNull()) continue;
auto row = row_.toArray();
r.port = static_cast<int16_t>(row.at(0).toInteger(-1));
r.note = static_cast<int16_t>(row.at(1).toInteger(-1));
for (int i = 2; i < row.size(); i += 2) r.addParam(static_cast<char>(row.at(i).toInteger(' ')), static_cast<unsigned char>(row.at(i+1).toInteger()));
}
p->rows = std::max(p->rows, static_cast<int>(ch.rows.size()));
}
p->setLength(p->rows);
}
project->updatePatternIndices();
}
{ /* Sequence */ } {
QCborArray seq = main.value("sequence").toArray();
project->sequence.reserve(static_cast<size_t>(seq.size()));
for (auto s : seq) {
size_t ss = static_cast<size_t>(s.toInteger());
if (ss >= project->patterns.size()) project->sequence.push_back(nullptr);
else project->sequence.push_back(project->patterns[ss].get());
}
}
{ /* Graph */ } {
QCborMap g = main.value("graph").toMap();
if (!g.isEmpty()) project->rootGraph->loadData(g);
}
return project;
}
bool Xybrid::FileOps::saveNode(std::shared_ptr<Xybrid::Data::Node> node, QString fileName) {
QFile file(fileName);
if (!file.open(QFile::WriteOnly)) return false;
// we handle header and let the node handle the rest of its conversion
QCborArray root;
root << "xybrid:node" << XYBRID_VERSION << node->toCbor();
// write out
QCborStreamWriter w(&file);
root.toCborValue().toCbor(w);
file.close();
return true;
}
std::shared_ptr<Xybrid::Data::Node> Xybrid::FileOps::loadNode(QString fileName, std::shared_ptr<Graph> parent) {
QCborArray root;
{
QFile file(fileName);
if (!file.open(QFile::ReadOnly)) return nullptr;
QCborStreamReader read(&file);
// if it's not an array it'll just return an empty which will fail the header check anyway
root = QCborValue::fromCbor(read).toArray();
file.close(); // don't want to leave this hanging any longer than necessary
}
// header and sanity checks
if (root.at(0) != QString("xybrid:node")) return nullptr; // not a node
if (auto v = root.at(1); !v.isInteger() || v.toInteger() > XYBRID_VERSION) return nullptr; // invalid version or too new
if (!root.at(2).isMap()) return nullptr; // so close, but... nope
return Node::fromCbor(root.at(2), parent); // let Node handle the rest
}