From 3f265cdb461080c7360c87681dee1daf3d8375f2 Mon Sep 17 00:00:00 2001 From: Jack Andersen Date: Mon, 16 Jul 2018 18:48:38 -1000 Subject: [PATCH] Initial ProjectModel implementation --- Editor/CMakeLists.txt | 2 +- Editor/Common.cpp | 10 +- Editor/Common.hpp | 26 +- Editor/MainWindow.cpp | 201 +++++++++++++--- Editor/MainWindow.hpp | 52 ++++ Editor/MainWindow.ui | 3 + Editor/ProjectModel.cpp | 249 +++++++++++++++----- Editor/ProjectModel.hpp | 147 +++++++++--- Editor/resources/IconADSR.svg | 81 +++++++ Editor/resources/IconCurve.svg | 87 +++++++ Editor/resources/IconGroup.svg | 74 ++++++ Editor/resources/IconLayers.svg | 10 +- Editor/resources/IconNewADSR.svg | 86 +++++++ Editor/resources/IconNewCurve.svg | 92 ++++++++ Editor/resources/IconNewGroup.svg | 78 ++++++ Editor/resources/IconNewSongGroup.svg | 17 +- Editor/resources/IconNewSoundGroup.svg | 24 +- Editor/resources/IconSongGroup.svg | 25 +- Editor/resources/IconSoundGroup.svg | 46 ++-- Editor/resources/resources.qrc | 6 + include/amuse/AudioGroup.hpp | 42 ++++ include/amuse/AudioGroupPool.hpp | 42 ++-- include/amuse/AudioGroupProject.hpp | 17 +- include/amuse/AudioGroupSampleDirectory.hpp | 5 + include/amuse/Common.hpp | 21 +- lib/AudioGroupProject.cpp | 80 +++++-- lib/AudioGroupSampleDirectory.cpp | 30 ++- lib/Common.cpp | 4 + lib/Voice.cpp | 4 +- 29 files changed, 1337 insertions(+), 224 deletions(-) create mode 100644 Editor/resources/IconADSR.svg create mode 100644 Editor/resources/IconCurve.svg create mode 100644 Editor/resources/IconGroup.svg create mode 100644 Editor/resources/IconNewADSR.svg create mode 100644 Editor/resources/IconNewCurve.svg create mode 100644 Editor/resources/IconNewGroup.svg diff --git a/Editor/CMakeLists.txt b/Editor/CMakeLists.txt index 2410f47..1d7379f 100644 --- a/Editor/CMakeLists.txt +++ b/Editor/CMakeLists.txt @@ -54,4 +54,4 @@ target_link_libraries(amuse-gui ${PLAT_LIBS} ${Qt5Network_LIBRARIES} ${Qt5Xml_LIBRARIES} ${Qt5Svg_LIBRARIES} - amuse boo ${BOO_SYS_LIBS} logvisor zeus athena-core athena-libyaml xxhash ${ZLIB_LIBRARIES} ${LZO_LIB}) + amuse boo ${BOO_SYS_LIBS} logvisor athena-core athena-libyaml xxhash ${ZLIB_LIBRARIES} ${LZO_LIB}) diff --git a/Editor/Common.cpp b/Editor/Common.cpp index dede7b9..36da45b 100644 --- a/Editor/Common.cpp +++ b/Editor/Common.cpp @@ -20,18 +20,18 @@ QString SysStringToQString(const boo::SystemString& str) #endif } -bool MkPath(const QString& path, QWidget* parent) +bool MkPath(const QString& path, UIMessenger& messenger) { QFileInfo fInfo(path); - return MkPath(fInfo.dir(), fInfo.fileName(), parent); + return MkPath(fInfo.dir(), fInfo.fileName(), messenger); } -bool MkPath(const QDir& dir, const QString& file, QWidget* parent) +bool MkPath(const QDir& dir, const QString& file, UIMessenger& messenger) { if (!dir.mkpath(file)) { - QString msg = QString(parent->tr("A directory at '%1/%2' could not be created.")).arg(dir.path()).arg(file); - QMessageBox::critical(parent, parent->tr("Unable to create directory"), msg); + QString msg = QString(QObject::tr("A directory at '%1/%2' could not be created.")).arg(dir.path()).arg(file); + messenger.critical(QObject::tr("Unable to create directory"), msg); return false; } return true; diff --git a/Editor/Common.hpp b/Editor/Common.hpp index c731b87..3c5ef16 100644 --- a/Editor/Common.hpp +++ b/Editor/Common.hpp @@ -4,11 +4,33 @@ #include "boo/System.hpp" #include #include +#include + +class UIMessenger : public QObject +{ + Q_OBJECT +public: + using QObject::QObject; +signals: + QMessageBox::StandardButton information(const QString &title, + const QString &text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + QMessageBox::StandardButton question(const QString &title, + const QString &text, QMessageBox::StandardButtons buttons = + QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No), + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + QMessageBox::StandardButton warning(const QString &title, + const QString &text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + QMessageBox::StandardButton critical(const QString &title, + const QString &text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); +}; boo::SystemString QStringToSysString(const QString& str); QString SysStringToQString(const boo::SystemString& str); -bool MkPath(const QString& path, QWidget* parent); -bool MkPath(const QDir& dir, const QString& file, QWidget* parent); +bool MkPath(const QString& path, UIMessenger& messenger); +bool MkPath(const QDir& dir, const QString& file, UIMessenger& messenger); #endif //AMUSE_COMMON_HPP diff --git a/Editor/MainWindow.cpp b/Editor/MainWindow.cpp index 8cf58c1..ded9bc0 100644 --- a/Editor/MainWindow.cpp +++ b/Editor/MainWindow.cpp @@ -3,15 +3,27 @@ #include #include #include +#include #include #include "amuse/ContainerRegistry.hpp" #include "Common.hpp" +static void connectMessenger(UIMessenger* messenger, Qt::ConnectionType type) +{ + +} + MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), - m_undoStack(new QUndoStack(this)) + m_mainMessenger(this), + m_undoStack(new QUndoStack(this)), + m_backgroundThread(this) { + m_backgroundThread.start(); + m_ui.setupUi(this); + connectMessenger(&m_mainMessenger, Qt::DirectConnection); + m_ui.actionNew_Project->setShortcut(QKeySequence::New); connect(m_ui.actionNew_Project, SIGNAL(triggered()), this, SLOT(newAction())); m_ui.actionOpen_Project->setShortcut(QKeySequence::Open); @@ -55,9 +67,39 @@ MainWindow::MainWindow(QWidget* parent) MainWindow::~MainWindow() { + m_backgroundThread.quit(); + m_backgroundThread.wait(); printf("IM DYING\n"); } +void MainWindow::connectMessenger(UIMessenger* messenger, Qt::ConnectionType type) +{ + connect(messenger, SIGNAL(information(const QString&, + const QString&, QMessageBox::StandardButtons, + QMessageBox::StandardButton)), + this, SLOT(msgInformation(const QString&, + const QString&, QMessageBox::StandardButtons, + QMessageBox::StandardButton)), type); + connect(messenger, SIGNAL(question(const QString&, + const QString&, QMessageBox::StandardButtons, + QMessageBox::StandardButton)), + this, SLOT(msgQuestion(const QString&, + const QString&, QMessageBox::StandardButtons, + QMessageBox::StandardButton)), type); + connect(messenger, SIGNAL(warning(const QString&, + const QString&, QMessageBox::StandardButtons, + QMessageBox::StandardButton)), + this, SLOT(msgWarning(const QString&, + const QString&, QMessageBox::StandardButtons, + QMessageBox::StandardButton)), type); + connect(messenger, SIGNAL(critical(const QString&, + const QString&, QMessageBox::StandardButtons, + QMessageBox::StandardButton)), + this, SLOT(msgCritical(const QString&, + const QString&, QMessageBox::StandardButtons, + QMessageBox::StandardButton)), type); +} + bool MainWindow::setProjectPath(const QString& path) { if (m_projectModel && m_projectModel->path() == path) @@ -138,14 +180,52 @@ void MainWindow::refreshMIDIIO() m_ui.menuMIDI->addAction(tr("No MIDI Devices Found"))->setEnabled(false); } +void MainWindow::startBackgroundTask(const QString& windowTitle, const QString& label, + std::function&& task) +{ + assert(m_backgroundTask == nullptr && "existing background process"); + setEnabled(false); + + m_backgroundTask = new BackgroundTask(std::move(task)); + m_backgroundTask->moveToThread(&m_backgroundThread); + + m_backgroundDialog = new QProgressDialog(this); + m_backgroundDialog->setWindowTitle(windowTitle); + m_backgroundDialog->setLabelText(label); + m_backgroundDialog->setMinimumWidth(300); + m_backgroundDialog->setAutoClose(false); + m_backgroundDialog->setAutoReset(false); + m_backgroundDialog->setMinimumDuration(0); + m_backgroundDialog->setMinimum(0); + m_backgroundDialog->setMaximum(0); + m_backgroundDialog->setValue(0); + + connect(m_backgroundTask, SIGNAL(setMinimum(int)), + m_backgroundDialog, SLOT(setMinimum(int)), Qt::QueuedConnection); + connect(m_backgroundTask, SIGNAL(setMaximum(int)), + m_backgroundDialog, SLOT(setMaximum(int)), Qt::QueuedConnection); + connect(m_backgroundTask, SIGNAL(setValue(int)), + m_backgroundDialog, SLOT(setValue(int)), Qt::QueuedConnection); + connect(m_backgroundTask, SIGNAL(setLabelText(const QString&)), + m_backgroundDialog, SLOT(setLabelText(const QString&)), Qt::QueuedConnection); + connect(m_backgroundTask, SIGNAL(finished()), + this, SLOT(onBackgroundTaskFinished()), Qt::QueuedConnection); + m_backgroundDialog->open(m_backgroundTask, SLOT(cancel())); + + connectMessenger(&m_backgroundTask->uiMessenger(), Qt::BlockingQueuedConnection); + + QMetaObject::invokeMethod(m_backgroundTask, "run", Qt::QueuedConnection); +} + void MainWindow::newAction() { QString path = QFileDialog::getSaveFileName(this, tr("New Project")); if (path.isEmpty()) return; - if (!MkPath(path, this)) + if (!MkPath(path, m_mainMessenger)) + return; + if (!setProjectPath(path)) return; - setProjectPath(path); } void MainWindow::openAction() @@ -153,7 +233,10 @@ void MainWindow::openAction() QString path = QFileDialog::getExistingDirectory(this, tr("Open Project")); if (path.isEmpty()) return; - setProjectPath(path); + if (!setProjectPath(path)) + return; + + } void MainWindow::importAction() @@ -176,8 +259,8 @@ void MainWindow::importAction() int impMode = QMessageBox::question(this, tr("Sample Import Mode"), tr("Amuse can import samples as WAV files for ease of editing, " "import original compressed data for lossless repacking, or both. " - "Exporting the project will prefer compressed files over WAVs, so " - "be sure to delete the compressed version if you edit the WAV."), + "Exporting the project will prefer whichever version was modified " + "most recently."), tr("Import Compressed"), tr("Import WAVs"), tr("Import Both")); switch (impMode) @@ -189,6 +272,7 @@ void MainWindow::importAction() default: return; } + ProjectModel::ImportMode importMode = ProjectModel::ImportMode(impMode); /* Special handling for raw groups - gather sibling groups in filesystem */ if (tp == amuse::ContainerRegistry::Type::Raw4) @@ -211,25 +295,36 @@ void MainWindow::importAction() QFileInfo fInfo(path); QString newPath = QFileInfo(fInfo.dir(), newName).filePath(); - if (!MkPath(fInfo.dir(), newName, this)) + if (!MkPath(fInfo.dir(), newName, m_mainMessenger)) return; if (!setProjectPath(newPath)) return; } - QDir dir = QFileInfo(path).dir(); - QStringList filters; - filters << "*.proj" << "*.pro"; - QStringList files = dir.entryList(filters, QDir::Files); - for (const QString& fPath : files) + ProjectModel* model = m_projectModel; + startBackgroundTask(tr("Importing"), tr("Scanning Project"), + [model, path, importMode](BackgroundTask& task) { - auto data = amuse::ContainerRegistry::LoadContainer(QStringToSysString(dir.filePath(fPath)).c_str()); - for (auto& p : data) - if (!m_projectModel->importGroupData(SysStringToQString(p.first), std::move(p.second))) - return; - } - m_projectModel->extractSamples(ProjectModel::ImportMode(impMode), this); - m_projectModel->saveToFile(this); + QDir dir = QFileInfo(path).dir(); + QStringList filters; + filters << "*.proj" << "*.pro"; + QStringList files = dir.entryList(filters, QDir::Files); + for (const QString& fPath : files) + { + auto data = amuse::ContainerRegistry::LoadContainer(QStringToSysString(dir.filePath(fPath)).c_str()); + for (auto& p : data) + { + task.setLabelText(tr("Importing %1").arg(SysStringToQString(p.first))); + if (task.isCanceled()) + return; + if (!model->importGroupData(SysStringToQString(p.first), p.second, + importMode, task.uiMessenger())) + return; + } + } + }); + + return; } else if (scanMode == QMessageBox::No) @@ -245,20 +340,31 @@ void MainWindow::importAction() { QFileInfo fInfo(path); QString newPath = QFileInfo(fInfo.dir(), fInfo.completeBaseName()).filePath(); - if (!MkPath(fInfo.dir(), fInfo.completeBaseName(), this)) + if (!MkPath(fInfo.dir(), fInfo.completeBaseName(), m_mainMessenger)) return; if (!setProjectPath(newPath)) return; } - /* Handle single container */ - auto data = amuse::ContainerRegistry::LoadContainer(QStringToSysString(path).c_str()); - for (auto& p : data) - if (!m_projectModel->importGroupData(SysStringToQString(p.first), std::move(p.second))) - return; - - m_projectModel->extractSamples(ProjectModel::ImportMode(impMode), this); - m_projectModel->saveToFile(this); + ProjectModel* model = m_projectModel; + startBackgroundTask(tr("Importing"), tr("Scanning Project"), + [model, path, importMode](BackgroundTask& task) + { + /* Handle single container */ + auto data = amuse::ContainerRegistry::LoadContainer(QStringToSysString(path).c_str()); + task.setMaximum(int(data.size())); + int curVal = 0; + for (auto& p : data) + { + task.setLabelText(tr("Importing %1").arg(SysStringToQString(p.first))); + if (task.isCanceled()) + return; + if (!model->importGroupData(SysStringToQString(p.first), p.second, + importMode, task.uiMessenger())) + return; + task.setValue(++curVal); + } + }); } void MainWindow::exportAction() @@ -395,3 +501,42 @@ void MainWindow::onTextDelete() le->del(); } } + +void MainWindow::onBackgroundTaskFinished() +{ + m_backgroundDialog->reset(); + m_backgroundDialog->deleteLater(); + m_backgroundDialog = nullptr; + m_backgroundTask->deleteLater(); + m_backgroundTask = nullptr; + m_projectModel->ensureModelData(); + setEnabled(true); +} + +QMessageBox::StandardButton MainWindow::msgInformation(const QString &title, + const QString &text, QMessageBox::StandardButtons buttons, + QMessageBox::StandardButton defaultButton) +{ + return QMessageBox::information(this, title, text, buttons, defaultButton); +} + +QMessageBox::StandardButton MainWindow::msgQuestion(const QString &title, + const QString &text, QMessageBox::StandardButtons buttons, + QMessageBox::StandardButton defaultButton) +{ + return QMessageBox::question(this, title, text, buttons, defaultButton); +} + +QMessageBox::StandardButton MainWindow::msgWarning(const QString &title, + const QString &text, QMessageBox::StandardButtons buttons, + QMessageBox::StandardButton defaultButton) +{ + return QMessageBox::warning(this, title, text, buttons, defaultButton); +} + +QMessageBox::StandardButton MainWindow::msgCritical(const QString &title, + const QString &text, QMessageBox::StandardButtons buttons, + QMessageBox::StandardButton defaultButton) +{ + return QMessageBox::critical(this, title, text, buttons, defaultButton); +} diff --git a/Editor/MainWindow.hpp b/Editor/MainWindow.hpp index 6fc5ad6..84cf248 100644 --- a/Editor/MainWindow.hpp +++ b/Editor/MainWindow.hpp @@ -3,6 +3,8 @@ #include #include +#include +#include #include "ui_MainWindow.h" #include "amuse/Engine.hpp" #include "amuse/BooBackend.hpp" @@ -15,10 +17,35 @@ class MainWindow; class AudioGroupModel; +class BackgroundTask : public QObject +{ + Q_OBJECT + std::function m_task; + UIMessenger m_threadMessenger; + bool m_cancelled = false; +public: + explicit BackgroundTask(std::function&& task) + : m_task(std::move(task)), m_threadMessenger(this) {} + bool isCanceled() const { QCoreApplication::processEvents(); return m_cancelled; } + UIMessenger& uiMessenger() { return m_threadMessenger; } + +signals: + void setMinimum(int minimum); + void setMaximum(int maximum); + void setValue(int value); + void setLabelText(const QString& text); + void finished(); + +public slots: + void run() { m_task(*this); emit finished(); } + void cancel() { m_cancelled = true; } +}; + class MainWindow : public QMainWindow { Q_OBJECT Ui::MainWindow m_ui; + UIMessenger m_mainMessenger; ProjectModel* m_projectModel = nullptr; AudioGroupModel* m_focusAudioGroup = nullptr; @@ -38,11 +65,20 @@ class MainWindow : public QMainWindow QMetaObject::Connection m_deleteConn; QMetaObject::Connection m_canEditConn; + BackgroundTask* m_backgroundTask = nullptr; + QProgressDialog* m_backgroundDialog = nullptr; + QThread m_backgroundThread; + + void connectMessenger(UIMessenger* messenger, Qt::ConnectionType type); + bool setProjectPath(const QString& path); void setFocusAudioGroup(AudioGroupModel* group); void refreshAudioIO(); void refreshMIDIIO(); + void startBackgroundTask(const QString& windowTitle, const QString& label, + std::function&& task); + public: explicit MainWindow(QWidget* parent = Q_NULLPTR); ~MainWindow(); @@ -70,6 +106,22 @@ public slots: void onTextSelect(); void onTextDelete(); + void onBackgroundTaskFinished(); + + QMessageBox::StandardButton msgInformation(const QString &title, + const QString &text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + QMessageBox::StandardButton msgQuestion(const QString &title, + const QString &text, QMessageBox::StandardButtons buttons = + QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No), + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + QMessageBox::StandardButton msgWarning(const QString &title, + const QString &text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + QMessageBox::StandardButton msgCritical(const QString &title, + const QString &text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + }; diff --git a/Editor/MainWindow.ui b/Editor/MainWindow.ui index 0346d52..c916b8e 100644 --- a/Editor/MainWindow.ui +++ b/Editor/MainWindow.ui @@ -52,6 +52,9 @@ 0 + + false + diff --git a/Editor/ProjectModel.cpp b/Editor/ProjectModel.cpp index 6e222a1..a26b439 100644 --- a/Editor/ProjectModel.cpp +++ b/Editor/ProjectModel.cpp @@ -4,111 +4,250 @@ #include "Common.hpp" #include "athena/YAMLDocWriter.hpp" +QIcon ProjectModel::GroupNode::Icon; +QIcon ProjectModel::SongGroupNode::Icon; +QIcon ProjectModel::SoundGroupNode::Icon; + ProjectModel::ProjectModel(const QString& path, QObject* parent) : QAbstractItemModel(parent), m_dir(path) { + m_root = std::make_unique(); + GroupNode::Icon = QIcon(":/icons/IconGroup.svg"); + SongGroupNode::Icon = QIcon(":/icons/IconSongGroup.svg"); + SoundGroupNode::Icon = QIcon(":/icons/IconSoundGroup.svg"); } -ProjectModel::ProjectGroup::ProjectGroup(amuse::IntrusiveAudioGroupData&& data) -: m_data(std::move(data)), - m_proj(amuse::AudioGroupProject::CreateAudioGroupProject(m_data)), - m_pool(amuse::AudioGroupPool::CreateAudioGroupPool(m_data)), - m_sdir(amuse::AudioGroupSampleDirectory::CreateAudioGroupSampleDirectory(m_data)) -{} - -bool ProjectModel::importGroupData(const QString& groupName, amuse::IntrusiveAudioGroupData&& data) +bool ProjectModel::importGroupData(const QString& groupName, const amuse::AudioGroupData& data, + ImportMode mode, UIMessenger& messenger) { - setIdDatabases(); + m_projectDatabase.setIdDatabases(); - ProjectGroup& grp = m_groups.insert(std::make_pair(groupName, std::move(data))).first->second; + amuse::AudioGroupDatabase& grp = m_groups.insert(std::make_pair(groupName, data)).first->second; grp.setIdDatabases(); - amuse::AudioGroupProject::BootstrapObjectIDs(grp.m_data); + amuse::AudioGroupProject::BootstrapObjectIDs(data); - return true; -} - -bool ProjectModel::extractSamples(ImportMode mode, QWidget* parent) -{ - setIdDatabases(); - - if (!MkPath(m_dir.path(), parent)) + if (!MkPath(m_dir.path(), messenger)) + return false; + QDir dir(QFileInfo(m_dir, groupName).filePath()); + if (!MkPath(dir.path(), messenger)) return false; - for (auto& g : m_groups) + amuse::SystemString sysDir = QStringToSysString(dir.path()); + switch (mode) { - g.second.setIdDatabases(); - - QDir dir(QFileInfo(m_dir, g.first).filePath()); - if (!MkPath(dir.path(), parent)) - return false; - - amuse::SystemString sysDir = QStringToSysString(dir.path()); - switch (mode) - { - case ImportMode::Original: - g.second.m_sdir.extractAllCompressed(sysDir, g.second.m_data.getSamp()); - break; - case ImportMode::WAVs: - g.second.m_sdir.extractAllWAV(sysDir, g.second.m_data.getSamp()); - break; - case ImportMode::Both: - g.second.m_sdir.extractAllCompressed(sysDir, g.second.m_data.getSamp()); - g.second.m_sdir.extractAllWAV(sysDir, g.second.m_data.getSamp()); - break; - default: - break; - } + case ImportMode::Original: + grp.getSdir().extractAllCompressed(sysDir, data.getSamp()); + break; + case ImportMode::WAVs: + grp.getSdir().extractAllWAV(sysDir, data.getSamp()); + break; + case ImportMode::Both: + grp.getSdir().extractAllWAV(sysDir, data.getSamp()); + grp.getSdir().extractAllCompressed(sysDir, data.getSamp()); + break; + default: + break; } + grp.getProj().toYAML(sysDir); + grp.getPool().toYAML(sysDir); + + m_needsReset = true; return true; } -bool ProjectModel::saveToFile(QWidget* parent) +bool ProjectModel::saveToFile(UIMessenger& messenger) { - setIdDatabases(); + m_projectDatabase.setIdDatabases(); - if (!MkPath(m_dir.path(), parent)) + if (!MkPath(m_dir.path(), messenger)) return false; for (auto& g : m_groups) { QDir dir(QFileInfo(m_dir, g.first).filePath()); - if (!MkPath(dir.path(), parent)) + if (!MkPath(dir.path(), messenger)) return false; g.second.setIdDatabases(); amuse::SystemString groupPath = QStringToSysString(dir.path()); - g.second.m_proj.toYAML(groupPath); - g.second.m_pool.toYAML(groupPath); + g.second.getProj().toYAML(groupPath); + g.second.getPool().toYAML(groupPath); } return true; } -QModelIndex ProjectModel::index(int row, int column, const QModelIndex& parent) const +void ProjectModel::_resetModelData() { - return createIndex(row, column, nullptr); + beginResetModel(); + m_projectDatabase.setIdDatabases(); + m_root = std::make_unique(); + m_root->reserve(m_groups.size()); + for (auto it = m_groups.begin() ; it != m_groups.end() ; ++it) + { + it->second.setIdDatabases(); + GroupNode& gn = m_root->makeChild(it); + auto& songGroups = it->second.getProj().songGroups(); + auto& sfxGroups = it->second.getProj().sfxGroups(); + auto& soundMacros = it->second.getPool().soundMacros(); + auto& tables = it->second.getPool().tables(); + auto& keymaps = it->second.getPool().keymaps(); + auto& layers = it->second.getPool().layers(); + gn.reserve(songGroups.size() + sfxGroups.size() + 4); + for (const auto& grp : SortUnorderedMap(songGroups)) + gn.makeChild(grp.first, grp.second.get()); + for (const auto& grp : SortUnorderedMap(sfxGroups)) + gn.makeChild(grp.first, grp.second.get()); + if (soundMacros.size()) + { + CollectionNode& col = + gn.makeChild(tr("Sound Macros"), QIcon(":/icons/IconSoundMacro.svg")); + col.reserve(soundMacros.size()); + for (const auto& macro : SortUnorderedMap(soundMacros)) + col.makeChild>(macro.first, macro.second.get()); + } + if (tables.size()) + { + auto tablesSort = SortUnorderedMap(tables); + size_t ADSRCount = 0; + size_t curveCount = 0; + for (auto& t : tablesSort) + { + amuse::ITable::Type tp = t.second.get()->Isa(); + if (tp == amuse::ITable::Type::ADSR || tp == amuse::ITable::Type::ADSRDLS) + ADSRCount += 1; + else if (tp == amuse::ITable::Type::Curve) + curveCount += 1; + } + if (ADSRCount) + { + CollectionNode& col = + gn.makeChild(tr("ADSRs"), QIcon(":/icons/IconADSR.svg")); + col.reserve(ADSRCount); + for (auto& t : tablesSort) + { + amuse::ITable::Type tp = t.second.get()->Isa(); + if (tp == amuse::ITable::Type::ADSR || tp == amuse::ITable::Type::ADSRDLS) + col.makeChild>(t.first, *t.second.get()); + } + } + if (curveCount) + { + CollectionNode& col = + gn.makeChild(tr("Curves"), QIcon(":/icons/IconCurve.svg")); + col.reserve(curveCount); + for (auto& t : tablesSort) + { + amuse::ITable::Type tp = t.second.get()->Isa(); + if (tp == amuse::ITable::Type::Curve) + col.makeChild>(t.first, static_cast(*t.second.get())); + } + } + } + if (keymaps.size()) + { + CollectionNode& col = + gn.makeChild(tr("Keymaps"), QIcon(":/icons/IconKeymap.svg")); + col.reserve(keymaps.size()); + for (auto& keymap : SortUnorderedMap(keymaps)) + col.makeChild>(keymap.first, keymap.second.get()); + } + if (layers.size()) + { + CollectionNode& col = + gn.makeChild(tr("Layers"), QIcon(":/icons/IconLayers.svg")); + col.reserve(layers.size()); + for (auto& keymap : SortUnorderedMap(layers)) + col.makeChild>>(keymap.first, keymap.second.get()); + } + } + endResetModel(); } -QModelIndex ProjectModel::parent(const QModelIndex& child) const +void ProjectModel::ensureModelData() { - return {}; + if (m_needsReset) + { + _resetModelData(); + m_needsReset = false; + } +} + +QModelIndex ProjectModel::index(int row, int column, const QModelIndex& parent) const +{ + if (!hasIndex(row, column, parent)) + return QModelIndex(); + + INode* parentItem; + if (!parent.isValid()) + parentItem = m_root.get(); + else + parentItem = static_cast(parent.internalPointer()); + + INode* childItem = parentItem->child(row); + if (childItem) + return createIndex(row, column, childItem); + else + return QModelIndex(); +} + +QModelIndex ProjectModel::parent(const QModelIndex& index) const +{ + if (!index.isValid()) + return QModelIndex(); + + INode* childItem = static_cast(index.internalPointer()); + INode* parentItem = childItem->parent(); + + if (parentItem == m_root.get()) + return QModelIndex(); + + return createIndex(parentItem->row(), 0, parentItem); } int ProjectModel::rowCount(const QModelIndex& parent) const { - return 0; + INode* parentItem; + + if (!parent.isValid()) + parentItem = m_root.get(); + else + parentItem = static_cast(parent.internalPointer()); + + return parentItem->childCount(); } int ProjectModel::columnCount(const QModelIndex& parent) const { - return 0; + return 1; } QVariant ProjectModel::data(const QModelIndex& index, int role) const { - return {}; + if (!index.isValid()) + return QVariant(); + + INode* item = static_cast(index.internalPointer()); + + switch (role) + { + case Qt::DisplayRole: + return item->text(); + case Qt::DecorationRole: + return item->icon(); + default: + return {}; + } +} + +Qt::ItemFlags ProjectModel::flags(const QModelIndex& index) const +{ + if (!index.isValid()) + return 0; + + return QAbstractItemModel::flags(index); } bool ProjectModel::canDelete() const diff --git a/Editor/ProjectModel.hpp b/Editor/ProjectModel.hpp index 2813ff7..b5065f7 100644 --- a/Editor/ProjectModel.hpp +++ b/Editor/ProjectModel.hpp @@ -3,7 +3,10 @@ #include #include +#include #include +#include "Common.hpp" +#include "amuse/AudioGroup.hpp" #include "amuse/AudioGroupData.hpp" #include "amuse/AudioGroupProject.hpp" #include "amuse/AudioGroupPool.hpp" @@ -19,54 +22,134 @@ public: WAVs, Both }; - struct ProjectGroup - { - amuse::IntrusiveAudioGroupData m_data; - amuse::AudioGroupProject m_proj; - amuse::AudioGroupPool m_pool; - amuse::AudioGroupSampleDirectory m_sdir; - amuse::NameDB m_soundMacroDb; - amuse::NameDB m_sampleDb; - amuse::NameDB m_tableDb; - amuse::NameDB m_keymapDb; - amuse::NameDB m_layersDb; - - explicit ProjectGroup(amuse::IntrusiveAudioGroupData&& data); - void setIdDatabases() - { - amuse::SoundMacroId::CurNameDB = &m_soundMacroDb; - amuse::SampleId::CurNameDB = &m_sampleDb; - amuse::TableId::CurNameDB = &m_tableDb; - amuse::KeymapId::CurNameDB = &m_keymapDb; - amuse::LayersId::CurNameDB = &m_layersDb; - } - }; private: QDir m_dir; - amuse::NameDB m_songDb; - amuse::NameDB m_sfxDb; - std::map m_groups; + amuse::ProjectDatabase m_projectDatabase; + std::map m_groups; - void setIdDatabases() + class INode { - amuse::SongId::CurNameDB = &m_songDb; - amuse::SFXId::CurNameDB = &m_sfxDb; - } + enum class Type + { + Group, // Top-level group + SongGroup, + SfxGroup, + Collection, // Classified object collection, one of the following: + SoundMacro, + ADSR, + Curve, + Keymap, + Layer + }; + INode* m_parent; + std::vector> m_children; + int m_row; + public: + virtual ~INode() = default; + INode(INode* parent, int row) : m_parent(parent), m_row(row) {} + + int childCount() const { return int(m_children.size()); } + INode* child(int row) const { return m_children[row].get(); } + INode* parent() const { return m_parent; } + int row() const { return m_row; } + + void reserve(size_t sz) { m_children.reserve(sz); } + template + T& makeChild(_Args&&... args) + { + m_children.push_back(std::make_unique(this, m_children.size(), std::forward<_Args>(args)...)); + return static_cast(*m_children.back()); + } + + virtual QString text() const = 0; + virtual QIcon icon() const = 0; + }; + struct RootNode : INode + { + RootNode() : INode(nullptr, 0) {} + + QString text() const { return {}; } + QIcon icon() const { return {}; } + }; + struct GroupNode : INode + { + std::map::iterator m_it; + GroupNode(INode* parent, int row, std::map::iterator it) + : INode(parent, row), m_it(it) {} + + static QIcon Icon; + QString text() const { return m_it->first; } + QIcon icon() const { return Icon; } + }; + struct SongGroupNode : INode + { + amuse::GroupId m_id; + QString m_name; + amuse::SongGroupIndex& m_index; + SongGroupNode(INode* parent, int row, amuse::GroupId id, amuse::SongGroupIndex& index) + : INode(parent, row), m_id(id), m_name(amuse::GroupId::CurNameDB->resolveNameFromId(id).data()), m_index(index) {} + + static QIcon Icon; + QString text() const { return m_name; } + QIcon icon() const { return Icon; } + }; + struct SoundGroupNode : INode + { + amuse::GroupId m_id; + QString m_name; + amuse::SFXGroupIndex& m_index; + SoundGroupNode(INode* parent, int row, amuse::GroupId id, amuse::SFXGroupIndex& index) + : INode(parent, row), m_id(id), m_name(amuse::GroupId::CurNameDB->resolveNameFromId(id).data()), m_index(index) {} + + static QIcon Icon; + QString text() const { return m_name; } + QIcon icon() const { return Icon; } + }; + struct CollectionNode : INode + { + QString m_name; + QIcon m_icon; + CollectionNode(INode* parent, int row, const QString& name, const QIcon& icon) + : INode(parent, row), m_name(name), m_icon(icon) {} + + QString text() const { return m_name; } + QIcon icon() const { return m_icon; } + }; + template + struct PoolObjectNode : INode + { + ID m_id; + QString m_name; + T& m_obj; + PoolObjectNode(INode* parent, int row, ID id, T& obj) + : INode(parent, row), m_id(id), m_name(ID::CurNameDB->resolveNameFromId(id).data()), m_obj(obj) {} + + QString text() const { return m_name; } + QIcon icon() const { return {}; } + }; + + std::unique_ptr m_root; + + bool m_needsReset = false; + void _resetModelData(); public: explicit ProjectModel(const QString& path, QObject* parent = Q_NULLPTR); - bool importGroupData(const QString& groupName, amuse::IntrusiveAudioGroupData&& data); - bool extractSamples(ImportMode mode, QWidget* parent); - bool saveToFile(QWidget* parent); + bool importGroupData(const QString& groupName, const amuse::AudioGroupData& data, + ImportMode mode, UIMessenger& messenger); + bool saveToFile(UIMessenger& messenger); + + void ensureModelData(); QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const; QModelIndex parent(const QModelIndex& child) const; int rowCount(const QModelIndex& parent = QModelIndex()) const; int columnCount(const QModelIndex& parent = QModelIndex()) const; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + Qt::ItemFlags flags(const QModelIndex& index) const; QString path() const { return m_dir.path(); } bool canDelete() const; diff --git a/Editor/resources/IconADSR.svg b/Editor/resources/IconADSR.svg new file mode 100644 index 0000000..12d86a1 --- /dev/null +++ b/Editor/resources/IconADSR.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/Editor/resources/IconCurve.svg b/Editor/resources/IconCurve.svg new file mode 100644 index 0000000..ce01968 --- /dev/null +++ b/Editor/resources/IconCurve.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/Editor/resources/IconGroup.svg b/Editor/resources/IconGroup.svg new file mode 100644 index 0000000..d38db5b --- /dev/null +++ b/Editor/resources/IconGroup.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/Editor/resources/IconLayers.svg b/Editor/resources/IconLayers.svg index daeef8d..d03b25e 100644 --- a/Editor/resources/IconLayers.svg +++ b/Editor/resources/IconLayers.svg @@ -14,7 +14,7 @@ viewBox="0 0 4.2333332 4.2333335" id="svg2" version="1.1" - inkscape:version="0.91+devel+osxmenu r12922" + inkscape:version="0.92.2 2405546, 2018-03-11" sodipodi:docname="IconLayers.svg"> @@ -26,7 +26,7 @@ inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:zoom="4.92" - inkscape:cx="19.961362" + inkscape:cx="-13.575223" inkscape:cy="7.1823475" inkscape:document-units="px" inkscape:current-layer="layer1" @@ -34,8 +34,8 @@ units="px" inkscape:window-width="1260" inkscape:window-height="787" - inkscape:window-x="131" - inkscape:window-y="148" + inkscape:window-x="1749" + inkscape:window-y="677" inkscape:window-maximized="0" showborder="false" objecttolerance="4" @@ -55,7 +55,7 @@ image/svg+xml - + diff --git a/Editor/resources/IconNewADSR.svg b/Editor/resources/IconNewADSR.svg new file mode 100644 index 0000000..ec47429 --- /dev/null +++ b/Editor/resources/IconNewADSR.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/Editor/resources/IconNewCurve.svg b/Editor/resources/IconNewCurve.svg new file mode 100644 index 0000000..58e7262 --- /dev/null +++ b/Editor/resources/IconNewCurve.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/Editor/resources/IconNewGroup.svg b/Editor/resources/IconNewGroup.svg new file mode 100644 index 0000000..0eda093 --- /dev/null +++ b/Editor/resources/IconNewGroup.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/Editor/resources/IconNewSongGroup.svg b/Editor/resources/IconNewSongGroup.svg index 526ee29..e02035d 100644 --- a/Editor/resources/IconNewSongGroup.svg +++ b/Editor/resources/IconNewSongGroup.svg @@ -14,8 +14,8 @@ viewBox="0 0 4.2333332 4.2333335" id="svg2" version="1.1" - inkscape:version="0.91+devel+osxmenu r12922" - sodipodi:docname="NewSongGroup.svg"> + inkscape:version="0.92.2 2405546, 2018-03-11" + sodipodi:docname="IconNewSongGroup.svg"> image/svg+xml - + @@ -65,11 +65,16 @@ id="layer1" transform="translate(0,-292.76665)"> + + inkscape:version="0.92.2 2405546, 2018-03-11" + sodipodi:docname="IconNewSoundGroup.svg"> image/svg+xml - + @@ -65,11 +65,17 @@ id="layer1" transform="translate(0,-292.76665)"> + @@ -25,17 +25,17 @@ borderopacity="1.0" inkscape:pageopacity="0" inkscape:pageshadow="2" - inkscape:zoom="29.730996" - inkscape:cx="3.7002328" - inkscape:cy="9.0045012" + inkscape:zoom="34.350339" + inkscape:cx="8.8858389" + inkscape:cy="8.8343343" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="true" units="px" inkscape:window-width="1260" inkscape:window-height="787" - inkscape:window-x="0" - inkscape:window-y="23" + inkscape:window-x="1944" + inkscape:window-y="288" inkscape:window-maximized="0" showborder="false" objecttolerance="4" @@ -45,7 +45,7 @@ type="xygrid" id="grid4173" empspacing="1" - visible="false" /> + visible="true" /> @@ -65,10 +65,19 @@ id="layer1" transform="translate(0,-292.76665)"> + + diff --git a/Editor/resources/IconSoundGroup.svg b/Editor/resources/IconSoundGroup.svg index d38db5b..a362d7a 100644 --- a/Editor/resources/IconSoundGroup.svg +++ b/Editor/resources/IconSoundGroup.svg @@ -14,10 +14,8 @@ viewBox="0 0 4.2333332 4.2333335" id="svg2" version="1.1" - inkscape:version="0.91+devel+osxmenu r12922" + inkscape:version="0.92.2 2405546, 2018-03-11" sodipodi:docname="IconSoundGroup.svg"> - + id="grid4173" + type="xygrid" /> + @@ -60,15 +60,21 @@ + inkscape:groupmode="layer" + inkscape:label="Layer 1"> + id="rect4141" + d="m 1.3229167,293.04538 c 0.2645833,0 0.5291666,0.25044 0.5291666,0.51502 l 0,0.26458 1.8488138,0.0102 c 0.3165926,0.002 0.5239173,0.20975 0.5245413,0.52471 l 0.00313,1.57936 c 6.239e-4,0.31495 -0.2079439,0.53411 -0.5245413,0.53411 l -3.17666623,0 C 0.21076252,296.47339 0,296.2566 0,295.94165 c 4.054e-5,-0.82258 0,-2.38125 0,-2.38125 0,-0.26458 0.26458333,-0.51612 0.52916667,-0.51612 z" + style="opacity:1;fill:#04e2ff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.67643285;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + diff --git a/Editor/resources/resources.qrc b/Editor/resources/resources.qrc index 1dc301d..b23dbdc 100644 --- a/Editor/resources/resources.qrc +++ b/Editor/resources/resources.qrc @@ -13,6 +13,12 @@ IconSongGroup.svg IconSoundGroup.svg IconSoundMacro.svg + IconGroup.svg + IconNewGroup.svg + IconADSR.svg + IconNewADSR.svg + IconCurve.svg + IconNewCurve.svg FaceGrey.svg diff --git a/include/amuse/AudioGroup.hpp b/include/amuse/AudioGroup.hpp index 862a322..ae17471 100644 --- a/include/amuse/AudioGroup.hpp +++ b/include/amuse/AudioGroup.hpp @@ -29,6 +29,48 @@ public: const AudioGroupProject& getProj() const { return m_proj; } const AudioGroupPool& getPool() const { return m_pool; } const AudioGroupSampleDirectory& getSdir() const { return m_sdir; } + AudioGroupProject& getProj() { return m_proj; } + AudioGroupPool& getPool() { return m_pool; } + AudioGroupSampleDirectory& getSdir() { return m_sdir; } +}; + +class AudioGroupDatabase : public AudioGroup +{ + amuse::NameDB m_soundMacroDb; + amuse::NameDB m_sampleDb; + amuse::NameDB m_tableDb; + amuse::NameDB m_keymapDb; + amuse::NameDB m_layersDb; + +public: + explicit AudioGroupDatabase(const AudioGroupData& data) + : AudioGroup(data) {} + explicit AudioGroupDatabase(SystemStringView groupPath) + : AudioGroup(groupPath) {} + + void setIdDatabases() + { + amuse::SoundMacroId::CurNameDB = &m_soundMacroDb; + amuse::SampleId::CurNameDB = &m_sampleDb; + amuse::TableId::CurNameDB = &m_tableDb; + amuse::KeymapId::CurNameDB = &m_keymapDb; + amuse::LayersId::CurNameDB = &m_layersDb; + } +}; + +class ProjectDatabase +{ + amuse::NameDB m_songDb; + amuse::NameDB m_sfxDb; + amuse::NameDB m_groupDb; + +public: + void setIdDatabases() + { + amuse::SongId::CurNameDB = &m_songDb; + amuse::SFXId::CurNameDB = &m_sfxDb; + amuse::GroupId::CurNameDB = &m_groupDb; + } }; } diff --git a/include/amuse/AudioGroupPool.hpp b/include/amuse/AudioGroupPool.hpp index 4f059c4..2ef4fa4 100644 --- a/include/amuse/AudioGroupPool.hpp +++ b/include/amuse/AudioGroupPool.hpp @@ -664,7 +664,7 @@ struct SoundMacro AT_DECL_DNAV Value midiControl; Value scalingPercentage; - Value combine; + Value combine; Value isVar; Value fineScaling; bool Do(SoundMacroState& st, Voice& vox) const; @@ -676,7 +676,7 @@ struct SoundMacro AT_DECL_DNAV Value midiControl; Value scalingPercentage; - Value combine; + Value combine; Value isVar; Value fineScaling; bool Do(SoundMacroState& st, Voice& vox) const; @@ -688,7 +688,7 @@ struct SoundMacro AT_DECL_DNAV Value midiControl; Value scalingPercentage; - Value combine; + Value combine; Value isVar; Value fineScaling; bool Do(SoundMacroState& st, Voice& vox) const; @@ -700,7 +700,7 @@ struct SoundMacro AT_DECL_DNAV Value midiControl; Value scalingPercentage; - Value combine; + Value combine; Value isVar; Value fineScaling; bool Do(SoundMacroState& st, Voice& vox) const; @@ -712,7 +712,7 @@ struct SoundMacro AT_DECL_DNAV Value midiControl; Value scalingPercentage; - Value combine; + Value combine; Value isVar; Value fineScaling; bool Do(SoundMacroState& st, Voice& vox) const; @@ -724,7 +724,7 @@ struct SoundMacro AT_DECL_DNAV Value midiControl; Value scalingPercentage; - Value combine; + Value combine; Value isVar; Value fineScaling; bool Do(SoundMacroState& st, Voice& vox) const; @@ -736,7 +736,7 @@ struct SoundMacro AT_DECL_DNAV Value midiControl; Value scalingPercentage; - Value combine; + Value combine; Value isVar; Value fineScaling; bool Do(SoundMacroState& st, Voice& vox) const; @@ -748,7 +748,7 @@ struct SoundMacro AT_DECL_DNAV Value midiControl; Value scalingPercentage; - Value combine; + Value combine; Value isVar; Value fineScaling; bool Do(SoundMacroState& st, Voice& vox) const; @@ -760,7 +760,7 @@ struct SoundMacro AT_DECL_DNAV Value midiControl; Value scalingPercentage; - Value combine; + Value combine; Value isVar; Value fineScaling; bool Do(SoundMacroState& st, Voice& vox) const; @@ -772,7 +772,7 @@ struct SoundMacro AT_DECL_DNAV Value midiControl; Value scalingPercentage; - Value combine; + Value combine; Value isVar; Value fineScaling; bool Do(SoundMacroState& st, Voice& vox) const; @@ -784,7 +784,7 @@ struct SoundMacro AT_DECL_DNAV Value midiControl; Value scalingPercentage; - Value combine; + Value combine; Value isVar; Value fineScaling; bool Do(SoundMacroState& st, Voice& vox) const; @@ -796,7 +796,7 @@ struct SoundMacro AT_DECL_DNAV Value midiControl; Value scalingPercentage; - Value combine; + Value combine; Value isVar; Value fineScaling; bool Do(SoundMacroState& st, Voice& vox) const; @@ -808,7 +808,7 @@ struct SoundMacro AT_DECL_DNAV Value midiControl; Value scalingPercentage; - Value combine; + Value combine; Value isVar; Value fineScaling; bool Do(SoundMacroState& st, Voice& vox) const; @@ -820,7 +820,7 @@ struct SoundMacro AT_DECL_DNAV Value midiControl; Value scalingPercentage; - Value combine; + Value combine; Value isVar; Value fineScaling; bool Do(SoundMacroState& st, Voice& vox) const; @@ -1156,6 +1156,15 @@ public: static AudioGroupPool CreateAudioGroupPool(const AudioGroupData& data); static AudioGroupPool CreateAudioGroupPool(SystemStringView groupPath); + const std::unordered_map& soundMacros() const { return m_soundMacros; } + const std::unordered_map>& tables() const { return m_tables; } + const std::unordered_map& keymaps() const { return m_keymaps; } + const std::unordered_map>& layers() const { return m_layers; } + std::unordered_map& soundMacros() { return m_soundMacros; } + std::unordered_map>& tables() { return m_tables; } + std::unordered_map& keymaps() { return m_keymaps; } + std::unordered_map>& layers() { return m_layers; } + const SoundMacro* soundMacro(ObjectId id) const; const Keymap* keymap(ObjectId id) const; const std::vector* layer(ObjectId id) const; @@ -1164,6 +1173,11 @@ public: const Curve* tableAsCurves(ObjectId id) const; bool toYAML(SystemStringView groupPath) const; + + AudioGroupPool(const AudioGroupPool&) = delete; + AudioGroupPool& operator=(const AudioGroupPool&) = delete; + AudioGroupPool(AudioGroupPool&&) = default; + AudioGroupPool& operator=(AudioGroupPool&&) = default; }; } diff --git a/include/amuse/AudioGroupProject.hpp b/include/amuse/AudioGroupProject.hpp index 6556743..8f02821 100644 --- a/include/amuse/AudioGroupProject.hpp +++ b/include/amuse/AudioGroupProject.hpp @@ -25,7 +25,7 @@ GroupHeader : BigDNA { AT_DECL_DNA Value groupEndOff; - Value groupId; + GroupIdDNA groupId; Value type; Value soundMacroIdsOff; Value samplIdsOff; @@ -180,8 +180,8 @@ struct SFXGroupIndex : AudioGroupIndex /** Collection of SongGroup and SFXGroup indexes */ class AudioGroupProject { - std::unordered_map m_songGroups; - std::unordered_map m_sfxGroups; + std::unordered_map m_songGroups; + std::unordered_map m_sfxGroups; AudioGroupProject() = default; AudioGroupProject(athena::io::IStreamReader& r, GCNDataTag); @@ -199,10 +199,17 @@ public: const SongGroupIndex* getSongGroupIndex(int groupId) const; const SFXGroupIndex* getSFXGroupIndex(int groupId) const; - const std::unordered_map& songGroups() const { return m_songGroups; } - const std::unordered_map& sfxGroups() const { return m_sfxGroups; } + const std::unordered_map& songGroups() const { return m_songGroups; } + const std::unordered_map& sfxGroups() const { return m_sfxGroups; } + std::unordered_map& songGroups() { return m_songGroups; } + std::unordered_map& sfxGroups() { return m_sfxGroups; } bool toYAML(SystemStringView groupPath) const; + + AudioGroupProject(const AudioGroupProject&) = delete; + AudioGroupProject& operator=(const AudioGroupProject&) = delete; + AudioGroupProject(AudioGroupProject&&) = default; + AudioGroupProject& operator=(AudioGroupProject&&) = default; }; } diff --git a/include/amuse/AudioGroupSampleDirectory.hpp b/include/amuse/AudioGroupSampleDirectory.hpp index 5847ebd..6a69f9f 100644 --- a/include/amuse/AudioGroupSampleDirectory.hpp +++ b/include/amuse/AudioGroupSampleDirectory.hpp @@ -268,6 +268,11 @@ public: void extractAllWAV(amuse::SystemStringView destDir, const unsigned char* samp) const; void extractCompressed(SampleId id, amuse::SystemStringView destDir, const unsigned char* samp) const; void extractAllCompressed(amuse::SystemStringView destDir, const unsigned char* samp) const; + + AudioGroupSampleDirectory(const AudioGroupSampleDirectory&) = delete; + AudioGroupSampleDirectory& operator=(const AudioGroupSampleDirectory&) = delete; + AudioGroupSampleDirectory(AudioGroupSampleDirectory&&) = default; + AudioGroupSampleDirectory& operator=(AudioGroupSampleDirectory&&) = default; }; } diff --git a/include/amuse/Common.hpp b/include/amuse/Common.hpp index 8db53ba..ab2bfdf 100644 --- a/include/amuse/Common.hpp +++ b/include/amuse/Common.hpp @@ -86,6 +86,7 @@ DECL_ID_TYPE(KeymapId) DECL_ID_TYPE(LayersId) DECL_ID_TYPE(SongId) DECL_ID_TYPE(SFXId) +DECL_ID_TYPE(GroupId) /* MusyX has object polymorphism between Keymaps and Layers when * referenced by a song group's page object. When the upper bit is set, @@ -417,12 +418,26 @@ struct PCDataTag { }; +template +static std::vector>> SortUnorderedMap(T& um) +{ + std::vector>> ret; + ret.reserve(um.size()); + for (auto& p : um) + ret.emplace_back(p.first, p.second); + std::sort(ret.begin(), ret.end(), [](const auto& a, const auto& b) { return a.first < b.first; }); + return ret; +} + template static std::vector>> SortUnorderedMap(const T& um) { - std::vector>> ret(um.cbegin(), um.cend()); + std::vector>> ret; + ret.reserve(um.size()); + for (const auto& p : um) + ret.emplace_back(p.first, p.second); std::sort(ret.begin(), ret.end(), [](const auto& a, const auto& b) { return a.first < b.first; }); return ret; } @@ -444,6 +459,7 @@ DECL_ID_HASH(KeymapId) DECL_ID_HASH(LayersId) DECL_ID_HASH(SongId) DECL_ID_HASH(SFXId) +DECL_ID_HASH(GroupId) } namespace amuse @@ -458,6 +474,7 @@ struct NameDB Layer, Song, SFX, + Group, Sample }; diff --git a/lib/AudioGroupProject.cpp b/lib/AudioGroupProject.cpp index fd0c2f9..a0962bb 100644 --- a/lib/AudioGroupProject.cpp +++ b/lib/AudioGroupProject.cpp @@ -210,6 +210,15 @@ AudioGroupProject AudioGroupProject::CreateAudioGroupProject(const AudioGroupDat } } +std::string ParseStringSlashId(const std::string& str, uint16_t& idOut) +{ + size_t slashPos = str.find('/'); + if (slashPos == std::string::npos) + return {}; + idOut = uint16_t(strtoul(str.data() + slashPos + 1, nullptr, 0)); + return {str.begin(), str.begin() + slashPos}; +} + AudioGroupProject AudioGroupProject::CreateAudioGroupProject(SystemStringView groupPath) { AudioGroupProject ret; @@ -222,15 +231,20 @@ AudioGroupProject AudioGroupProject::CreateAudioGroupProject(SystemStringView gr athena::io::YAMLDocReader r; if (r.parse(&fi) && r.ValidateClassType("amuse::Project")) { - size_t songGroupCount; - if (auto __v = r.enterSubVector("songGroups", songGroupCount)) + if (auto __v = r.enterSubRecord("songGroups")) { - ret.m_songGroups.reserve(songGroupCount); - for (int g = 0; g < songGroupCount; ++g) + ret.m_songGroups.reserve(r.getCurNode()->m_mapChildren.size()); + for (const auto& grp : r.getCurNode()->m_mapChildren) { - if (auto __r = r.enterSubRecord(nullptr)) + if (auto __r = r.enterSubRecord(grp.first.c_str())) { - SongGroupIndex& idx = ret.m_songGroups[g]; + uint16_t groupId; + std::string groupName = ParseStringSlashId(grp.first, groupId); + if (groupName.empty() || groupId == 0xffff) + continue; + GroupId::CurNameDB->registerPair(groupName, groupId); + + SongGroupIndex& idx = ret.m_songGroups[groupId]; if (auto __v2 = r.enterSubRecord("normPages")) { idx.m_normPages.reserve(r.getCurNode()->m_mapChildren.size()); @@ -253,8 +267,12 @@ AudioGroupProject AudioGroupProject::CreateAudioGroupProject(SystemStringView gr size_t chanCount; if (auto __v3 = r.enterSubVector(song.first.c_str(), chanCount)) { - ObjectId songId = SongId::CurNameDB->generateId(NameDB::Type::Song); - SongId::CurNameDB->registerPair(song.first, songId); + uint16_t songId; + std::string songName = ParseStringSlashId(song.first, songId); + if (songName.empty() || songId == 0xffff) + continue; + SongId::CurNameDB->registerPair(songName, songId); + std::array& setup = idx.m_midiSetups[songId]; for (int i = 0; i < 16 && i < chanCount; ++i) if (auto __r2 = r.enterSubRecord(nullptr)) @@ -266,20 +284,28 @@ AudioGroupProject AudioGroupProject::CreateAudioGroupProject(SystemStringView gr } } - size_t sfxGroupCount; - if (auto __v = r.enterSubVector("sfxGroups", sfxGroupCount)) + if (auto __v = r.enterSubRecord("sfxGroups")) { - ret.m_sfxGroups.reserve(sfxGroupCount); - for (int g = 0; g < sfxGroupCount; ++g) + ret.m_sfxGroups.reserve(r.getCurNode()->m_mapChildren.size()); + for (const auto& grp : r.getCurNode()->m_mapChildren) { if (auto __r = r.enterSubRecord(nullptr)) { - SFXGroupIndex& idx = ret.m_sfxGroups[g]; + uint16_t groupId; + std::string groupName = ParseStringSlashId(grp.first, groupId); + if (groupName.empty() || groupId == 0xffff) + continue; + GroupId::CurNameDB->registerPair(groupName, groupId); + + SFXGroupIndex& idx = ret.m_sfxGroups[groupId]; for (const auto& sfx : r.getCurNode()->m_mapChildren) if (auto __r2 = r.enterSubRecord(sfx.first.c_str())) { - ObjectId sfxId = SFXId::CurNameDB->generateId(NameDB::Type::SFX); - SFXId::CurNameDB->registerPair(sfx.first, sfxId); + uint16_t sfxId; + std::string sfxName = ParseStringSlashId(sfx.first, sfxId); + if (sfxName.empty() || sfxId == 0xffff) + continue; + SFXId::CurNameDB->registerPair(sfxName, sfxId); idx.m_sfxEntries[sfxId].read(r); } } @@ -321,6 +347,8 @@ void AudioGroupProject::BootstrapObjectIDs(athena::io::IStreamReader& r, GCNData GroupHeader header; header.read(r); + GroupId::CurNameDB->registerPair(NameDB::generateName(header.groupId, NameDB::Type::Group), header.groupId); + /* Sound Macros */ r.seek(header.soundMacroIdsOff, athena::Begin); while (!AtEnd16(r)) @@ -386,6 +414,8 @@ void AudioGroupProject::BootstrapObjectIDs(athena::io::IStreamReader& r, bool ab GroupHeader header; header.read(r); + GroupId::CurNameDB->registerPair(NameDB::generateName(header.groupId, NameDB::Type::Group), header.groupId); + /* Sound Macros */ r.seek(subDataOff + header.soundMacroIdsOff, athena::Begin); while (!AtEnd16(r)) @@ -501,11 +531,13 @@ bool AudioGroupProject::toYAML(SystemStringView groupPath) const if (!m_songGroups.empty()) { - if (auto __v = w.enterSubVector("songGroups")) + if (auto __v = w.enterSubRecord("songGroups")) { for (const auto& p : SortUnorderedMap(m_songGroups)) { - if (auto __r = w.enterSubRecord(nullptr)) + char groupString[64]; + snprintf(groupString, 64, "%s/0x%04X", GroupId::CurNameDB->resolveNameFromId(p.first).data(), int(p.first.id)); + if (auto __r = w.enterSubRecord(groupString)) { if (!p.second.get().m_normPages.empty()) { @@ -545,7 +577,9 @@ bool AudioGroupProject::toYAML(SystemStringView groupPath) const { for (const auto& song : SortUnorderedMap(p.second.get().m_midiSetups)) { - if (auto __v3 = w.enterSubVector(SongId::CurNameDB->resolveNameFromId(song.first).data())) + char songString[64]; + snprintf(songString, 64, "%s/0x%04X", SongId::CurNameDB->resolveNameFromId(song.first).data(), int(song.first.id)); + if (auto __v3 = w.enterSubVector(songString)) for (int i = 0; i < 16; ++i) if (auto __r2 = w.enterSubRecord(nullptr)) { @@ -562,15 +596,19 @@ bool AudioGroupProject::toYAML(SystemStringView groupPath) const if (!m_sfxGroups.empty()) { - if (auto __v = w.enterSubVector("sfxGroups")) + if (auto __v = w.enterSubRecord("sfxGroups")) { for (const auto& p : SortUnorderedMap(m_sfxGroups)) { - if (auto __r = w.enterSubRecord(nullptr)) + char groupString[64]; + snprintf(groupString, 64, "%s/0x%04X", GroupId::CurNameDB->resolveNameFromId(p.first).data(), int(p.first.id)); + if (auto __r = w.enterSubRecord(groupString)) { for (const auto& sfx : SortUnorderedMap(p.second.get().m_sfxEntries)) { - if (auto __r2 = w.enterSubRecord(SFXId::CurNameDB->resolveNameFromId(sfx.first).data())) + char sfxString[64]; + snprintf(sfxString, 64, "%s/0x%04X", SFXId::CurNameDB->resolveNameFromId(sfx.first).data(), int(sfx.first.id)); + if (auto __r2 = w.enterSubRecord(sfxString)) { w.setStyle(athena::io::YAMLNodeStyle::Flow); sfx.second.get().write(w); diff --git a/lib/AudioGroupSampleDirectory.cpp b/lib/AudioGroupSampleDirectory.cpp index 1e6f6f1..fe2bd06 100644 --- a/lib/AudioGroupSampleDirectory.cpp +++ b/lib/AudioGroupSampleDirectory.cpp @@ -146,11 +146,23 @@ static uint32_t DSPNibbleToSample(uint32_t nibble) void AudioGroupSampleDirectory::Entry::loadLooseData(SystemStringView basePath) { - Sstat theStat; - SystemString filePath = SystemString(basePath) + _S(".dsp"); - if (!Stat(filePath.c_str(), &theStat) && (!m_looseData || theStat.st_mtime > m_looseModTime)) + SystemString wavPath = SystemString(basePath) + _S(".wav"); + SystemString dspPath = SystemString(basePath) + _S(".dsp"); + Sstat wavStat, dspStat; + bool wavValid = !Stat(wavPath.c_str(), &wavStat) && S_ISREG(wavStat.st_mode); + bool dspValid = !Stat(dspPath.c_str(), &dspStat) && S_ISREG(dspStat.st_mode); + + if (wavValid && dspValid) { - athena::io::FileReader r(filePath); + if (wavStat.st_mtime > dspStat.st_mtime) + dspValid = false; + else + wavValid = false; + } + + if (dspValid && (!m_looseData || dspStat.st_mtime > m_looseModTime)) + { + athena::io::FileReader r(dspPath); if (!r.hasError()) { DSPADPCMHeader header; @@ -175,13 +187,14 @@ void AudioGroupSampleDirectory::Entry::loadLooseData(SystemStringView basePath) m_looseData.reset(new uint8_t[dataLen]); r.readUBytesToBuf(m_looseData.get(), dataLen); - m_looseModTime = theStat.st_mtime; + m_looseModTime = dspStat.st_mtime; return; } } - if (!Stat(filePath.c_str(), &theStat) && (!m_looseData || theStat.st_mtime > m_looseModTime)) + + if (wavValid && (!m_looseData || wavStat.st_mtime > m_looseModTime)) { - athena::io::FileReader r(filePath); + athena::io::FileReader r(wavPath); if (!r.hasError()) { atUint32 riffMagic = r.readUint32Little(); @@ -216,7 +229,6 @@ void AudioGroupSampleDirectory::Entry::loadLooseData(SystemStringView basePath) m_loopStartSample = loop.start; m_loopLengthSamples = loop.end - loop.start + 1; } - } else if (chunkMagic == SBIG('data')) { @@ -227,7 +239,7 @@ void AudioGroupSampleDirectory::Entry::loadLooseData(SystemStringView basePath) r.seek(startPos + chunkSize, athena::Begin); } - m_looseModTime = theStat.st_mtime; + m_looseModTime = wavStat.st_mtime; return; } } diff --git a/lib/Common.cpp b/lib/Common.cpp index a5233d8..993a143 100644 --- a/lib/Common.cpp +++ b/lib/Common.cpp @@ -96,6 +96,7 @@ DEFINE_ID_TYPE(KeymapId, "keymap") DEFINE_ID_TYPE(LayersId, "layers") DEFINE_ID_TYPE(SongId, "song") DEFINE_ID_TYPE(SFXId, "sfx") +DEFINE_ID_TYPE(GroupId, "group") template<> template<> void PageObjectIdDNA::Enumerate(athena::io::IStreamReader& reader) @@ -225,6 +226,9 @@ std::string NameDB::generateName(ObjectId id, Type tp) case Type::SFX: snprintf(name, 32, "sfx%04X", id.id); break; + case Type::Group: + snprintf(name, 32, "group%04X", id.id); + break; case Type::Sample: snprintf(name, 32, "sample%04X", id.id); break; diff --git a/lib/Voice.cpp b/lib/Voice.cpp index 3198fee..ccd143d 100644 --- a/lib/Voice.cpp +++ b/lib/Voice.cpp @@ -236,8 +236,8 @@ void Voice::_procSamplePre(int16_t& samp) float start = m_envelopeStart; float end = m_envelopeEnd; float t = clamp(0.f, float(m_envelopeTime / m_envelopeDur), 1.f); - if (m_envelopeCurve) - t = m_envelopeCurve->data.at(t * 127.f) / 127.f; + if (m_envelopeCurve && m_envelopeCurve->data.size() >= 128) + t = m_envelopeCurve->data[t * 127.f] / 127.f; m_curVol = clamp(0.f, (start * (1.0f - t)) + (end * t), 1.f); // printf("%d %f\n", m_vid, m_curVol);