#include "fileops.h" #include "uisocket.h" #include "data/project.h" #include "data/graph.h" #include #include #include #include #include #include #include #include #include #include #include #define qs QStringLiteral using Xybrid::Data::Project; using Xybrid::Data::Pattern; using Xybrid::Data::Sample; using Xybrid::Data::Graph; using Xybrid::Data::Node; namespace FileOps = Xybrid::FileOps; 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,2); constexpr const QSize dlgSize(700, 500); } const QString FileOps::Filter::project = qs("Xybrid project (*.xyp);;All files (*)"); const QString FileOps::Filter::node = qs("Xybrid node (*.xyn);;All files (*)"); const QString FileOps::Filter::audioIn = qs("Audio files (*.mp3 *.ogg *.flac *.wav);;MPEG Layer 3 (*.mp3);;All files (*)"); const QString FileOps::Filter::audioOut = qs("Audio files (*.mp3 *.flac);;MPEG Layer 3 (*.mp3);;FLAC (*.flac)"); // only supported formats QString FileOps::showOpenDialog(QWidget* parent, const QString& caption, const QString& directory, const QString& filter) { QFileDialog dlg(parent, caption, directory, filter); dlg.resize(dlgSize); dlg.setFileMode(QFileDialog::ExistingFile); dlg.setAcceptMode(QFileDialog::AcceptOpen); if (!dlg.exec()) return QString(); // canceled auto sf = dlg.selectedFiles().at(0); return sf; } QString FileOps::showSaveAsDialog(QWidget* parent, const QString& caption, const QString& directory, const QString& filter, const QString& suffix) { QFileDialog dlg(parent, caption, directory, filter); dlg.resize(dlgSize); dlg.setDefaultSuffix(suffix); dlg.setFileMode(QFileDialog::AnyFile); dlg.setAcceptMode(QFileDialog::AcceptSave); if (!dlg.exec()) return QString(); // canceled auto sf = dlg.selectedFiles().at(0); return sf; } bool 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[qs("artist")] = project->artist; meta[qs("title")] = project->title; meta[qs("comment")] = project->comment; meta[qs("tempo")] = project->tempo; main[qs("meta")] = meta; } { /* Sequence */ } { QCborArray seq; for (auto& s : project->sequence) seq << s; main[qs("sequence")] = seq; } { /* Patterns */ } { QCborArray ptns; for (auto& p : project->patterns) { QCborMap pm; pm[qs("name")] = p->name; pm[qs("fold")] = p->fold; pm[qs("tempo")] = p->tempo; { QCborArray ts; ts << p->time.beatsPerMeasure << p->time.rowsPerBeat << p->time.ticksPerRow; pm[qs("time")] = ts; } QCborArray chns; bool needsCount = true; for (auto& ch : p->channels) { QCborMap chm; chm[qs("name")] = 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[qs("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[qs("channels")] = chns; ptns << pm; } main[qs("patterns")] = ptns; } { /* Samples */ } { QCborMap smp; for (auto& s : qAsConst(project->samples)) smp[QCborValue(s->uuid)] = s->toCbor(); main[qs("samples")] = smp; } { /* Graph */ } { QCborMap g; project->rootGraph->saveData(g); main[qs("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 FileOps::loadProject(QString fileName, bool asTemplate) { 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) != qs("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 auto project = std::make_shared(); if (!asTemplate) project->fileName = fileName; QCborMap main = root.at(2).toMap(); { /* Project metadata */ } { QCborMap meta = main.value("meta").toMap(); project->artist = meta.value("artist").toString(); project->title = meta.value("title").toString(); project->comment = meta.value("comment").toString(); project->tempo = meta.value("tempo").toDouble(project->tempo); } { /* 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(); p->fold = static_cast(pm.value("fold").toInteger(0)); p->tempo = pm.value("tempo").toDouble(p->tempo); 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(); 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) project->sequence.emplace_back(project.get(), QCborValue(s)); } { /* Samples */ } { QCborMap smp = main.value("samples").toMap(); for (auto it = smp.constBegin(), end = smp.constEnd(); it != end; ++it) { auto s = Sample::fromCbor(it.value(), it.key().toUuid()); s->project = project.get(); project->samples.insert(s->uuid, s); //qDebug() << "loaded sample " << s->name << "of length" << s->length() << "with" << s->numChannels() << "channels"; } } { /* Graph */ } { QCborMap g = main.value("graph").toMap(); if (!g.isEmpty()) project->rootGraph->loadData(g); } return project; } std::shared_ptr FileOps::newProject(bool useTemplate) { std::shared_ptr project; if (useTemplate) { project = loadProject(Config::Directories::userDefaultTemplate, true); if (!project) project = loadProject(":/template/default.xyp", true); } if (!project) { project = std::make_shared(); project->sequence.emplace_back(project->newPattern()); } return project; } bool FileOps::saveNode(std::shared_ptr node, QString fileName) { QFile file(fileName); if (!file.open({QFile::WriteOnly})) return false; Sample::startExport(); // we handle header and let the node handle the rest of its conversion QCborArray root; root << "xybrid:node" << XYBRID_VERSION << node->toCbor(); // and write in any exported samples if (auto v = Sample::finishExport(); !v.empty()) { QCborMap smp; for (auto& s : v) smp[QCborValue(s->uuid)] = s->toCbor(); root << smp; } // write out QCborStreamWriter w(&file); root.toCborValue().toCbor(w); file.close(); return true; } std::shared_ptr 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 if (root.at(3).isMap()) { // node file has samples, load in any we don't already have auto smp = root.at(3).toMap(); auto project = parent->project; for (auto it = smp.constBegin(), end = smp.constEnd(); it != end; ++it) { auto uuid = it.key().toUuid(); if (project->samples.find(uuid) != project->samples.end()) continue; // we already have this; next auto s = Sample::fromCbor(it.value(), uuid); s->project = project; project->samples.insert(s->uuid, s); } emit project->socket->updatePatternLists(); } return Node::fromCbor(root.at(2), parent); // let Node handle the rest }