From fec074ad3072497cc5f65bfe9867ec4f9991dd67 Mon Sep 17 00:00:00 2001 From: Jack Andersen Date: Wed, 15 Aug 2018 20:26:44 -1000 Subject: [PATCH] Studio setup window and volume LUT --- CMakeLists.txt | 4 +- Editor/CMakeLists.txt | 6 +- Editor/EditorWidget.cpp | 100 +++ Editor/EditorWidget.hpp | 55 ++ Editor/MainWindow.cpp | 123 +++- Editor/MainWindow.hpp | 10 + Editor/ProjectModel.cpp | 111 ++- Editor/ProjectModel.hpp | 5 +- Editor/SoundMacroEditor.cpp | 28 - Editor/SoundMacroEditor.hpp | 13 +- Editor/StatusBarWidget.cpp | 91 +++ Editor/StatusBarWidget.hpp | 88 +-- Editor/StudioSetupWidget.cpp | 1058 +++++++++++++++++++++++++++ Editor/StudioSetupWidget.hpp | 177 +++++ Editor/resources/IconFX.svg | 84 +++ Editor/resources/IconPaintbrush.svg | 37 +- Editor/resources/IconVolume0.svg | 74 ++ Editor/resources/IconVolume1.svg | 79 ++ Editor/resources/IconVolume2.svg | 85 +++ Editor/resources/IconVolume3.svg | 90 +++ Editor/resources/lang_de.ts | 339 +++++---- Editor/resources/resources.qrc | 5 + include/amuse/EffectBase.hpp | 11 + include/amuse/EffectChorus.hpp | 5 + include/amuse/EffectDelay.hpp | 5 + include/amuse/EffectReverb.hpp | 10 + include/amuse/Submix.hpp | 23 +- include/amuse/Voice.hpp | 16 + include/amuse/VolumeTable.hpp | 10 + lib/Common.cpp | 12 +- lib/SoundMacroState.cpp | 1 + lib/Voice.cpp | 25 +- lib/VolumeTable.cpp | 295 ++++++++ 33 files changed, 2744 insertions(+), 331 deletions(-) create mode 100644 Editor/StatusBarWidget.cpp create mode 100644 Editor/StudioSetupWidget.cpp create mode 100644 Editor/StudioSetupWidget.hpp create mode 100644 Editor/resources/IconFX.svg create mode 100644 Editor/resources/IconVolume0.svg create mode 100644 Editor/resources/IconVolume1.svg create mode 100644 Editor/resources/IconVolume2.svg create mode 100644 Editor/resources/IconVolume3.svg create mode 100644 include/amuse/VolumeTable.hpp create mode 100644 lib/VolumeTable.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c792c6c..27f5ee0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,7 @@ set(SOURCES lib/Common.cpp lib/DSPCodec.cpp lib/N64MusyXCodec.cpp + lib/VolumeTable.cpp atdna_AudioGroupPool.cpp atdna_AudioGroupProject.cpp atdna_AudioGroupSampleDirectory.cpp) @@ -79,7 +80,8 @@ set(HEADERS include/amuse/Common.hpp include/amuse/amuse.hpp include/amuse/DSPCodec.hpp - include/amuse/N64MusyXCodec.hpp) + include/amuse/N64MusyXCodec.hpp + include/amuse/VolumeTable.hpp) unset(EXTRAS) if(TARGET boo) diff --git a/Editor/CMakeLists.txt b/Editor/CMakeLists.txt index 00f8279..ebff1a3 100644 --- a/Editor/CMakeLists.txt +++ b/Editor/CMakeLists.txt @@ -46,13 +46,14 @@ QT5_WRAP_CPP(AMUSE_MOC SampleEditor.hpp SoundGroupEditor.hpp SongGroupEditor.hpp - NewSoundMacroDialog.hpp) + NewSoundMacroDialog.hpp + StudioSetupWidget.hpp) add_executable(amuse-gui WIN32 MACOSX_BUNDLE Common.hpp Common.cpp MainWindow.ui ${MAIN_WINDOW_UI} MainWindow.hpp MainWindow.cpp KeyboardWidget.hpp KeyboardWidget.cpp - StatusBarWidget.hpp + StatusBarWidget.hpp StatusBarWidget.cpp ProjectModel.hpp ProjectModel.cpp EditorWidget.hpp EditorWidget.cpp SoundMacroEditor.hpp SoundMacroEditor.cpp @@ -64,6 +65,7 @@ add_executable(amuse-gui WIN32 MACOSX_BUNDLE SoundGroupEditor.hpp SoundGroupEditor.cpp SongGroupEditor.hpp SongGroupEditor.cpp NewSoundMacroDialog.hpp NewSoundMacroDialog.cpp + StudioSetupWidget.hpp StudioSetupWidget.cpp MIDIReader.hpp MIDIReader.cpp resources/resources.qrc qrc_resources.cpp ${QM_FILES} qrc_translation_res.cpp diff --git a/Editor/EditorWidget.cpp b/Editor/EditorWidget.cpp index 17a2486..76b5e75 100644 --- a/Editor/EditorWidget.cpp +++ b/Editor/EditorWidget.cpp @@ -1,6 +1,7 @@ #include "EditorWidget.hpp" #include "MainWindow.hpp" #include +#include EditorWidget::EditorWidget(QWidget* parent) : QWidget(parent) @@ -18,6 +19,77 @@ void EditorUndoCommand::redo() g_MainWindow->openEditor(m_node.get()); } +FieldSlider::FieldSlider(QWidget* parent) +: QWidget(parent), m_slider(Qt::Horizontal) +{ + setFixedHeight(22); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + QHBoxLayout* layout = new QHBoxLayout; + layout->addWidget(&m_slider); + layout->addWidget(&m_value); + layout->setContentsMargins(QMargins()); + setLayout(layout); + m_value.setFixedWidth(42); + connect(&m_slider, SIGNAL(valueChanged(int)), this, SLOT(doValueChanged(int))); + m_slider.setValue(0); +} + +void FieldSlider::doValueChanged(int value) +{ + m_value.setText(QString::number(value)); + emit valueChanged(value); +} + +FieldDoubleSlider::FieldDoubleSlider(QWidget* parent) +: QWidget(parent), m_slider(Qt::Horizontal) +{ + setFixedHeight(22); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + QHBoxLayout* layout = new QHBoxLayout; + layout->addWidget(&m_slider); + layout->addWidget(&m_value); + layout->setContentsMargins(QMargins()); + setLayout(layout); + m_value.setFixedWidth(42); + connect(&m_slider, SIGNAL(valueChanged(int)), this, SLOT(doValueChanged(int))); + m_slider.setRange(0, 1000); + m_slider.setValue(0); +} + +double FieldDoubleSlider::value() const +{ + double t = m_slider.value() / 1000.0; + return t * (m_max - m_min) + m_min; +} +void FieldDoubleSlider::setValue(double value) +{ + if (value < m_min) + value = m_min; + else if (value > m_max) + value = m_max; + + double t = (value - m_min) / (m_max - m_min); + m_slider.setValue(int(t * 1000.0)); + doValueChanged(int(t * 1000.0)); +} +void FieldDoubleSlider::setRange(double min, double max) +{ + m_min = min; m_max = max; + double curValue = value(); + if (curValue < min) + setValue(min); + else if (curValue > max) + setValue(max); +} + +void FieldDoubleSlider::doValueChanged(int value) +{ + double t = value / 1000.0; + t = t * (m_max - m_min) + m_min; + m_value.setText(QString::number(t, 'g', 2)); + emit valueChanged(t); +} + FieldProjectNode::FieldProjectNode(ProjectModel::CollectionNode* collection, QWidget* parent) : FieldComboBox(parent) { @@ -77,3 +149,31 @@ AddRemoveButtons::AddRemoveButtons(QWidget* parent) m_removeButton.move(32, 0); m_removeAction.setEnabled(false); } + +static QIcon ListingDeleteIcon; +static QIcon ListingDeleteHoveredIcon; + +void ListingDeleteButton::enterEvent(QEvent* event) +{ + setIcon(ListingDeleteHoveredIcon); +} + +void ListingDeleteButton::leaveEvent(QEvent* event) +{ + setIcon(ListingDeleteIcon); +} + +ListingDeleteButton::ListingDeleteButton(QWidget* parent) +: QPushButton(parent) +{ + if (ListingDeleteIcon.isNull()) + ListingDeleteIcon = QIcon(QStringLiteral(":/icons/IconSoundMacroDelete.svg")); + if (ListingDeleteHoveredIcon.isNull()) + ListingDeleteHoveredIcon = QIcon(QStringLiteral(":/icons/IconSoundMacroDeleteHovered.svg")); + + setVisible(false); + setFixedSize(21, 21); + setFlat(true); + setToolTip(tr("Delete this SoundMacro")); + setIcon(ListingDeleteIcon); +} diff --git a/Editor/EditorWidget.hpp b/Editor/EditorWidget.hpp index ba3ee94..85d5ebc 100644 --- a/Editor/EditorWidget.hpp +++ b/Editor/EditorWidget.hpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include "ProjectModel.hpp" class EditorWidget : public QWidget @@ -68,6 +70,50 @@ public: void wheelEvent(QWheelEvent* event) { event->ignore(); } }; +class FieldSlider : public QWidget +{ +Q_OBJECT + QSlider m_slider; + QLabel m_value; +public: + explicit FieldSlider(QWidget* parent = Q_NULLPTR); + + /* Don't scroll */ + void wheelEvent(QWheelEvent* event) { event->ignore(); } + + int value() const { return m_slider.value(); } + void setValue(int value) { m_slider.setValue(value); doValueChanged(value); } + void setRange(int min, int max) { m_slider.setRange(min, max); } + +private slots: + void doValueChanged(int value); +signals: + void valueChanged(int value); +}; + +class FieldDoubleSlider : public QWidget +{ + Q_OBJECT + QSlider m_slider; + QLabel m_value; + double m_min = 0.0; + double m_max = 1.0; +public: + explicit FieldDoubleSlider(QWidget* parent = Q_NULLPTR); + + /* Don't scroll */ + void wheelEvent(QWheelEvent* event) { event->ignore(); } + + double value() const; + void setValue(double value); + void setRange(double min, double max); + +private slots: + void doValueChanged(int value); +signals: + void valueChanged(double value); +}; + class FieldComboBox : public QComboBox { Q_OBJECT @@ -143,4 +189,13 @@ public: QAction* removeAction() { return &m_removeAction; } }; +class ListingDeleteButton : public QPushButton +{ +Q_OBJECT +public: + explicit ListingDeleteButton(QWidget* parent = Q_NULLPTR); + void enterEvent(QEvent* event); + void leaveEvent(QEvent* event); +}; + #endif //AMUSE_EDITOR_WIDGET_HPP diff --git a/Editor/MainWindow.cpp b/Editor/MainWindow.cpp index 80d6c95..c4c80b2 100644 --- a/Editor/MainWindow.cpp +++ b/Editor/MainWindow.cpp @@ -7,7 +7,6 @@ #include #include #include "amuse/ContainerRegistry.hpp" -#include "amuse/SongConverter.hpp" #include "Common.hpp" #include "SongGroupEditor.hpp" #include "SoundGroupEditor.hpp" @@ -45,6 +44,9 @@ MainWindow::MainWindow(QWidget* parent) connectMessenger(&m_mainMessenger, Qt::DirectConnection); m_ui.statusbar->connectKillClicked(this, SLOT(killSounds())); + m_ui.statusbar->connectFXPressed(this, SLOT(fxPressed())); + m_ui.statusbar->setVolumeValue(70); + m_ui.statusbar->connectVolumeSlider(this, SLOT(volumeChanged(int))); m_ui.keyboardContents->setStatusFocus(new StatusBarFocus(m_ui.statusbar)); m_ui.velocitySlider->setStatusFocus(new StatusBarFocus(m_ui.statusbar)); @@ -89,6 +91,7 @@ MainWindow::MainWindow(QWidget* parent) updateRecentFileActions(); + connect(m_undoStack, SIGNAL(cleanChanged(bool)), this, SLOT(cleanChanged(bool))); QAction* undoAction = m_undoStack->createUndoAction(this); undoAction->setShortcut(QKeySequence::Undo); m_ui.menuEdit->insertAction(m_ui.actionCut, undoAction); @@ -128,6 +131,10 @@ MainWindow::MainWindow(QWidget* parent) m_ui.editorContents->addWidget(m_sampleEditor); m_ui.editorContents->setCurrentWidget(m_faceSvg); + m_studioSetup = new StudioSetupWidget(this); + connect(m_studioSetup, SIGNAL(hidden()), this, SLOT(studioSetupHidden())); + connect(m_studioSetup, SIGNAL(shown()), this, SLOT(studioSetupShown())); + connect(m_ui.actionNew_Subproject, SIGNAL(triggered()), this, SLOT(newSubprojectAction())); connect(m_ui.actionNew_SFX_Group, SIGNAL(triggered()), this, SLOT(newSFXGroupAction())); connect(m_ui.actionNew_Song_Group, SIGNAL(triggered()), this, SLOT(newSongGroupAction())); @@ -145,6 +152,8 @@ MainWindow::MainWindow(QWidget* parent) m_voxEngine = boo::NewAudioVoiceEngine(); m_voxAllocator = std::make_unique(*m_voxEngine); m_engine = std::make_unique(*m_voxAllocator); + m_voxEngine->setVolume(0.7f); + m_studioSetup->loadData(m_engine->getDefaultStudio().get()); m_ctrlVals[7] = 127; m_ctrlVals[10] = 64; @@ -191,7 +200,7 @@ void MainWindow::updateWindowTitle() { if (!m_projectModel) { - setWindowTitle(tr("Amuse")); + setWindowTitle(tr("Amuse[*]")); return; } @@ -199,12 +208,12 @@ void MainWindow::updateWindowTitle() if (EditorWidget* w = getEditorWidget()) { ProjectModel::BasePoolObjectNode* objNode = static_cast(w->currentNode()); - setWindowTitle(tr("Amuse [%1/%2/%3]").arg(dir.dirName()).arg( + setWindowTitle(tr("%1/%2/%3[*] - Amuse").arg(dir.dirName()).arg( m_projectModel->getGroupNode(objNode)->text()).arg(objNode->text())); return; } - setWindowTitle(tr("Amuse [%1]").arg(dir.dirName())); + setWindowTitle(tr("%1[*] - Amuse").arg(dir.dirName())); } void MainWindow::updateRecentFileActions() @@ -229,9 +238,6 @@ void MainWindow::updateRecentFileActions() bool MainWindow::setProjectPath(const QString& path) { - if (m_projectModel && m_projectModel->path() == path) - return true; - QDir dir(path); if (dir.path().isEmpty() || dir.path() == QStringLiteral(".") || dir.path() == QStringLiteral("..")) { @@ -615,6 +621,44 @@ void MainWindow::aboutToDeleteNode(ProjectModel::INode* node) closeEditor(); } +void MainWindow::closeEvent(QCloseEvent* ev) +{ + if (!m_projectModel) + { + ev->accept(); + return; + } + + if (!m_undoStack->isClean()) + { + QDir dir(m_projectModel->path()); + int result = QMessageBox::question(this, tr("Unsaved Changes"), tr("Save Changes in %1?").arg(dir.dirName()), + QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Save); + if (result == QMessageBox::Save) + { + saveAction(); + ev->accept(); + } + else if (result == QMessageBox::Discard) + { + ev->accept(); + } + else + { + ev->ignore(); + } + } + else + { + ev->accept(); + } +} + +void MainWindow::showEvent(QShowEvent* ev) +{ + m_studioSetup->updateWindowPosition(); +} + void MainWindow::newAction() { QString path = QFileDialog::getSaveFileName(this, tr("New Project")); @@ -626,7 +670,9 @@ void MainWindow::newAction() return; m_projectModel->clearProjectData(); - m_ui.actionImport_Groups->setDisabled(m_projectModel->ensureModelData()); + bool hasGroups = m_projectModel->ensureModelData(); + m_ui.actionImport_Groups->setDisabled(hasGroups); + m_ui.actionImport_Songs->setEnabled(hasGroups); } bool MainWindow::openProject(const QString& path) @@ -710,7 +756,11 @@ void MainWindow::clearRecentFilesAction() void MainWindow::saveAction() { - + if (m_projectModel) + { + m_projectModel->saveToFile(m_mainMessenger); + m_undoStack->setClean(); + } } void MainWindow::revertAction() @@ -894,28 +944,7 @@ void MainWindow::importSongsAction() return; closeEditor(); - - std::vector> songs = - amuse::ContainerRegistry::LoadSongs(QStringToSysString(path).c_str()); - - for (const auto& song : songs) - { - int version; - bool isBig; - auto midiData = - amuse::SongConverter::SongToMIDI(song.second.m_data.get(), version, isBig); - if (!midiData.empty()) - { - QFileInfo fi(m_projectModel->dir(), SysStringToQString(song.first + ".mid")); - QFile f(fi.filePath()); - if (f.open(QFile::WriteOnly)) - { - f.write((const char*)midiData.data(), midiData.size()); - m_projectModel->setMIDIPathOfSong( - song.second.m_setupId, m_projectModel->dir().relativeFilePath(fi.filePath())); - } - } - } + m_projectModel->importSongsData(path); } void MainWindow::exportAction() @@ -1201,6 +1230,19 @@ void MainWindow::killSounds() v->kill(); } +void MainWindow::fxPressed() +{ + if (m_studioSetup->isVisible()) + m_studioSetup->hide(); + else + m_studioSetup->show(); +} + +void MainWindow::volumeChanged(int vol) +{ + m_engine->setVolume(vol / 100.f); +} + void MainWindow::outlineCutAction() { @@ -1342,6 +1384,21 @@ void MainWindow::onTextDelete() } } +void MainWindow::cleanChanged(bool clean) +{ + setWindowModified(!clean); +} + +void MainWindow::studioSetupHidden() +{ + m_ui.statusbar->setFXDown(false); +} + +void MainWindow::studioSetupShown() +{ + m_ui.statusbar->setFXDown(true); +} + void MainWindow::onBackgroundTaskFinished() { m_backgroundDialog->reset(); @@ -1349,7 +1406,9 @@ void MainWindow::onBackgroundTaskFinished() m_backgroundDialog = nullptr; m_backgroundTask->deleteLater(); m_backgroundTask = nullptr; - m_ui.actionImport_Groups->setDisabled(m_projectModel->ensureModelData()); + 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 091c2a5..1a1ccd0 100644 --- a/Editor/MainWindow.hpp +++ b/Editor/MainWindow.hpp @@ -14,6 +14,7 @@ #include "ProjectModel.hpp" #include "EditorWidget.hpp" #include "MIDIReader.hpp" +#include "StudioSetupWidget.hpp" #define MaxRecentFiles 4 @@ -89,6 +90,7 @@ class MainWindow : public QMainWindow KeymapEditor* m_keymapEditor = nullptr; LayersEditor* m_layersEditor = nullptr; SampleEditor* m_sampleEditor = nullptr; + StudioSetupWidget* m_studioSetup = nullptr; std::unique_ptr m_voxEngine; std::unique_ptr m_voxAllocator; @@ -154,6 +156,8 @@ public: void pushUndoCommand(QUndoCommand* cmd); void updateFocus(); void aboutToDeleteNode(ProjectModel::INode* node); + void closeEvent(QCloseEvent* ev); + void showEvent(QShowEvent* ev); QString getGroupName(ProjectModel::GroupNode* group) const; ProjectModel::GroupNode* getSelectedGroupNode() const; @@ -197,6 +201,8 @@ public slots: void modulationChanged(int mod); void pitchChanged(int pitch); void killSounds(); + void fxPressed(); + void volumeChanged(int vol); void outlineCutAction(); void outlineCopyAction(); @@ -211,6 +217,10 @@ public slots: void onOutlineSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected); void onTextSelect(); void onTextDelete(); + void cleanChanged(bool clean); + + void studioSetupHidden(); + void studioSetupShown(); void onBackgroundTaskFinished(); diff --git a/Editor/ProjectModel.cpp b/Editor/ProjectModel.cpp index 93321f8..76209f6 100644 --- a/Editor/ProjectModel.cpp +++ b/Editor/ProjectModel.cpp @@ -4,6 +4,8 @@ #include "Common.hpp" #include "athena/YAMLDocWriter.hpp" #include "MainWindow.hpp" +#include "amuse/SongConverter.hpp" +#include "amuse/ContainerRegistry.hpp" #include QIcon ProjectModel::GroupNode::Icon; @@ -409,13 +411,32 @@ bool ProjectModel::openGroupData(const QString& groupName, UIMessenger& messenge return true; } -bool ProjectModel::openSongsData() +void ProjectModel::_resetSongRefCount() +{ + for (auto& song : m_midiFiles) + song.second.m_refCount = 0; + for (const auto& g : m_groups) + for (const auto& g2 : g.second.getProj().songGroups()) + for (const auto& m : g2.second->m_midiSetups) + ++m_midiFiles[m.first].m_refCount; + for (auto it = m_midiFiles.begin(); it != m_midiFiles.end();) + { + if (it->second.m_refCount == 0) + { + it = m_midiFiles.erase(it); + continue; + } + ++it; + } +} + +void ProjectModel::openSongsData() { m_midiFiles.clear(); QFileInfo songsFile(m_dir, QStringLiteral("!songs.yaml")); if (songsFile.exists()) { - athena::io::FileReader r(QStringToSysString(songsFile.path())); + athena::io::FileReader r(QStringToSysString(songsFile.filePath())); if (!r.hasError()) { athena::io::YAMLDocReader dr; @@ -425,35 +446,44 @@ bool ProjectModel::openSongsData() for (auto& p : dr.getRootNode()->m_mapChildren) { char* endPtr; - amuse::SongId id = uint16_t(strtoul(p.first.c_str(), &endPtr, 0)); + amuse::SongId id = uint16_t(strtoul(p.first.c_str(), &endPtr, 16)); if (endPtr == p.first.c_str() || id.id == 0xffff) continue; - - m_midiFiles.clear(); QString path = QString::fromStdString(p.second->m_scalarString); - m_root->oneLevelTraverse([this, id, path](INode* n) - { - GroupNode* gn = static_cast(n); - amuse::AudioGroupDatabase* db = gn->getAudioGroup(); - for (const auto& p : db->getProj().songGroups()) - { - for (const auto& m : p.second->m_midiSetups) - { - if (id == m.first) - { - Song& song = m_midiFiles[id]; - song.m_path = path; - ++song.m_refCount; - } - } - } - return true; - }); + setMIDIPathOfSong(id, path); } + _resetSongRefCount(); } } } - return true; +} + +void ProjectModel::importSongsData(const QString& path) +{ + std::vector> songs = + amuse::ContainerRegistry::LoadSongs(QStringToSysString(path).c_str()); + + for (const auto& song : songs) + { + int version; + bool isBig; + auto midiData = + amuse::SongConverter::SongToMIDI(song.second.m_data.get(), version, isBig); + if (!midiData.empty()) + { + QFileInfo fi(m_dir, SysStringToQString(song.first + ".mid")); + QFile f(fi.filePath()); + if (f.open(QFile::WriteOnly)) + { + f.write((const char*)midiData.data(), midiData.size()); + setMIDIPathOfSong( + song.second.m_setupId, m_dir.relativeFilePath(fi.filePath())); + } + } + } + + saveSongsIndex(); + _resetSongRefCount(); } bool ProjectModel::reloadSampleData(const QString& groupName, UIMessenger& messenger) @@ -509,6 +539,24 @@ bool ProjectModel::importGroupData(const QString& groupName, const amuse::AudioG return true; } +void ProjectModel::saveSongsIndex() +{ + if (!m_midiFiles.empty()) + { + QFileInfo songsFile(m_dir, QStringLiteral("!songs.yaml")); + athena::io::YAMLDocWriter dw("amuse::Songs"); + for (auto& p : amuse::SortUnorderedMap(m_midiFiles)) + { + char id[16]; + snprintf(id, 16, "%04X", p.first.id); + dw.writeString(id, p.second.get().m_path.toUtf8().data()); + } + athena::io::FileWriter w(QStringToSysString(songsFile.filePath())); + if (!w.hasError()) + dw.finish(&w); + } +} + bool ProjectModel::saveToFile(UIMessenger& messenger) { m_projectDatabase.setIdDatabases(); @@ -528,20 +576,7 @@ bool ProjectModel::saveToFile(UIMessenger& messenger) g.second.getPool().toYAML(groupPath); } - if (!m_midiFiles.empty()) - { - QFileInfo songsFile(m_dir, QStringLiteral("!songs.yaml")); - athena::io::YAMLDocWriter dw("amuse::Songs"); - for (auto& p : m_midiFiles) - { - char id[16]; - snprintf(id, 16, "%04X", p.first.id); - dw.writeString(id, p.second.m_path.toUtf8().data()); - } - athena::io::FileWriter w(QStringToSysString(songsFile.path())); - if (!w.hasError()) - dw.finish(&w); - } + saveSongsIndex(); return true; } diff --git a/Editor/ProjectModel.hpp b/Editor/ProjectModel.hpp index 6d6edde..82631e9 100644 --- a/Editor/ProjectModel.hpp +++ b/Editor/ProjectModel.hpp @@ -349,16 +349,19 @@ public: bool m_needsReset = false; void _buildGroupNode(GroupNode& gn); void _resetModelData(); + void _resetSongRefCount(); public: explicit ProjectModel(const QString& path, QObject* parent = Q_NULLPTR); bool clearProjectData(); bool openGroupData(const QString& groupName, UIMessenger& messenger); - bool openSongsData(); + void openSongsData(); + void importSongsData(const QString& path); bool reloadSampleData(const QString& groupName, UIMessenger& messenger); bool importGroupData(const QString& groupName, const amuse::AudioGroupData& data, ImportMode mode, UIMessenger& messenger); + void saveSongsIndex(); bool saveToFile(UIMessenger& messenger); bool ensureModelData(); diff --git a/Editor/SoundMacroEditor.cpp b/Editor/SoundMacroEditor.cpp index 20c7854..36db78d 100644 --- a/Editor/SoundMacroEditor.cpp +++ b/Editor/SoundMacroEditor.cpp @@ -118,34 +118,6 @@ FieldSoundMacroStep::FieldSoundMacroStep(FieldProjectNode* macroField, QWidget* setLayout(layout); } -static QIcon SoundMacroDeleteIcon; -static QIcon SoundMacroDeleteHoveredIcon; - -void SoundMacroDeleteButton::enterEvent(QEvent* event) -{ - setIcon(SoundMacroDeleteHoveredIcon); -} - -void SoundMacroDeleteButton::leaveEvent(QEvent* event) -{ - setIcon(SoundMacroDeleteIcon); -} - -SoundMacroDeleteButton::SoundMacroDeleteButton(QWidget* parent) -: QPushButton(parent) -{ - if (SoundMacroDeleteIcon.isNull()) - SoundMacroDeleteIcon = QIcon(QStringLiteral(":/icons/IconSoundMacroDelete.svg")); - if (SoundMacroDeleteHoveredIcon.isNull()) - SoundMacroDeleteHoveredIcon = QIcon(QStringLiteral(":/icons/IconSoundMacroDeleteHovered.svg")); - - setVisible(false); - setFixedSize(21, 21); - setFlat(true); - setToolTip(tr("Delete this SoundMacro")); - setIcon(SoundMacroDeleteIcon); -} - CommandWidget::CommandWidget(amuse::SoundMacro::ICmd* cmd, amuse::SoundMacro::CmdOp op, SoundMacroListing* listing) : QWidget(nullptr), m_cmd(cmd), m_introspection(amuse::SoundMacro::GetCmdIntrospection(op)) { diff --git a/Editor/SoundMacroEditor.hpp b/Editor/SoundMacroEditor.hpp index 30c2490..0fbbe00 100644 --- a/Editor/SoundMacroEditor.hpp +++ b/Editor/SoundMacroEditor.hpp @@ -24,6 +24,8 @@ public: explicit TargetButton(QWidget* parent = Q_NULLPTR); void mouseReleaseEvent(QMouseEvent* event) { event->ignore(); } void mouseMoveEvent(QMouseEvent* event) { event->ignore(); } + void focusOutEvent(QFocusEvent* event) { event->ignore(); } + void keyPressEvent(QKeyEvent* event) { event->ignore(); } }; class FieldSoundMacroStep : public QWidget @@ -47,22 +49,13 @@ public: void cancel(); }; -class SoundMacroDeleteButton : public QPushButton -{ - Q_OBJECT -public: - explicit SoundMacroDeleteButton(QWidget* parent = Q_NULLPTR); - void enterEvent(QEvent* event); - void leaveEvent(QEvent* event); -}; - class CommandWidget : public QWidget { Q_OBJECT friend class SoundMacroListing; QFont m_numberFont; QLabel m_titleLabel; - SoundMacroDeleteButton m_deleteButton; + ListingDeleteButton m_deleteButton; QStaticText m_numberText; int m_index = -1; amuse::SoundMacro::ICmd* m_cmd; diff --git a/Editor/StatusBarWidget.cpp b/Editor/StatusBarWidget.cpp new file mode 100644 index 0000000..368b418 --- /dev/null +++ b/Editor/StatusBarWidget.cpp @@ -0,0 +1,91 @@ +#include "StatusBarWidget.hpp" + +FXButton::FXButton(QWidget* parent) +: QPushButton(parent) +{ + setIcon(QIcon(QStringLiteral(":/icons/IconFX.svg"))); + setToolTip(tr("Access studio setup window for experimenting with audio effects")); +} + +StatusBarWidget::StatusBarWidget(QWidget* parent) +: QStatusBar(parent), m_volumeSlider(Qt::Horizontal) +{ + addWidget(&m_normalMessage); + m_killButton.setIcon(QIcon(QStringLiteral(":/icons/IconKill.svg"))); + m_killButton.setVisible(false); + m_killButton.setToolTip(tr("Immediately kill active voices")); + m_voiceCount.setVisible(false); + m_volumeIcons[0] = QIcon(QStringLiteral(":/icons/IconVolume0")); + m_volumeIcons[1] = QIcon(QStringLiteral(":/icons/IconVolume1")); + m_volumeIcons[2] = QIcon(QStringLiteral(":/icons/IconVolume2")); + m_volumeIcons[3] = QIcon(QStringLiteral(":/icons/IconVolume3")); + m_volumeIcon.setFixedSize(16, 16); + m_volumeIcon.setPixmap(m_volumeIcons[0].pixmap(16, 16)); + connect(&m_volumeSlider, SIGNAL(valueChanged(int)), this, SLOT(volumeChanged(int))); + m_volumeSlider.setRange(0, 100); + m_volumeSlider.setFixedWidth(100); + addPermanentWidget(&m_voiceCount); + addPermanentWidget(&m_killButton); + addPermanentWidget(&m_fxButton); + addPermanentWidget(&m_volumeIcon); + addPermanentWidget(&m_volumeSlider); +} + +void StatusBarWidget::setVoiceCount(int voices) +{ + if (voices != m_cachedVoiceCount) + { + m_voiceCount.setText(QString::number(voices)); + m_cachedVoiceCount = voices; + setKillVisible(voices != 0); + } +} + +void StatusBarWidget::volumeChanged(int vol) +{ + int idx = int(std::round(vol * (3.f / 100.f))); + if (idx != m_lastVolIdx) + { + m_lastVolIdx = idx; + m_volumeIcon.setPixmap(m_volumeIcons[idx].pixmap(16, 16)); + } +} + +void StatusBarFocus::setMessage(const QString& message) +{ + m_message = message; + if (StatusBarWidget* widget = qobject_cast(parent())) + { + if (widget->m_curFocus == this) + { + if (m_message.isEmpty()) + widget->clearMessage(); + else + widget->showMessage(m_message); + } + } +} + +void StatusBarFocus::enter() +{ + if (StatusBarWidget* widget = qobject_cast(parent())) + { + widget->m_curFocus = this; + if (m_message.isEmpty()) + widget->clearMessage(); + else + widget->showMessage(m_message); + } +} + +void StatusBarFocus::exit() +{ + if (StatusBarWidget* widget = qobject_cast(parent())) + { + if (widget->m_curFocus == this) + { + widget->clearMessage(); + widget->m_curFocus = nullptr; + } + } +} diff --git a/Editor/StatusBarWidget.hpp b/Editor/StatusBarWidget.hpp index c533544..47ccf9c 100644 --- a/Editor/StatusBarWidget.hpp +++ b/Editor/StatusBarWidget.hpp @@ -4,42 +4,53 @@ #include #include #include +#include +#include +#include class StatusBarFocus; +class FXButton : public QPushButton +{ + Q_OBJECT +public: + explicit FXButton(QWidget* parent = Q_NULLPTR); + void mouseReleaseEvent(QMouseEvent* event) { event->ignore(); } + void mouseMoveEvent(QMouseEvent* event) { event->ignore(); } + void focusOutEvent(QFocusEvent* event) { event->ignore(); } + void keyPressEvent(QKeyEvent* event) { event->ignore(); } +}; + class StatusBarWidget : public QStatusBar { friend class StatusBarFocus; Q_OBJECT QLabel m_normalMessage; QPushButton m_killButton; + FXButton m_fxButton; + QIcon m_volumeIcons[4]; + QLabel m_volumeIcon; + QSlider m_volumeSlider; + int m_lastVolIdx = 0; QLabel m_voiceCount; int m_cachedVoiceCount = -1; StatusBarFocus* m_curFocus = nullptr; void setKillVisible(bool vis) { m_killButton.setVisible(vis); m_voiceCount.setVisible(vis); } public: - explicit StatusBarWidget(QWidget* parent = Q_NULLPTR) : QStatusBar(parent) - { - addWidget(&m_normalMessage); - m_killButton.setIcon(QIcon(QStringLiteral(":/icons/IconKill.svg"))); - m_killButton.setVisible(false); - m_killButton.setToolTip(tr("Immediately kill active voices")); - m_voiceCount.setVisible(false); - addPermanentWidget(&m_voiceCount); - addPermanentWidget(&m_killButton); - } + explicit StatusBarWidget(QWidget* parent = Q_NULLPTR); void setNormalMessage(const QString& message) { m_normalMessage.setText(message); } - void setVoiceCount(int voices) - { - if (voices != m_cachedVoiceCount) - { - m_voiceCount.setText(QString::number(voices)); - m_cachedVoiceCount = voices; - setKillVisible(voices != 0); - } - } + void setVoiceCount(int voices); void connectKillClicked(const QObject* receiver, const char* method) { connect(&m_killButton, SIGNAL(clicked(bool)), receiver, method); } + void connectFXPressed(const QObject* receiver, const char* method) + { connect(&m_fxButton, SIGNAL(pressed()), receiver, method); } + 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 setVolumeValue(int vol) { m_volumeSlider.setValue(vol); } + +private slots: + void volumeChanged(int vol); }; class StatusBarFocus : public QObject @@ -50,42 +61,9 @@ public: explicit StatusBarFocus(StatusBarWidget* statusWidget) : QObject(statusWidget) {} ~StatusBarFocus() { exit(); } - void setMessage(const QString& message) - { - m_message = message; - if (StatusBarWidget* widget = qobject_cast(parent())) - { - if (widget->m_curFocus == this) - { - if (m_message.isEmpty()) - widget->clearMessage(); - else - widget->showMessage(m_message); - } - } - } - void enter() - { - if (StatusBarWidget* widget = qobject_cast(parent())) - { - widget->m_curFocus = this; - if (m_message.isEmpty()) - widget->clearMessage(); - else - widget->showMessage(m_message); - } - } - void exit() - { - if (StatusBarWidget* widget = qobject_cast(parent())) - { - if (widget->m_curFocus == this) - { - widget->clearMessage(); - widget->m_curFocus = nullptr; - } - } - } + void setMessage(const QString& message); + void enter(); + void exit(); }; #endif //AMUSE_STATUSBAR_WIDGET_HPP diff --git a/Editor/StudioSetupWidget.cpp b/Editor/StudioSetupWidget.cpp new file mode 100644 index 0000000..dfef058 --- /dev/null +++ b/Editor/StudioSetupWidget.cpp @@ -0,0 +1,1058 @@ +#include "StudioSetupWidget.hpp" +#include "amuse/EffectChorus.hpp" +#include "amuse/EffectDelay.hpp" +#include "amuse/EffectReverb.hpp" +#include +#include + +using namespace std::literals; + +static const EffectIntrospection ReverbStdIntrospective = +{ + amuse::EffectType::ReverbStd, + "Reverb Std"sv, + "Standard Reverb"sv, + { + { + EffectIntrospection::Field::Type::Float, + "Coloration"sv, + 0.f, 1.f, 0.f + }, + { + EffectIntrospection::Field::Type::Float, + "Mix"sv, + 0.f, 1.f, 0.f + }, + { + EffectIntrospection::Field::Type::Float, + "Time"sv, + 0.01f, 10.f, 0.01f + }, + { + EffectIntrospection::Field::Type::Float, + "Damping"sv, + 0.f, 1.f, 0.f + }, + { + EffectIntrospection::Field::Type::Float, + "Pre Delay"sv, + 0.f, 0.1f, 0.f + } + } +}; + +typedef float (amuse::EffectReverbStd::*ReverbStdGetFunc)() const; +typedef void (amuse::EffectReverbStd::*ReverbStdSetFunc)(float); +static const ReverbStdGetFunc ReverbStdGetters[] = +{ + &amuse::EffectReverbStd::getColoration, + &amuse::EffectReverbStd::getMix, + &amuse::EffectReverbStd::getTime, + &amuse::EffectReverbStd::getDamping, + &amuse::EffectReverbStd::getPreDelay +}; +static const ReverbStdSetFunc ReverbStdSetters[] = +{ + &amuse::EffectReverbStd::setColoration, + &amuse::EffectReverbStd::setMix, + &amuse::EffectReverbStd::setTime, + &amuse::EffectReverbStd::setDamping, + &amuse::EffectReverbStd::setPreDelay +}; + +static const EffectIntrospection ReverbHiIntrospective = +{ + amuse::EffectType::ReverbHi, + "Reverb Hi"sv, + "High Reverb"sv, + { + { + EffectIntrospection::Field::Type::Float, + "Coloration"sv, + 0.f, 1.f, 0.f + }, + { + EffectIntrospection::Field::Type::Float, + "Mix"sv, + 0.f, 1.f, 0.f + }, + { + EffectIntrospection::Field::Type::Float, + "Time"sv, + 0.01f, 10.f, 0.01f + }, + { + EffectIntrospection::Field::Type::Float, + "Damping"sv, + 0.f, 1.f, 0.f + }, + { + EffectIntrospection::Field::Type::Float, + "Pre Delay"sv, + 0.f, 0.1f, 0.f + }, + { + EffectIntrospection::Field::Type::Float, + "Crosstalk"sv, + 0.f, 1.f, 0.f + } + } +}; + +typedef float (amuse::EffectReverbHi::*ReverbHiGetFunc)() const; +typedef void (amuse::EffectReverbHi::*ReverbHiSetFunc)(float); +static const ReverbHiGetFunc ReverbHiGetters[] = +{ + &amuse::EffectReverbHi::getColoration, + &amuse::EffectReverbHi::getMix, + &amuse::EffectReverbHi::getTime, + &amuse::EffectReverbHi::getDamping, + &amuse::EffectReverbHi::getPreDelay, + &amuse::EffectReverbHi::getCrosstalk +}; +static const ReverbHiSetFunc ReverbHiSetters[] = +{ + &amuse::EffectReverbHi::setColoration, + &amuse::EffectReverbHi::setMix, + &amuse::EffectReverbHi::setTime, + &amuse::EffectReverbHi::setDamping, + &amuse::EffectReverbHi::setPreDelay, + &amuse::EffectReverbHi::setCrosstalk +}; + +static const EffectIntrospection DelayIntrospective = +{ + amuse::EffectType::Delay, + "Delay"sv, + "Delay"sv, + { + { + EffectIntrospection::Field::Type::UInt32x8, + "Delay"sv, + 10, 5000, 10 + }, + { + EffectIntrospection::Field::Type::UInt32x8, + "Feedback"sv, + 0, 100, 0 + }, + { + EffectIntrospection::Field::Type::UInt32x8, + "Output"sv, + 0, 100, 0 + }, + } +}; + +typedef uint32_t (amuse::EffectDelay::*DelayGetFunc)(int) const; +typedef void (amuse::EffectDelay::*DelaySetFunc)(int, uint32_t); +static const DelayGetFunc DelayGetters[] = +{ + &amuse::EffectDelay::getChanDelay, + &amuse::EffectDelay::getChanFeedback, + &amuse::EffectDelay::getChanOutput +}; +static const DelaySetFunc DelaySetters[] = +{ + &amuse::EffectDelay::setChanDelay, + &amuse::EffectDelay::setChanFeedback, + &amuse::EffectDelay::setChanOutput +}; + +static const EffectIntrospection ChorusIntrospective = +{ + amuse::EffectType::Chorus, + "Chorus"sv, + "Chorus"sv, + { + { + EffectIntrospection::Field::Type::UInt32, + "Base Delay"sv, + 5, 15, 5 + }, + { + EffectIntrospection::Field::Type::UInt32, + "Variation"sv, + 0, 5, 0 + }, + { + EffectIntrospection::Field::Type::UInt32, + "Period"sv, + 500, 10000, 500 + }, + } +}; + +typedef uint32_t (amuse::EffectChorus::*ChorusGetFunc)() const; +typedef void (amuse::EffectChorus::*ChorusSetFunc)(uint32_t); +static const ChorusGetFunc ChorusGetters[] = +{ + &amuse::EffectChorus::getBaseDelay, + &amuse::EffectChorus::getVariation, + &amuse::EffectChorus::getPeriod +}; +static const ChorusSetFunc ChorusSetters[] = +{ + &amuse::EffectChorus::setBaseDelay, + &amuse::EffectChorus::setVariation, + &amuse::EffectChorus::setPeriod +}; + +static const EffectIntrospection* GetEffectIntrospection(amuse::EffectType type) +{ + switch (type) + { + case amuse::EffectType::ReverbStd: + return &ReverbStdIntrospective; + case amuse::EffectType::ReverbHi: + return &ReverbHiIntrospective; + case amuse::EffectType::Delay: + return &DelayIntrospective; + case amuse::EffectType::Chorus: + return &ChorusIntrospective; + default: + return nullptr; + } +} + +template +static T GetEffectParm(const amuse::EffectBaseTypeless* effect, int idx, int chanIdx) +{ + switch (effect->Isa()) + { + case amuse::EffectType::ReverbStd: + return (static_cast*>(effect)->*ReverbStdGetters[idx])(); + case amuse::EffectType::ReverbHi: + return (static_cast*>(effect)->*ReverbHiGetters[idx])(); + case amuse::EffectType::Delay: + return (static_cast*>(effect)->*DelayGetters[idx])(chanIdx); + case amuse::EffectType::Chorus: + return (static_cast*>(effect)->*ChorusGetters[idx])(); + default: + return 0.f; + } +} + +template +static void SetEffectParm(amuse::EffectBaseTypeless* effect, int idx, int chanIdx, T val) +{ + switch (effect->Isa()) + { + case amuse::EffectType::ReverbStd: + (static_cast*>(effect)->*ReverbStdSetters[idx])(val); break; + case amuse::EffectType::ReverbHi: + (static_cast*>(effect)->*ReverbHiSetters[idx])(val); break; + case amuse::EffectType::Delay: + (static_cast*>(effect)->*DelaySetters[idx])(chanIdx, val); break; + case amuse::EffectType::Chorus: + (static_cast*>(effect)->*ChorusSetters[idx])(val); break; + default: + break; + } +} + +EffectWidget::EffectWidget(amuse::EffectBaseTypeless* effect, amuse::EffectType type) +: QWidget(nullptr), m_effect(effect), m_introspection(GetEffectIntrospection(type)) +{ + QFont titleFont = m_titleLabel.font(); + 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)); + m_numberText.setTextWidth(25); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + m_numberFont.setWeight(QFont::Bold); + m_numberFont.setStyleHint(QFont::Monospace); + m_numberFont.setPointSize(16); + + setContentsMargins(QMargins()); + setFixedHeight(100); + + QVBoxLayout* mainLayout = new QVBoxLayout; + mainLayout->setContentsMargins(QMargins()); + mainLayout->setSpacing(0); + + QHBoxLayout* headLayout = new QHBoxLayout; + headLayout->setContentsMargins(QMargins()); + headLayout->setSpacing(0); + headLayout->addWidget(&m_titleLabel); + + m_deleteButton.setVisible(true); + connect(&m_deleteButton, SIGNAL(clicked(bool)), this, SLOT(deleteClicked())); + headLayout->addWidget(&m_deleteButton); + + mainLayout->addLayout(headLayout); + mainLayout->addSpacing(8); + + QGridLayout* layout = new QGridLayout; + layout->setSpacing(6); + layout->setContentsMargins(64, 0, 12, 12); + if (m_introspection) + { + m_titleLabel.setText(tr(m_introspection->m_name.data())); + m_titleLabel.setToolTip(tr(m_introspection->m_description.data())); + for (int f = 0; f < 7; ++f) + { + const EffectIntrospection::Field& field = m_introspection->m_fields[f]; + if (!field.m_name.empty()) + { + QString fieldName = tr(field.m_name.data()); + layout->addWidget(new QLabel(fieldName), 0, f); + switch (field.m_tp) + { + case EffectIntrospection::Field::Type::UInt32: + { + FieldSlider* sb = new FieldSlider; + sb->setProperty("fieldIndex", f); + sb->setRange(int(field.m_min), int(field.m_max)); + sb->setToolTip(QStringLiteral("[%1,%2]").arg(int(field.m_min)).arg(int(field.m_max))); + sb->setValue(GetEffectParm(m_effect, f, 0)); + connect(sb, SIGNAL(valueChanged(int)), this, SLOT(numChanged(int))); + layout->addWidget(sb, 1, f); + break; + } + case EffectIntrospection::Field::Type::Float: + { + FieldDoubleSlider* sb = new FieldDoubleSlider; + sb->setProperty("fieldIndex", f); + sb->setRange(field.m_min, field.m_max); + sb->setToolTip(QStringLiteral("[%1,%2]").arg(field.m_min).arg(field.m_max)); + sb->setValue(GetEffectParm(m_effect, f, 0)); + connect(sb, SIGNAL(valueChanged(double)), this, SLOT(numChanged(double))); + layout->addWidget(sb, 1, f); + break; + } + default: + break; + } + } + } + } + mainLayout->addLayout(layout); + layout->setRowMinimumHeight(0, 22); + layout->setRowMinimumHeight(1, 22); + setLayout(mainLayout); +} + +void EffectWidget::paintEvent(QPaintEvent* event) +{ + /* Rounded frame */ + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + QPoint points[] = + { + {1, 20}, + {1, 99}, + {width() - 1, 99}, + {width() - 1, 1}, + {20, 1}, + {1, 20}, + }; + painter.setBrush(palette().brush(QPalette::Background)); + painter.drawPolygon(points, 6); + painter.setPen(QPen(QColor(127, 127, 127), 2.0)); + painter.drawPolyline(points, 6); + + QPoint headPoints[] = + { + {1, 20}, + {1, 55}, + {35, 20}, + {width() - 1, 20}, + {width() - 1, 1}, + {20, 1}, + {1, 20} + }; + painter.setBrush(QColor(127, 127, 127)); + painter.drawPolygon(headPoints, 7); + + painter.drawRect(17, 51, 32, 32); + + QTransform rotate; + rotate.rotate(-45.0); + painter.setTransform(rotate); + painter.setFont(m_numberFont); + painter.setPen(palette().color(QPalette::Background)); + painter.drawStaticText(-15, 10, m_numberText); +} + +EffectWidget::EffectWidget(amuse::EffectBaseTypeless* cmd) +: EffectWidget(cmd, cmd->Isa()) {} + +EffectWidget::EffectWidget(amuse::EffectType type) +: EffectWidget(nullptr, type) {} + +void EffectWidget::numChanged(int value) +{ + SetEffectParm(m_effect, sender()->property("fieldIndex").toInt(), 0, value); +} + +void EffectWidget::numChanged(double value) +{ + SetEffectParm(m_effect, sender()->property("fieldIndex").toInt(), 0, value); +} + +void EffectWidget::deleteClicked() +{ + if (m_index != -1) + if (EffectListing* listing = getParent()) + listing->deleteEffect(m_index); +} + +EffectListing* EffectWidget::getParent() const +{ + return qobject_cast(parentWidget()->parentWidget()); +} + +void EffectWidget::setIndex(int index) +{ + m_index = index; + m_numberText.setText(QString::number(index)); + update(); +} + +void EffectWidgetContainer::animateOpen() +{ + int newHeight = 200 + parentWidget()->layout()->spacing(); + m_animation = new QPropertyAnimation(this, "minimumHeight"); + m_animation->setDuration(abs(minimumHeight() - newHeight) * 4); + m_animation->setStartValue(minimumHeight()); + m_animation->setEndValue(newHeight); + m_animation->setEasingCurve(QEasingCurve::InOutExpo); + connect(m_animation, SIGNAL(valueChanged(const QVariant&)), parentWidget(), SLOT(update())); + connect(m_animation, SIGNAL(destroyed(QObject*)), this, SLOT(animationDestroyed())); + m_animation->start(QAbstractAnimation::DeleteWhenStopped); +} + +void EffectWidgetContainer::animateClosed() +{ + m_animation = new QPropertyAnimation(this, "minimumHeight"); + m_animation->setDuration(abs(minimumHeight() - 100) * 4); + m_animation->setStartValue(minimumHeight()); + m_animation->setEndValue(100); + m_animation->setEasingCurve(QEasingCurve::InOutExpo); + connect(m_animation, SIGNAL(valueChanged(const QVariant&)), parentWidget(), SLOT(update())); + connect(m_animation, SIGNAL(destroyed(QObject*)), this, SLOT(animationDestroyed())); + m_animation->start(QAbstractAnimation::DeleteWhenStopped); +} + +void EffectWidgetContainer::snapOpen() +{ + if (m_animation) + m_animation->stop(); + setMinimumHeight(200 + parentWidget()->layout()->spacing()); +} + +void EffectWidgetContainer::snapClosed() +{ + if (m_animation) + m_animation->stop(); + setMinimumHeight(100); +} + +void EffectWidgetContainer::animationDestroyed() +{ + m_animation = nullptr; +} + +EffectWidgetContainer::EffectWidgetContainer(EffectWidget* child, QWidget* parent) + : QWidget(parent), m_effectWidget(child) +{ + setMinimumHeight(100); + setContentsMargins(QMargins()); + QVBoxLayout* outerLayout = new QVBoxLayout; + outerLayout->setAlignment(Qt::AlignBottom); + outerLayout->setContentsMargins(QMargins()); + outerLayout->setSpacing(0); + outerLayout->addWidget(child); + setLayout(outerLayout); +} + +void EffectListing::startAutoscroll(QWidget* source, QMouseEvent* event, int delta) +{ + if (m_autoscrollTimer == -1) + m_autoscrollTimer = startTimer(50); + m_autoscrollDelta = delta; + m_autoscrollSource = source; + m_autoscrollEvent = *event; +} + +void EffectListing::stopAutoscroll() +{ + if (m_autoscrollTimer != -1) + { + killTimer(m_autoscrollTimer); + m_autoscrollTimer = -1; + } + m_autoscrollDelta = 0; + m_autoscrollSource = nullptr; +} + +void EffectListing::timerEvent(QTimerEvent* event) +{ + if (QScrollArea* scrollArea = qobject_cast(parentWidget()->parentWidget())) + { + QScrollBar* bar = scrollArea->verticalScrollBar(); + int oldValue = bar->value(); + bar->setValue(oldValue + m_autoscrollDelta); + int valueDelta = bar->value() - oldValue; + if (valueDelta != 0) + { + if (m_autoscrollSource) + QApplication::sendEvent(m_autoscrollSource, &m_autoscrollEvent); + update(); + } + } +} + +bool EffectListing::beginDrag(EffectWidget* widget) +{ + int origIdx = m_layout->indexOf(widget->parentWidget()); + if (origIdx < 0 || origIdx >= m_layout->count() - 1) + return false; + if (origIdx < m_layout->count() - 1) + { + // Animate next item open + m_dragOpenIdx = origIdx; + if (EffectWidgetContainer* nextItem = + qobject_cast(m_layout->itemAt(origIdx + 1)->widget())) + nextItem->snapOpen(); + } + else + { + m_dragOpenIdx = -1; + } + m_origIdx = origIdx; + m_dragItem = m_layout->takeAt(origIdx); + m_dragItem->widget()->raise(); + return true; +} + +void EffectListing::endDrag() +{ + int insertIdx; + if (m_dragOpenIdx != -1) + { + if (EffectWidgetContainer* prevItem = + qobject_cast(m_layout->itemAt(m_dragOpenIdx)->widget())) + prevItem->snapClosed(); + insertIdx = m_dragOpenIdx; + m_dragOpenIdx = -1; + } + else + { + insertIdx = m_layout->count() - 1; + } + + if (m_prevDragOpen) + { + m_prevDragOpen->snapClosed(); + m_prevDragOpen = nullptr; + } + + if (m_origIdx != insertIdx) + std::swap(m_submix->getEffectStack()[m_origIdx], m_submix->getEffectStack()[insertIdx]); + m_layout->insertItem(insertIdx, m_dragItem); + m_dragItem = nullptr; + stopAutoscroll(); + reindex(); +} + +void EffectListing::cancelDrag() +{ + if (m_dragOpenIdx != -1) + { + if (EffectWidgetContainer* prevItem = + qobject_cast(m_layout->itemAt(m_dragOpenIdx)->widget())) + prevItem->snapClosed(); + m_dragOpenIdx = -1; + } + m_layout->insertItem(m_origIdx, m_dragItem); + if (m_prevDragOpen) + { + m_prevDragOpen->snapClosed(); + m_prevDragOpen = nullptr; + } + m_dragItem = nullptr; + stopAutoscroll(); +} + +void EffectListing::_moveDrag(int hoverIdx, const QPoint& pt, QWidget* source, QMouseEvent* event) +{ + QRect scrollVpRect = parentWidget()->parentWidget()->rect(); + QPoint scrollVpPoint = mapTo(parentWidget()->parentWidget(), pt); + if (scrollVpRect.bottom() - scrollVpPoint.y() < 50) + startAutoscroll(source, event, 10); // Scroll Down + else if (scrollVpRect.top() - scrollVpPoint.y() > -50) + startAutoscroll(source, event, -10); + else + stopAutoscroll(); + + hoverIdx = std::max(0, std::min(hoverIdx, m_layout->count() - 1)); + if (hoverIdx != m_dragOpenIdx) + { + if (m_dragOpenIdx != -1) + if (EffectWidgetContainer* prevItem = + qobject_cast(m_layout->itemAt(m_dragOpenIdx)->widget())) + { + m_prevDragOpen = prevItem; + prevItem->animateClosed(); + } + if (EffectWidgetContainer* nextItem = + qobject_cast(m_layout->itemAt(hoverIdx)->widget())) + nextItem->animateOpen(); + m_dragOpenIdx = hoverIdx; + } + update(); +} + +void EffectListing::moveDrag(EffectWidget* widget, const QPoint& pt, QWidget* source, QMouseEvent* event) +{ + EffectWidgetContainer* container = static_cast(widget->parentWidget()); + int pitch = 100 + m_layout->spacing(); + _moveDrag((container->pos().y() - m_layout->contentsMargins().top() + pitch / 2) / pitch, pt, source, event); +} + +int EffectListing::moveInsertDrag(const QPoint& pt, QWidget* source, QMouseEvent* event) +{ + int pitch = 100 + m_layout->spacing(); + _moveDrag(pt.y() / pitch, pt, source, event); + return m_dragOpenIdx; +} + +void EffectListing::insertDragout() +{ + if (m_dragOpenIdx != -1) + { + if (EffectWidgetContainer* prevItem = + qobject_cast(m_layout->itemAt(m_dragOpenIdx)->widget())) + { + m_prevDragOpen = prevItem; + prevItem->animateClosed(); + } + m_dragOpenIdx = -1; + } + stopAutoscroll(); +} + +void EffectListing::insert(amuse::EffectType type, const QString& text) +{ + int insertIdx; + if (m_dragOpenIdx != -1) + { + if (EffectWidgetContainer* prevItem = + qobject_cast(m_layout->itemAt(m_dragOpenIdx)->widget())) + prevItem->snapClosed(); + insertIdx = m_dragOpenIdx; + m_dragOpenIdx = -1; + } + else + { + insertIdx = m_layout->count() - 1; + } + + if (m_prevDragOpen) + { + m_prevDragOpen->snapClosed(); + m_prevDragOpen = nullptr; + } + + std::unique_ptr newEffect; + switch (type) + { + case amuse::EffectType::ReverbStd: + newEffect = m_submix->_makeEffect(amuse::EffectReverbStdInfo{}); + break; + case amuse::EffectType::ReverbHi: + newEffect = m_submix->_makeEffect(amuse::EffectReverbHiInfo{}); + break; + case amuse::EffectType::Delay: + newEffect = m_submix->_makeEffect(amuse::EffectDelayInfo{}); + break; + case amuse::EffectType::Chorus: + newEffect = m_submix->_makeEffect(amuse::EffectChorusInfo{}); + break; + default: + break; + } + auto it = m_submix->getEffectStack().insert(m_submix->getEffectStack().begin() + insertIdx, std::move(newEffect)); + m_layout->insertWidget(insertIdx, new EffectWidgetContainer(new EffectWidget(it->get()))); + + stopAutoscroll(); + reindex(); +} + +void EffectListing::deleteEffect(int index) +{ + QLayoutItem* item = m_layout->takeAt(index); + m_submix->getEffectStack().erase(m_submix->getEffectStack().begin() + index); + item->widget()->deleteLater(); + delete item; + reindex(); +} + +void EffectListing::reindex() +{ + for (int i = 0; i < m_layout->count() - 1; ++i) + if (EffectWidgetContainer* item = + qobject_cast(m_layout->itemAt(i)->widget())) + item->m_effectWidget->setIndex(i); +} + +void EffectListing::clear() +{ + while (m_layout->count() > 2) + { + QLayoutItem* item = m_layout->takeAt(0); + item->widget()->deleteLater(); + delete item; + } +} + +bool EffectListing::loadData(amuse::Submix* submix) +{ + m_submix = submix; + clear(); + int i = 0; + for (auto& effect : submix->getEffectStack()) + m_layout->insertWidget(i++, new EffectWidgetContainer(new EffectWidget(effect.get()))); + reindex(); + update(); + return true; +} + +void EffectListing::unloadData() +{ + m_submix = nullptr; + clear(); + reindex(); + update(); +} + +EffectListing::EffectListing(QWidget* parent) +: QWidget(parent), m_layout(new QVBoxLayout) +{ + m_layout->addStretch(); + setLayout(m_layout); + reindex(); +} + +EffectCatalogueItem::EffectCatalogueItem(amuse::EffectType type, const QString& name, + const QString& doc, QWidget* parent) +: QWidget(parent), m_type(type), m_label(name) +{ + QHBoxLayout* layout = new QHBoxLayout; + QString iconPath = QStringLiteral(":/commands/%1.svg").arg(name); + if (QFile(iconPath).exists()) + m_iconLab.setPixmap(QIcon(iconPath).pixmap(32, 32)); + else + m_iconLab.setPixmap(QIcon(QStringLiteral(":/icons/IconOpen.svg")).pixmap(32, 32)); + layout->addWidget(&m_iconLab); + layout->addWidget(&m_label); + layout->addStretch(); + layout->setContentsMargins(QMargins()); + setLayout(layout); + setToolTip(doc); +} + +EffectCatalogueItem::EffectCatalogueItem(const EffectCatalogueItem& other, QWidget* parent) +: QWidget(parent), m_type(other.getType()) +{ + QHBoxLayout* layout = new QHBoxLayout; + QHBoxLayout* oldLayout = static_cast(other.layout()); + m_iconLab.setPixmap(*static_cast(oldLayout->itemAt(0)->widget())->pixmap()); + layout->addWidget(&m_iconLab); + m_label.setText(static_cast(oldLayout->itemAt(1)->widget())->text()); + layout->addWidget(&m_label); + layout->addStretch(); + layout->setContentsMargins(QMargins()); + setLayout(layout); +} + +static const char* EffectStrings[] = +{ + QT_TRANSLATE_NOOP("EffectCatalogue", "Reverb Standard"), + QT_TRANSLATE_NOOP("EffectCatalogue", "Reverb High"), + QT_TRANSLATE_NOOP("EffectCatalogue", "Delay"), + QT_TRANSLATE_NOOP("EffectCatalogue", "Chorus") +}; + +static const char* EffectDocStrings[] = +{ + QT_TRANSLATE_NOOP("EffectCatalogue", "Reverb Standard"), + QT_TRANSLATE_NOOP("EffectCatalogue", "Reverb High"), + QT_TRANSLATE_NOOP("EffectCatalogue", "Delay"), + QT_TRANSLATE_NOOP("EffectCatalogue", "Chorus") +}; + +EffectCatalogue::EffectCatalogue(QWidget* parent) +: QTreeWidget(parent) +{ + setSelectionMode(QAbstractItemView::NoSelection); + setColumnCount(1); + setHeaderHidden(true); + + for (int i = 1; i < int(amuse::EffectType::EffectTypeMAX); ++i) + { + QTreeWidgetItem* item = new QTreeWidgetItem(this); + setItemWidget(item, 0, new EffectCatalogueItem(amuse::EffectType(i), + tr(EffectStrings[i-1]), tr(EffectDocStrings[i-1]))); + } +} + +void EffectCatalogue::mousePressEvent(QMouseEvent* event) +{ + QTreeWidget::mousePressEvent(event); + event->ignore(); +} + +void EffectCatalogue::mouseReleaseEvent(QMouseEvent* event) +{ + QTreeWidget::mouseReleaseEvent(event); + event->ignore(); +} + +void EffectCatalogue::mouseMoveEvent(QMouseEvent* event) +{ + StudioSetupWidget* editor = qobject_cast(parentWidget()->parentWidget()->parentWidget()); + if (!editor || !editor->m_draggedItem) + QTreeWidget::mouseMoveEvent(event); + event->ignore(); +} + +EffectListing* StudioSetupWidget::getCurrentListing() const +{ + return static_cast(static_cast(m_tabs->currentWidget())->widget()); +} + +void StudioSetupWidget::beginCommandDrag(EffectWidget* widget, const QPoint& eventPt, const QPoint& pt) +{ + if (widget->getParent()->beginDrag(widget)) + { + m_draggedPt = pt; + m_draggedCmd = widget; + } +} + +void StudioSetupWidget::beginCatalogueDrag(EffectCatalogueItem* item, const QPoint& eventPt, const QPoint& pt) +{ + m_draggedPt = pt; + m_draggedItem = new EffectCatalogueItem(*item, this); + m_draggedItem->setGeometry(item->geometry()); + m_draggedItem->move(eventPt - m_draggedPt); + m_draggedItem->raise(); + m_draggedItem->show(); +} + +void StudioSetupWidget::mousePressEvent(QMouseEvent* event) +{ + if (m_catalogue->geometry().contains(event->pos())) + { + QPoint fromParent1 = m_catalogue->mapFrom(this, event->pos()); + QWidget* ch = m_catalogue->childAt(fromParent1); + if (ch) + { + EffectCatalogueItem* child = nullptr; + while (ch && !(child = qobject_cast(ch))) + ch = ch->parentWidget(); + if (child) + { + QPoint fromParent2 = child->mapFrom(m_catalogue, fromParent1); + beginCatalogueDrag(child, event->pos(), fromParent2); + } + } + } + else + { + EffectListing* listing = getCurrentListing(); + if (listing->parentWidget()->parentWidget()->parentWidget()->geometry().contains(event->pos())) + { + QPoint fromParent1 = listing->mapFrom(this, event->pos()); + QWidget* ch = listing->childAt(fromParent1); + if (ch) + { + EffectWidget* child = nullptr; + while (ch && !(child = qobject_cast(ch))) + ch = ch->parentWidget(); + if (child) + { + QPoint fromParent2 = child->mapFrom(listing, fromParent1); + beginCommandDrag(child, event->pos(), fromParent2); + } + } + } + } +} + +void StudioSetupWidget::mouseReleaseEvent(QMouseEvent* event) +{ + EffectListing* listing = getCurrentListing(); + if (m_draggedItem) + { + amuse::EffectType type = m_draggedItem->getType(); + QString text = m_draggedItem->getText(); + m_draggedItem->deleteLater(); + m_draggedItem = nullptr; + + if (listing->parentWidget()->parentWidget()->parentWidget()->geometry().contains(event->pos())) + { + if (m_dragInsertIdx != -1) + listing->insert(type, text); + else + listing->insertDragout(); + } + else + { + listing->insertDragout(); + } + m_dragInsertIdx = -1; + } + else if (m_draggedCmd) + { + listing->endDrag(); + m_draggedCmd = nullptr; + } +} + +void StudioSetupWidget::mouseMoveEvent(QMouseEvent* event) +{ + EffectListing* listing = getCurrentListing(); + if (m_draggedItem) + { + m_draggedItem->move(event->pos() - m_draggedPt); + if (listing->parentWidget()->parentWidget()->parentWidget()->geometry().contains(event->pos())) + { + m_dragInsertIdx = listing->moveInsertDrag(listing->mapFrom(this, event->pos()), this, event); + } + else if (m_dragInsertIdx != -1) + { + listing->insertDragout(); + m_dragInsertIdx = -1; + } + m_catalogue->update(); + update(); + } + else if (m_draggedCmd) + { + QPoint listingPt = listing->mapFrom(this, event->pos()); + EffectWidgetContainer* container = static_cast(m_draggedCmd->parentWidget()); + container->move(container->x(), listingPt.y() - m_draggedPt.y()); + if (listing->parentWidget()->parentWidget()->parentWidget()->geometry().contains(event->pos())) + listing->moveDrag(m_draggedCmd, listingPt, this, event); + listing->update(); + update(); + } +} + +void StudioSetupWidget::keyPressEvent(QKeyEvent* event) +{ + if (event->key() == Qt::Key_Escape) + { + EffectListing* listing = getCurrentListing(); + if (m_draggedItem) + { + m_draggedItem->deleteLater(); + m_draggedItem = nullptr; + listing->insertDragout(); + } + else if (m_draggedCmd) + { + listing->cancelDrag(); + m_draggedCmd = nullptr; + } + } +} + +void StudioSetupWidget::hideEvent(QHideEvent* event) +{ + emit hidden(); +} + +void StudioSetupWidget::showEvent(QShowEvent* event) +{ + emit shown(); +} + +void StudioSetupWidget::updateWindowPosition() +{ + QWidget* parent = parentWidget(); + move(parent->width() / 2 - width() / 2 + parent->x(), + parent->height() / 2 - height() / 2 + parent->y()); +} + +void StudioSetupWidget::catalogueDoubleClicked(QTreeWidgetItem* item, int column) +{ + if (EffectCatalogueItem* cItem = qobject_cast(m_catalogue->itemWidget(item, column))) + { + amuse::EffectType type = cItem->getType(); + if (type != amuse::EffectType::Invalid) + { + EffectListing* listing = getCurrentListing(); + listing->insert(type, cItem->getText()); + } + } +} + +bool StudioSetupWidget::loadData(amuse::Studio* studio) +{ + m_listing[0]->loadData(&studio->getAuxA()); + m_listing[1]->loadData(&studio->getAuxB()); + return true; +} + +void StudioSetupWidget::unloadData() +{ + m_listing[0]->unloadData(); + m_listing[1]->unloadData(); +} + +StudioSetupWidget::StudioSetupWidget(QWidget* parent) +: QWidget(parent, Qt::Tool), m_splitter(new QSplitter), m_tabs(new QTabWidget), m_catalogue(new EffectCatalogue) +{ + setWindowTitle(tr("Studio Setup")); + setGeometry(0, 0, 900, 450); + + QScrollArea* scrollAreas[2]; + for (int i = 0; i < 2; ++i) + { + m_listing[i] = new EffectListing; + QScrollArea* listingScroll = new QScrollArea; + scrollAreas[i] = listingScroll; + listingScroll->setWidget(m_listing[i]); + listingScroll->setWidgetResizable(true); + QSizePolicy sizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + sizePolicy.setHorizontalStretch(1); + sizePolicy.setVerticalStretch(0); + listingScroll->setSizePolicy(sizePolicy); + listingScroll->setMinimumWidth(350); + m_splitter->addWidget(listingScroll); + } + m_tabs->addTab(scrollAreas[0], tr("Aux A")); + m_tabs->addTab(scrollAreas[1], tr("Aux B")); + m_splitter->addWidget(m_tabs); + + { + QSizePolicy sizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + sizePolicy.setHorizontalStretch(0); + sizePolicy.setVerticalStretch(0); + m_catalogue->setSizePolicy(sizePolicy); + m_catalogue->setMinimumWidth(150); + m_catalogue->setGeometry(0, 0, 215, 0); + m_catalogue->setMaximumWidth(300); + m_splitter->addWidget(m_catalogue); + } + + connect(m_catalogue, SIGNAL(itemDoubleClicked(QTreeWidgetItem*, int)), + this, SLOT(catalogueDoubleClicked(QTreeWidgetItem*, int))); + + m_splitter->setCollapsible(0, false); + QGridLayout* layout = new QGridLayout; + layout->setContentsMargins(QMargins()); + layout->addWidget(m_splitter); + setLayout(layout); +} diff --git a/Editor/StudioSetupWidget.hpp b/Editor/StudioSetupWidget.hpp new file mode 100644 index 0000000..abc334f --- /dev/null +++ b/Editor/StudioSetupWidget.hpp @@ -0,0 +1,177 @@ +#ifndef AMUSE_STUDIO_SETUP_WIDGET_HPP +#define AMUSE_STUDIO_SETUP_WIDGET_HPP + +#include "EditorWidget.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "amuse/Studio.hpp" + +struct EffectIntrospection +{ + struct Field + { + enum class Type + { + Invalid, + UInt32, + UInt32x8, + Float + }; + Type m_tp; + std::string_view m_name; + float m_min, m_max, m_default; + }; + amuse::EffectType m_tp; + std::string_view m_name; + std::string_view m_description; + Field m_fields[7]; +}; + +class EffectListing; + +class EffectWidget : public QWidget +{ + Q_OBJECT + friend class EffectListing; + QFont m_numberFont; + QLabel m_titleLabel; + ListingDeleteButton m_deleteButton; + QStaticText m_numberText; + int m_index = -1; + amuse::EffectBaseTypeless* m_effect; + const EffectIntrospection* m_introspection; + void setIndex(int index); +private slots: + void numChanged(int); + void numChanged(double); + void deleteClicked(); +private: + EffectWidget(amuse::EffectBaseTypeless* effect, amuse::EffectType type); +public: + EffectListing* getParent() const; + EffectWidget(amuse::EffectBaseTypeless* effect); + EffectWidget(amuse::EffectType op); + void paintEvent(QPaintEvent* event); + QString getText() const { return m_titleLabel.text(); } +}; + +class EffectWidgetContainer : public QWidget +{ + Q_OBJECT + friend class EffectListing; + EffectWidget* m_effectWidget; + QPropertyAnimation* m_animation = nullptr; + void animateOpen(); + void animateClosed(); + void snapOpen(); + void snapClosed(); +private slots: + void animationDestroyed(); +public: + EffectWidgetContainer(EffectWidget* child, QWidget* parent = Q_NULLPTR); +}; + +class EffectListing : public QWidget +{ + Q_OBJECT + friend class EffectWidget; + friend class StudioSetupWidget; + amuse::Submix* m_submix = nullptr; + QVBoxLayout* m_layout; + QLayoutItem* m_dragItem = nullptr; + int m_origIdx = -1; + int m_dragOpenIdx = -1; + EffectWidgetContainer* m_prevDragOpen = nullptr; + int m_autoscrollTimer = -1; + int m_autoscrollDelta = 0; + QWidget* m_autoscrollSource = nullptr; + QMouseEvent m_autoscrollEvent = {{}, {}, {}, {}, {}}; + void startAutoscroll(QWidget* source, QMouseEvent* event, int delta); + void stopAutoscroll(); + bool beginDrag(EffectWidget* widget); + void endDrag(); + void cancelDrag(); + void _moveDrag(int hoverIdx, const QPoint& pt, QWidget* source, QMouseEvent* event); + void moveDrag(EffectWidget* widget, const QPoint& pt, QWidget* source, QMouseEvent* event); + int moveInsertDrag(const QPoint& pt, QWidget* source, QMouseEvent* event); + void insertDragout(); + void insert(amuse::EffectType type, const QString& text); + void deleteEffect(int index); + void reindex(); + void clear(); +public: + explicit EffectListing(QWidget* parent = Q_NULLPTR); + bool loadData(amuse::Submix* submix); + void unloadData(); + void timerEvent(QTimerEvent* event); +}; + +class EffectCatalogueItem : public QWidget +{ + Q_OBJECT + amuse::EffectType m_type; + QLabel m_iconLab; + QLabel m_label; +public: + explicit EffectCatalogueItem(amuse::EffectType type, const QString& name, + const QString& doc, QWidget* parent = Q_NULLPTR); + explicit EffectCatalogueItem(const EffectCatalogueItem& other, QWidget* parent = Q_NULLPTR); + amuse::EffectType getType() const { return m_type; } + QString getText() const { return m_label.text(); } +}; + +class EffectCatalogue : public QTreeWidget +{ + Q_OBJECT +public: + explicit EffectCatalogue(QWidget* parent = Q_NULLPTR); + void mousePressEvent(QMouseEvent* event); + void mouseReleaseEvent(QMouseEvent* event); + void mouseMoveEvent(QMouseEvent* event); +}; + +class StudioSetupWidget : public QWidget +{ + Q_OBJECT + friend class EffectCatalogue; + QSplitter* m_splitter; + QTabWidget* m_tabs; + EffectListing* m_listing[2]; + EffectCatalogue* m_catalogue; + EffectWidget* m_draggedCmd = nullptr; + EffectCatalogueItem* m_draggedItem = nullptr; + QPoint m_draggedPt; + int m_dragInsertIdx = -1; + EffectListing* getCurrentListing() const; + void beginCommandDrag(EffectWidget* widget, const QPoint& eventPt, const QPoint& pt); + void beginCatalogueDrag(EffectCatalogueItem* item, const QPoint& eventPt, const QPoint& pt); +public: + explicit StudioSetupWidget(QWidget* parent = Q_NULLPTR); + bool loadData(amuse::Studio* studio); + void unloadData(); + + void mousePressEvent(QMouseEvent* event); + void mouseReleaseEvent(QMouseEvent* event); + void mouseMoveEvent(QMouseEvent* event); + void keyPressEvent(QKeyEvent* event); + + void hideEvent(QHideEvent* event); + void showEvent(QShowEvent* event); + void updateWindowPosition(); + +public slots: + void catalogueDoubleClicked(QTreeWidgetItem* item, int column); + +signals: + void hidden(); + void shown(); +}; + +#endif // AMUSE_STUDIO_SETUP_WIDGET_HPP diff --git a/Editor/resources/IconFX.svg b/Editor/resources/IconFX.svg new file mode 100644 index 0000000..c14119b --- /dev/null +++ b/Editor/resources/IconFX.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/Editor/resources/IconPaintbrush.svg b/Editor/resources/IconPaintbrush.svg index 9b377fa..115d546 100644 --- a/Editor/resources/IconPaintbrush.svg +++ b/Editor/resources/IconPaintbrush.svg @@ -25,9 +25,9 @@ borderopacity="1.0" inkscape:pageopacity="0" inkscape:pageshadow="2" - inkscape:zoom="39.33" - inkscape:cx="14.81887" - inkscape:cy="20.680873" + inkscape:zoom="119.2182" + inkscape:cx="10.254924" + inkscape:cy="16.654642" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="true" @@ -56,7 +56,7 @@ image/svg+xml - + @@ -66,38 +66,53 @@ id="layer1" transform="translate(0,-288.53332)"> + + + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccc" /> diff --git a/Editor/resources/IconVolume0.svg b/Editor/resources/IconVolume0.svg new file mode 100644 index 0000000..0a1e89f --- /dev/null +++ b/Editor/resources/IconVolume0.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/Editor/resources/IconVolume1.svg b/Editor/resources/IconVolume1.svg new file mode 100644 index 0000000..45fb416 --- /dev/null +++ b/Editor/resources/IconVolume1.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/Editor/resources/IconVolume2.svg b/Editor/resources/IconVolume2.svg new file mode 100644 index 0000000..4175dc8 --- /dev/null +++ b/Editor/resources/IconVolume2.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/Editor/resources/IconVolume3.svg b/Editor/resources/IconVolume3.svg new file mode 100644 index 0000000..3e7e03c --- /dev/null +++ b/Editor/resources/IconVolume3.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/Editor/resources/lang_de.ts b/Editor/resources/lang_de.ts index 9e4190a..a193873 100644 --- a/Editor/resources/lang_de.ts +++ b/Editor/resources/lang_de.ts @@ -99,12 +99,12 @@ AddRemoveButtons - + Add Row - + Remove Row @@ -112,7 +112,7 @@ CommandWidget - + Change %1 @@ -146,6 +146,41 @@ + + EffectCatalogue + + + + Reverb Standard + + + + + + Reverb High + + + + + + Delay + + + + + + Chorus + + + + + FXButton + + + Access studio setup window for experimenting with audio effects + + + KeymapControls @@ -283,6 +318,14 @@ + + ListingDeleteButton + + + Delete this SoundMacro + + + MIDIFileDelegate @@ -322,7 +365,6 @@ MainWindow - Amuse @@ -517,269 +559,284 @@ - + Clear Recent Projects - + Quit - - Amuse [%1/%2/%3] + + Amuse[*] - - Amuse [%1] + + %1/%2/%3[*] - Amuse - - - The directory at '%1' must not be empty. - - - - - - Directory empty + + %1[*] - Amuse - The directory at '%1' must exist for the Amuse editor. + + 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 - + 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 - + 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? @@ -950,17 +1007,17 @@ PageObjectProxyModel - + SoundMacros: - + Keymaps: - + Layers: @@ -997,188 +1054,188 @@ ProjectModel - + 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 @@ -1480,72 +1537,72 @@ SoundMacroCatalogue - + Control - + Pitch - + Sample - + Setup - + Special - + Structure - + Volume - + Commands to control the voice - + Commands to control the voice's pitch - + Commands to control the voice's sample playback - + Commands to setup the voice's mixing process - + Miscellaneous commands - + Commands to control macro branching - + Commands to control the voice's volume @@ -1558,28 +1615,20 @@ - - SoundMacroDeleteButton - - - Delete this SoundMacro - - - SoundMacroListing - + Reorder %1 - + Insert %1 - + Delete %1 @@ -1587,11 +1636,29 @@ StatusBarWidget - + Immediately kill active voices + + StudioSetupWidget + + + Studio Setup + + + + + Aux A + + + + + Aux B + + + TargetButton diff --git a/Editor/resources/resources.qrc b/Editor/resources/resources.qrc index 44b4a96..685ebb1 100644 --- a/Editor/resources/resources.qrc +++ b/Editor/resources/resources.qrc @@ -29,6 +29,11 @@ IconAdd.svg IconRemove.svg IconStop.svg + IconVolume0.svg + IconVolume1.svg + IconVolume2.svg + IconVolume3.svg + IconFX.svg FaceGrey.svg diff --git a/include/amuse/EffectBase.hpp b/include/amuse/EffectBase.hpp index fbaf44d..2f45064 100644 --- a/include/amuse/EffectBase.hpp +++ b/include/amuse/EffectBase.hpp @@ -8,11 +8,22 @@ namespace amuse { struct ChannelMap; +enum class EffectType +{ + Invalid, + ReverbStd, + ReverbHi, + Delay, + Chorus, + EffectTypeMAX +}; + class EffectBaseTypeless { public: virtual ~EffectBaseTypeless() = default; virtual void resetOutputSampleRate(double sampleRate) = 0; + virtual EffectType Isa() const = 0; }; template diff --git a/include/amuse/EffectChorus.hpp b/include/amuse/EffectChorus.hpp index 3d2cf4c..ffcd13d 100644 --- a/include/amuse/EffectChorus.hpp +++ b/include/amuse/EffectChorus.hpp @@ -46,6 +46,7 @@ public: x90_baseDelay = baseDelay; m_dirty = true; } + uint32_t getBaseDelay() const { return x90_baseDelay; } void setVariation(uint32_t variation) { @@ -53,6 +54,7 @@ public: x94_variation = variation; m_dirty = true; } + uint32_t getVariation() const { return x94_variation; } void setPeriod(uint32_t period) { @@ -60,6 +62,7 @@ public: x98_period = period; m_dirty = true; } + uint32_t getPeriod() const { return x98_period; } void updateParams(const EffectChorusInfo& info) { @@ -115,6 +118,8 @@ public: void applyEffect(T* audio, size_t frameCount, const ChannelMap& chanMap); void resetOutputSampleRate(double sampleRate) { _setup(sampleRate); } + + EffectType Isa() const { return EffectType::Chorus; } }; } diff --git a/include/amuse/EffectDelay.hpp b/include/amuse/EffectDelay.hpp index 4c05028..44838bb 100644 --- a/include/amuse/EffectDelay.hpp +++ b/include/amuse/EffectDelay.hpp @@ -69,6 +69,7 @@ public: x3c_delay[chanIdx] = delay; m_dirty = true; } + uint32_t getChanDelay(int chanIdx) const { return x3c_delay[chanIdx]; } void setFeedback(uint32_t feedback) { @@ -84,6 +85,7 @@ public: x48_feedback[chanIdx] = feedback; m_dirty = true; } + uint32_t getChanFeedback(int chanIdx) const { return x48_feedback[chanIdx]; } void setOutput(uint32_t output) { @@ -99,6 +101,7 @@ public: x54_output[chanIdx] = output; m_dirty = true; } + uint32_t getChanOutput(int chanIdx) const { return x54_output[chanIdx]; } void setParams(const EffectDelayInfo& info) { @@ -134,6 +137,8 @@ public: void applyEffect(T* audio, size_t frameCount, const ChannelMap& chanMap); void resetOutputSampleRate(double sampleRate) { _setup(sampleRate); } + + EffectType Isa() const { return EffectType::Delay; } }; } diff --git a/include/amuse/EffectReverb.hpp b/include/amuse/EffectReverb.hpp index e4cb047..7ea664b 100644 --- a/include/amuse/EffectReverb.hpp +++ b/include/amuse/EffectReverb.hpp @@ -83,30 +83,35 @@ public: x140_x1c8_coloration = clamp(0.f, coloration, 1.f); m_dirty = true; } + float getColoration() const { return x140_x1c8_coloration; } void setMix(float mix) { x144_x1cc_mix = clamp(0.f, mix, 1.f); m_dirty = true; } + float getMix() const { return x144_x1cc_mix; } void setTime(float time) { x148_x1d0_time = clamp(0.01f, time, 10.f); m_dirty = true; } + float getTime() const { return x148_x1d0_time; } void setDamping(float damping) { x14c_x1d4_damping = clamp(0.f, damping, 1.f); m_dirty = true; } + float getDamping() const { return x14c_x1d4_damping; } void setPreDelay(float preDelay) { x150_x1d8_preDelay = clamp(0.f, preDelay, 0.1f); m_dirty = true; } + float getPreDelay() const { return x150_x1d8_preDelay; } void setParams(const EffectReverbStdInfo& info) { @@ -136,6 +141,7 @@ public: x1dc_crosstalk = clamp(0.f, crosstalk, 1.f); m_dirty = true; } + float getCrosstalk() const { return x1dc_crosstalk; } void setParams(const EffectReverbHiInfo& info) { @@ -174,6 +180,8 @@ public: void applyEffect(T* audio, size_t frameCount, const ChannelMap& chanMap); void resetOutputSampleRate(double sampleRate) { _setup(sampleRate); } + + EffectType Isa() const { return EffectType::ReverbStd; } }; /** High-quality 3-stage reverb with per-channel low-pass and crosstalk */ @@ -207,6 +215,8 @@ public: void applyEffect(T* audio, size_t frameCount, const ChannelMap& chanMap); void resetOutputSampleRate(double sampleRate) { _setup(sampleRate); } + + EffectType Isa() const { return EffectType::ReverbHi; } }; } diff --git a/include/amuse/Submix.hpp b/include/amuse/Submix.hpp index 80b25c5..ade93bb 100644 --- a/include/amuse/Submix.hpp +++ b/include/amuse/Submix.hpp @@ -29,34 +29,39 @@ class Submix public: Submix(Engine& engine); - /** Add new effect to effect stack and assume ownership */ + /** Construct new effect */ template - T& makeEffect(Args... args) + std::unique_ptr _makeEffect(Args... args) { switch (m_backendSubmix->getSampleFormat()) { case SubmixFormat::Int16: { using ImpType = typename T::template ImpType; - m_effectStack.emplace_back(new ImpType(args..., m_backendSubmix->getSampleRate())); - return static_cast(*m_effectStack.back()); + return std::make_unique(args..., m_backendSubmix->getSampleRate()); } case SubmixFormat::Int32: default: { using ImpType = typename T::template ImpType; - m_effectStack.emplace_back(new ImpType(args..., m_backendSubmix->getSampleRate())); - return static_cast(*m_effectStack.back()); + return std::make_unique(args..., m_backendSubmix->getSampleRate()); } case SubmixFormat::Float: { using ImpType = typename T::template ImpType; - m_effectStack.emplace_back(new ImpType(args..., m_backendSubmix->getSampleRate())); - return static_cast(*m_effectStack.back()); + return std::make_unique(args..., m_backendSubmix->getSampleRate()); } } } + /** Add new effect to effect stack and assume ownership */ + template + T& makeEffect(Args... args) + { + m_effectStack.push_back(_makeEffect(args...)); + return static_cast&>(*m_effectStack.back()); + } + /** Add new chorus effect to effect stack and assume ownership */ EffectChorus& makeChorus(uint32_t baseDelay, uint32_t variation, uint32_t period); @@ -101,6 +106,8 @@ public: void resetOutputSampleRate(double sampleRate); Engine& getEngine() { return m_root; } + + std::vector>& getEffectStack() { return m_effectStack; } }; } diff --git a/include/amuse/Voice.hpp b/include/amuse/Voice.hpp index db0bb5d..6a49d41 100644 --- a/include/amuse/Voice.hpp +++ b/include/amuse/Voice.hpp @@ -42,6 +42,16 @@ class Voice : public Entity friend class SoundMacro::CmdTrapEvent; friend class SoundMacro::CmdUntrapEvent; friend class SoundMacro::CmdGetMessage; + friend class SoundMacro::CmdModeSelect; + + struct VolumeCache + { + bool m_curDLS = false; /**< Current DLS status of cached lookup */ + float m_curVolLUTKey = -1.f; /**< Current input level cached out of LUT */ + float m_curVolLUTVal = 0.f; /**< Current output level cached out of LUT */ + float getVolume(float vol, bool dls); + float getCachedVolume() const { return m_curVolLUTVal; } + }; void _setObjectId(ObjectId id) { m_objectId = id; } @@ -72,6 +82,8 @@ class Voice : public Entity uint64_t m_voiceSamples = 0; /**< Count of samples processed over voice's lifetime */ float m_lastLevel = 0.f; /**< Last computed level ([0,1] mapped to [-10,0] clamped decibels) */ float m_nextLevel = 0.f; /**< Next computed level used for lerp-mode amplitude */ + VolumeCache m_nextLevelCache; + VolumeCache m_lerpedCache; VoiceState m_voxState = VoiceState::Dead; /**< Current high-level state of voice */ bool m_sustained = false; /**< Sustain pedal pressed for this voice */ @@ -92,6 +104,7 @@ class Voice : public Entity int32_t m_curPitch; /**< Current base pitch in cents */ bool m_pitchDirty = true; /**< m_curPitch has been updated and needs sending to voice */ bool m_needsSlew = false; /**< next _setTotalPitch will be slewed */ + bool m_dlsVol = false; /**< Use DLS LUT for resolving voice volume */ Envelope m_volAdsr; /**< Volume envelope */ double m_envelopeTime = -1.f; /**< time since last ENVELOPE command, -1 for no active volume-sweep */ @@ -147,10 +160,13 @@ class Voice : public Entity void _macroKeyOff(); void _macroSampleEnd(); void _procSamplePre(int16_t& samp); + VolumeCache m_masterCache; template T _procSampleMaster(double time, T samp); + VolumeCache m_auxACache; template T _procSampleAuxA(double time, T samp); + VolumeCache m_auxBCache; template T _procSampleAuxB(double time, T samp); void _setTotalPitch(int32_t cents, bool slew); diff --git a/include/amuse/VolumeTable.hpp b/include/amuse/VolumeTable.hpp new file mode 100644 index 0000000..625b667 --- /dev/null +++ b/include/amuse/VolumeTable.hpp @@ -0,0 +1,10 @@ +#ifndef __AMUSE_VOLUME_TABLE_HPP__ +#define __AMUSE_VOLUME_TABLE_HPP__ + +namespace amuse +{ +float LookupVolume(float vol); +float LookupDLSVolume(float vol); +} + +#endif // __AMUSE_VOLUME_TABLE_HPP__ \ No newline at end of file diff --git a/lib/Common.cpp b/lib/Common.cpp index effc46b..34b1b08 100644 --- a/lib/Common.cpp +++ b/lib/Common.cpp @@ -80,7 +80,8 @@ void type##DNA::_write(athena::io::YAMLDocWriter& w) \ if (id.id == 0xffff) \ return; \ std::string_view name = type::CurNameDB->resolveNameFromId(id); \ - w.writeString(nullptr, name); \ + if (!name.empty()) \ + w.writeString(nullptr, name); \ } \ template \ const char* type##DNA::DNAType() \ @@ -188,17 +189,20 @@ void PageObjectIdDNA::_write(athena::io::YAMLDocWriter& w) if (id.id & 0x8000) { std::string_view name = LayersId::CurNameDB->resolveNameFromId(id); - w.writeString(nullptr, name); + if (!name.empty()) + w.writeString(nullptr, name); } else if (id.id & 0x4000) { std::string_view name = KeymapId::CurNameDB->resolveNameFromId(id); - w.writeString(nullptr, name); + if (!name.empty()) + w.writeString(nullptr, name); } else { std::string_view name = SoundMacroId::CurNameDB->resolveNameFromId(id); - w.writeString(nullptr, name); + if (!name.empty()) + w.writeString(nullptr, name); } } template diff --git a/lib/SoundMacroState.cpp b/lib/SoundMacroState.cpp index 0f9cca0..90f7369 100644 --- a/lib/SoundMacroState.cpp +++ b/lib/SoundMacroState.cpp @@ -2707,6 +2707,7 @@ const SoundMacro::CmdIntrospection SoundMacro::CmdModeSelect::Introspective = }; bool SoundMacro::CmdModeSelect::Do(SoundMacroState& st, Voice& vox) const { + vox.m_dlsVol = dlsVol; return false; } diff --git a/lib/Voice.cpp b/lib/Voice.cpp index 33bd512..5fa2a10 100644 --- a/lib/Voice.cpp +++ b/lib/Voice.cpp @@ -7,12 +7,27 @@ #include "amuse/Engine.hpp" #include "amuse/DSPCodec.hpp" #include "amuse/N64MusyXCodec.hpp" +#include "amuse/VolumeTable.hpp" #include #include namespace amuse { +float Voice::VolumeCache::getVolume(float vol, bool dls) +{ + if (vol != m_curVolLUTKey || dls != m_curDLS) + { + m_curVolLUTKey = vol; + m_curDLS = dls; + if (dls) + m_curVolLUTVal = LookupDLSVolume(vol); + else + m_curVolLUTVal = LookupVolume(vol); + } + return m_curVolLUTVal; +} + void Voice::_destroy() { Entity::_destroy(); @@ -224,7 +239,7 @@ void Voice::_procSamplePre(int16_t& samp) float l = clamp(0.f, m_lastLevel * (1.f - t) + m_nextLevel * t, 1.f); /* Apply total volume to sample using decibel scale */ - samp = ApplyVolume(l * m_engine.m_masterVolume, samp); + samp = ApplyVolume(m_lerpedCache.getVolume(l * m_engine.m_masterVolume, m_dlsVol), samp); return; } @@ -318,14 +333,14 @@ void Voice::_procSamplePre(int16_t& samp) m_nextLevel = clamp(0.f, m_nextLevel, 1.f); /* Apply total volume to sample using decibel scale */ - samp = ApplyVolume(m_nextLevel * m_engine.m_masterVolume, samp); + samp = ApplyVolume(m_nextLevelCache.getVolume(m_nextLevel * m_engine.m_masterVolume, m_dlsVol), samp); } template T Voice::_procSampleMaster(double time, T samp) { float evalVol = m_state.m_volumeSel ? (m_state.m_volumeSel.evaluate(time, *this, m_state) / 127.f) : 1.f; - return ApplyVolume(clamp(0.f, evalVol, 1.f), samp); + return ApplyVolume(m_masterCache.getVolume(clamp(0.f, evalVol, 1.f), m_dlsVol), samp); } template @@ -334,7 +349,7 @@ T Voice::_procSampleAuxA(double time, T samp) float evalVol = m_state.m_volumeSel ? (m_state.m_volumeSel.evaluate(time, *this, m_state) / 127.f) : 1.f; evalVol *= m_state.m_reverbSel ? (m_state.m_reverbSel.evaluate(time, *this, m_state) / 127.f) : m_curReverbVol; evalVol += m_state.m_preAuxASel ? (m_state.m_preAuxASel.evaluate(time, *this, m_state) / 127.f) : 0.f; - return ApplyVolume(clamp(0.f, evalVol, 1.f), samp); + return ApplyVolume(m_auxACache.getVolume(clamp(0.f, evalVol, 1.f), m_dlsVol), samp); } template @@ -343,7 +358,7 @@ T Voice::_procSampleAuxB(double time, T samp) float evalVol = m_state.m_volumeSel ? (m_state.m_volumeSel.evaluate(time, *this, m_state) / 127.f) : 1.f; evalVol *= m_state.m_postAuxB ? (m_state.m_postAuxB.evaluate(time, *this, m_state) / 127.f) : m_curAuxBVol; evalVol += m_state.m_preAuxBSel ? (m_state.m_preAuxBSel.evaluate(time, *this, m_state) / 127.f) : 0.f; - return ApplyVolume(clamp(0.f, evalVol, 1.f), samp); + return ApplyVolume(m_auxBCache.getVolume(clamp(0.f, evalVol, 1.f), m_dlsVol), samp); } uint32_t Voice::_GetBlockSampleCount(SampleFormat fmt) diff --git a/lib/VolumeTable.cpp b/lib/VolumeTable.cpp new file mode 100644 index 0000000..ad53442 --- /dev/null +++ b/lib/VolumeTable.cpp @@ -0,0 +1,295 @@ +#include "amuse/VolumeTable.hpp" +#include "amuse/Common.hpp" + +namespace amuse +{ + +static const float VolumeTable[] = +{ + 0.000000, + 0.000031, + 0.000153, + 0.000397, + 0.000702, + 0.001129, + 0.001648, + 0.002228, + 0.002930, + 0.003723, + 0.004608, + 0.005585, + 0.006653, + 0.007843, + 0.009125, + 0.010498, + 0.011963, + 0.013550, + 0.015198, + 0.016999, + 0.018860, + 0.020844, + 0.022919, + 0.025117, + 0.027406, + 0.029817, + 0.032319, + 0.034944, + 0.037660, + 0.040468, + 0.043428, + 0.046480, + 0.049623, + 0.052889, + 0.056276, + 0.059786, + 0.063387, + 0.067110, + 0.070956, + 0.074923, + 0.078982, + 0.083163, + 0.087466, + 0.091922, + 0.096469, + 0.101138, + 0.105930, + 0.110843, + 0.115879, + 0.121036, + 0.126347, + 0.131748, + 0.137303, + 0.142979, + 0.148778, + 0.154729, + 0.160772, + 0.166997, + 0.173315, + 0.179785, + 0.186407, + 0.193121, + 0.200018, + 0.207007, + 0.214179, + 0.221473, + 0.228919, + 0.236488, + 0.244209, + 0.252083, + 0.260079, + 0.268258, + 0.276559, + 0.285012, + 0.293649, + 0.302408, + 0.311319, + 0.320383, + 0.329600, + 0.339000, + 0.348521, + 0.358226, + 0.368084, + 0.378094, + 0.388287, + 0.398633, + 0.409131, + 0.419813, + 0.430647, + 0.441664, + 0.452864, + 0.464217, + 0.475753, + 0.487442, + 0.499313, + 0.511399, + 0.523606, + 0.536027, + 0.548631, + 0.561419, + 0.574389, + 0.587542, + 0.600879, + 0.614399, + 0.628132, + 0.642018, + 0.656148, + 0.670431, + 0.684927, + 0.699637, + 0.714530, + 0.729637, + 0.744926, + 0.760430, + 0.776147, + 0.792077, + 0.808191, + 0.824549, + 0.841090, + 0.857845, + 0.874844, + 0.892056, + 0.909452, + 0.927122, + 0.945006, + 0.963073, + 0.981414, + 1.000000, + 1.000000 +}; + +static const float DLSVolumeTable[] = +{ + 0.000000, + 0.000062, + 0.000248, + 0.000558, + 0.000992, + 0.001550, + 0.002232, + 0.003038, + 0.003968, + 0.005022, + 0.006200, + 0.007502, + 0.008928, + 0.010478, + 0.012152, + 0.013950, + 0.015872, + 0.017918, + 0.020088, + 0.022382, + 0.024800, + 0.027342, + 0.030008, + 0.032798, + 0.035712, + 0.038750, + 0.041912, + 0.045198, + 0.048608, + 0.052142, + 0.055800, + 0.059582, + 0.063488, + 0.067518, + 0.071672, + 0.075950, + 0.080352, + 0.084878, + 0.089528, + 0.094302, + 0.099200, + 0.104222, + 0.109368, + 0.114638, + 0.120032, + 0.125550, + 0.131192, + 0.136958, + 0.142848, + 0.148862, + 0.155000, + 0.161262, + 0.167648, + 0.174158, + 0.180792, + 0.187550, + 0.194432, + 0.201438, + 0.208568, + 0.215822, + 0.223200, + 0.230702, + 0.238328, + 0.246078, + 0.253953, + 0.261951, + 0.270073, + 0.278319, + 0.286689, + 0.295183, + 0.303801, + 0.312543, + 0.321409, + 0.330399, + 0.339513, + 0.348751, + 0.358113, + 0.367599, + 0.377209, + 0.386943, + 0.396801, + 0.406783, + 0.416889, + 0.427119, + 0.437473, + 0.447951, + 0.458553, + 0.469279, + 0.480129, + 0.491103, + 0.502201, + 0.513423, + 0.524769, + 0.536239, + 0.547833, + 0.559551, + 0.571393, + 0.583359, + 0.595449, + 0.607663, + 0.620001, + 0.632463, + 0.645049, + 0.657759, + 0.670593, + 0.683551, + 0.696633, + 0.709839, + 0.723169, + 0.736623, + 0.750202, + 0.763904, + 0.777730, + 0.791680, + 0.805754, + 0.819952, + 0.834274, + 0.848720, + 0.863290, + 0.877984, + 0.892802, + 0.907744, + 0.922810, + 0.938000, + 0.953314, + 0.968752, + 0.984314, + 1.000000, + 1.000000 +}; + +float LookupVolume(float vol) +{ + vol = amuse::clamp(0.f, vol * 127.f, 127.f); + float f = std::floor(vol); + float c = std::ceil(vol); + if (f == c) + return VolumeTable[int(f)]; + float t = vol - f; + return (1.f - t) * VolumeTable[int(f)] + t * VolumeTable[int(c)]; +} + +float LookupDLSVolume(float vol) +{ + vol = amuse::clamp(0.f, vol * 127.f, 127.f); + float f = std::floor(vol); + float c = std::ceil(vol); + if (f == c) + return DLSVolumeTable[int(f)]; + float t = vol - f; + return (1.f - t) * DLSVolumeTable[int(f)] + t * DLSVolumeTable[int(c)]; +} + +}