#include "fileops.h" #include "uisocket.h" #include "data/project.h" #include "data/graph.h" #include #include #include #include #include #include #include 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(wip)) + (static_cast(revision)<<8) + (static_cast(minor)<<16) + (static_cast(major)<<24); } constexpr const uint32_t XYBRID_VERSION = packedVersion(0,0,0,1); } bool Xybrid::FileOps::saveProject(std::shared_ptr 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(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::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(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(ptns.size())); for (auto pm_ : ptns) { auto pm = pm_.toMap(); auto p = std::make_shared(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(ts[0].toInteger()), static_cast(ts[1].toInteger()), static_cast(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(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(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(row.at(0).toInteger(-1)); r.note = static_cast(row.at(1).toInteger(-1)); for (int i = 2; i < row.size(); i += 2) r.addParam(static_cast(row.at(i).toInteger(' ')), static_cast(row.at(i+1).toInteger())); } p->rows = std::max(p->rows, static_cast(ch.rows.size())); } p->setLength(p->rows); } project->updatePatternIndices(); } { /* Sequence */ } { QCborArray seq = main.value("sequence").toArray(); project->sequence.reserve(static_cast(seq.size())); for (auto s : seq) { size_t ss = static_cast(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 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::FileOps::loadNode(QString fileName, std::shared_ptr 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 }