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 fe520f8e9e7be9be0e279b24f6295fc1ae854755
parent 8db7a2163f134f21b1334b2a4e5c8553a5793544
Author: dsp56300 <dsp56300@users.noreply.github.com>
Date:   Sat, 19 Mar 2022 01:25:44 +0100

rework patch browser to be able to be put into multiple predefined containers

Diffstat:
Msource/jucePlugin/CMakeLists.txt | 2--
Msource/jucePlugin/skins/Hoverland/VirusC_Hoverland.json | 50++++++++++++++++++++++++++++++++++----------------
Msource/jucePlugin/ui3/FxPage.cpp | 2++
Msource/jucePlugin/ui3/MidiPorts.cpp | 3+++
Msource/jucePlugin/ui3/PatchBrowser.cpp | 395++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msource/jucePlugin/ui3/PatchBrowser.h | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msource/jucePlugin/ui3/VirusEditor.cpp | 2+-
Dsource/jucePlugin/ui3/VirusPatchBrowser.cpp | 380-------------------------------------------------------------------------------
Dsource/jucePlugin/ui3/VirusPatchBrowser.h | 84-------------------------------------------------------------------------------
9 files changed, 499 insertions(+), 500 deletions(-)

diff --git a/source/jucePlugin/CMakeLists.txt b/source/jucePlugin/CMakeLists.txt @@ -64,8 +64,6 @@ set(SOURCES_UI3 ui3/Tabs.h ui3/VirusEditor.cpp ui3/VirusEditor.h - ui3/VirusPatchBrowser.cpp - ui3/VirusPatchBrowser.h ) # https://forum.juce.com/t/help-needed-using-binarydata-with-cmake-juce-6/40486 diff --git a/source/jucePlugin/skins/Hoverland/VirusC_Hoverland.json b/source/jucePlugin/skins/Hoverland/VirusC_Hoverland.json @@ -98,7 +98,7 @@ "alignV" : "C", "bold" : "1", "x" : "818.6", - "y" : "95.9", + "y" : "95.90002", "width" : "440", "height" : "75" } @@ -113,7 +113,7 @@ "alignV" : "T", "bold" : "1", "x" : "1190.6", - "y" : "92.9", + "y" : "92.90002", "width" : "368", "height" : "80" } @@ -1111,7 +1111,7 @@ }, "spritesheet" : { "x" : "552.3", - "y" : "916.13", + "y" : "916.1299", "width" : "36", "height" : "36", "texture" : "Mult_36x36_x2i", @@ -1125,7 +1125,7 @@ }, "spritesheet" : { "x" : "614.38", - "y" : "916.13", + "y" : "916.1299", "width" : "36", "height" : "36", "texture" : "Mult_36x36_x2i", @@ -1207,7 +1207,7 @@ }, "spritesheet" : { "x" : "552.3", - "y" : "988.13", + "y" : "988.1299", "width" : "36", "height" : "36", "texture" : "Mult_36x36_x2i", @@ -1221,7 +1221,7 @@ }, "spritesheet" : { "x" : "614.38", - "y" : "988.13", + "y" : "988.1299", "width" : "36", "height" : "36", "texture" : "Mult_36x36_x2i", @@ -3276,7 +3276,7 @@ "normalImageOn" : "1", "downImageOn" : "0", "x" : "178", - "y" : "679.4", + "y" : "679.3999", "width" : "46", "height" : "38", "texture" : "lfo_btn_46x76_x2", @@ -5438,7 +5438,7 @@ "alignH" : "L", "alignV" : "C", "x" : "969", - "y" : "617.4", + "y" : "617.3999", "width" : "162", "height" : "36" }, @@ -5538,7 +5538,7 @@ "alignH" : "L", "alignV" : "C", "x" : "1608", - "y" : "617.4", + "y" : "617.3999", "width" : "160", "height" : "36" }, @@ -5554,7 +5554,7 @@ "alignH" : "L", "alignV" : "C", "x" : "1822", - "y" : "617.4", + "y" : "617.3999", "width" : "160", "height" : "36" }, @@ -5570,7 +5570,7 @@ "alignH" : "L", "alignV" : "C", "x" : "1608", - "y" : "716.4", + "y" : "716.3999", "width" : "160", "height" : "36" }, @@ -5586,7 +5586,7 @@ "alignH" : "L", "alignV" : "C", "x" : "1822", - "y" : "716.4", + "y" : "716.3999", "width" : "160", "height" : "36" }, @@ -5602,7 +5602,7 @@ "alignH" : "L", "alignV" : "C", "x" : "1608", - "y" : "815.4", + "y" : "815.3999", "width" : "160", "height" : "36" }, @@ -5618,7 +5618,7 @@ "alignH" : "L", "alignV" : "C", "x" : "1822", - "y" : "815.4", + "y" : "815.3999", "width" : "160", "height" : "36" }, @@ -5665,13 +5665,31 @@ }, "children" : [ { - "name" : "ContainerPresetBrowser", + "name" : "ContainerFileSelector", "component" : { "x" : "88", "y" : "96", - "width" : "1857", + "width" : "928", "height" : "1055" } + }, + { + "name" : "ContainerPatchList", + "component" : { + "x" : "1016", + "y" : "96", + "width" : "929", + "height" : "1005" + } + }, + { + "name" : "ContainerPatchListSearchBox", + "component" : { + "x" : "1016", + "y" : "1101", + "width" : "929", + "height" : "50" + } } ] }, diff --git a/source/jucePlugin/ui3/FxPage.cpp b/source/jucePlugin/ui3/FxPage.cpp @@ -2,6 +2,8 @@ #include "VirusEditor.h" +#include "../VirusController.h" + namespace genericVirusUI { FxPage::FxPage(VirusEditor& _editor) : m_editor(_editor) diff --git a/source/jucePlugin/ui3/MidiPorts.cpp b/source/jucePlugin/ui3/MidiPorts.cpp @@ -2,6 +2,9 @@ #include "VirusEditor.h" +#include "../VirusController.h" +#include "../PluginProcessor.h" + namespace genericVirusUI { MidiPorts::MidiPorts(VirusEditor& _editor) : m_editor(_editor) diff --git a/source/jucePlugin/ui3/PatchBrowser.cpp b/source/jucePlugin/ui3/PatchBrowser.cpp @@ -2,28 +2,397 @@ #include "VirusEditor.h" +#include "../../virusLib/microcontrollerTypes.h" + +#include "../VirusController.h" +#include "juce_cryptography/hashing/juce_MD5.h" + +#include "../../synthLib/midiToSysex.h" + +using namespace juce; + +const juce::Array<juce::String> ModelList = {"A","B","C","TI"}; + +const Array<String> g_categories = { "", "Lead", "Bass", "Pad", "Decay", "Pluck", + "Acid", "Classic", "Arpeggiator", "Effects", "Drums", "Percussion", + "Input", "Vocoder", "Favourite 1", "Favourite 2", "Favourite 3" }; + namespace genericVirusUI { - PatchBrowser::PatchBrowser(const VirusEditor& _editor) : m_patchBrowser(_editor.getParameterBinding(), _editor.getController()) + PatchBrowser::PatchBrowser(const VirusEditor& _editor) : m_editor(_editor), m_controller(_editor.getController()), + m_fileFilter("*.syx;*.mid;*.midi", "*", "Virus Patch Dumps"), + m_bankList(FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, File::getSpecialLocation(File::SpecialLocationType::currentApplicationFile), &m_fileFilter, nullptr), + m_search("Search Box"), + m_patchList("Patch Browser"), + m_properties(m_controller.getConfig()) + { + const auto bankDir = m_properties->getValue("virus_bank_dir", ""); + + if (bankDir.isNotEmpty() && File(bankDir).isDirectory()) + m_bankList.setRoot(bankDir); + + m_patchList.getHeader().addColumn("#", Columns::INDEX, 32); + m_patchList.getHeader().addColumn("Name", Columns::NAME, 130); + m_patchList.getHeader().addColumn("Category1", Columns::CAT1, 84); + m_patchList.getHeader().addColumn("Category2", Columns::CAT2, 84); + m_patchList.getHeader().addColumn("Arp", Columns::ARP, 32); + m_patchList.getHeader().addColumn("Uni", Columns::UNI, 32); + m_patchList.getHeader().addColumn("ST+-", Columns::ST, 32); + m_patchList.getHeader().addColumn("Ver", Columns::VER, 32); + + fitInParent(m_bankList, "ContainerFileSelector"); + fitInParent(m_patchList, "ContainerPatchList"); + + m_search.setColour(TextEditor::textColourId, Colours::white); + m_search.onTextChange = [this] + { + m_filteredPatches.clear(); + for (const auto& patch : m_patches) + { + const auto searchValue = m_search.getText(); + if (searchValue.isEmpty() || patch.name.containsIgnoreCase(searchValue)) + m_filteredPatches.add(patch); + } + m_patchList.updateContent(); + m_patchList.deselectAllRows(); + m_patchList.repaint(); + }; + m_search.setTextToShowWhenEmpty("Search...", Colours::grey); + + fitInParent(m_search, "ContainerPatchListSearchBox"); + + m_bankList.addListener(this); + m_patchList.setModel(this); + } + + void PatchBrowser::fitInParent(juce::Component& _component, const std::string& _parentName) const + { + auto* parent = m_editor.findComponent(_parentName); + + _component.setTransform(juce::AffineTransform::scale(2.0f)); + + const auto& bounds = parent->getBounds(); + const auto w = bounds.getWidth() >> 1; + const auto h = bounds.getHeight() >> 1; + + _component.setBounds(0,0, w,h); + + parent->addAndMakeVisible(_component); + } + + void PatchBrowser::selectionChanged() {} + + virusLib::VirusModel guessVersion(const uint8_t* _data) { - // We use the old patch browser for now. Fit into the desired parent - auto* pagePresets = _editor.findComponent("ContainerPresetBrowser"); + const auto v = _data[0]; + + if (v < 5) + return virusLib::A; + if (v == 6) + return virusLib::B; + if (v == 7) + return virusLib::C; + return virusLib::TI; + } + + uint32_t PatchBrowser::load(std::vector<Patch>& _result, std::set<std::string>* _dedupeChecksums, const std::vector<std::vector<uint8_t>>& _packets) + { + uint32_t count = 0; + for (const auto& packet : _packets) + { + if (load(_result, _dedupeChecksums, packet)) + ++count; + } + return count; + } + + static juce::String parseAsciiText(const std::vector<uint8_t>& msg, const int start) + { + char text[Virus::Controller::kNameLength + 1]; + text[Virus::Controller::kNameLength] = 0; // termination + for (int pos = 0; pos < Virus::Controller::kNameLength; ++pos) + text[pos] = static_cast<char>(msg[start + pos]); + return {text}; + } + + + bool PatchBrowser::load(std::vector<Patch>& _result, std::set<std::string>* _dedupeChecksums, const std::vector<uint8_t>& _data) + { + if (_data.size() < 267) + return false; + + auto* it = &_data.front(); + + if (*it == (uint8_t)0xf0 + && *(it + 1) == (uint8_t)0x00 + && *(it + 2) == (uint8_t)0x20 + && *(it + 3) == (uint8_t)0x33 + && *(it + 4) == (uint8_t)0x01 + && *(it + 6) == (uint8_t)virusLib::DUMP_SINGLE) + { + Patch patch; + patch.progNumber = static_cast<int>(_result.size()); + patch.sysex = _data; + patch.data.insert(patch.data.begin(), _data.begin() + 9, _data.end()); + patch.name = parseAsciiText(patch.data, 128 + 112); + patch.category1 = patch.data[251]; + patch.category2 = patch.data[252]; + patch.unison = patch.data[97]; + patch.transpose = patch.data[93]; + patch.model = guessVersion(&patch.data[0]); + + if (!_dedupeChecksums) + { + _result.push_back(patch); + } + else + { + const auto md5 = std::string(MD5(it + 9 + 17, 256 - 17 - 3).toHexString().toRawUTF8()); + + if (_dedupeChecksums->find(md5) == _dedupeChecksums->end()) + { + _dedupeChecksums->insert(md5); + _result.push_back(patch); + } + } + + return true; + } + return false; + } + + uint32_t PatchBrowser::loadBankFile(std::vector<Patch>& _result, std::set<std::string>* _dedupeChecksums, const File& file) + { + const auto ext = file.getFileExtension().toLowerCase(); + const auto path = file.getParentDirectory().getFullPathName(); + + if (ext == ".syx") + { + MemoryBlock data; + + if (!file.loadFileAsData(data)) + return 0; + + std::vector<uint8_t> d; + d.resize(data.getSize()); + memcpy(&d[0], data.getData(), data.getSize()); - const auto parentBounds = pagePresets->getBounds(); + std::vector<std::vector<uint8_t>> packets; + splitMultipleSysex(packets, d); - const auto w = parentBounds.getWidth() >> 1; - const auto h = parentBounds.getHeight() >> 1; + return load(_result, _dedupeChecksums, packets); + } - m_patchBrowser.setBounds(0,0,w,h); + if (ext == ".mid" || ext == ".midi") + { + std::vector<uint8_t> data; + + synthLib::MidiToSysex::readFile(data, file.getFullPathName().getCharPointer()); + + if (data.empty()) + return 0; + + std::vector<std::vector<uint8_t>> packets; + splitMultipleSysex(packets, data); + + return load(_result, _dedupeChecksums, packets); + } + return 0; + } + + void PatchBrowser::fileClicked(const File& file, const MouseEvent& e) + { + const auto ext = file.getFileExtension().toLowerCase(); + const auto path = file.getParentDirectory().getFullPathName(); + if (file.isDirectory() && e.mods.isRightButtonDown()) + { + auto p = PopupMenu(); + p.addItem("Add directory contents to patch list", [this, file]() + { + m_patches.clear(); + m_checksums.clear(); + std::set<std::string> dedupeChecksums; + + std::vector<Patch> patches; + + for (const auto& f : RangedDirectoryIterator(file, false, "*.syx;*.mid;*.midi", File::findFiles)) + loadBankFile(patches, &dedupeChecksums, f.getFile()); + + m_filteredPatches.clear(); + + for (const auto& patch : patches) + { + const auto searchValue = m_search.getText(); + + m_patches.add(patch); + + if (searchValue.isEmpty() || patch.name.containsIgnoreCase(searchValue)) + m_filteredPatches.add(patch); + } + m_patchList.updateContent(); + m_patchList.deselectAllRows(); + m_patchList.repaint(); + }); + p.showMenuAsync(PopupMenu::Options()); + + return; + } + m_properties->setValue("virus_bank_dir", path); + + if (file.existsAsFile() && ext == ".syx" || ext == ".midi" || ext == ".mid") + { + m_patches.clear(); + std::vector<Patch> patches; + loadBankFile(patches, nullptr, file); + m_filteredPatches.clear(); + for (const auto& patch : patches) + { + const auto searchValue = m_search.getText(); + m_patches.add(patch); + if (searchValue.isEmpty() || patch.name.containsIgnoreCase(searchValue)) + m_filteredPatches.add(patch); + } + m_patchList.updateContent(); + m_patchList.deselectAllRows(); + m_patchList.repaint(); + } + } + + int PatchBrowser::getNumRows() { return m_filteredPatches.size(); } + + void PatchBrowser::paintRowBackground(Graphics& g, int rowNumber, int width, int height, bool rowIsSelected) + { + const auto alternateColour = m_patchList.getLookAndFeel() + .findColour(ListBox::backgroundColourId) + .interpolatedWith(m_patchList.getLookAndFeel().findColour(ListBox::textColourId), 0.03f); + if (rowIsSelected) + g.fillAll(Colours::lightblue); + else if (rowNumber & 1) + g.fillAll(alternateColour); + } + + void PatchBrowser::paintCell(Graphics& g, int rowNumber, int columnId, int width, int height, bool rowIsSelected) { + g.setColour(rowIsSelected ? Colours::darkblue + : m_patchList.getLookAndFeel().findColour(ListBox::textColourId)); // [5] + + if (rowNumber >= getNumRows()) + return; // Juce what are you up to? + + const auto rowElement = m_filteredPatches[rowNumber]; + //auto text = rowElement.name; + String text = ""; + if (columnId == Columns::INDEX) + text = String(rowElement.progNumber); + else if (columnId == Columns::NAME) + text = rowElement.name; + else if (columnId == Columns::CAT1) + text = g_categories[rowElement.category1]; + else if (columnId == Columns::CAT2) + text = g_categories[rowElement.category2]; + else if (columnId == Columns::ARP) + text = rowElement.data[129] != 0 ? "Y" : " "; + else if (columnId == Columns::UNI) + text = rowElement.unison == 0 ? " " : String(rowElement.unison + 1); + else if (columnId == Columns::ST) + text = rowElement.transpose != 64 ? String(rowElement.transpose - 64) : " "; + else if (columnId == Columns::VER) { + if (rowElement.model < ModelList.size()) + text = ModelList[rowElement.model]; + } + g.drawText(text, 2, 0, width - 4, height, Justification::centredLeft, true); // [6] + g.setColour(m_patchList.getLookAndFeel().findColour(ListBox::backgroundColourId)); + g.fillRect(width - 1, 0, 1, height); // [7] + } + + void PatchBrowser::selectedRowsChanged(int lastRowSelected) + { + const auto idx = m_patchList.getSelectedRow(); + + if (idx == -1) + return; + + // force to edit buffer + const auto part = m_controller.isMultiMode() ? m_controller.getCurrentPart() : static_cast<uint8_t>(virusLib::ProgramType::SINGLE); + + auto sysex = m_filteredPatches[idx].sysex; + sysex[7] = toMidiByte(virusLib::BankNumber::EditBuffer); + sysex[8] = part; + + m_controller.sendSysEx(sysex); + + m_controller.sendSysEx(m_controller.constructMessage({ virusLib::REQUEST_SINGLE, 0x0, part })); + } + + void PatchBrowser::cellDoubleClicked(int rowNumber, int columnId, const MouseEvent&) + { + if (rowNumber == m_patchList.getSelectedRow()) + selectedRowsChanged(0); + } + + class PatchBrowser::PatchBrowserSorter + { + public: + PatchBrowserSorter(const int attributeToSortBy, const bool forwards) + : m_attributeToSort(attributeToSortBy), + m_direction(forwards ? 1 : -1) + {} + + int compareElements(const Patch& first, const Patch& second) const + { + if (m_attributeToSort == Columns::INDEX) + return m_direction * (first.progNumber - second.progNumber); + if (m_attributeToSort == Columns::NAME) + return m_direction * first.name.compareIgnoreCase(second.name); + if (m_attributeToSort == Columns::CAT1) + return m_direction * (first.category1 - second.category1); + if (m_attributeToSort == Columns::CAT2) + return m_direction * (first.category2 - second.category2); + if (m_attributeToSort == Columns::ARP) + return m_direction * (first.data[129] - second.data[129]); + if (m_attributeToSort == Columns::UNI) + return m_direction * (first.unison - second.unison); + if (m_attributeToSort == Columns::VER) + return m_direction * (first.model - second.model); + if (m_attributeToSort == Columns::ST) + return m_direction * (first.transpose - second.transpose); + return m_direction * (first.progNumber - second.progNumber); + } + + private: + const int m_attributeToSort; + const int m_direction; + }; + + void PatchBrowser::sortOrderChanged(int newSortColumnId, bool isForwards) + { + if (newSortColumnId != 0) + { + PatchBrowserSorter sorter(newSortColumnId, isForwards); + m_filteredPatches.sort(sorter); + m_patchList.updateContent(); + } + } + + void PatchBrowser::splitMultipleSysex(std::vector<std::vector<uint8_t>>& _dst, const std::vector<uint8_t>& _src) + { + for (size_t i = 0; i < _src.size(); ++i) + { + if (_src[i] != 0xf0) + continue; - const auto searchY = h - m_patchBrowser.getSearchBox().getHeight() - 5; + for (size_t j = i + 1; j < _src.size(); ++j) + { + if (_src[j] != 0xf7) + continue; - m_patchBrowser.getBankList().setBounds(0, 0, w>>1, h); - m_patchBrowser.getPatchList().setBounds(w>>1, 0, w>>1, searchY); - m_patchBrowser.getSearchBox().setBounds(w>>1, searchY, w>>1, m_patchBrowser.getSearchBox().getHeight()); + std::vector<uint8_t> entry; + entry.insert(entry.begin(), _src.begin() + i, _src.begin() + j + 1); - pagePresets->addAndMakeVisible(&m_patchBrowser); + _dst.emplace_back(entry); - m_patchBrowser.setTransform(juce::AffineTransform::scale(2.0f)); + i = j; + break; + } + } } } diff --git a/source/jucePlugin/ui3/PatchBrowser.h b/source/jucePlugin/ui3/PatchBrowser.h @@ -1,16 +1,89 @@ #pragma once -#include "VirusPatchBrowser.h" +#include <set> + +#include <juce_audio_processors/juce_audio_processors.h> + +namespace Virus +{ + class Controller; +} + +namespace virusLib +{ + enum VirusModel : uint8_t; +} namespace genericVirusUI { class VirusEditor; - class PatchBrowser + struct Patch + { + int progNumber = 0; + juce::String name; + uint8_t category1 = 0; + uint8_t category2 = 0; + std::vector<uint8_t> data; + std::vector<uint8_t> sysex; + virusLib::VirusModel model; + uint8_t unison = 0; + uint8_t transpose = 0; + }; + + class PatchBrowser : public juce::FileBrowserListener, juce::TableListBoxModel { public: explicit PatchBrowser(const VirusEditor& _editor); + + static uint32_t load(std::vector<Patch>& _result, std::set<std::string>* _dedupeChecksums, const std::vector<std::vector<uint8_t>>& _packets); + static bool load(std::vector<Patch>& _result, std::set<std::string>* _dedupeChecksums, const std::vector<uint8_t>& _data); + static uint32_t loadBankFile(std::vector<Patch>& _result, std::set<std::string>* _dedupeChecksums, const juce::File& file); + + static void splitMultipleSysex(std::vector<std::vector<uint8_t>>& _dst, const std::vector<uint8_t>& _src); + private: - ::PatchBrowser m_patchBrowser; - }; + juce::FileBrowserComponent& getBankList() {return m_bankList; } + juce::TableListBox& getPatchList() {return m_patchList; } + juce::TextEditor& getSearchBox() {return m_search; } + + void fitInParent(juce::Component& _component, const std::string& _parentName) const; + + // Inherited via FileBrowserListener + void selectionChanged() override; + void fileClicked(const juce::File &file, const juce::MouseEvent &e) override; + void fileDoubleClicked(const juce::File &file) override {} + void browserRootChanged(const juce::File &newRoot) override {} + + // Inherited via TableListBoxModel + int getNumRows() override; + void paintRowBackground(juce::Graphics &, int rowNumber, int width, int height, bool rowIsSelected) override; + void paintCell(juce::Graphics &, int rowNumber, int columnId, int width, int height, bool rowIsSelected) override; + void selectedRowsChanged(int lastRowSelected) override; + void cellDoubleClicked (int rowNumber, int columnId, const juce::MouseEvent &) override; + void sortOrderChanged(int newSortColumnId, bool isForwards) override; + class PatchBrowserSorter; + enum Columns + { + INDEX = 1, + NAME = 2, + CAT1 = 3, + CAT2 = 4, + ARP = 5, + UNI = 6, + ST = 7, + VER = 8, + }; + + const VirusEditor& m_editor; + Virus::Controller& m_controller; + juce::WildcardFileFilter m_fileFilter; + juce::FileBrowserComponent m_bankList; + juce::TextEditor m_search; + juce::TableListBox m_patchList; + juce::Array<Patch> m_patches; + juce::Array<Patch> m_filteredPatches; + juce::PropertiesFile *m_properties; + juce::HashMap<juce::String, bool> m_checksums; + }; } diff --git a/source/jucePlugin/ui3/VirusEditor.cpp b/source/jucePlugin/ui3/VirusEditor.cpp @@ -272,7 +272,7 @@ namespace genericVirusUI const auto ext = result.getFileExtension().toLowerCase(); std::vector<Patch> patches; - ::PatchBrowser::loadBankFile(patches, nullptr, result); + PatchBrowser::loadBankFile(patches, nullptr, result); if (patches.empty()) return; diff --git a/source/jucePlugin/ui3/VirusPatchBrowser.cpp b/source/jucePlugin/ui3/VirusPatchBrowser.cpp @@ -1,380 +0,0 @@ -#include "VirusPatchBrowser.h" - -#include "../VirusParameterBinding.h" - -#include <juce_gui_extra/juce_gui_extra.h> -#include <juce_cryptography/juce_cryptography.h> - -#include "../../synthLib/midiToSysex.h" - -using namespace juce; -using namespace virusLib; - -const Array<String> g_categories = {"", "Lead", "Bass", "Pad", "Decay", "Pluck", - "Acid", "Classic", "Arpeggiator", "Effects", "Drums", "Percussion", - "Input", "Vocoder", "Favourite 1", "Favourite 2", "Favourite 3"}; - -PatchBrowser::PatchBrowser(VirusParameterBinding & _parameterBinding, Virus::Controller& _controller) : - m_parameterBinding(_parameterBinding), - m_controller(_controller), - m_fileFilter("*.syx;*.mid;*.midi", "*", "Virus Patch Dumps"), - m_bankList(FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, File::getSpecialLocation(File::SpecialLocationType::currentApplicationFile), &m_fileFilter, nullptr), - m_search("Search Box"), - m_patchList("Patch Browser"), - m_properties(m_controller.getConfig()) -{ - const auto bankDir = m_properties->getValue("virus_bank_dir", ""); - if (bankDir != "" && File(bankDir).isDirectory()) - { - m_bankList.setRoot(bankDir); - } - - setBounds(22, 30, 1000, 600); - m_bankList.setBounds(16, 28, 480, 540); - - m_patchList.setBounds(m_bankList.getBounds().translated(m_bankList.getWidth(), 0)); - - m_patchList.getHeader().addColumn("#", Columns::INDEX, 32); - m_patchList.getHeader().addColumn("Name", Columns::NAME, 130); - m_patchList.getHeader().addColumn("Category1", Columns::CAT1, 84); - m_patchList.getHeader().addColumn("Category2", Columns::CAT2, 84); - m_patchList.getHeader().addColumn("Arp", Columns::ARP, 32); - m_patchList.getHeader().addColumn("Uni", Columns::UNI, 32); - m_patchList.getHeader().addColumn("ST+-", Columns::ST, 32); - m_patchList.getHeader().addColumn("Ver", Columns::VER, 32); - addAndMakeVisible(m_bankList); - addAndMakeVisible(m_patchList); - - m_search.setSize(m_patchList.getWidth(), 20); - m_search.setColour(TextEditor::textColourId, Colours::white); - m_search.setTopLeftPosition(m_patchList.getBounds().getBottomLeft().translated(0, 8)); - m_search.onTextChange = [this] - { - m_filteredPatches.clear(); - for(const auto& patch : m_patches) - { - const auto searchValue = m_search.getText(); - if (searchValue.isEmpty()) - m_filteredPatches.add(patch); - else if(patch.name.containsIgnoreCase(searchValue)) - m_filteredPatches.add(patch); - } - m_patchList.updateContent(); - m_patchList.deselectAllRows(); - m_patchList.repaint(); - }; - m_search.setTextToShowWhenEmpty("search...", Colours::grey); - addAndMakeVisible(m_search); - m_bankList.addListener(this); - m_patchList.setModel(this); -} - -void PatchBrowser::selectionChanged() {} - -VirusModel guessVersion(const uint8_t* _data) -{ - const auto v = _data[0]; - - if (v < 5) - return VirusModel::A; - if (v == 6) - return VirusModel::B; - if (v == 7) - return VirusModel::C; - return VirusModel::TI; -} - -uint32_t PatchBrowser::load(std::vector<Patch>& _result, std::set<std::string>* _dedupeChecksums, const std::vector<std::vector<uint8_t>>& _packets) -{ - uint32_t count = 0; - for (const auto& packet : _packets) - { - if (load(_result, _dedupeChecksums, packet)) - ++count; - } - return count; -} - -bool PatchBrowser::load(std::vector<Patch>& _result, std::set<std::string>* _dedupeChecksums, const std::vector<uint8_t>& _data) -{ - if (_data.size() < 267) - return false; - - auto* it = &_data.front(); - - if (*it == (uint8_t)0xf0 - && *(it + 1) == (uint8_t)0x00 - && *(it + 2) == (uint8_t)0x20 - && *(it + 3) == (uint8_t)0x33 - && *(it + 4) == (uint8_t)0x01 - && *(it + 6) == (uint8_t)virusLib::DUMP_SINGLE) - { - Patch patch; - patch.progNumber = static_cast<int>(_result.size()); - patch.sysex = _data; - patch.data.insert(patch.data.begin(), _data.begin() + 9, _data.end()); - patch.name = parseAsciiText(patch.data, 128 + 112); - patch.category1 = patch.data[251]; - patch.category2 = patch.data[252]; - patch.unison = patch.data[97]; - patch.transpose = patch.data[93]; - if ((uint8_t)*(it + 266) != (uint8_t)0xf7 && (uint8_t)*(it + 266) != (uint8_t)0xf8) { - patch.model = VirusModel::TI; - } - else { - patch.model = guessVersion(&patch.data[0]); - } - - if(!_dedupeChecksums) - { - _result.push_back(patch); - } - else - { - const auto md5 = std::string(MD5(it + 9 + 17, 256 - 17 - 3).toHexString().toRawUTF8()); - - if (_dedupeChecksums->find(md5) == _dedupeChecksums->end()) - { - _dedupeChecksums->insert(md5); - _result.push_back(patch); - } - } - - return true; - } - return false; -} - -uint32_t PatchBrowser::loadBankFile(std::vector<Patch>& _result, std::set<std::string>* _dedupeChecksums, const File& file) -{ - const auto ext = file.getFileExtension().toLowerCase(); - const auto path = file.getParentDirectory().getFullPathName(); - - if (ext == ".syx") - { - MemoryBlock data; - - if (!file.loadFileAsData(data)) - return 0; - - std::vector<uint8_t> d; - d.resize(data.getSize()); - memcpy(&d[0], data.getData(), data.getSize()); - - std::vector<std::vector<uint8_t>> packets; - splitMultipleSysex(packets, d); - - return load(_result, _dedupeChecksums, packets); - } - - if (ext == ".mid" || ext == ".midi") - { - std::vector<uint8_t> data; - - synthLib::MidiToSysex::readFile(data, file.getFullPathName().getCharPointer()); - - if(data.empty()) - return 0; - - std::vector<std::vector<uint8_t>> packets; - splitMultipleSysex(packets, data); - - return load(_result, _dedupeChecksums, packets); - } - return 0; -} - -void PatchBrowser::fileClicked(const File &file, const MouseEvent &e) -{ - const auto ext = file.getFileExtension().toLowerCase(); - const auto path = file.getParentDirectory().getFullPathName(); - if (file.isDirectory() && e.mods.isRightButtonDown()) - { - auto p = PopupMenu(); - p.addItem("Add directory contents to patch list", [this, file]() - { - m_patches.clear(); - m_checksums.clear(); - std::set<std::string> dedupeChecksums; - - std::vector<Patch> patches; - - for (const auto& f : RangedDirectoryIterator(file, false, "*.syx;*.mid;*.midi", File::findFiles)) - loadBankFile(patches, &dedupeChecksums, f.getFile()); - - m_filteredPatches.clear(); - - for(const auto& patch : patches) - { - const auto searchValue = m_search.getText(); - - m_patches.add(patch); - - if (searchValue.isEmpty() || patch.name.containsIgnoreCase(searchValue)) - m_filteredPatches.add(patch); - } - m_patchList.updateContent(); - m_patchList.deselectAllRows(); - m_patchList.repaint(); - }); - p.showMenuAsync(PopupMenu::Options()); - - return; - } - m_properties->setValue("virus_bank_dir", path); - - if(file.existsAsFile() && ext == ".syx" || ext == ".midi" || ext == ".mid") - { - m_patches.clear(); - std::vector<Patch> patches; - loadBankFile(patches, nullptr, file); - m_filteredPatches.clear(); - for(const auto& patch : patches) - { - const auto searchValue = m_search.getText(); - m_patches.add(patch); - if (searchValue.isEmpty() || patch.name.containsIgnoreCase(searchValue)) - m_filteredPatches.add(patch); - } - m_patchList.updateContent(); - m_patchList.deselectAllRows(); - m_patchList.repaint(); - } -} - -int PatchBrowser::getNumRows() { return m_filteredPatches.size(); } - -void PatchBrowser::paintRowBackground(Graphics &g, int rowNumber, int width, int height, bool rowIsSelected) { - const auto alternateColour = getLookAndFeel() - .findColour(ListBox::backgroundColourId) - .interpolatedWith(getLookAndFeel().findColour(ListBox::textColourId), 0.03f); - if (rowIsSelected) - g.fillAll(Colours::lightblue); - else if (rowNumber & 1) - g.fillAll(alternateColour); -} - -void PatchBrowser::paintCell(Graphics &g, int rowNumber, int columnId, int width, int height, bool rowIsSelected) { - g.setColour(rowIsSelected ? Colours::darkblue - : getLookAndFeel().findColour(ListBox::textColourId)); // [5] - - if (rowNumber >= getNumRows()) - return; // Juce what are you up to? - - const auto rowElement = m_filteredPatches[rowNumber]; - //auto text = rowElement.name; - String text = ""; - if (columnId == Columns::INDEX) - text = String(rowElement.progNumber); - else if (columnId == Columns::NAME) - text = rowElement.name; - else if (columnId == Columns::CAT1) - text = g_categories[rowElement.category1]; - else if (columnId == Columns::CAT2) - text = g_categories[rowElement.category2]; - else if (columnId == Columns::ARP) - text = rowElement.data[129] != 0 ? "Y" : " "; - else if(columnId == Columns::UNI) - text = rowElement.unison == 0 ? " " : String(rowElement.unison+1); - else if(columnId == Columns::ST) - text = rowElement.transpose != 64 ? String(rowElement.transpose - 64) : " "; - else if (columnId == Columns::VER) { - if(rowElement.model < ModelList.size()) - text = ModelList[rowElement.model]; - } - g.drawText(text, 2, 0, width - 4, height, Justification::centredLeft, true); // [6] - g.setColour(getLookAndFeel().findColour(ListBox::backgroundColourId)); - g.fillRect(width - 1, 0, 1, height); // [7] -} - -void PatchBrowser::selectedRowsChanged(int lastRowSelected) -{ - const auto idx = m_patchList.getSelectedRow(); - - if (idx == -1) - return; - - // force to edit buffer - const auto part = m_controller.isMultiMode() ? m_controller.getCurrentPart() : static_cast<uint8_t>(virusLib::ProgramType::SINGLE); - - auto sysex = m_filteredPatches[idx].sysex; - sysex[7] = toMidiByte(virusLib::BankNumber::EditBuffer); - sysex[8] = part; - - m_controller.sendSysEx(sysex); - - m_controller.sendSysEx(m_controller.constructMessage({ virusLib::REQUEST_SINGLE, 0x0, part })); -} - -void PatchBrowser::cellDoubleClicked(int rowNumber, int columnId, const MouseEvent &) -{ - if(rowNumber == m_patchList.getSelectedRow()) - selectedRowsChanged(0); -} - -class PatchBrowser::PatchBrowserSorter -{ -public: - PatchBrowserSorter (const int attributeToSortBy, const bool forwards) - : attributeToSort (attributeToSortBy), - direction (forwards ? 1 : -1) - {} - - int compareElements (const Patch& first, const Patch& second) const - { - if(attributeToSort == Columns::INDEX) - return direction * (first.progNumber - second.progNumber); - if (attributeToSort == Columns::NAME) - return direction * first.name.compareIgnoreCase(second.name); - if (attributeToSort == Columns::CAT1) - return direction * (first.category1 - second.category1); - if (attributeToSort == Columns::CAT2) - return direction * (first.category2 - second.category2); - if (attributeToSort == Columns::ARP) - return direction * (first.data[129]- second.data[129]); - if (attributeToSort == Columns::UNI) - return direction * (first.unison - second.unison); - if (attributeToSort == Columns::VER) - return direction * (first.model - second.model); - if (attributeToSort == Columns::ST) - return direction * (first.transpose - second.transpose); - return direction * (first.progNumber - second.progNumber); - } - -private: - const int attributeToSort; - const int direction; -}; - -void PatchBrowser::sortOrderChanged(int newSortColumnId, bool isForwards) -{ - if (newSortColumnId != 0) - { - PatchBrowserSorter sorter (newSortColumnId, isForwards); - m_filteredPatches.sort(sorter); - m_patchList.updateContent(); - } -} - -void PatchBrowser::splitMultipleSysex(std::vector<std::vector<uint8_t>>& _dst, const std::vector<uint8_t>& _src) -{ - for(size_t i=0; i<_src.size(); ++i) - { - if(_src[i] != 0xf0) - continue; - - for(size_t j=i+1; j < _src.size(); ++j) - { - if(_src[j] != 0xf7) - continue; - - std::vector<uint8_t> entry; - entry.insert(entry.begin(), _src.begin() + i, _src.begin() + j + 1); - - _dst.emplace_back(entry); - - i = j; - break; - } - } -} diff --git a/source/jucePlugin/ui3/VirusPatchBrowser.h b/source/jucePlugin/ui3/VirusPatchBrowser.h @@ -1,84 +0,0 @@ -#pragma once - -#include "../PluginProcessor.h" -#include <juce_gui_extra/juce_gui_extra.h> -#include "../VirusController.h" -class VirusParameterBinding; - -const juce::Array<juce::String> ModelList = {"A","B","C","TI"}; -struct Patch -{ - int progNumber; - juce::String name; - uint8_t category1; - uint8_t category2; - std::vector<uint8_t> data; - std::vector<uint8_t> sysex; - virusLib::VirusModel model; - uint8_t unison; - uint8_t transpose; -}; - -class PatchBrowser : public juce::Component, juce::FileBrowserListener, juce::TableListBoxModel -{ - -public: - PatchBrowser(VirusParameterBinding &_parameterBinding, Virus::Controller& _controller); - - static uint32_t load(std::vector<Patch>& _result, std::set<std::string>* _dedupeChecksums, const std::vector<std::vector<uint8_t>>& _packets); - static bool load(std::vector<Patch>& _result, std::set<std::string>* _dedupeChecksums, const std::vector<uint8_t>& _data); - static uint32_t loadBankFile(std::vector<Patch>& _result, std::set<std::string>* _dedupeChecksums, const juce::File& file); - - static void splitMultipleSysex(std::vector<std::vector<uint8_t>>& _dst, const std::vector<uint8_t>& _src); - - juce::FileBrowserComponent& getBankList() {return m_bankList; } - juce::TableListBox& getPatchList() {return m_patchList; } - juce::TextEditor& getSearchBox() {return m_search; } - -private: - VirusParameterBinding &m_parameterBinding; - Virus::Controller& m_controller; - template <typename T> static juce::String parseAsciiText(const T &msg, const int start) - { - char text[Virus::Controller::kNameLength + 1]; - text[Virus::Controller::kNameLength] = 0; // termination - for (auto pos = 0; pos < Virus::Controller::kNameLength; ++pos) - text[pos] = msg[start + pos]; - return juce::String(text); - } - juce::WildcardFileFilter m_fileFilter; - juce::FileBrowserComponent m_bankList; - juce::TextEditor m_search; - juce::TableListBox m_patchList; - juce::Array<Patch> m_patches; - juce::Array<Patch> m_filteredPatches; - juce::PropertiesFile *m_properties; - juce::HashMap<juce::String, bool> m_checksums; - // Inherited via FileBrowserListener - void selectionChanged() override; - void fileClicked(const juce::File &file, const juce::MouseEvent &e) override; - void fileDoubleClicked(const juce::File &file) override {} - void browserRootChanged(const juce::File &newRoot) override {} - - // Inherited via TableListBoxModel - virtual int getNumRows() override; - virtual void paintRowBackground(juce::Graphics &, int rowNumber, int width, int height, - bool rowIsSelected) override; - virtual void paintCell(juce::Graphics &, int rowNumber, int columnId, int width, int height, - bool rowIsSelected) override; - - virtual void selectedRowsChanged(int lastRowSelected) override; - virtual void cellDoubleClicked (int rowNumber, int columnId, const juce::MouseEvent &) override; - void sortOrderChanged(int newSortColumnId, bool isForwards) override; - class PatchBrowserSorter; - enum Columns { - INDEX = 1, - NAME = 2, - CAT1 = 3, - CAT2 = 4, - ARP = 5, - UNI = 6, - ST = 7, - VER = 8, - }; -};