From 08988fe3ec5f30e6fc24ab7dbd04cec1a9ab06f9 Mon Sep 17 00:00:00 2001 From: Jack Andersen Date: Sat, 18 Aug 2018 14:28:52 -1000 Subject: [PATCH] Group export and various bug fixes --- Editor/MIDIReader.cpp | 15 +- Editor/MIDIReader.hpp | 5 +- Editor/MainWindow.cpp | 167 +++++++-- Editor/MainWindow.hpp | 27 +- Editor/ProjectModel.cpp | 42 +++ Editor/ProjectModel.hpp | 2 + Editor/SongGroupEditor.cpp | 5 +- Editor/SongGroupEditor.hpp | 1 - Editor/StatusBarWidget.cpp | 24 +- Editor/StatusBarWidget.hpp | 8 + Editor/StudioSetupWidget.cpp | 102 +++++- Editor/StudioSetupWidget.hpp | 35 +- Editor/main.cpp | 3 +- Editor/resources/IconA.svg | 79 +++++ Editor/resources/IconB.svg | 79 +++++ Editor/resources/IconVolume0.svg | 12 +- Editor/resources/IconVolume1.svg | 16 +- Editor/resources/IconVolume2.svg | 20 +- Editor/resources/IconVolume3.svg | 24 +- Editor/resources/lang_de.ts | 370 +++++++++++++------- Editor/resources/resources.qrc | 2 + include/amuse/AudioGroup.hpp | 1 + include/amuse/AudioGroupPool.hpp | 4 + include/amuse/AudioGroupProject.hpp | 3 + include/amuse/AudioGroupSampleDirectory.hpp | 13 + include/amuse/BooBackend.hpp | 14 +- include/amuse/Common.hpp | 22 ++ include/amuse/Engine.hpp | 3 + include/amuse/IBackendVoiceAllocator.hpp | 3 +- lib/AudioGroupPool.cpp | 136 +++++++ lib/AudioGroupProject.cpp | 283 ++++++++++++++- lib/AudioGroupSampleDirectory.cpp | 94 +++++ lib/BooBackend.cpp | 72 ++-- lib/SongConverter.cpp | 77 ++-- lib/SongState.cpp | 56 +-- 35 files changed, 1528 insertions(+), 291 deletions(-) create mode 100644 Editor/resources/IconA.svg create mode 100644 Editor/resources/IconB.svg diff --git a/Editor/MIDIReader.cpp b/Editor/MIDIReader.cpp index 8e4ca0a..519389f 100644 --- a/Editor/MIDIReader.cpp +++ b/Editor/MIDIReader.cpp @@ -1,8 +1,8 @@ #include "MIDIReader.hpp" #include "MainWindow.hpp" -MIDIReader::MIDIReader(amuse::Engine& engine, const char* name, bool useLock) -: amuse::BooBackendMIDIReader(engine, name, useLock) {} +MIDIReader::MIDIReader(amuse::Engine& engine, bool useLock) +: amuse::BooBackendMIDIReader(engine, useLock) {} void MIDIReader::noteOff(uint8_t chan, uint8_t key, uint8_t velocity) { @@ -46,7 +46,9 @@ void MIDIReader::noteOn(uint8_t chan, uint8_t key, uint8_t velocity) m_chanVoxs.erase(keySearch); } - m_chanVoxs[key] = g_MainWindow->startEditorVoice(key, velocity); + amuse::ObjToken newVox = g_MainWindow->startEditorVoice(key, velocity); + if (newVox) + m_chanVoxs[key] = newVox; } void MIDIReader::notePressure(uint8_t /*chan*/, uint8_t /*key*/, uint8_t /*pressure*/) {} @@ -120,10 +122,7 @@ VoiceAllocator::VoiceAllocator(boo::IAudioVoiceEngine& booEngine) : amuse::BooBackendVoiceAllocator(booEngine) {} std::unique_ptr -VoiceAllocator::allocateMIDIReader(amuse::Engine& engine, const char* name) +VoiceAllocator::allocateMIDIReader(amuse::Engine& engine) { - std::unique_ptr ret = std::make_unique(engine, name, m_booEngine.useMIDILock()); - if (!static_cast(*ret).getMidiIn()) - return {}; - return ret; + return std::make_unique(engine, m_booEngine.useMIDILock()); } diff --git a/Editor/MIDIReader.hpp b/Editor/MIDIReader.hpp index a6bd9f1..4f5a64f 100644 --- a/Editor/MIDIReader.hpp +++ b/Editor/MIDIReader.hpp @@ -12,8 +12,7 @@ class MIDIReader : public amuse::BooBackendMIDIReader std::unordered_set> m_keyoffVoxs; amuse::ObjToken m_lastVoice; public: - MIDIReader(amuse::Engine& engine, const char* name, bool useLock); - boo::IMIDIIn* getMidiIn() const { return m_midiIn.get(); } + MIDIReader(amuse::Engine& engine, bool useLock); void noteOff(uint8_t chan, uint8_t key, uint8_t velocity); void noteOn(uint8_t chan, uint8_t key, uint8_t velocity); @@ -47,7 +46,7 @@ class VoiceAllocator : public amuse::BooBackendVoiceAllocator { public: VoiceAllocator(boo::IAudioVoiceEngine& booEngine); - std::unique_ptr allocateMIDIReader(amuse::Engine& engine, const char* name = nullptr); + std::unique_ptr allocateMIDIReader(amuse::Engine& engine); }; #endif // AMUSE_MIDI_READER_HPP diff --git a/Editor/MainWindow.cpp b/Editor/MainWindow.cpp index c4c80b2..ce6d738 100644 --- a/Editor/MainWindow.cpp +++ b/Editor/MainWindow.cpp @@ -47,6 +47,8 @@ MainWindow::MainWindow(QWidget* parent) m_ui.statusbar->connectFXPressed(this, SLOT(fxPressed())); m_ui.statusbar->setVolumeValue(70); m_ui.statusbar->connectVolumeSlider(this, SLOT(volumeChanged(int))); + m_ui.statusbar->connectASlider(this, SLOT(auxAChanged(int))); + m_ui.statusbar->connectBSlider(this, SLOT(auxBChanged(int))); m_ui.keyboardContents->setStatusFocus(new StatusBarFocus(m_ui.statusbar)); m_ui.velocitySlider->setStatusFocus(new StatusBarFocus(m_ui.statusbar)); @@ -299,12 +301,25 @@ bool MainWindow::setProjectPath(const QString& path) void MainWindow::refreshAudioIO() { QList audioActions = m_ui.menuAudio->actions(); - if (audioActions.size() > 3) - for (auto it = audioActions.begin() + 3 ; it != audioActions.end() ; ++it) + if (audioActions.size() > 2) + for (auto it = audioActions.begin() + 2 ; it != audioActions.end() ; ++it) m_ui.menuAudio->removeAction(*it); bool addedDev = false; - // TODO: Do + if (m_voxEngine) + { + std::string curOut = m_voxEngine->getCurrentAudioOutput(); + for (const auto& dev : m_voxEngine->enumerateAudioOutputs()) + { + QAction* act = m_ui.menuAudio->addAction(QString::fromStdString(dev.second)); + act->setCheckable(true); + act->setData(QString::fromStdString(dev.first)); + if (curOut == dev.first) + act->setChecked(true); + connect(act, SIGNAL(triggered()), this, SLOT(setAudioIO())); + addedDev = true; + } + } if (!addedDev) m_ui.menuAudio->addAction(tr("No Audio Devices Found"))->setEnabled(false); @@ -320,11 +335,22 @@ void MainWindow::refreshMIDIIO() bool addedDev = false; if (m_voxEngine) { - for (const auto& dev : m_voxEngine->enumerateMIDIDevices()) + if (m_voxEngine->supportsVirtualMIDIIn()) + { + QAction* act = m_ui.menuMIDI->addAction(tr("Virtual MIDI-In")); + act->setCheckable(true); + act->setData(QStringLiteral("")); + act->setChecked(static_cast(m_engine->getMIDIReader())->hasVirtualIn()); + connect(act, SIGNAL(triggered(bool)), this, SLOT(setMIDIIO(bool))); + addedDev = true; + } + for (const auto& dev : m_voxEngine->enumerateMIDIInputs()) { QAction* act = m_ui.menuMIDI->addAction(QString::fromStdString(dev.second)); + act->setCheckable(true); act->setData(QString::fromStdString(dev.first)); - connect(act, SIGNAL(triggered()), this, SLOT(setMIDIIO())); + act->setChecked(static_cast(m_engine->getMIDIReader())->hasMIDIIn(dev.first.c_str())); + connect(act, SIGNAL(triggered(bool)), this, SLOT(setMIDIIO(bool))); addedDev = true; } } @@ -414,13 +440,13 @@ void MainWindow::keyReleaseEvent(QKeyEvent* ev) setSustain(false); } -void MainWindow::startBackgroundTask(const QString& windowTitle, const QString& label, +void MainWindow::startBackgroundTask(int id, 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 = new BackgroundTask(id, std::move(task)); m_backgroundTask->moveToThread(&m_backgroundThread); m_backgroundDialog = new QProgressDialog(this); @@ -442,8 +468,8 @@ void MainWindow::startBackgroundTask(const QString& windowTitle, const QString& 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); + connect(m_backgroundTask, SIGNAL(finished(int)), + this, SLOT(onBackgroundTaskFinished(int)), Qt::QueuedConnection); m_backgroundDialog->open(m_backgroundTask, SLOT(cancel())); connectMessenger(&m_backgroundTask->uiMessenger(), Qt::BlockingQueuedConnection); @@ -579,6 +605,8 @@ amuse::ObjToken MainWindow::startEditorVoice(uint8_t key, uint8_t vox->setPedal(m_ctrlVals[64] >= 0x40); vox->setPitchWheel(m_pitch); vox->installCtrlValues(m_ctrlVals); + vox->setReverbVol(m_auxAVol); + vox->setAuxBVol(m_auxBVol); } } return vox; @@ -589,7 +617,10 @@ amuse::ObjToken MainWindow::startSFX(amuse::GroupId groupId, amuse if (ProjectModel::INode* node = getEditorNode()) { amuse::AudioGroupDatabase* group = projectModel()->getGroupNode(node)->getAudioGroup(); - return m_engine->fxStart(group, groupId, sfxId, 1.f, 0.f); + auto ret = m_engine->fxStart(group, groupId, sfxId, 1.f, 0.f); + ret->setReverbVol(m_auxAVol); + ret->setAuxBVol(m_auxBVol); + return ret; } return {}; } @@ -600,7 +631,13 @@ amuse::ObjToken MainWindow::startSong(amuse::GroupId groupId, if (ProjectModel::INode* node = getEditorNode()) { amuse::AudioGroupDatabase* group = projectModel()->getGroupNode(node)->getAudioGroup(); - return m_engine->seqPlay(group, groupId, songId, arrData); + auto ret = m_engine->seqPlay(group, groupId, songId, arrData); + for (uint8_t i = 0; i < 16; ++i) + { + ret->setCtrlValue(i, 0x5b, int8_t(m_auxAVol * 127.f)); + ret->setCtrlValue(i, 0x5d, int8_t(m_auxBVol * 127.f)); + } + return ret; } return {}; } @@ -702,7 +739,7 @@ bool MainWindow::openProject(const QString& path) return false; ProjectModel* model = m_projectModel; - startBackgroundTask(tr("Opening"), tr("Scanning Project"), + startBackgroundTask(TaskOpen, tr("Opening"), tr("Scanning Project"), [dir, model](BackgroundTask& task) { QStringList childDirs = dir.entryList(QDir::Dirs); @@ -710,6 +747,8 @@ bool MainWindow::openProject(const QString& path) { if (task.isCanceled()) return; + if (chDir == QStringLiteral("out")) + continue; QString chPath = dir.filePath(chDir); if (QFileInfo(chPath, QStringLiteral("!project.yaml")).exists() && QFileInfo(chPath, QStringLiteral("!pool.yaml")).exists()) @@ -785,7 +824,7 @@ void MainWindow::reloadSampleDataAction() if (!dir.exists()) return; - startBackgroundTask(tr("Reloading Samples"), tr("Scanning Project"), + startBackgroundTask(TaskReloadSamples, tr("Reloading Samples"), tr("Scanning Project"), [dir, model](BackgroundTask& task) { QStringList childDirs = dir.entryList(QDir::Dirs); @@ -793,6 +832,8 @@ void MainWindow::reloadSampleDataAction() { if (task.isCanceled()) return; + if (chDir == QStringLiteral("out")) + continue; QString chPath = dir.filePath(chDir); if (QFileInfo(chPath, QStringLiteral("!project.yaml")).exists() && QFileInfo(chPath, QStringLiteral("!pool.yaml")).exists()) @@ -869,7 +910,7 @@ void MainWindow::importAction() } ProjectModel* model = m_projectModel; - startBackgroundTask(tr("Importing"), tr("Scanning Project"), + startBackgroundTask(TaskImport, tr("Importing"), tr("Scanning Project"), [model, path, importMode](BackgroundTask& task) { QDir dir = QFileInfo(path).dir(); @@ -914,7 +955,7 @@ void MainWindow::importAction() } ProjectModel* model = m_projectModel; - startBackgroundTask(tr("Importing"), tr("Scanning Project"), + startBackgroundTask(TaskImport, tr("Importing"), tr("Scanning Project"), [model, path, importMode](BackgroundTask& task) { /* Handle single container */ @@ -949,7 +990,32 @@ void MainWindow::importSongsAction() void MainWindow::exportAction() { + if (!m_projectModel) + return; + QFileInfo dirInfo(m_projectModel->dir(), QStringLiteral("out")); + if (!MkPath(dirInfo.filePath(), m_mainMessenger)) + return; + + QDir dir(dirInfo.filePath()); + + ProjectModel* model = m_projectModel; + startBackgroundTask(BackgroundTaskId::TaskExport, tr("Exporting"), tr("Scanning Project"), + [model, dir](BackgroundTask& task) + { + QStringList groupList = model->getGroupList(); + task.setMaximum(groupList.size()); + int curVal = 0; + for (QString group : groupList) + { + task.setLabelText(tr("Exporting %1").arg(group)); + if (task.isCanceled()) + return; + if (!model->exportGroup(dir.path(), group, task.uiMessenger())) + return; + task.setValue(++curVal); + } + }); } bool TreeDelegate::editorEvent(QEvent* event, @@ -1177,13 +1243,32 @@ void MainWindow::aboutToShowMIDIIOMenu() void MainWindow::setAudioIO() { - // TODO: Do + QByteArray devName = qobject_cast(sender())->data().toString().toUtf8(); + if (m_voxEngine) + m_voxEngine->setCurrentAudioOutput(devName.data()); } -void MainWindow::setMIDIIO() +void MainWindow::setMIDIIO(bool checked) { - // TODO: Do - //qobject_cast(sender())->data().toString().toUtf8().constData(); + QAction* action = qobject_cast(sender()); + QByteArray devName = action->data().toString().toUtf8(); + if (m_voxEngine) + { + MIDIReader* mr = static_cast(m_engine->getMIDIReader()); + if (devName == "") + { + mr->setVirtualIn(checked); + action->setChecked(mr->hasVirtualIn()); + } + else + { + if (checked) + mr->addMIDIIn(devName.data()); + else + mr->removeMIDIIn(devName.data()); + action->setChecked(mr->hasMIDIIn(devName.data())); + } + } } void MainWindow::notePressed(int key) @@ -1243,6 +1328,26 @@ void MainWindow::volumeChanged(int vol) m_engine->setVolume(vol / 100.f); } +void MainWindow::auxAChanged(int vol) +{ + m_auxAVol = vol / 100.f; + for (auto& vox : m_engine->getActiveVoices()) + vox->setReverbVol(m_auxAVol); + for (auto& seq : m_engine->getActiveSequencers()) + for (uint8_t i = 0; i < 16; ++i) + seq->setCtrlValue(i, 0x5b, int8_t(m_auxAVol * 127.f)); +} + +void MainWindow::auxBChanged(int vol) +{ + m_auxBVol = vol / 100.f; + for (auto& vox : m_engine->getActiveVoices()) + vox->setAuxBVol(m_auxBVol); + for (auto& seq : m_engine->getActiveSequencers()) + for (uint8_t i = 0; i < 16; ++i) + seq->setCtrlValue(i, 0x5d, int8_t(m_auxBVol * 127.f)); +} + void MainWindow::outlineCutAction() { @@ -1399,16 +1504,32 @@ void MainWindow::studioSetupShown() m_ui.statusbar->setFXDown(true); } -void MainWindow::onBackgroundTaskFinished() +void MainWindow::onBackgroundTaskFinished(int id) { m_backgroundDialog->reset(); m_backgroundDialog->deleteLater(); m_backgroundDialog = nullptr; m_backgroundTask->deleteLater(); m_backgroundTask = nullptr; - bool hasGroups = m_projectModel->ensureModelData(); - m_ui.actionImport_Groups->setDisabled(hasGroups); - m_ui.actionImport_Songs->setEnabled(hasGroups); + + if (id == TaskExport) + { + if (m_mainMessenger.question(tr("Export Complete"), tr("%1?"). + arg(ShowInGraphicalShellString())) == QMessageBox::Yes) + { + QFileInfo dirInfo(m_projectModel->dir(), QStringLiteral("out")); + QDir dir(dirInfo.filePath()); + QStringList entryList = dir.entryList(QDir::Files); + ShowInGraphicalShell(this, entryList.empty() ? dirInfo.filePath() : QFileInfo(dir, entryList.first()).filePath()); + } + } + else + { + bool hasGroups = m_projectModel->ensureModelData(); + m_ui.actionImport_Groups->setDisabled(hasGroups); + m_ui.actionImport_Songs->setEnabled(hasGroups); + } + setEnabled(true); } diff --git a/Editor/MainWindow.hpp b/Editor/MainWindow.hpp index 1a1ccd0..900d111 100644 --- a/Editor/MainWindow.hpp +++ b/Editor/MainWindow.hpp @@ -32,15 +32,24 @@ class KeymapEditor; class LayersEditor; class SampleEditor; +enum BackgroundTaskId +{ + TaskOpen, + TaskImport, + TaskExport, + TaskReloadSamples +}; + class BackgroundTask : public QObject { Q_OBJECT + int m_id; 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) {} + explicit BackgroundTask(int id, std::function&& task) + : m_id(id), m_task(std::move(task)), m_threadMessenger(this) {} bool isCanceled() const { QCoreApplication::processEvents(); return m_cancelled; } UIMessenger& uiMessenger() { return m_threadMessenger; } @@ -49,10 +58,10 @@ signals: void setMaximum(int maximum); void setValue(int value); void setLabelText(const QString& text); - void finished(); + void finished(int id); public slots: - void run() { m_task(*this); emit finished(); } + void run() { m_task(*this); emit finished(m_id); } void cancel() { m_cancelled = true; } }; @@ -99,6 +108,8 @@ class MainWindow : public QMainWindow int m_velocity = 90; float m_pitch = 0.f; int8_t m_ctrlVals[128] = {}; + float m_auxAVol = 0.f; + float m_auxBVol = 0.f; bool m_uiDisabled = false; QUndoStack* m_undoStack; @@ -125,7 +136,7 @@ class MainWindow : public QMainWindow void keyPressEvent(QKeyEvent* ev); void keyReleaseEvent(QKeyEvent* ev); - void startBackgroundTask(const QString& windowTitle, const QString& label, + void startBackgroundTask(int id, const QString& windowTitle, const QString& label, std::function&& task); bool _setEditor(EditorWidget* widget); @@ -193,7 +204,7 @@ public slots: void aboutToShowMIDIIOMenu(); void setAudioIO(); - void setMIDIIO(); + void setMIDIIO(bool checked); void notePressed(int key); void noteReleased(); @@ -203,6 +214,8 @@ public slots: void killSounds(); void fxPressed(); void volumeChanged(int vol); + void auxAChanged(int vol); + void auxBChanged(int vol); void outlineCutAction(); void outlineCopyAction(); @@ -222,7 +235,7 @@ public slots: void studioSetupHidden(); void studioSetupShown(); - void onBackgroundTaskFinished(); + void onBackgroundTaskFinished(int id); QMessageBox::StandardButton msgInformation(const QString &title, const QString &text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, diff --git a/Editor/ProjectModel.cpp b/Editor/ProjectModel.cpp index 76209f6..11437dd 100644 --- a/Editor/ProjectModel.cpp +++ b/Editor/ProjectModel.cpp @@ -581,6 +581,48 @@ bool ProjectModel::saveToFile(UIMessenger& messenger) return true; } +QStringList ProjectModel::getGroupList() const +{ + QStringList list; + list.reserve(m_root->childCount()); + m_root->oneLevelTraverse([&list](INode* node) + { + list.push_back(node->name()); + return true; + }); + return list; +} + +bool ProjectModel::exportGroup(const QString& path, const QString& groupName, UIMessenger& messenger) const +{ + auto search = m_groups.find(groupName); + if (search == m_groups.cend()) + { + messenger.critical(tr("Export Error"), tr("Unable to find group %1").arg(groupName)); + return false; + } + const amuse::AudioGroupDatabase& group = search->second; + m_projectDatabase.setIdDatabases(); + auto basePath = QStringToSysString(QFileInfo(QDir(path), groupName).filePath()); + group.setIdDatabases(); + if (!group.getProj().toGCNData(basePath, group.getPool(), group.getSdir())) + { + messenger.critical(tr("Export Error"), tr("Unable to export %1.proj").arg(groupName)); + return false; + } + if (!group.getPool().toData(basePath)) + { + messenger.critical(tr("Export Error"), tr("Unable to export %1.pool").arg(groupName)); + return false; + } + if (!group.getSdir().toGCNData(basePath, group)) + { + messenger.critical(tr("Export Error"), tr("Unable to export %1.sdir").arg(groupName)); + return false; + } + return true; +} + void ProjectModel::_buildGroupNode(GroupNode& gn) { amuse::AudioGroup& group = gn.m_it->second; diff --git a/Editor/ProjectModel.hpp b/Editor/ProjectModel.hpp index 82631e9..5dbddb2 100644 --- a/Editor/ProjectModel.hpp +++ b/Editor/ProjectModel.hpp @@ -363,6 +363,8 @@ public: ImportMode mode, UIMessenger& messenger); void saveSongsIndex(); bool saveToFile(UIMessenger& messenger); + QStringList getGroupList() const; + bool exportGroup(const QString& path, const QString& groupName, UIMessenger& messenger) const; bool ensureModelData(); diff --git a/Editor/SongGroupEditor.cpp b/Editor/SongGroupEditor.cpp index 2f087a3..1dac26c 100644 --- a/Editor/SongGroupEditor.cpp +++ b/Editor/SongGroupEditor.cpp @@ -1150,7 +1150,7 @@ SetupTableView::SetupTableView(QWidget* parent) void ColoredTabBarStyle::drawControl(QStyle::ControlElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const { - if (element == QStyle::CE_TabBarTab) + if (qobject_cast(widget) && element == QStyle::CE_TabBarTab) { QStyleOptionTab optionTab = *static_cast(option); switch (optionTab.position) @@ -1171,10 +1171,9 @@ void ColoredTabBarStyle::drawControl(QStyle::ControlElement element, const QStyl } ColoredTabBar::ColoredTabBar(QWidget* parent) -: QTabBar(parent), m_style(new ColoredTabBarStyle(style())) +: QTabBar(parent) { setDrawBase(false); - setStyle(m_style); } ColoredTabWidget::ColoredTabWidget(QWidget* parent) diff --git a/Editor/SongGroupEditor.hpp b/Editor/SongGroupEditor.hpp index c78bddd..ebcfa49 100644 --- a/Editor/SongGroupEditor.hpp +++ b/Editor/SongGroupEditor.hpp @@ -206,7 +206,6 @@ public: class ColoredTabBar : public QTabBar { Q_OBJECT - ColoredTabBarStyle* m_style; public: explicit ColoredTabBar(QWidget* parent = Q_NULLPTR); }; diff --git a/Editor/StatusBarWidget.cpp b/Editor/StatusBarWidget.cpp index 368b418..42ae23b 100644 --- a/Editor/StatusBarWidget.cpp +++ b/Editor/StatusBarWidget.cpp @@ -8,7 +8,8 @@ FXButton::FXButton(QWidget* parent) } StatusBarWidget::StatusBarWidget(QWidget* parent) -: QStatusBar(parent), m_volumeSlider(Qt::Horizontal) +: QStatusBar(parent), m_volumeSlider(Qt::Horizontal), + m_aSlider(Qt::Horizontal), m_bSlider(Qt::Horizontal) { addWidget(&m_normalMessage); m_killButton.setIcon(QIcon(QStringLiteral(":/icons/IconKill.svg"))); @@ -19,14 +20,35 @@ StatusBarWidget::StatusBarWidget(QWidget* parent) m_volumeIcons[1] = QIcon(QStringLiteral(":/icons/IconVolume1")); m_volumeIcons[2] = QIcon(QStringLiteral(":/icons/IconVolume2")); m_volumeIcons[3] = QIcon(QStringLiteral(":/icons/IconVolume3")); + m_aIcon.setFixedSize(16, 16); + m_aIcon.setPixmap(QIcon(QStringLiteral(":/icons/IconA.svg")).pixmap(16, 16)); + QString aTip = tr("Aux A send level for all voices"); + m_aIcon.setToolTip(aTip); + m_aSlider.setRange(0, 100); + m_aSlider.setFixedWidth(100); + m_aSlider.setToolTip(aTip); + m_bIcon.setFixedSize(16, 16); + m_bIcon.setPixmap(QIcon(QStringLiteral(":/icons/IconB.svg")).pixmap(16, 16)); + QString bTip = tr("Aux B send level for all voices"); + m_bIcon.setToolTip(bTip); + m_bSlider.setRange(0, 100); + m_bSlider.setFixedWidth(100); + m_bSlider.setToolTip(bTip); m_volumeIcon.setFixedSize(16, 16); m_volumeIcon.setPixmap(m_volumeIcons[0].pixmap(16, 16)); + QString volTip = tr("Master volume level"); + m_volumeIcon.setToolTip(volTip); connect(&m_volumeSlider, SIGNAL(valueChanged(int)), this, SLOT(volumeChanged(int))); m_volumeSlider.setRange(0, 100); m_volumeSlider.setFixedWidth(100); + m_volumeSlider.setToolTip(volTip); addPermanentWidget(&m_voiceCount); addPermanentWidget(&m_killButton); addPermanentWidget(&m_fxButton); + addPermanentWidget(&m_aIcon); + addPermanentWidget(&m_aSlider); + addPermanentWidget(&m_bIcon); + addPermanentWidget(&m_bSlider); addPermanentWidget(&m_volumeIcon); addPermanentWidget(&m_volumeSlider); } diff --git a/Editor/StatusBarWidget.hpp b/Editor/StatusBarWidget.hpp index 47ccf9c..43018b0 100644 --- a/Editor/StatusBarWidget.hpp +++ b/Editor/StatusBarWidget.hpp @@ -31,6 +31,10 @@ class StatusBarWidget : public QStatusBar QIcon m_volumeIcons[4]; QLabel m_volumeIcon; QSlider m_volumeSlider; + QLabel m_aIcon; + QSlider m_aSlider; + QLabel m_bIcon; + QSlider m_bSlider; int m_lastVolIdx = 0; QLabel m_voiceCount; int m_cachedVoiceCount = -1; @@ -47,6 +51,10 @@ public: void setFXDown(bool down) { m_fxButton.setDown(down); } void connectVolumeSlider(const QObject* receiver, const char* method) { connect(&m_volumeSlider, SIGNAL(valueChanged(int)), receiver, method); } + void connectASlider(const QObject* receiver, const char* method) + { connect(&m_aSlider, SIGNAL(valueChanged(int)), receiver, method); } + void connectBSlider(const QObject* receiver, const char* method) + { connect(&m_bSlider, SIGNAL(valueChanged(int)), receiver, method); } void setVolumeValue(int vol) { m_volumeSlider.setValue(vol); } private slots: diff --git a/Editor/StudioSetupWidget.cpp b/Editor/StudioSetupWidget.cpp index dfef058..5f77fb8 100644 --- a/Editor/StudioSetupWidget.cpp +++ b/Editor/StudioSetupWidget.cpp @@ -4,6 +4,7 @@ #include "amuse/EffectReverb.hpp" #include #include +#include using namespace std::literals; @@ -251,6 +252,90 @@ static void SetEffectParm(amuse::EffectBaseTypeless* effect, int idx, int chanId } } +static const char* ChanNames[] = +{ + QT_TRANSLATE_NOOP("Uint32X8Popup", "Front Left"), + QT_TRANSLATE_NOOP("Uint32X8Popup", "Front Right"), + QT_TRANSLATE_NOOP("Uint32X8Popup", "Rear Left"), + QT_TRANSLATE_NOOP("Uint32X8Popup", "Rear Right"), + QT_TRANSLATE_NOOP("Uint32X8Popup", "Front Center"), + QT_TRANSLATE_NOOP("Uint32X8Popup", "LFE"), + QT_TRANSLATE_NOOP("Uint32X8Popup", "Side Left"), + QT_TRANSLATE_NOOP("Uint32X8Popup", "Side Right") +}; + +Uint32X8Popup::Uint32X8Popup(int min, int max, QWidget* parent) +: QFrame(parent, Qt::Popup) +{ + setAttribute(Qt::WA_WindowPropagation); + setAttribute(Qt::WA_X11NetWmWindowTypeCombo); + Uint32X8Button* combo = static_cast(parent); + QStyleOptionComboBox opt = combo->comboStyleOption(); + setFrameStyle(combo->style()->styleHint(QStyle::SH_ComboBox_PopupFrameStyle, &opt, combo)); + + QGridLayout* layout = new QGridLayout; + for (int i = 0; i < 8; ++i) + { + layout->addWidget(new QLabel(tr(ChanNames[i])), i, 0); + FieldSlider* slider = new FieldSlider; + m_sliders[i] = slider; + slider->setToolTip(QStringLiteral("[%1,%2]").arg(min).arg(max)); + slider->setProperty("chanIdx", i); + slider->setRange(min, max); + connect(slider, SIGNAL(valueChanged(int)), this, SLOT(doValueChanged(int))); + layout->addWidget(slider, i, 1); + } + setLayout(layout); +} + +void Uint32X8Popup::setValue(int chanIdx, int val) +{ + m_sliders[chanIdx]->setValue(val); +} + +void Uint32X8Popup::doValueChanged(int val) +{ + FieldSlider* slider = static_cast(sender()); + int chanIdx = slider->property("chanIdx").toInt(); + emit valueChanged(chanIdx, val); +} + +Uint32X8Button::Uint32X8Button(int min, int max, QWidget* parent) +: QPushButton(parent), m_popup(new Uint32X8Popup(min, max, this)) +{ + connect(this, SIGNAL(pressed()), this, SLOT(onPressed())); +} + +void Uint32X8Button::paintEvent(QPaintEvent*) +{ + QStylePainter painter(this); + painter.setPen(palette().color(QPalette::Text)); + + // draw the combobox frame, focusrect and selected etc. + QStyleOptionComboBox opt = comboStyleOption(); + painter.drawComplexControl(QStyle::CC_ComboBox, opt); + + // draw the icon and text + painter.drawControl(QStyle::CE_ComboBoxLabel, opt); +} + +QStyleOptionComboBox Uint32X8Button::comboStyleOption() const +{ + QStyleOptionComboBox opt; + opt.initFrom(this); + opt.editable = false; + opt.frame = true; + opt.currentText = tr("Channels"); + return opt; +} + +void Uint32X8Button::onPressed() +{ + QPoint pt = parentWidget()->mapToGlobal(pos()); + m_popup->move(pt.x(), pt.y()); + m_popup->show(); +} + EffectWidget::EffectWidget(amuse::EffectBaseTypeless* effect, amuse::EffectType type) : QWidget(nullptr), m_effect(effect), m_introspection(GetEffectIntrospection(type)) { @@ -258,8 +343,6 @@ EffectWidget::EffectWidget(amuse::EffectBaseTypeless* effect, amuse::EffectType titleFont.setWeight(QFont::Bold); m_titleLabel.setFont(titleFont); m_titleLabel.setForegroundRole(QPalette::Background); - //m_titleLabel.setAutoFillBackground(true); - //m_titleLabel.setBackgroundRole(QPalette::Text); m_titleLabel.setContentsMargins(46, 0, 0, 0); m_titleLabel.setFixedHeight(20); m_numberText.setTextOption(QTextOption(Qt::AlignRight)); @@ -315,6 +398,16 @@ EffectWidget::EffectWidget(amuse::EffectBaseTypeless* effect, amuse::EffectType layout->addWidget(sb, 1, f); break; } + case EffectIntrospection::Field::Type::UInt32x8: + { + Uint32X8Button* sb = new Uint32X8Button(int(field.m_min), int(field.m_max)); + sb->popup()->setProperty("fieldIndex", f); + for (int i = 0; i < 8; ++i) + sb->popup()->setValue(i, GetEffectParm(m_effect, f, i)); + connect(sb->popup(), SIGNAL(valueChanged(int, int)), this, SLOT(chanNumChanged(int, int))); + layout->addWidget(sb, 1, f); + break; + } case EffectIntrospection::Field::Type::Float: { FieldDoubleSlider* sb = new FieldDoubleSlider; @@ -397,6 +490,11 @@ void EffectWidget::numChanged(double value) SetEffectParm(m_effect, sender()->property("fieldIndex").toInt(), 0, value); } +void EffectWidget::chanNumChanged(int chanIdx, int value) +{ + SetEffectParm(m_effect, sender()->property("fieldIndex").toInt(), chanIdx, value); +} + void EffectWidget::deleteClicked() { if (m_index != -1) diff --git a/Editor/StudioSetupWidget.hpp b/Editor/StudioSetupWidget.hpp index abc334f..c11732c 100644 --- a/Editor/StudioSetupWidget.hpp +++ b/Editor/StudioSetupWidget.hpp @@ -13,6 +13,8 @@ #include #include "amuse/Studio.hpp" +class EffectListing; + struct EffectIntrospection { struct Field @@ -34,7 +36,31 @@ struct EffectIntrospection Field m_fields[7]; }; -class EffectListing; +class Uint32X8Popup : public QFrame +{ + Q_OBJECT + FieldSlider* m_sliders[8]; +public: + explicit Uint32X8Popup(int min, int max, QWidget* parent = Q_NULLPTR); + void setValue(int chanIdx, int val); +private slots: + void doValueChanged(int val); +signals: + void valueChanged(int chanIdx, int val); +}; + +class Uint32X8Button : public QPushButton +{ + Q_OBJECT + Uint32X8Popup* m_popup; +public: + explicit Uint32X8Button(int min, int max, QWidget* parent = Q_NULLPTR); + void paintEvent(QPaintEvent* event); + Uint32X8Popup* popup() const { return m_popup; } + QStyleOptionComboBox comboStyleOption() const; +private slots: + void onPressed(); +}; class EffectWidget : public QWidget { @@ -51,13 +77,14 @@ class EffectWidget : public QWidget private slots: void numChanged(int); void numChanged(double); + void chanNumChanged(int, int); void deleteClicked(); private: - EffectWidget(amuse::EffectBaseTypeless* effect, amuse::EffectType type); + explicit EffectWidget(amuse::EffectBaseTypeless* effect, amuse::EffectType type); public: EffectListing* getParent() const; - EffectWidget(amuse::EffectBaseTypeless* effect); - EffectWidget(amuse::EffectType op); + explicit EffectWidget(amuse::EffectBaseTypeless* effect); + explicit EffectWidget(amuse::EffectType op); void paintEvent(QPaintEvent* event); QString getText() const { return m_titleLabel.text(); } }; diff --git a/Editor/main.cpp b/Editor/main.cpp index 4d1088b..0a94624 100644 --- a/Editor/main.cpp +++ b/Editor/main.cpp @@ -3,6 +3,7 @@ #include #include #include "MainWindow.hpp" +#include "SongGroupEditor.hpp" #include "boo/IApplication.hpp" #include #include @@ -61,7 +62,7 @@ int main(int argc, char* argv[]) QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); #endif - QApplication::setStyle(QStyleFactory::create("Fusion")); + QApplication::setStyle(new ColoredTabBarStyle(QStyleFactory::create("Fusion"))); QApplication a(argc, argv); QApplication::setWindowIcon(MakeAppIcon()); diff --git a/Editor/resources/IconA.svg b/Editor/resources/IconA.svg new file mode 100644 index 0000000..331a327 --- /dev/null +++ b/Editor/resources/IconA.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/Editor/resources/IconB.svg b/Editor/resources/IconB.svg new file mode 100644 index 0000000..38a7f22 --- /dev/null +++ b/Editor/resources/IconB.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/Editor/resources/IconVolume0.svg b/Editor/resources/IconVolume0.svg index 0a1e89f..4c63dff 100644 --- a/Editor/resources/IconVolume0.svg +++ b/Editor/resources/IconVolume0.svg @@ -9,9 +9,9 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="16" - height="16" - viewBox="0 0 4.2333332 4.2333335" + width="20" + height="20" + viewBox="0 0 5.2916665 5.2916669" id="svg2" version="1.1" inkscape:version="0.92.2 2405546, 2018-03-11" @@ -63,10 +63,10 @@ inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" - transform="translate(0,-292.76665)"> + transform="translate(0,-291.70832)"> diff --git a/Editor/resources/IconVolume1.svg b/Editor/resources/IconVolume1.svg index 45fb416..53eedd1 100644 --- a/Editor/resources/IconVolume1.svg +++ b/Editor/resources/IconVolume1.svg @@ -9,9 +9,9 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="16" - height="16" - viewBox="0 0 4.2333332 4.2333335" + width="20" + height="20" + viewBox="0 0 5.2916665 5.2916669" id="svg2" version="1.1" inkscape:version="0.92.2 2405546, 2018-03-11" @@ -63,16 +63,16 @@ inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" - transform="translate(0,-292.76665)"> + transform="translate(0,-291.70832)"> diff --git a/Editor/resources/IconVolume2.svg b/Editor/resources/IconVolume2.svg index 4175dc8..9243bfd 100644 --- a/Editor/resources/IconVolume2.svg +++ b/Editor/resources/IconVolume2.svg @@ -9,9 +9,9 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="16" - height="16" - viewBox="0 0 4.2333332 4.2333335" + width="20" + height="20" + viewBox="0 0 5.2916665 5.2916669" id="svg2" version="1.1" inkscape:version="0.92.2 2405546, 2018-03-11" @@ -63,21 +63,21 @@ inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" - transform="translate(0,-292.76665)"> + transform="translate(0,-291.70832)"> diff --git a/Editor/resources/IconVolume3.svg b/Editor/resources/IconVolume3.svg index 3e7e03c..d53066b 100644 --- a/Editor/resources/IconVolume3.svg +++ b/Editor/resources/IconVolume3.svg @@ -9,9 +9,9 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="16" - height="16" - viewBox="0 0 4.2333332 4.2333335" + width="20" + height="20" + viewBox="0 0 5.2916665 5.2916669" id="svg2" version="1.1" inkscape:version="0.92.2 2405546, 2018-03-11" @@ -63,27 +63,27 @@ inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" - transform="translate(0,-292.76665)"> + transform="translate(0,-291.70832)"> diff --git a/Editor/resources/lang_de.ts b/Editor/resources/lang_de.ts index a193873..4cc0c4e 100644 --- a/Editor/resources/lang_de.ts +++ b/Editor/resources/lang_de.ts @@ -149,26 +149,26 @@ EffectCatalogue - - + + Reverb Standard - - + + Reverb High - - + + Delay - - + + Chorus @@ -350,13 +350,13 @@ MIDIPlayerWidget - + Stop - - + + Play @@ -559,284 +559,310 @@ - + Clear Recent Projects - + Quit - + Amuse[*] - + %1/%2/%3[*] - Amuse - + %1[*] - Amuse - - + + The directory at '%1' must not be empty. - - + + Directory empty - + The directory at '%1' must exist for the Amuse editor. - + Directory does not exist - + __amuse_test__ - + The directory at '%1' must be writable for the Amuse editor: %2 - + Unable to write to directory - + No Audio Devices Found - + + Virtual MIDI-In + + + + No MIDI Devices Found - + SUSTAIN - + Unsaved Changes - + Save Changes in %1? - + New Project - + The directory at '%1' does not exist. - + Bad Directory - + Opening - - - - + + + + + Scanning Project - + Opening %1 - + Open Project - + Reloading Samples - + Scanning %1 - + Import Project - + The file at '%1' could not be interpreted as a MusyX container. - + Unsupported MusyX Container - + Sample Import Mode - + 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 whichever version was modified most recently. - + Import Compressed - + Import WAVs - + Import Both - + Raw Import Mode - + Would you like to scan for all MusyX group files in this directory? - + Project Name - + What should this project be named? - - + + Importing - - + + Importing %1 - + Import Songs - + + Exporting + + + + + Exporting %1 + + + + + Export Complete + + + + + %1? + + + + New Subproject - + New SFX Group - + What should the new SFX group in %1 be named? - + New Song Group - + What should the new Song group in %1 be named? - + New ADSR - + What should the new ADSR in %1 be named? - + New Curve - + What should the new Curve in %1 be named? - + New Keymap - + What should the new Keymap in %1 be named? - + New Layers - + What should the new Layers in %1 be named? - + What should this subproject be named? @@ -1055,187 +1081,215 @@ ProjectModel - Sound Macros + + + + Export Error + + + + + Unable to find group %1 + + + + + Unable to export %1.proj + + + + + Unable to export %1.pool - ADSRs - - - - - Curves + Unable to export %1.sdir + Sound Macros + + + + + ADSRs + + + + + Curves + + + + Keymaps - + Layers - + Samples - + Subproject Conflict - + The subproject %1 is already defined - + Add Subproject %1 - + Sound Group Conflict - - + + The group %1 is already defined - + Add Sound Group %1 - + Song Group Conflict - + Add Song Group %1 - + Sound Macro Conflict - + The macro %1 is already defined - + Add Sound Macro %1 - + ADSR Conflict - + The ADSR %1 is already defined - + Add ADSR %1 - + Curve Conflict - + The Curve %1 is already defined - + Add Curve %1 - + Keymap Conflict - + The Keymap %1 is already defined - + Add Keymap %1 - + Layers Conflict - + Layers %1 is already defined - + Add Layers %1 - + Delete Subproject %1 - + Delete SongGroup %1 - + Delete SFXGroup %1 - + Delete SoundMacro %1 - + Delete ADSR %1 - + Delete Curve %1 - + Delete Keymap %1 - + Delete Layers %1 @@ -1481,37 +1535,37 @@ SongGroupEditor - + Add new page entry - + Remove selected page entries - + Normal Pages - + Add Page Entry - + Add Setup Entry - + Drum Pages - + MIDI Setups @@ -1636,25 +1690,40 @@ StatusBarWidget - + Immediately kill active voices + + + Aux A send level for all voices + + + + + Aux B send level for all voices + + + + + Master volume level + + StudioSetupWidget - + Studio Setup - + Aux A - + Aux B @@ -1667,6 +1736,57 @@ + + Uint32X8Button + + + Channels + + + + + Uint32X8Popup + + + Front Left + + + + + Front Right + + + + + Rear Left + + + + + Rear Right + + + + + Front Center + + + + + LFE + + + + + Side Left + + + + + Side Right + + + VelocitySlider diff --git a/Editor/resources/resources.qrc b/Editor/resources/resources.qrc index 685ebb1..42a1e68 100644 --- a/Editor/resources/resources.qrc +++ b/Editor/resources/resources.qrc @@ -34,6 +34,8 @@ IconVolume2.svg IconVolume3.svg IconFX.svg + IconB.svg + IconA.svg FaceGrey.svg diff --git a/include/amuse/AudioGroup.hpp b/include/amuse/AudioGroup.hpp index 620e540..52ef54a 100644 --- a/include/amuse/AudioGroup.hpp +++ b/include/amuse/AudioGroup.hpp @@ -12,6 +12,7 @@ class AudioGroupData; /** Runtime audio group index container */ class AudioGroup { + friend class AudioGroupSampleDirectory; AudioGroupProject m_proj; AudioGroupPool m_pool; AudioGroupSampleDirectory m_sdir; diff --git a/include/amuse/AudioGroupPool.hpp b/include/amuse/AudioGroupPool.hpp index 5edb0df..306e91a 100644 --- a/include/amuse/AudioGroupPool.hpp +++ b/include/amuse/AudioGroupPool.hpp @@ -1132,6 +1132,8 @@ struct SoundMacro template void readCmds(athena::io::IStreamReader& r, uint32_t size); + template + void writeCmds(athena::io::IStreamWriter& w) const; ICmd* insertNewCmd(int idx, CmdOp op) { @@ -1423,6 +1425,8 @@ public: const Curve* tableAsCurves(ObjectId id) const; bool toYAML(SystemStringView groupPath) const; + template + bool toData(SystemStringView groupPath) const; AudioGroupPool(const AudioGroupPool&) = delete; AudioGroupPool& operator=(const AudioGroupPool&) = delete; diff --git a/include/amuse/AudioGroupProject.hpp b/include/amuse/AudioGroupProject.hpp index 3cc1cd9..4bba14a 100644 --- a/include/amuse/AudioGroupProject.hpp +++ b/include/amuse/AudioGroupProject.hpp @@ -11,6 +11,8 @@ namespace amuse { class AudioGroupData; +class AudioGroupPool; +class AudioGroupSampleDirectory; enum class GroupType : atUint16 { @@ -205,6 +207,7 @@ public: std::unordered_map>& sfxGroups() { return m_sfxGroups; } bool toYAML(SystemStringView groupPath) const; + bool toGCNData(SystemStringView groupPath, const AudioGroupPool& pool, const AudioGroupSampleDirectory& sdir) const; AudioGroupProject(const AudioGroupProject&) = delete; AudioGroupProject& operator=(const AudioGroupProject&) = delete; diff --git a/include/amuse/AudioGroupSampleDirectory.hpp b/include/amuse/AudioGroupSampleDirectory.hpp index 30bef8b..503f360 100644 --- a/include/amuse/AudioGroupSampleDirectory.hpp +++ b/include/amuse/AudioGroupSampleDirectory.hpp @@ -8,6 +8,7 @@ namespace amuse { class AudioGroupData; +class AudioGroupDatabase; struct DSPADPCMHeader : BigDNA { @@ -167,6 +168,16 @@ public: Value m_loopStartSample; Value m_loopLengthSamples; Value m_adpcmParmOffset; + + void _setLoopStartSample(atUint32 sample) + { + m_loopLengthSamples += m_loopStartSample - sample; + m_loopStartSample = sample; + } + void setLoopEndSample(atUint32 sample) + { + m_loopLengthSamples = sample + 1 - m_loopStartSample; + } }; template struct AT_SPECIALIZE_PARMS(athena::Endian::Big, athena::Endian::Little) @@ -346,6 +357,8 @@ public: void reloadSampleData(SystemStringView groupPath); + bool toGCNData(SystemStringView groupPath, const AudioGroupDatabase& group) const; + AudioGroupSampleDirectory(const AudioGroupSampleDirectory&) = delete; AudioGroupSampleDirectory& operator=(const AudioGroupSampleDirectory&) = delete; AudioGroupSampleDirectory(AudioGroupSampleDirectory&&) = default; diff --git a/include/amuse/BooBackend.hpp b/include/amuse/BooBackend.hpp index 5ee5a3f..b1e7e78 100644 --- a/include/amuse/BooBackend.hpp +++ b/include/amuse/BooBackend.hpp @@ -73,7 +73,8 @@ class BooBackendMIDIReader : public IMIDIReader, public boo::IMIDIReader friend class BooBackendVoiceAllocator; protected: Engine& m_engine; - std::unique_ptr m_midiIn; + std::unordered_map> m_midiIns; + std::unique_ptr m_virtualIn; boo::MIDIDecoder m_decoder; bool m_useLock; @@ -83,9 +84,14 @@ protected: public: ~BooBackendMIDIReader(); - BooBackendMIDIReader(Engine& engine, const char* name, bool useLock); + BooBackendMIDIReader(Engine& engine, bool useLock); + + void addMIDIIn(const char* name); + void removeMIDIIn(const char* name); + bool hasMIDIIn(const char* name) const; + void setVirtualIn(bool v); + bool hasVirtualIn() const; - std::string description(); void pumpReader(double dt); void noteOff(uint8_t chan, uint8_t key, uint8_t velocity); @@ -129,7 +135,7 @@ public: std::unique_ptr allocateVoice(Voice& clientVox, double sampleRate, bool dynamicPitch); std::unique_ptr allocateSubmix(Submix& clientSmx, bool mainOut, int busId); std::vector> enumerateMIDIDevices(); - std::unique_ptr allocateMIDIReader(Engine& engine, const char* name = nullptr); + std::unique_ptr allocateMIDIReader(Engine& engine); void setCallbackInterface(Engine* engine); AudioChannelSet getAvailableSet(); void setVolume(float vol); diff --git a/include/amuse/Common.hpp b/include/amuse/Common.hpp index 61ff3ff..047ca95 100644 --- a/include/amuse/Common.hpp +++ b/include/amuse/Common.hpp @@ -569,6 +569,28 @@ static std::vector +static std::vector SortUnorderedSet(T& us) +{ + std::vector ret; + ret.reserve(us.size()); + for (auto& p : us) + ret.emplace_back(p); + std::sort(ret.begin(), ret.end()); + return ret; +} + +template +static std::vector SortUnorderedSet(const T& us) +{ + std::vector ret; + ret.reserve(us.size()); + for (const auto& p : us) + ret.emplace_back(p); + std::sort(ret.begin(), ret.end()); + return ret; +} } namespace std diff --git a/include/amuse/Engine.hpp b/include/amuse/Engine.hpp index 1ed13df..c4529bd 100644 --- a/include/amuse/Engine.hpp +++ b/include/amuse/Engine.hpp @@ -75,6 +75,9 @@ public: /** Access voice backend of engine */ IBackendVoiceAllocator& getBackend() { return m_backend; } + /** Access MIDI reader */ + IMIDIReader* getMIDIReader() const { return m_midiReader.get(); } + /** Add audio group data pointers to engine; must remain resident! */ const AudioGroup* addAudioGroup(const AudioGroupData& data); diff --git a/include/amuse/IBackendVoiceAllocator.hpp b/include/amuse/IBackendVoiceAllocator.hpp index 0c28e9a..a4abd82 100644 --- a/include/amuse/IBackendVoiceAllocator.hpp +++ b/include/amuse/IBackendVoiceAllocator.hpp @@ -28,7 +28,6 @@ class IMIDIReader { public: virtual ~IMIDIReader() = default; - virtual std::string description() = 0; virtual void pumpReader(double dt) = 0; }; @@ -48,7 +47,7 @@ public: virtual std::vector> enumerateMIDIDevices() = 0; /** Amuse obtains an interactive MIDI-in connection from the OS this way */ - virtual std::unique_ptr allocateMIDIReader(Engine& engine, const char* name = nullptr) = 0; + virtual std::unique_ptr allocateMIDIReader(Engine& engine) = 0; /** Amuse obtains speaker-configuration from the platform this way */ virtual AudioChannelSet getAvailableSet() = 0; diff --git a/lib/AudioGroupPool.cpp b/lib/AudioGroupPool.cpp index 28fd2e5..8770c68 100644 --- a/lib/AudioGroupPool.cpp +++ b/lib/AudioGroupPool.cpp @@ -1,3 +1,4 @@ +#include #include "amuse/AudioGroupPool.hpp" #include "amuse/Common.hpp" #include "amuse/Entity.hpp" @@ -393,6 +394,21 @@ void SoundMacro::readCmds(athena::io::IStreamReader& r, uint32_t size) template void SoundMacro::readCmds(athena::io::IStreamReader& r, uint32_t size); template void SoundMacro::readCmds(athena::io::IStreamReader& r, uint32_t size); +template +void SoundMacro::writeCmds(athena::io::IStreamWriter& w) const +{ + for (const auto& cmd : m_cmds) + { + uint32_t data[2]; + athena::io::MemoryWriter mw((uint8_t*)data, 8); + mw.writeUByte(uint8_t(cmd->Isa())); + cmd->write(mw); + athena::io::Write::Do({}, data, w); + } +} +template void SoundMacro::writeCmds(athena::io::IStreamWriter& w) const; +template void SoundMacro::writeCmds(athena::io::IStreamWriter& w) const; + void SoundMacro::buildFromPrototype(const SoundMacro& other) { m_cmds.reserve(other.m_cmds.size()); @@ -1063,6 +1079,126 @@ bool AudioGroupPool::toYAML(SystemStringView groupPath) const return w.finish(&fo); } +template +bool AudioGroupPool::toData(SystemStringView groupPath) const +{ + SystemString poolPath(groupPath); + poolPath += _S(".pool"); + athena::io::FileWriter fo(poolPath); + if (fo.hasError()) + return false; + + PoolHeader head = {}; + head.write(fo); + + const uint32_t term = 0xffffffff; + + if (!m_soundMacros.empty()) + { + head.soundMacrosOffset = fo.position(); + for (const auto& p : m_soundMacros) + { + auto startPos = fo.position(); + ObjectHeader objHead = {}; + objHead.write(fo); + p.second->template writeCmds(fo); + objHead.size = fo.position() - startPos; + objHead.objectId = p.first; + fo.seek(startPos, athena::Begin); + objHead.write(fo); + fo.seek(startPos + objHead.size, athena::Begin); + } + athena::io::Write::Do({}, term, fo); + } + + if (!m_tables.empty()) + { + head.tablesOffset = fo.position(); + for (const auto& p : m_tables) + { + auto startPos = fo.position(); + ObjectHeader objHead = {}; + objHead.write(fo); + switch ((*p.second)->Isa()) + { + case ITable::Type::ADSR: + static_cast(p.second->get())->write(fo); + break; + case ITable::Type::ADSRDLS: + static_cast(p.second->get())->write(fo); + break; + case ITable::Type::Curve: + { + const auto& data = static_cast(p.second->get())->data; + fo.writeUBytes(data.data(), data.size()); + break; + } + default: + break; + } + objHead.size = fo.position() - startPos; + objHead.objectId = p.first; + fo.seek(startPos, athena::Begin); + objHead.write(fo); + fo.seek(startPos + objHead.size, athena::Begin); + } + athena::io::Write::Do({}, term, fo); + } + + if (!m_keymaps.empty()) + { + head.keymapsOffset = fo.position(); + for (const auto& p : m_keymaps) + { + auto startPos = fo.position(); + ObjectHeader objHead = {}; + objHead.write(fo); + for (const auto& km : *p.second) + { + KeymapDNA kmData = km.toDNA(); + kmData.write(fo); + } + objHead.size = fo.position() - startPos; + objHead.objectId = p.first; + fo.seek(startPos, athena::Begin); + objHead.write(fo); + fo.seek(startPos + objHead.size, athena::Begin); + } + athena::io::Write::Do({}, term, fo); + } + + if (!m_layers.empty()) + { + head.layersOffset = fo.position(); + for (const auto& p : m_layers) + { + auto startPos = fo.position(); + ObjectHeader objHead = {}; + objHead.write(fo); + uint32_t count = p.second->size(); + athena::io::Write::Do({}, count, fo); + for (const auto& lm : *p.second) + { + LayerMappingDNA lmData = lm.toDNA(); + lmData.write(fo); + } + objHead.size = fo.position() - startPos; + objHead.objectId = p.first; + fo.seek(startPos, athena::Begin); + objHead.write(fo); + fo.seek(startPos + objHead.size, athena::Begin); + } + athena::io::Write::Do({}, term, fo); + } + + fo.seek(0, athena::Begin); + head.write(fo); + + return true; +} +template bool AudioGroupPool::toData(SystemStringView groupPath) const; +template bool AudioGroupPool::toData(SystemStringView groupPath) const; + template <> void amuse::Curve::Enumerate(athena::io::IStreamReader& r) { diff --git a/lib/AudioGroupProject.cpp b/lib/AudioGroupProject.cpp index 49ce862..9d073e8 100644 --- a/lib/AudioGroupProject.cpp +++ b/lib/AudioGroupProject.cpp @@ -1,4 +1,6 @@ #include "amuse/AudioGroupProject.hpp" +#include "amuse/AudioGroupPool.hpp" +#include "amuse/AudioGroupSampleDirectory.hpp" #include "amuse/AudioGroupData.hpp" #include "athena/MemoryReader.hpp" #include "athena/FileWriter.hpp" @@ -7,6 +9,13 @@ namespace amuse { +static bool AtEnd64(athena::io::IStreamReader& r) +{ + uint64_t v = r.readUint64Big(); + r.seek(-8, athena::Current); + return v == 0xffffffffffffffff; +} + static bool AtEnd32(athena::io::IStreamReader& r) { uint32_t v = r.readUint32Big(); @@ -50,6 +59,37 @@ static void ReadRangedObjectIds(NameDB* db, athena::io::IStreamReader& r, NameDB } } +template +static void WriteRangedObjectIds(athena::io::IStreamWriter& w, const T& list) +{ + if (list.cbegin() == list.cend()) + return; + bool inRange = false; + uint16_t lastId = list.cbegin()->first & 0x3fff; + for (auto it = list.cbegin() + 1; it != list.cend(); ++it) + { + uint16_t thisId = it->first & 0x3fff; + if (thisId == lastId + 1) + { + if (!inRange) + { + inRange = true; + lastId |= 0x8000; + athena::io::Write::Do({}, lastId, w); + } + } + else + { + inRange = false; + athena::io::Write::Do({}, lastId, w); + } + lastId = thisId; + } + athena::io::Write::Do({}, lastId, w); + uint16_t term = 0xffff; + athena::io::Write::Do({}, term, w); +} + AudioGroupProject::AudioGroupProject(athena::io::IStreamReader& r, GCNDataTag) { while (!AtEnd32(r)) @@ -91,7 +131,7 @@ AudioGroupProject::AudioGroupProject(athena::io::IStreamReader& r, GCNDataTag) /* Normal pages */ r.seek(header.pageTableOff, athena::Begin); - while (!AtEnd16(r)) + while (!AtEnd64(r)) { SongGroupIndex::PageEntryDNA entry; entry.read(r); @@ -100,7 +140,7 @@ AudioGroupProject::AudioGroupProject(athena::io::IStreamReader& r, GCNDataTag) /* Drum pages */ r.seek(header.drumTableOff, athena::Begin); - while (!AtEnd16(r)) + while (!AtEnd64(r)) { SongGroupIndex::PageEntryDNA entry; entry.read(r); @@ -698,4 +738,243 @@ bool AudioGroupProject::toYAML(SystemStringView groupPath) const return w.finish(&fo); } +#if 0 +struct ObjectIdPool +{ + std::unordered_set soundMacros; + std::unordered_set samples; + std::unordered_set tables; + std::unordered_set keymaps; + std::unordered_set layers; + + void _recursiveAddSoundMacro(SoundMacroId id, const AudioGroupPool& pool) + { + if (soundMacros.find(id) != soundMacros.cend()) + return; + const SoundMacro* macro = pool.soundMacro(id); + if (!macro) + return; + soundMacros.insert(id); + for (const auto& cmd : macro->m_cmds) + { + switch (cmd->Isa()) + { + case SoundMacro::CmdOp::StartSample: + samples.insert(static_cast(cmd.get())->sample); + break; + case SoundMacro::CmdOp::SetAdsr: + tables.insert(static_cast(cmd.get())->table); + break; + case SoundMacro::CmdOp::ScaleVolume: + tables.insert(static_cast(cmd.get())->table); + break; + case SoundMacro::CmdOp::Envelope: + tables.insert(static_cast(cmd.get())->table); + break; + case SoundMacro::CmdOp::FadeIn: + tables.insert(static_cast(cmd.get())->table); + break; + case SoundMacro::CmdOp::SetPitchAdsr: + tables.insert(static_cast(cmd.get())->table); + break; + case SoundMacro::CmdOp::SplitKey: + _recursiveAddSoundMacro(static_cast(cmd.get())->macro, pool); + break; + case SoundMacro::CmdOp::SplitVel: + _recursiveAddSoundMacro(static_cast(cmd.get())->macro, pool); + break; + case SoundMacro::CmdOp::Goto: + _recursiveAddSoundMacro(static_cast(cmd.get())->macro, pool); + break; + case SoundMacro::CmdOp::PlayMacro: + _recursiveAddSoundMacro(static_cast(cmd.get())->macro, pool); + break; + case SoundMacro::CmdOp::SplitMod: + _recursiveAddSoundMacro(static_cast(cmd.get())->macro, pool); + break; + case SoundMacro::CmdOp::SplitRnd: + _recursiveAddSoundMacro(static_cast(cmd.get())->macro, pool); + break; + case SoundMacro::CmdOp::GoSub: + _recursiveAddSoundMacro(static_cast(cmd.get())->macro, pool); + break; + case SoundMacro::CmdOp::TrapEvent: + _recursiveAddSoundMacro(static_cast(cmd.get())->macro, pool); + break; + case SoundMacro::CmdOp::SendMessage: + _recursiveAddSoundMacro(static_cast(cmd.get())->macro, pool); + break; + default: + break; + } + + } + } + + void addRootId(ObjectId id, const AudioGroupPool& pool) + { + if (id & 0x8000) + { + if (const std::vector* lms = pool.layer(id)) + { + layers.insert(id); + for (const auto& lm : *lms) + _recursiveAddSoundMacro(lm.macro, pool); + } + } + else if (id & 0x4000) + { + if (const auto* kms = pool.keymap(id)) + { + keymaps.insert(id); + for (int i = 0; i < 128; ++i) + _recursiveAddSoundMacro(kms[i].macro, pool); + } + } + else + { + _recursiveAddSoundMacro(id, pool); + } + } + + void cleanup() + { + soundMacros.erase(SoundMacroId{}); + samples.erase(SampleId{}); + tables.erase(TableId{}); + keymaps.erase(KeymapId{}); + layers.erase(LayersId{}); + } +}; +#endif + +bool AudioGroupProject::toGCNData(SystemStringView groupPath, const AudioGroupPool& pool, + const AudioGroupSampleDirectory& sdir) const +{ + constexpr athena::Endian DNAE = athena::Big; + + SystemString projPath(groupPath); + projPath += _S(".proj"); + athena::io::FileWriter fo(projPath); + if (fo.hasError()) + return false; + + std::vector groupIds; + groupIds.reserve(m_songGroups.size() + m_sfxGroups.size()); + for (auto& p : m_songGroups) + groupIds.push_back(p.first); + for (auto& p : m_sfxGroups) + groupIds.push_back(p.first); + std::sort(groupIds.begin(), groupIds.end()); + + const uint64_t term64 = 0xffffffffffffffff; + const uint16_t padding = 0; + + for (GroupId id : groupIds) + { + auto search = m_songGroups.find(id); + if (search != m_songGroups.end()) + { + const SongGroupIndex& index = *search->second; + + auto groupStart = fo.position(); + GroupHeader header = {}; + header.write(fo); + + header.groupId = id; + header.type = GroupType::Song; + + header.soundMacroIdsOff = fo.position(); + WriteRangedObjectIds(fo, SortUnorderedMap(pool.soundMacros())); + header.samplIdsOff = fo.position(); + WriteRangedObjectIds(fo, SortUnorderedMap(sdir.sampleEntries())); + header.tableIdsOff = fo.position(); + WriteRangedObjectIds(fo, SortUnorderedMap(pool.tables())); + header.keymapIdsOff = fo.position(); + WriteRangedObjectIds(fo, SortUnorderedMap(pool.keymaps())); + header.layerIdsOff = fo.position(); + WriteRangedObjectIds(fo, SortUnorderedMap(pool.layers())); + + header.pageTableOff = fo.position(); + for (auto& p : SortUnorderedMap(index.m_normPages)) + { + SongGroupIndex::PageEntryDNA entry = p.second.get().toDNA(p.first); + entry.write(fo); + } + athena::io::Write::Do({}, term64, fo); + + header.drumTableOff = fo.position(); + for (auto& p : SortUnorderedMap(index.m_drumPages)) + { + SongGroupIndex::PageEntryDNA entry = p.second.get().toDNA(p.first); + entry.write(fo); + } + athena::io::Write::Do({}, term64, fo); + + header.midiSetupsOff = fo.position(); + for (auto& p : SortUnorderedMap(index.m_midiSetups)) + { + uint16_t songId = p.first.id; + athena::io::Write::Do({}, songId, fo); + athena::io::Write::Do({}, padding, fo); + + const std::array& setup = p.second.get(); + for (int i = 0; i < 16 ; ++i) + setup[i].write(fo); + } + + header.groupEndOff = fo.position(); + fo.seek(groupStart, athena::Begin); + header.write(fo); + fo.seek(header.groupEndOff, athena::Begin); + } + else + { + auto search2 = m_sfxGroups.find(id); + if (search2 != m_sfxGroups.end()) + { + const SFXGroupIndex& index = *search2->second; + + auto groupStart = fo.position(); + GroupHeader header = {}; + header.write(fo); + + header.groupId = id; + header.type = GroupType::SFX; + + header.soundMacroIdsOff = fo.position(); + WriteRangedObjectIds(fo, SortUnorderedMap(pool.soundMacros())); + header.samplIdsOff = fo.position(); + WriteRangedObjectIds(fo, SortUnorderedMap(sdir.sampleEntries())); + header.tableIdsOff = fo.position(); + WriteRangedObjectIds(fo, SortUnorderedMap(pool.tables())); + header.keymapIdsOff = fo.position(); + WriteRangedObjectIds(fo, SortUnorderedMap(pool.keymaps())); + header.layerIdsOff = fo.position(); + WriteRangedObjectIds(fo, SortUnorderedMap(pool.layers())); + + header.pageTableOff = fo.position(); + uint16_t count = index.m_sfxEntries.size(); + athena::io::Write::Do({}, count, fo); + athena::io::Write::Do({}, padding, fo); + for (auto& p : SortUnorderedMap(index.m_sfxEntries)) + { + SFXGroupIndex::SFXEntryDNA entry = p.second.get().toDNA(p.first); + entry.write(fo); + } + + header.groupEndOff = fo.position(); + fo.seek(groupStart, athena::Begin); + header.write(fo); + fo.seek(header.groupEndOff, athena::Begin); + } + } + } + + const uint32_t finalTerm = 0xffffffff; + athena::io::Write::Do({}, finalTerm, fo); + + return true; +} + } diff --git a/lib/AudioGroupSampleDirectory.cpp b/lib/AudioGroupSampleDirectory.cpp index b4698e1..b595b23 100644 --- a/lib/AudioGroupSampleDirectory.cpp +++ b/lib/AudioGroupSampleDirectory.cpp @@ -4,6 +4,7 @@ #include "amuse/DSPCodec.hpp" #include "amuse/N64MusyXCodec.hpp" #include "amuse/DirectoryEnumerator.hpp" +#include "amuse/AudioGroup.hpp" #include "athena/MemoryReader.hpp" #include "athena/FileWriter.hpp" #include "athena/FileReader.hpp" @@ -872,4 +873,97 @@ void AudioGroupSampleDirectory::reloadSampleData(SystemStringView groupPath) } } } + +bool AudioGroupSampleDirectory::toGCNData(SystemStringView groupPath, const AudioGroupDatabase& group) const +{ + constexpr athena::Endian DNAE = athena::Big; + group.setIdDatabases(); + + SystemString sdirPath(groupPath); + SystemString sampPath(groupPath); + sdirPath += _S(".sdir"); + sampPath += _S(".samp"); + athena::io::FileWriter fo(sdirPath); + if (fo.hasError()) + return false; + athena::io::FileWriter sfo(sampPath); + if (sfo.hasError()) + return false; + + std::vector, ADPCMParms>> entries; + entries.reserve(m_entries.size()); + size_t sampleOffset = 0; + size_t adpcmOffset = 0; + for (const auto& ent : SortUnorderedMap(m_entries)) + { + amuse::SystemString path = group.getSampleBasePath(ent.first); + path += _S(".dsp"); + SampleFileState state = group.getSampleFileState(ent.first, ent.second.get().get(), &path); + switch (state) + { + case SampleFileState::MemoryOnlyWAV: + case SampleFileState::MemoryOnlyCompressed: + case SampleFileState::WAVRecent: + case SampleFileState::WAVNoCompressed: + group.makeCompressedVersion(ent.first, ent.second.get().get()); + default: + break; + } + + athena::io::FileReader r(path); + if (!r.hasError()) + { + EntryDNA entryDNA = ent.second.get()->toDNA(ent.first); + + DSPADPCMHeader header; + header.read(r); + entryDNA.m_pitch = header.m_pitch; + entryDNA.m_sampleRate = atUint16(header.x8_sample_rate); + entryDNA.m_numSamples = header.x0_num_samples; + if (header.xc_loop_flag) + { + entryDNA._setLoopStartSample(DSPNibbleToSample(header.x10_loop_start_nibble)); + entryDNA.setLoopEndSample(DSPNibbleToSample(header.x14_loop_end_nibble)); + } + + ADPCMParms adpcmParms; + adpcmParms.dsp.m_bytesPerFrame = 8; + adpcmParms.dsp.m_ps = uint8_t(header.x3e_ps); + adpcmParms.dsp.m_lps = uint8_t(header.x44_loop_ps); + adpcmParms.dsp.m_hist1 = header.x40_hist1; + adpcmParms.dsp.m_hist2 = header.x42_hist2; + for (int i = 0; i < 8; ++i) + for (int j = 0; j < 2; ++j) + adpcmParms.dsp.m_coefs[i][j] = header.x1c_coef[i][j]; + + uint32_t dataLen = (header.x4_num_nibbles + 1) / 2; + auto dspData = r.readUBytes(dataLen); + sfo.writeUBytes(dspData.get(), dataLen); + sfo.seekAlign32(); + + entryDNA.m_sampleOff = sampleOffset; + sampleOffset += ROUND_UP_32(dataLen); + entryDNA.binarySize(adpcmOffset); + entries.push_back(std::make_pair(entryDNA, adpcmParms)); + } + } + adpcmOffset += 4; + + for (auto& p : entries) + { + p.first.m_adpcmParmOffset = adpcmOffset; + p.first.write(fo); + adpcmOffset += sizeof(ADPCMParms::DSPParms); + } + const uint32_t term = 0xffffffff; + athena::io::Write::Do({}, term, fo); + + for (auto& p : entries) + { + p.second.swapBigDSP(); + fo.writeUBytes((uint8_t*)&p.second, sizeof(ADPCMParms::DSPParms)); + } + + return true; +} } diff --git a/lib/BooBackend.cpp b/lib/BooBackend.cpp index 17ae5ff..dc2b680 100644 --- a/lib/BooBackend.cpp +++ b/lib/BooBackend.cpp @@ -92,31 +92,62 @@ double BooBackendSubmix::getSampleRate() const { return m_booSubmix->getSampleRa SubmixFormat BooBackendSubmix::getSampleFormat() const { return SubmixFormat(m_booSubmix->getSampleFormat()); } -std::string BooBackendMIDIReader::description() { return m_midiIn->description(); } - BooBackendMIDIReader::~BooBackendMIDIReader() {} -BooBackendMIDIReader::BooBackendMIDIReader(Engine& engine, const char* name, bool useLock) +BooBackendMIDIReader::BooBackendMIDIReader(Engine& engine, bool useLock) : m_engine(engine), m_decoder(*this), m_useLock(useLock) { BooBackendVoiceAllocator& voxAlloc = static_cast(engine.getBackend()); - if (!name) + auto devices = voxAlloc.m_booEngine.enumerateMIDIInputs(); + for (const auto& dev : devices) { - auto devices = voxAlloc.m_booEngine.enumerateMIDIDevices(); - for (const auto& dev : devices) - { - m_midiIn = voxAlloc.m_booEngine.newRealMIDIIn( - dev.first.c_str(), - std::bind(&BooBackendMIDIReader::_MIDIReceive, this, std::placeholders::_1, std::placeholders::_2)); - if (m_midiIn) - return; - } - m_midiIn = voxAlloc.m_booEngine.newVirtualMIDIIn( + auto midiIn = voxAlloc.m_booEngine.newRealMIDIIn(dev.first.c_str(), std::bind(&BooBackendMIDIReader::_MIDIReceive, this, std::placeholders::_1, std::placeholders::_2)); + if (midiIn) + m_midiIns[dev.first] = std::move(midiIn); + } + if (voxAlloc.m_booEngine.supportsVirtualMIDIIn()) + m_virtualIn = voxAlloc.m_booEngine.newVirtualMIDIIn( + std::bind(&BooBackendMIDIReader::_MIDIReceive, this, std::placeholders::_1, std::placeholders::_2)); +} + +void BooBackendMIDIReader::addMIDIIn(const char* name) +{ + BooBackendVoiceAllocator& voxAlloc = static_cast(m_engine.getBackend()); + auto midiIn = voxAlloc.m_booEngine.newRealMIDIIn(name, + std::bind(&BooBackendMIDIReader::_MIDIReceive, this, std::placeholders::_1, std::placeholders::_2)); + if (midiIn) + m_midiIns[name] = std::move(midiIn); +} + +void BooBackendMIDIReader::removeMIDIIn(const char* name) +{ + m_midiIns.erase(name); +} + +bool BooBackendMIDIReader::hasMIDIIn(const char* name) const +{ + return m_midiIns.find(name) != m_midiIns.cend(); +} + +void BooBackendMIDIReader::setVirtualIn(bool v) +{ + if (v) + { + BooBackendVoiceAllocator& voxAlloc = static_cast(m_engine.getBackend()); + if (voxAlloc.m_booEngine.supportsVirtualMIDIIn()) + m_virtualIn = voxAlloc.m_booEngine.newVirtualMIDIIn( + std::bind(&BooBackendMIDIReader::_MIDIReceive, this, std::placeholders::_1, std::placeholders::_2)); } else - m_midiIn = voxAlloc.m_booEngine.newRealMIDIIn( - name, std::bind(&BooBackendMIDIReader::_MIDIReceive, this, std::placeholders::_1, std::placeholders::_2)); + { + m_virtualIn.reset(); + } +} + +bool BooBackendMIDIReader::hasVirtualIn() const +{ + return m_virtualIn.operator bool(); } void BooBackendMIDIReader::_MIDIReceive(std::vector&& bytes, double time) @@ -273,15 +304,12 @@ std::unique_ptr BooBackendVoiceAllocator::allocateSubmix(Submix& std::vector> BooBackendVoiceAllocator::enumerateMIDIDevices() { - return m_booEngine.enumerateMIDIDevices(); + return m_booEngine.enumerateMIDIInputs(); } -std::unique_ptr BooBackendVoiceAllocator::allocateMIDIReader(Engine& engine, const char* name) +std::unique_ptr BooBackendVoiceAllocator::allocateMIDIReader(Engine& engine) { - std::unique_ptr ret = std::make_unique(engine, name, m_booEngine.useMIDILock()); - if (!static_cast(*ret).m_midiIn) - return {}; - return ret; + return std::make_unique(engine, m_booEngine.useMIDILock()); } void BooBackendVoiceAllocator::setCallbackInterface(Engine* engine) diff --git a/lib/SongConverter.cpp b/lib/SongConverter.cpp index df4da2a..795d68b 100644 --- a/lib/SongConverter.cpp +++ b/lib/SongConverter.cpp @@ -572,16 +572,42 @@ static void EncodeSignedValue(std::vector& vecOut, int16_t val) } } -static uint32_t DecodeTimeRLE(const unsigned char*& data) +static std::pair DecodeDelta(const unsigned char*& data) +{ + std::pair ret = {}; + do { + if (data[0] == 0x80 && data[1] == 0x00) + break; + ret.first += DecodeUnsignedValue(data); + ret.second = DecodeSignedValue(data); + } while (ret.second == 0); + return ret; +} + +static void EncodeDelta(std::vector& vecOut, uint32_t deltaTime, int32_t val) +{ + while (deltaTime > 32767) + { + EncodeUnsignedValue(vecOut, 32767); + EncodeSignedValue(vecOut, 0); + deltaTime -= 32767; + } + EncodeUnsignedValue(vecOut, deltaTime); + EncodeSignedValue(vecOut, val); +} + +static uint32_t DecodeTime(const unsigned char*& data) { uint32_t ret = 0; while (true) { uint16_t thisPart = SBig(*reinterpret_cast(data)); - if (thisPart == 0xffff) + uint16_t nextPart = *reinterpret_cast(data + 2); + if (nextPart == 0) { - ret += 65535; + // Automatically consume no-op command as continued time + ret += thisPart; data += 4; continue; } @@ -594,10 +620,11 @@ static uint32_t DecodeTimeRLE(const unsigned char*& data) return ret; } -static void EncodeTimeRLE(std::vector& vecOut, uint32_t val) +static void EncodeTime(std::vector& vecOut, uint32_t val) { while (val >= 65535) { + // Automatically emit no-op command as continued time vecOut.push_back(0xff); vecOut.push_back(0xff); vecOut.push_back(0); @@ -721,8 +748,9 @@ std::vector SongConverter::SongToMIDI(const unsigned char* data, int& v clamp(0, trk.m_pitchVal + 0x2000, 0x4000)}); if (trk.m_pitchWheelData[0] != 0x80 || trk.m_pitchWheelData[1] != 0x00) { - trk.m_nextPitchTick += DecodeUnsignedValue(trk.m_pitchWheelData); - trk.m_nextPitchDelta = DecodeSignedValue(trk.m_pitchWheelData); + auto delta = DecodeDelta(trk.m_pitchWheelData); + trk.m_nextPitchTick += delta.first; + trk.m_nextPitchDelta = delta.second; } else { @@ -743,8 +771,9 @@ std::vector SongConverter::SongToMIDI(const unsigned char* data, int& v uint8_t(clamp(0, trk.m_modVal / 128, 127)), 0}); if (trk.m_modWheelData[0] != 0x80 || trk.m_modWheelData[1] != 0x00) { - trk.m_nextModTick += DecodeUnsignedValue(trk.m_modWheelData); - trk.m_nextModDelta = DecodeSignedValue(trk.m_modWheelData); + auto delta = DecodeDelta(trk.m_modWheelData); + trk.m_nextModTick += delta.first; + trk.m_nextModDelta = delta.second; } else { @@ -797,7 +826,7 @@ std::vector SongConverter::SongToMIDI(const unsigned char* data, int& v } /* Set next delta-time */ - trk.m_eventWaitCountdown += int32_t(DecodeTimeRLE(trk.m_data)); + trk.m_eventWaitCountdown += int32_t(DecodeTime(trk.m_data)); } } else @@ -1047,12 +1076,12 @@ std::vector SongConverter::MIDIToSong(const std::vector& data, for (auto& prog : results) { bool didInit = false; - int startTick; - int lastEventTick; - int lastPitchTick; - int lastPitchVal; - int lastModTick; - int lastModVal; + int startTick = 0; + int lastEventTick = 0; + int lastPitchTick = 0; + int lastPitchVal = 0; + int lastModTick = 0; + int lastModVal = 0; Region region; for (auto& event : prog.second) @@ -1079,17 +1108,16 @@ std::vector SongConverter::MIDIToSong(const std::vector& data, { if (event.second.noteOrCtrl == 1) { - EncodeUnsignedValue(region.modBuf, uint32_t(eventTick - lastModTick)); - lastModTick = eventTick; int newMod = event.second.velOrVal * 128; - EncodeSignedValue(region.modBuf, newMod - lastModVal); + EncodeDelta(region.modBuf, eventTick - lastModTick, newMod - lastModVal); + lastModTick = eventTick; lastModVal = newMod; } else { if (version == 1) { - EncodeTimeRLE(region.eventBuf, uint32_t(eventTick - lastEventTick)); + EncodeTime(region.eventBuf, uint32_t(eventTick - lastEventTick)); lastEventTick = eventTick; region.eventBuf.push_back(0x80 | event.second.velOrVal); region.eventBuf.push_back(0x80 | event.second.noteOrCtrl); @@ -1120,7 +1148,7 @@ std::vector SongConverter::MIDIToSong(const std::vector& data, { if (version == 1) { - EncodeTimeRLE(region.eventBuf, uint32_t(eventTick - lastEventTick)); + EncodeTime(region.eventBuf, uint32_t(eventTick - lastEventTick)); lastEventTick = eventTick; region.eventBuf.push_back(0x80 | event.second.program); region.eventBuf.push_back(0); @@ -1148,10 +1176,9 @@ std::vector SongConverter::MIDIToSong(const std::vector& data, } case Event::Type::Pitch: { - EncodeUnsignedValue(region.pitchBuf, uint32_t(eventTick - lastPitchTick)); - lastPitchTick = eventTick; int newPitch = event.second.pitchBend - 0x2000; - EncodeSignedValue(region.pitchBuf, newPitch - lastPitchVal); + EncodeDelta(region.modBuf, eventTick - lastPitchTick, newPitch - lastPitchVal); + lastPitchTick = eventTick; lastPitchVal = newPitch; break; } @@ -1159,7 +1186,7 @@ std::vector SongConverter::MIDIToSong(const std::vector& data, { if (version == 1) { - EncodeTimeRLE(region.eventBuf, uint32_t(eventTick - lastEventTick)); + EncodeTime(region.eventBuf, uint32_t(eventTick - lastEventTick)); lastEventTick = eventTick; region.eventBuf.push_back(event.second.noteOrCtrl); region.eventBuf.push_back(event.second.velOrVal); @@ -1219,7 +1246,7 @@ std::vector SongConverter::MIDIToSong(const std::vector& data, if (lastModTick > lastEventTick) modDelta = lastModTick - lastEventTick; - EncodeTimeRLE(region.eventBuf, std::max(pitchDelta, modDelta)); + EncodeTime(region.eventBuf, std::max(pitchDelta, modDelta)); region.eventBuf.push_back(0xff); region.eventBuf.push_back(0xff); } diff --git a/lib/SongState.cpp b/lib/SongState.cpp index f24f995..ad34aef 100644 --- a/lib/SongState.cpp +++ b/lib/SongState.cpp @@ -39,16 +39,30 @@ static int16_t DecodeSignedValue(const unsigned char*& data) return ret; } -static uint32_t DecodeTimeRLE(const unsigned char*& data) +static std::pair DecodeDelta(const unsigned char*& data) +{ + std::pair ret = {}; + do { + if (data[0] == 0x80 && data[1] == 0x00) + break; + ret.first += DecodeUnsignedValue(data); + ret.second = DecodeSignedValue(data); + } while (ret.second == 0); + return ret; +} + +static uint32_t DecodeTime(const unsigned char*& data) { uint32_t ret = 0; while (true) { uint16_t thisPart = SBig(*reinterpret_cast(data)); - if (thisPart == 0xffff) + uint16_t nextPart = *reinterpret_cast(data + 2); + if (nextPart == 0) { - ret += 65535; + // Automatically consume no-op command as continued time + ret += thisPart; data += 4; continue; } @@ -118,8 +132,9 @@ void SongState::Track::setRegion(Sequencer* seq, const TrackRegion* region) m_pitchWheelData = m_parent->m_songData + header.m_pitchOff; if (m_pitchWheelData[0] != 0x80 || m_pitchWheelData[1] != 0x00) { - m_nextPitchTick = m_parent->m_curTick + DecodeUnsignedValue(m_pitchWheelData); - m_nextPitchDelta = DecodeSignedValue(m_pitchWheelData); + auto delta = DecodeDelta(m_pitchWheelData); + m_nextPitchTick = m_parent->m_curTick + delta.first; + m_nextPitchDelta = delta.second; } } @@ -131,8 +146,9 @@ void SongState::Track::setRegion(Sequencer* seq, const TrackRegion* region) m_modWheelData = m_parent->m_songData + header.m_modOff; if (m_modWheelData[0] != 0x80 || m_modWheelData[1] != 0x00) { - m_nextModTick = m_parent->m_curTick + DecodeUnsignedValue(m_modWheelData); - m_nextModDelta = DecodeSignedValue(m_modWheelData); + auto delta = DecodeDelta(m_modWheelData); + m_nextModTick = m_parent->m_curTick + delta.first; + m_nextModDelta = delta.second; } } @@ -145,7 +161,7 @@ void SongState::Track::setRegion(Sequencer* seq, const TrackRegion* region) seq->setCtrlValue(m_midiChan, 1, clamp(0, m_modVal * 128 / 16384, 127)); } if (m_parent->m_sngVersion == 1) - m_eventWaitCountdown = int32_t(DecodeTimeRLE(m_data)); + m_eventWaitCountdown = int32_t(DecodeTime(m_data)); else { int32_t absTick = (m_parent->m_bigEndian ? SBig(*reinterpret_cast(m_data)) @@ -231,10 +247,7 @@ int SongState::DetectVersion(const unsigned char* ptr, bool& isBig) { const unsigned char* dptr = ptr + header.m_pitchOff; while (dptr[0] != 0x80 || dptr[1] != 0x00) - { - DecodeUnsignedValue(dptr); - DecodeSignedValue(dptr); - } + DecodeDelta(dptr); dptr += 2; if (dptr >= (expectedEnd - 4) && (dptr <= expectedEnd)) continue; @@ -245,10 +258,7 @@ int SongState::DetectVersion(const unsigned char* ptr, bool& isBig) { const unsigned char* dptr = ptr + header.m_modOff; while (dptr[0] != 0x80 || dptr[1] != 0x00) - { - DecodeUnsignedValue(dptr); - DecodeSignedValue(dptr); - } + DecodeDelta(dptr); dptr += 2; if (dptr >= (expectedEnd - 4) && (dptr <= expectedEnd)) continue; @@ -261,7 +271,7 @@ int SongState::DetectVersion(const unsigned char* ptr, bool& isBig) while (true) { /* Delta time */ - DecodeTimeRLE(data); + DecodeTime(data); /* Load next command */ if (*reinterpret_cast(data) == 0xffff) @@ -427,8 +437,9 @@ bool SongState::Track::advance(Sequencer& seq, int32_t ticks) seq.setPitchWheel(m_midiChan, clamp(-1.f, m_pitchVal / 8191.f, 1.f)); if (m_pitchWheelData[0] != 0x80 || m_pitchWheelData[1] != 0x00) { - m_nextPitchTick += DecodeUnsignedValue(m_pitchWheelData); - m_nextPitchDelta = DecodeSignedValue(m_pitchWheelData); + auto delta = DecodeDelta(m_pitchWheelData); + m_nextPitchTick += delta.first; + m_nextPitchDelta = delta.second; } else { @@ -456,8 +467,9 @@ bool SongState::Track::advance(Sequencer& seq, int32_t ticks) seq.setCtrlValue(m_midiChan, 1, clamp(0, m_modVal / 128, 127)); if (m_modWheelData[0] != 0x80 || m_modWheelData[1] != 0x00) { - m_nextModTick += DecodeUnsignedValue(m_modWheelData); - m_nextModDelta = DecodeSignedValue(m_modWheelData); + auto delta = DecodeDelta(m_modWheelData); + m_nextModTick += delta.first; + m_nextModDelta = delta.second; } else { @@ -519,7 +531,7 @@ bool SongState::Track::advance(Sequencer& seq, int32_t ticks) } /* Set next delta-time */ - m_eventWaitCountdown += int32_t(DecodeTimeRLE(m_data)); + m_eventWaitCountdown += int32_t(DecodeTime(m_data)); } } else