gearmulator

Emulation of classic VA synths of the late 90s/2000s that are based on Motorola 56300 family DSPs
Log | Files | Refs | Submodules | README | LICENSE

commit e5f823df3f198e2aaee10bfff12166d84a349ce1
parent d5bacbaec3d93560108f502c4729e12a7add70aa
Author: dsp56300 <dsp56300@users.noreply.github.com>
Date:   Sun, 20 Mar 2022 19:04:04 +0100

add ability to load skins from disk

Diffstat:
Msource/jucePlugin/PluginEditor.cpp | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Msource/jucePlugin/PluginEditor.h | 20++++++++++++++++++--
Msource/jucePlugin/ui3/VirusEditor.cpp | 50+++++++++++++++++++++++++++++++++++++++++++++++++-
Msource/jucePlugin/ui3/VirusEditor.h | 6+++++-
Msource/synthLib/os.cpp | 9++++++---
Msource/synthLib/os.h | 2++
6 files changed, 168 insertions(+), 25 deletions(-)

diff --git a/source/jucePlugin/PluginEditor.cpp b/source/jucePlugin/PluginEditor.cpp @@ -6,27 +6,39 @@ #include "ui2/VirusEditor.h" #include "ui3/VirusEditor.h" +#include "../synthLib/os.h" + //============================================================================== AudioPluginAudioProcessorEditor::AudioPluginAudioProcessorEditor(AudioPluginAudioProcessor &p) : AudioProcessorEditor(&p), processorRef(p), m_parameterBinding(p) { + m_includedSkins.push_back({"Hoverland", "VirusC_Hoverland.json", ""}); + m_includedSkins.push_back({"Trancy", "VirusC_Trancy.json", ""}); + addMouseListener(this, true); const auto config = processorRef.getController().getConfig(); const auto scale = config->getIntValue("scale", 100); - const int skinId = config->getIntValue("skin", 0); + + Skin skin = readSkinFromConfig(); + + if(skin.jsonFilename.empty()) + { + skin = m_includedSkins[0]; + } - loadSkin(skinId); + loadSkin(skin); setGuiScale(scale); } -void AudioPluginAudioProcessorEditor::loadSkin(int index) +void AudioPluginAudioProcessorEditor::loadSkin(const Skin& _skin) { - if(m_currentSkinId == index) + if(m_currentSkin == _skin) return; - m_currentSkinId = index; + m_currentSkin = _skin; + writeSkinToConfig(_skin); if (m_virusEditor) { @@ -41,26 +53,24 @@ void AudioPluginAudioProcessorEditor::loadSkin(int index) try { - auto* editor = new genericVirusUI::VirusEditor(m_parameterBinding, processorRef.getController(), processorRef, "VirusC_Trancy.json", [this] { openMenu(); }); + auto* editor = new genericVirusUI::VirusEditor(m_parameterBinding, processorRef, _skin.jsonFilename, _skin.folder, [this] { openMenu(); }); m_virusEditor.reset(editor); setSize(m_virusEditor->getWidth(), m_virusEditor->getHeight()); m_rootScale = editor->getScale(); + + m_virusEditor->setTopLeftPosition(0, 0); + addAndMakeVisible(m_virusEditor.get()); } catch(const std::runtime_error& _err) { LOG("ERROR: Failed to create editor: " << _err.what()); - auto* errorLabel = new juce::Label(); - errorLabel->setText(juce::String("Failed to load editor\n\n") + _err.what(), juce::dontSendNotification); - errorLabel->setFont(juce::Font(juce::Font::getDefaultSansSerifFontName(), 36, 0)); - errorLabel->setJustificationType(juce::Justification::centred); - errorLabel->setSize(400, 300); + juce::AlertWindow::showMessageBoxAsync(juce::AlertWindow::WarningIcon, "Skin load failed", _err.what(), "OK"); + m_virusEditor.reset(); - m_virusEditor.reset(errorLabel); + if(!_skin.folder.empty()) + loadSkin(m_includedSkins[0]); } - - m_virusEditor->setTopLeftPosition(0, 0); - addAndMakeVisible(m_virusEditor.get()); } void AudioPluginAudioProcessorEditor::setGuiScale(int percent) @@ -80,10 +90,50 @@ void AudioPluginAudioProcessorEditor::openMenu() juce::PopupMenu menu; juce::PopupMenu skinMenu; - skinMenu.addItem("Modern", true, skinId == 0,[this] {loadSkin(0);}); - skinMenu.addItem("Classic", true, skinId == 1,[this] {loadSkin(1);}); - if(m_virusEditor) + auto addSkinEntry = [this, &skinMenu](const Skin& _skin) + { + skinMenu.addItem(_skin.displayName, true, _skin == m_currentSkin,[this, _skin] {loadSkin(_skin);}); + }; + + for (const auto & skin : m_includedSkins) + addSkinEntry(skin); + + bool haveSkinsOnDisk = false; + + // find more skins on disk + const auto modulePath = synthLib::getModulePath(); + + std::vector<std::string> entries; + synthLib::getDirectoryEntries(entries, modulePath + "skins"); + + for (const auto& entry : entries) + { + std::vector<std::string> files; + synthLib::getDirectoryEntries(files, entry); + + for (const auto& file : files) + { + if(synthLib::hasExtension(file, ".json")) + { + if(!haveSkinsOnDisk) + { + haveSkinsOnDisk = true; + skinMenu.addSeparator(); + } + + const auto relativePath = entry.substr(modulePath.size()); + auto jsonName = file; + const auto pathEndPos = jsonName.find_last_of("/\\"); + if(pathEndPos != std::string::npos) + jsonName = file.substr(pathEndPos+1); + const Skin skin{jsonName + " (" + relativePath + ")", jsonName, relativePath}; + addSkinEntry(skin); + } + } + } + + if(m_virusEditor && m_currentSkin.folder.empty()) { auto* editor = dynamic_cast<genericVirusUI::VirusEditor*>(m_virusEditor.get()); if(editor) @@ -130,6 +180,26 @@ void AudioPluginAudioProcessorEditor::exportCurrentSkin() const } } +AudioPluginAudioProcessorEditor::Skin AudioPluginAudioProcessorEditor::readSkinFromConfig() const +{ + const auto* config = processorRef.getController().getConfig(); + + Skin skin; + skin.displayName = config->getValue("skinDisplayName", "").toStdString(); + skin.jsonFilename = config->getValue("skinFile", "").toStdString(); + skin.folder = config->getValue("skinFolder", "").toStdString(); + return skin; +} + +void AudioPluginAudioProcessorEditor::writeSkinToConfig(const Skin& _skin) const +{ + auto* config = processorRef.getController().getConfig(); + + config->setValue("skinDisplayName", _skin.displayName.c_str()); + config->setValue("skinFile", _skin.jsonFilename.c_str()); + config->setValue("skinFolder", _skin.folder.c_str()); +} + AudioPluginAudioProcessorEditor::~AudioPluginAudioProcessorEditor() { m_virusEditor.reset(); diff --git a/source/jucePlugin/PluginEditor.h b/source/jucePlugin/PluginEditor.h @@ -16,11 +16,25 @@ public: void mouseDown(const juce::MouseEvent& event) override; private: + struct Skin + { + std::string displayName; + std::string jsonFilename; + std::string folder; + + bool operator == (const Skin& _other) const + { + return displayName == _other.displayName && jsonFilename == _other.jsonFilename && folder == _other.folder; + } + }; + void timerCallback() override; - void loadSkin(int index); + void loadSkin(const Skin& _skin); void setGuiScale(int percent); void openMenu(); void exportCurrentSkin() const; + Skin readSkinFromConfig() const; + void writeSkinToConfig(const Skin& _skin) const; // This reference is provided as a quick way for your editor to // access the processor object that created it. @@ -29,8 +43,10 @@ private: VirusParameterBinding m_parameterBinding; std::unique_ptr<juce::Component> m_virusEditor; - int m_currentSkinId = -1; + Skin m_currentSkin; float m_rootScale = 1.0f; + std::vector<Skin> m_includedSkins; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AudioPluginAudioProcessorEditor) }; diff --git a/source/jucePlugin/ui3/VirusEditor.cpp b/source/jucePlugin/ui3/VirusEditor.cpp @@ -7,12 +7,15 @@ #include "../VirusParameterBinding.h" #include "../version.h" +#include "../synthLib/os.h" + namespace genericVirusUI { - VirusEditor::VirusEditor(VirusParameterBinding& _binding, Virus::Controller& _controller, AudioPluginAudioProcessor &_processorRef, const std::string& _jsonFilename, std::function<void()> _openMenuCallback) : + VirusEditor::VirusEditor(VirusParameterBinding& _binding, AudioPluginAudioProcessor &_processorRef, const std::string& _jsonFilename, const std::string& _skinFolder, std::function<void()> _openMenuCallback) : Editor(static_cast<EditorInterface&>(*this)), m_processor(_processorRef), m_parameterBinding(_binding), + m_skinFolder(_skinFolder), m_openMenuCallback(std::move(_openMenuCallback)) { create(_jsonFilename); @@ -144,6 +147,51 @@ namespace genericVirusUI const char* VirusEditor::getResourceByFilename(const std::string& _name, uint32_t& _dataSize) { + if(!m_skinFolder.empty()) + { + auto readFromCache = [this, &_name, &_dataSize]() + { + const auto it = m_fileCache.find(_name); + if(it == m_fileCache.end()) + { + _dataSize = 0; + return static_cast<char*>(nullptr); + } + _dataSize = static_cast<uint32_t>(it->second.size()); + return &it->second.front(); + }; + + auto* res = readFromCache(); + + if(res) + return res; + + const auto modulePath = synthLib::getModulePath(); + const auto folder = m_skinFolder.find(modulePath) == 0 ? m_skinFolder : modulePath + m_skinFolder; + + // try to load from disk first + FILE* hFile = fopen((folder + _name).c_str(), "rb"); + if(hFile) + { + fseek(hFile, 0, SEEK_END); + _dataSize = ftell(hFile); + fseek(hFile, 0, SEEK_SET); + + std::vector<char> data; + data.resize(_dataSize); + const auto readCount = fread(&data.front(), 1, _dataSize, hFile); + fclose(hFile); + + if(readCount == _dataSize) + m_fileCache.insert(std::make_pair(_name, std::move(data))); + + res = readFromCache(); + + if(res) + return res; + } + } + for(size_t i=0; i<BinaryData::namedResourceListSize; ++i) { if (BinaryData::originalFilenames[i] != _name) diff --git a/source/jucePlugin/ui3/VirusEditor.h b/source/jucePlugin/ui3/VirusEditor.h @@ -16,7 +16,7 @@ namespace genericVirusUI class VirusEditor : public genericUI::EditorInterface, public genericUI::Editor { public: - VirusEditor(VirusParameterBinding& _binding, Virus::Controller& _controller, AudioPluginAudioProcessor &_processorRef, const std::string& _jsonFilename, std::function<void()> _openMenuCallback); + VirusEditor(VirusParameterBinding& _binding, AudioPluginAudioProcessor &_processorRef, const std::string& _jsonFilename, const std::string& _skinFolder, std::function<void()> _openMenuCallback); ~VirusEditor() override; void setPart(size_t _part); @@ -56,6 +56,8 @@ namespace genericVirusUI AudioPluginAudioProcessor& m_processor; VirusParameterBinding& m_parameterBinding; + const std::string m_skinFolder; + std::unique_ptr<Parts> m_parts; std::unique_ptr<Tabs> m_tabs; std::unique_ptr<MidiPorts> m_midiPorts; @@ -77,5 +79,7 @@ namespace genericVirusUI std::unique_ptr<juce::FileChooser> m_fileChooser; juce::String m_previousPath; std::function<void()> m_openMenuCallback; + + std::map<std::string, std::vector<char>> m_fileCache; }; } diff --git a/source/synthLib/os.cpp b/source/synthLib/os.cpp @@ -179,9 +179,7 @@ namespace synthLib for (const auto& file : files) { - const std::string ext = lowercase(getExtension(file)); - - if (ext != ".bin") + if(!hasExtension(file, ".bin")) continue; if (!_minSize && !_maxSize) @@ -215,6 +213,11 @@ namespace synthLib return findROM(_expectedSize, _expectedSize); } + bool hasExtension(const std::string& _filename, const std::string& _extension) + { + return lowercase(getExtension(_filename)) == lowercase(_extension); + } + void setFlushDenormalsToZero() { #if defined(_MSC_VER) diff --git a/source/synthLib/os.h b/source/synthLib/os.h @@ -14,5 +14,7 @@ namespace synthLib std::string findROM(size_t _minSize, size_t _maxSize); std::string findROM(size_t _expectedSize = 524288); + bool hasExtension(const std::string& _filename, const std::string& _extension); + void setFlushDenormalsToZero(); } // namespace synthLib