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 86dbbba430c784f80664acc3b19233239e25c98e
parent 69a035101fada3d50b2176d3156d521477a5f046
Author: 790 <790@users.noreply.github.com>
Date:   Wed, 12 Jan 2022 01:04:06 +0000

move parts to separate component. sync clock to host

Diffstat:
Msource/jucePlugin/PluginProcessor.cpp | 11++++++++++-
Msource/jucePlugin/VirusController.cpp | 6++++--
Msource/jucePlugin/VirusParameterBinding.cpp | 2+-
Msource/jucePlugin/ui/VirusEditor.cpp | 164+++++++++++--------------------------------------------------------------------
Msource/jucePlugin/ui/VirusEditor.h | 20+++++++-------------
Msource/jucePlugin/ui/Virus_ArpEditor.cpp | 2+-
Msource/jucePlugin/ui/Virus_Parts.cpp | 171++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msource/jucePlugin/ui/Virus_Parts.h | 27+++++++++++++++++++++------
Msource/jucePlugin/ui/Virus_PatchBrowser.cpp | 132++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msource/jucePlugin/ui/Virus_PatchBrowser.h | 38+++++++++++++++++++++++++++++---------
Msource/virusLib/microcontroller.cpp | 7+++++--
Msource/virusLib/microcontrollerTypes.h | 2++
12 files changed, 378 insertions(+), 204 deletions(-)

diff --git a/source/jucePlugin/PluginProcessor.cpp b/source/jucePlugin/PluginProcessor.cpp @@ -191,9 +191,18 @@ void AudioPluginAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::AudioPlayHead::CurrentPositionInfo pos{}; auto* playHead = getPlayHead(); - if(playHead) + if(playHead) { playHead->getCurrentPosition(pos); + if(pos.bpm > 0) { // sync virus interal clock to host + const uint8_t bpmValue = juce::jmin(127, juce::jmax(0, (int)pos.bpm-63)); // clamp to virus range, 63-190 + auto clockParam = getController().getParameter(Virus::Param_ClockTempo, 0); + if (clockParam != nullptr && (int)clockParam->getValueObject().getValue() != bpmValue) { + clockParam->getValueObject().setValue(bpmValue); + } + } + } + m_plugin.process(inputs, outputs, buffer.getNumSamples(), static_cast<float>(pos.bpm), static_cast<float>(pos.ppqPosition), pos.isPlaying); diff --git a/source/jucePlugin/VirusController.cpp b/source/jucePlugin/VirusController.cpp @@ -27,7 +27,6 @@ namespace Virus for(uint8_t i=3; i<=8; ++i) sendSysEx(constructMessage({MessageType::REQUEST_BANK_SINGLE, i})); - startTimer(5); } @@ -377,6 +376,9 @@ namespace Virus p->setValueFromSynth(patch.data[virusLib::MD_PART_MIDI_CHANNEL + (i*16) + pt], true); } } + if (auto* p = findSynthParam(pt, virusLib::PAGE_B, virusLib::CLOCK_TEMPO)) { + p->setValueFromSynth(patch.data[virusLib::MD_CLOCK_TEMPO], true); + } } } if (hasChecksum) @@ -1528,7 +1530,7 @@ namespace Virus {Parameter::Page::B, Parameter::Class::SOUNDBANK_B, 11, "Lfo3 Destination", {0,5}, numToLfoDest, {}, true, true, false}, {Parameter::Page::B, Parameter::Class::SOUNDBANK_B, 12, "Osc Lfo3 Amount", {0,127}, {},{}, true, false, false}, {Parameter::Page::B, Parameter::Class::SOUNDBANK_B, 13, "Lfo3 Fade-In Time", {0,127}, {},{}, true, false, false}, - {Parameter::Page::B, Parameter::Class::SOUNDBANK_B, 16, "Clock Tempo", {0,127}, numToClockTempo, {}, true, false, false}, + {Parameter::Page::B, Parameter::Class::SOUNDBANK_B|Parameter::Class::MULTI_OR_SINGLE|Parameter::Class::NON_PART_SENSITIVE, 16, "Clock Tempo", {0,127}, numToClockTempo, {}, true, false, false}, {Parameter::Page::B, Parameter::Class::SOUNDBANK_B, 17, "Arp Clock", {0,17}, numToMusicDivision, {}, true, true, false}, {Parameter::Page::B, Parameter::Class::SOUNDBANK_B, 18, "Lfo1 Clock", {0,21}, numToMusicDivision, {}, true, true, false}, {Parameter::Page::B, Parameter::Class::SOUNDBANK_B, 19, "Lfo2 Clock", {0,21}, numToMusicDivision, {}, true, true, false}, diff --git a/source/jucePlugin/VirusParameterBinding.cpp b/source/jucePlugin/VirusParameterBinding.cpp @@ -67,7 +67,7 @@ void VirusParameterBinding::bind(juce::ComboBox& _combo, Virus::ParameterType _p v->getValueObject().getValueSource().setValue(_combo.getSelectedId() - 1); }; - v->onValueChanged = [this, &_combo, v]() { _combo.setSelectedId((int)v->getValueObject().getValueSource().getValue() + 1, juce::NotificationType::dontSendNotification); }; + v->onValueChanged = [this, &_combo, v]() { _combo.setSelectedId((int)v->getValueObject().getValueSource().getValue() + 1, juce::dontSendNotification); }; m_bindings.add(v); } diff --git a/source/jucePlugin/ui/VirusEditor.cpp b/source/jucePlugin/ui/VirusEditor.cpp @@ -6,6 +6,7 @@ #include "Virus_LfoEditor.h" #include "Virus_OscEditor.h" #include "Virus_PatchBrowser.h" +#include "Virus_Parts.h" #include "../VirusParameterBinding.h" #include "../VirusController.h" @@ -15,7 +16,7 @@ constexpr auto kPanelWidth = 1377; constexpr auto kPanelHeight = 800; VirusEditor::VirusEditor(VirusParameterBinding &_parameterBinding, AudioPluginAudioProcessor &_processorRef) : - m_parameterBinding(_parameterBinding), processorRef(_processorRef), m_controller(processorRef.getController()), m_btSingleMode("Single\nMode"), m_btMultiSingleMode("Multi\nSingle"), m_btMultiMode("Multi\nMode"), + m_parameterBinding(_parameterBinding), processorRef(_processorRef), m_controller(processorRef.getController()), m_controlLabel("ctrlLabel", "") { setLookAndFeel(&m_lookAndFeel); @@ -31,6 +32,11 @@ VirusEditor::VirusEditor(VirusParameterBinding &_parameterBinding, AudioPluginAu m_lfoEditor = std::make_unique<LfoEditor>(_parameterBinding); m_oscEditor = std::make_unique<OscEditor>(_parameterBinding); m_patchBrowser = std::make_unique<PatchBrowser>(_parameterBinding, m_controller); + m_partList = std::make_unique<Parts>(_parameterBinding, m_controller); + + m_partList->setBounds(0,0, 338, kPanelHeight); + m_partList->setVisible(true); + addChildComponent(m_partList.get()); applyToSections([this](Component *s) { addChildComponent(s); }); @@ -48,121 +54,6 @@ VirusEditor::VirusEditor(VirusParameterBinding &_parameterBinding, AudioPluginAu m_presetButtons.m_load.onClick = [this]() { loadFile(); }; m_presetButtons.m_save.onClick = [this]() { saveFile(); }; - for (auto pt = 0; pt < 16; pt++) - { - m_partLabels[pt].setBounds(34, 161 + pt * (36), 24, 36); - m_partLabels[pt].setText(juce::String(pt + 1), juce::dontSendNotification); - m_partLabels[pt].setColour(0, juce::Colours::white); - m_partLabels[pt].setColour(1, juce::Colour(45, 24, 24)); - m_partLabels[pt].setJustificationType(Justification::centred); - addAndMakeVisible(m_partLabels[pt]); - - m_partSelect[pt].setBounds(35, 161 + pt*(36), 36, 36); - m_partSelect[pt].setButtonText(juce::String(pt)); - m_partSelect[pt].setRadioGroupId(kPartGroupId); - m_partSelect[pt].setClickingTogglesState(true); - m_partSelect[pt].onClick = [this, pt]() { - this->changePart(pt); - }; - addAndMakeVisible(m_partSelect[pt]); - - m_presetNames[pt].setBounds(80, 171 + pt * (36) - 2, 136, 16 + 4); - m_presetNames[pt].setButtonText(m_controller.getCurrentPartPresetName(pt)); - m_presetNames[pt].setColour(juce::TextButton::ColourIds::textColourOnId, juce::Colour(255,113,128)); - m_presetNames[pt].setColour(juce::TextButton::ColourIds::textColourOffId, juce::Colour(255, 113, 128)); - - m_presetNames[pt].onClick = [this, pt]() { - juce::PopupMenu selector; - - for (uint8_t b = 0; b < m_controller.getBankCount(); ++b) - { - const auto bank = virusLib::fromArrayIndex(b); - auto presetNames = m_controller.getSinglePresetNames(bank); - juce::PopupMenu p; - for (uint8_t j = 0; j < 128; j++) - { - const auto presetName = presetNames[j]; - p.addItem(presetNames[j], [this, bank, j, pt, presetName] { - m_controller.setCurrentPartPreset(pt, bank, j); - m_presetNames[pt].setButtonText(presetName); - }); - } - std::stringstream bankName; - bankName << "Bank " << static_cast<char>('A' + b); - selector.addSubMenu(std::string(bankName.str()), p); - } - selector.showMenu(juce::PopupMenu::Options()); - }; - addAndMakeVisible(m_presetNames[pt]); - - m_prevPatch[pt].setBounds(228, 173 + 36*pt - 2, 16, 14); - m_nextPatch[pt].setBounds(247, 173 + 36*pt - 2, 16, 14); - m_prevPatch[pt].setButtonText("<"); - m_nextPatch[pt].setButtonText(">"); - m_prevPatch[pt].setColour(juce::TextButton::ColourIds::textColourOnId, juce::Colour(255, 113, 128)); - m_nextPatch[pt].setColour(juce::TextButton::ColourIds::textColourOnId, juce::Colour(255, 113, 128)); - m_prevPatch[pt].setColour(juce::TextButton::ColourIds::textColourOffId, juce::Colour(255, 113, 128)); - m_nextPatch[pt].setColour(juce::TextButton::ColourIds::textColourOffId, juce::Colour(255, 113, 128)); - m_prevPatch[pt].onClick = [this, pt]() { - m_controller.setCurrentPartPreset( - pt, m_controller.getCurrentPartBank(pt), - std::max(0, m_controller.getCurrentPartProgram(pt) - 1)); - }; - m_nextPatch[pt].onClick = [this, pt]() { - m_controller.setCurrentPartPreset( - pt, m_controller.getCurrentPartBank(pt), - std::min(127, m_controller.getCurrentPartProgram(pt) + 1)); - }; - addAndMakeVisible(m_prevPatch[pt]); - addAndMakeVisible(m_nextPatch[pt]); - - m_partVolumes[pt].setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag); - m_partVolumes[pt].setTextBoxStyle(juce::Slider::NoTextBox, false, 0, 0); - m_partVolumes[pt].setBounds(m_nextPatch[pt].getBounds().translated(m_nextPatch[pt].getWidth()+8, 0)); - m_partVolumes[pt].setSize(18,18); - m_partVolumes[pt].getProperties().set(Virus::LookAndFeel::KnobStyleProp, Virus::LookAndFeel::KnobStyle::GENERIC_MULTI); - _parameterBinding.bind(m_partVolumes[pt], Virus::Param_PartVolume, pt); - addAndMakeVisible(m_partVolumes[pt]); - - m_partPans[pt].setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag); - m_partPans[pt].setTextBoxStyle(juce::Slider::NoTextBox, false, 0, 0); - m_partPans[pt].setBounds(m_partVolumes[pt].getBounds().translated(m_partVolumes[pt].getWidth()+4, 0)); - m_partPans[pt].setSize(18,18); - m_partPans[pt].getProperties().set(Virus::LookAndFeel::KnobStyleProp, Virus::LookAndFeel::KnobStyle::GENERIC_MULTI); - _parameterBinding.bind(m_partPans[pt], Virus::Param_Panorama, pt); - addAndMakeVisible(m_partPans[pt]); - } - m_partSelect[m_controller.getCurrentPart()].setToggleState(true, NotificationType::sendNotification); - - m_btSingleMode.setRadioGroupId(0x3cf); - m_btMultiMode.setRadioGroupId(0x3cf); - m_btMultiSingleMode.setRadioGroupId(0x3cf); - addAndMakeVisible(m_btSingleMode); - addAndMakeVisible(m_btMultiMode); - addAndMakeVisible(m_btMultiSingleMode); - m_btSingleMode.setTopLeftPosition(102, 756); - m_btSingleMode.setSize(70, 30); - //m_btMultiMode.getToggleStateValue().referTo(*m_controller.getParamValue(Virus::Param_PlayMode)); - const auto isMulti = m_controller.getParamValue(Virus::Param_PlayMode)>0; - m_btSingleMode.setClickingTogglesState(true); - m_btMultiMode.setClickingTogglesState(true); - m_btMultiSingleMode.setClickingTogglesState(true); - m_btSingleMode.setToggleState(true, juce::sendNotificationAsync); - //m_btMultiMode.setToggleState(isMulti, juce::dontSendNotification); - //m_btMultiSingleMode.setToggleState(isMulti, juce::dontSendNotification); - m_btSingleMode.setColour(TextButton::ColourIds::textColourOnId, juce::Colours::white); - m_btSingleMode.setColour(TextButton::ColourIds::textColourOffId, juce::Colours::grey); - m_btMultiMode.setColour(TextButton::ColourIds::textColourOnId, juce::Colours::white); - m_btMultiMode.setColour(TextButton::ColourIds::textColourOffId, juce::Colours::grey); - m_btMultiSingleMode.setColour(TextButton::ColourIds::textColourOnId, juce::Colours::white); - m_btMultiSingleMode.setColour(TextButton::ColourIds::textColourOffId, juce::Colours::grey); - m_btSingleMode.onClick = [this]() { setPlayMode(virusLib::PlayMode::PlayModeSingle); }; - m_btMultiSingleMode.onClick = [this]() { setPlayMode(virusLib::PlayMode::PlayModeMultiSingle); }; - m_btMultiMode.onClick = [this]() { setPlayMode(virusLib::PlayMode::PlayModeMulti); }; - - m_btMultiSingleMode.setBounds(m_btSingleMode.getBounds().translated(m_btSingleMode.getWidth()+4, 0)); - m_btMultiMode.setBounds(m_btMultiSingleMode.getBounds().translated(m_btMultiSingleMode.getWidth()+4, 0)); - juce::PropertiesFile::Options opts; opts.applicationName = "DSP56300 Emulator"; opts.filenameSuffix = ".settings"; @@ -270,11 +161,6 @@ void VirusEditor::timerCallback() for (auto pt = 0; pt < 16; pt++) { bool singlePartOrInMulti = pt == 0 || multiMode; - m_presetNames[pt].setVisible(singlePartOrInMulti); - m_prevPatch[pt].setVisible(singlePartOrInMulti); - m_nextPatch[pt].setVisible(singlePartOrInMulti); - if (singlePartOrInMulti) - m_presetNames[pt].setButtonText(m_controller.getCurrentPartPresetName(pt)); if (pt == m_controller.getCurrentPart()) { const auto patchName = m_controller.getCurrentPartPresetName(pt); @@ -282,7 +168,6 @@ void VirusEditor::timerCallback() m_patchName.setText(patchName, NotificationType::dontSendNotification); } } - //m_partVolumes[pt].setValue(m_controller.getParameter(Virus::Param_PartVolume, pt)->getValue(), juce::dontSendNotification); } } @@ -345,13 +230,6 @@ void VirusEditor::updateMidiOutput(int index) m_cmbMidiOutput.setSelectedItemIndex(index + 1, juce::dontSendNotification); m_lastOutputIndex = index; } -void VirusEditor::updatePartsPresetNames() -{ - for (auto i = 0; i < 16; i++) - { - m_presetNames[i].setButtonText(m_controller.getCurrentPartPresetName(i)); - } -} void VirusEditor::applyToSections(std::function<void(Component *)> action) { for (auto *section : {static_cast<Component *>(m_arpEditor.get()), static_cast<Component *>(m_fxEditor.get()), @@ -404,20 +282,14 @@ void VirusEditor::resized() applyToSections([this](Component *s) { s->setTopLeftPosition(338, 133); }); } -void VirusEditor::setPlayMode(uint8_t _mode) { - m_controller.getParameter(Virus::Param_PlayMode)->setValue(_mode); - changePart(0); +void VirusEditor::handleCommandMessage(int commandId) { + if (commandId == Commands::Rebind) { + recreateControls(); + } } -void VirusEditor::changePart(uint8_t _part) +void VirusEditor::recreateControls() { - for (auto &p : m_partSelect) - { - p.setToggleState(false, juce::dontSendNotification); - } - m_partSelect[_part].setToggleState(true, juce::dontSendNotification); - m_parameterBinding.setPart(_part); - removeChildComponent(m_oscEditor.get()); removeChildComponent(m_lfoEditor.get()); removeChildComponent(m_fxEditor.get()); @@ -530,7 +402,11 @@ void VirusEditor::loadFile() { if ((uint8_t)*it == (uint8_t)0xf0 && (it + 267) < end) { - if ((uint8_t) * (it + 1) == (uint8_t)0x00) + if ((uint8_t)*(it+1) == 0x00 + && (uint8_t)*(it+2) == 0x20 + && (uint8_t)*(it+3) == 0x33 + && (uint8_t)*(it+4) == 0x01 + && (uint8_t)*(it+6) == virusLib::DUMP_SINGLE) { auto syx = Virus::SysEx(it, it + 267); syx[7] = 0x01; // force to bank a @@ -540,7 +416,11 @@ void VirusEditor::loadFile() it += 266; } - else // some midi files have two bytes after the 0xf0 + else if((uint8_t)*(it+3) == 0x00 + && (uint8_t)*(it+4) == 0x20 + && (uint8_t)*(it+5) == 0x33 + && (uint8_t)*(it+6) == 0x01 + && (uint8_t)*(it+8) == virusLib::DUMP_SINGLE)// some midi files have two bytes after the 0xf0 { auto syx = Virus::SysEx(); syx.push_back(0xf0); diff --git a/source/jucePlugin/ui/VirusEditor.h b/source/jucePlugin/ui/VirusEditor.h @@ -20,30 +20,25 @@ public: VirusEditor(VirusParameterBinding &_parameterBinding, AudioPluginAudioProcessor &_processorRef); ~VirusEditor() override; void resized() override; - void changePart(uint8_t _part); + void recreateControls(); void updatePartsPresetNames(); void loadFile(); void saveFile(); - void setPlayMode(uint8_t _mode); + enum Commands { + None, + Rebind = 0x100 + }; private: void timerCallback() override; + void handleCommandMessage(int commandId) override; void updateMidiInput(int index); void updateMidiOutput(int index); juce::Label m_version; juce::Label m_patchName; juce::Label m_controlLabel; - Buttons::PartSelectButton m_partSelect[16]; - juce::Label m_partLabels[16]; - juce::TextButton m_presetNames[16]; - juce::TextButton m_nextPatch[16]; - juce::TextButton m_prevPatch[16]; - juce::Slider m_partVolumes[16]; - juce::Slider m_partPans[16]; - juce::TextButton m_btSingleMode; - juce::TextButton m_btMultiSingleMode; - juce::TextButton m_btMultiMode; + juce::ComboBox m_cmbMidiInput; juce::ComboBox m_cmbMidiOutput; juce::AudioDeviceManager deviceManager; @@ -51,7 +46,6 @@ private: int m_lastInputIndex = 0; int m_lastOutputIndex = 0; - static constexpr auto kPartGroupId = 0x3FBBC; struct MainButtons : juce::Component, juce::Value::Listener { MainButtons(); diff --git a/source/jucePlugin/ui/Virus_ArpEditor.cpp b/source/jucePlugin/ui/Virus_ArpEditor.cpp @@ -92,7 +92,7 @@ ArpEditor::Arpeggiator::Arpeggiator(VirusParameterBinding &_parameterBinding) addAndMakeVisible(m_arpHold); m_arpHold.setBounds(222, m_octaveRange.getY()+2, 28, 11); - _parameterBinding.bind(m_globalTempo, Virus::Param_ClockTempo); + _parameterBinding.bind(m_globalTempo, Virus::Param_ClockTempo, 0); _parameterBinding.bind(m_noteLength, Virus::Param_ArpNoteLength); _parameterBinding.bind(m_noteSwing, Virus::Param_ArpSwing); _parameterBinding.bind(m_mode, Virus::Param_ArpMode); diff --git a/source/jucePlugin/ui/Virus_Parts.cpp b/source/jucePlugin/ui/Virus_Parts.cpp @@ -1,11 +1,162 @@ -#include "Virus_Parts.h" -#include "BinaryData.h" -#include "Ui_Utils.h" -#include "../VirusParameterBinding.h" - -using namespace juce; - -Parts::Parts(VirusParameterBinding & _parameterBinding, Virus::Controller& _controller) : m_parameterBinding(_parameterBinding), m_controller(_controller) -{ - +#include "Virus_Parts.h" +#include "BinaryData.h" +#include "Ui_Utils.h" +#include "../VirusParameterBinding.h" +#include "VirusEditor.h" +using namespace juce; + +Parts::Parts(VirusParameterBinding & _parameterBinding, Virus::Controller& _controller) : m_parameterBinding(_parameterBinding), m_controller(_controller), + m_btSingleMode("Single\nMode"), m_btMultiSingleMode("Multi\nSingle"), m_btMultiMode("Multi\nMode") +{ + for (auto pt = 0; pt < 16; pt++) + { + m_partLabels[pt].setBounds(34, 161 + pt * (36), 24, 36); + m_partLabels[pt].setText(juce::String(pt + 1), juce::dontSendNotification); + m_partLabels[pt].setColour(0, juce::Colours::white); + m_partLabels[pt].setColour(1, juce::Colour(45, 24, 24)); + m_partLabels[pt].setJustificationType(Justification::centred); + addAndMakeVisible(m_partLabels[pt]); + + m_partSelect[pt].setBounds(35, 161 + pt*(36), 36, 36); + m_partSelect[pt].setButtonText(juce::String(pt)); + m_partSelect[pt].setRadioGroupId(kPartGroupId); + m_partSelect[pt].setClickingTogglesState(true); + m_partSelect[pt].onClick = [this, pt]() { + this->changePart(pt); + }; + addAndMakeVisible(m_partSelect[pt]); + + m_presetNames[pt].setBounds(80, 171 + pt * (36) - 2, 136, 16 + 4); + m_presetNames[pt].setButtonText(m_controller.getCurrentPartPresetName(pt)); + m_presetNames[pt].setColour(juce::TextButton::ColourIds::textColourOnId, juce::Colour(255,113,128)); + m_presetNames[pt].setColour(juce::TextButton::ColourIds::textColourOffId, juce::Colour(255, 113, 128)); + + m_presetNames[pt].onClick = [this, pt]() { + juce::PopupMenu selector; + + for (uint8_t b = 0; b < m_controller.getBankCount(); ++b) + { + const auto bank = virusLib::fromArrayIndex(b); + auto presetNames = m_controller.getSinglePresetNames(bank); + juce::PopupMenu p; + for (uint8_t j = 0; j < 128; j++) + { + const auto presetName = presetNames[j]; + p.addItem(presetNames[j], [this, bank, j, pt, presetName] { + m_controller.setCurrentPartPreset(pt, bank, j); + m_presetNames[pt].setButtonText(presetName); + }); + } + std::stringstream bankName; + bankName << "Bank " << static_cast<char>('A' + b); + selector.addSubMenu(std::string(bankName.str()), p); + } + selector.showMenu(juce::PopupMenu::Options()); + }; + addAndMakeVisible(m_presetNames[pt]); + + m_prevPatch[pt].setBounds(228, 173 + 36*pt - 2, 16, 14); + m_nextPatch[pt].setBounds(247, 173 + 36*pt - 2, 16, 14); + m_prevPatch[pt].setButtonText("<"); + m_nextPatch[pt].setButtonText(">"); + m_prevPatch[pt].setColour(juce::TextButton::ColourIds::textColourOnId, juce::Colour(255, 113, 128)); + m_nextPatch[pt].setColour(juce::TextButton::ColourIds::textColourOnId, juce::Colour(255, 113, 128)); + m_prevPatch[pt].setColour(juce::TextButton::ColourIds::textColourOffId, juce::Colour(255, 113, 128)); + m_nextPatch[pt].setColour(juce::TextButton::ColourIds::textColourOffId, juce::Colour(255, 113, 128)); + m_prevPatch[pt].onClick = [this, pt]() { + m_controller.setCurrentPartPreset( + pt, m_controller.getCurrentPartBank(pt), + std::max(0, m_controller.getCurrentPartProgram(pt) - 1)); + }; + m_nextPatch[pt].onClick = [this, pt]() { + m_controller.setCurrentPartPreset( + pt, m_controller.getCurrentPartBank(pt), + std::min(127, m_controller.getCurrentPartProgram(pt) + 1)); + }; + addAndMakeVisible(m_prevPatch[pt]); + addAndMakeVisible(m_nextPatch[pt]); + + m_partVolumes[pt].setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag); + m_partVolumes[pt].setTextBoxStyle(juce::Slider::NoTextBox, false, 0, 0); + m_partVolumes[pt].setBounds(m_nextPatch[pt].getBounds().translated(m_nextPatch[pt].getWidth()+8, 0)); + m_partVolumes[pt].setSize(18,18); + m_partVolumes[pt].getProperties().set(Virus::LookAndFeel::KnobStyleProp, Virus::LookAndFeel::KnobStyle::GENERIC_MULTI); + m_partPans[pt].setTooltip("Part Volume"); + _parameterBinding.bind(m_partVolumes[pt], Virus::Param_PartVolume, pt); + addAndMakeVisible(m_partVolumes[pt]); + + m_partPans[pt].setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag); + m_partPans[pt].setTextBoxStyle(juce::Slider::NoTextBox, false, 0, 0); + m_partPans[pt].setBounds(m_partVolumes[pt].getBounds().translated(m_partVolumes[pt].getWidth()+4, 0)); + m_partPans[pt].setSize(18,18); + m_partPans[pt].getProperties().set(Virus::LookAndFeel::KnobStyleProp, Virus::LookAndFeel::KnobStyle::GENERIC_MULTI); + m_partPans[pt].setTooltip("Part Pan"); + _parameterBinding.bind(m_partPans[pt], Virus::Param_Panorama, pt); + addAndMakeVisible(m_partPans[pt]); + } + m_partSelect[m_controller.getCurrentPart()].setToggleState(true, NotificationType::sendNotification); + + m_btSingleMode.setRadioGroupId(0x3cf); + m_btMultiMode.setRadioGroupId(0x3cf); + m_btMultiSingleMode.setRadioGroupId(0x3cf); + addAndMakeVisible(m_btSingleMode); + addAndMakeVisible(m_btMultiMode); + addAndMakeVisible(m_btMultiSingleMode); + m_btSingleMode.setTopLeftPosition(102, 756); + m_btSingleMode.setSize(70, 30); + //m_btMultiMode.getToggleStateValue().referTo(*m_controller.getParamValue(Virus::Param_PlayMode)); + const auto isMulti = m_controller.getParamValue(Virus::Param_PlayMode)>0; + m_btSingleMode.setClickingTogglesState(true); + m_btMultiMode.setClickingTogglesState(true); + m_btMultiSingleMode.setClickingTogglesState(true); + m_btSingleMode.setToggleState(true, juce::sendNotificationAsync); + //m_btMultiMode.setToggleState(isMulti, juce::dontSendNotification); + //m_btMultiSingleMode.setToggleState(isMulti, juce::dontSendNotification); + m_btSingleMode.setColour(TextButton::ColourIds::textColourOnId, juce::Colours::white); + m_btSingleMode.setColour(TextButton::ColourIds::textColourOffId, juce::Colours::grey); + m_btMultiMode.setColour(TextButton::ColourIds::textColourOnId, juce::Colours::white); + m_btMultiMode.setColour(TextButton::ColourIds::textColourOffId, juce::Colours::grey); + m_btMultiSingleMode.setColour(TextButton::ColourIds::textColourOnId, juce::Colours::white); + m_btMultiSingleMode.setColour(TextButton::ColourIds::textColourOffId, juce::Colours::grey); + m_btSingleMode.onClick = [this]() { setPlayMode(virusLib::PlayMode::PlayModeSingle); }; + m_btMultiSingleMode.onClick = [this]() { setPlayMode(virusLib::PlayMode::PlayModeMultiSingle); }; + m_btMultiMode.onClick = [this]() { setPlayMode(virusLib::PlayMode::PlayModeMulti); }; + + m_btMultiSingleMode.setBounds(m_btSingleMode.getBounds().translated(m_btSingleMode.getWidth()+4, 0)); + m_btMultiMode.setBounds(m_btMultiSingleMode.getBounds().translated(m_btMultiSingleMode.getWidth()+4, 0)); + + startTimerHz(5); + setSize(338, 800); +} + +void Parts::changePart(uint8_t _part) +{ + for (auto &p : m_partSelect) + { + p.setToggleState(false, juce::dontSendNotification); + } + m_partSelect[_part].setToggleState(true, juce::dontSendNotification); + m_parameterBinding.setPart(_part); + getParentComponent()->postCommandMessage(VirusEditor::Commands::Rebind); +} + +void Parts::setPlayMode(uint8_t _mode) { + m_controller.getParameter(Virus::Param_PlayMode)->setValue(_mode); + getParentComponent()->postCommandMessage(VirusEditor::Commands::Rebind); +} + +void Parts::timerCallback() +{ + // ugly (polling!) way for refreshing presets names as this is temporary ui + const auto multiMode = m_controller.isMultiMode(); + for (auto pt = 0; pt < 16; pt++) + { + bool singlePartOrInMulti = pt == 0 || multiMode; + m_presetNames[pt].setVisible(singlePartOrInMulti); + m_prevPatch[pt].setVisible(singlePartOrInMulti); + m_nextPatch[pt].setVisible(singlePartOrInMulti); + if (singlePartOrInMulti) + m_presetNames[pt].setButtonText(m_controller.getCurrentPartPresetName(pt)); + } + } \ No newline at end of file diff --git a/source/jucePlugin/ui/Virus_Parts.h b/source/jucePlugin/ui/Virus_Parts.h @@ -6,11 +6,26 @@ #include "../VirusController.h" class VirusParameterBinding; -class Parts : public juce::Component +class Parts : public juce::Component, private juce::Timer { - public: - Parts(VirusParameterBinding& _parameterBinding, Virus::Controller& _controller); - private: - Virus::Controller &m_controller; - VirusParameterBinding &m_parameterBinding; + public: + Parts(VirusParameterBinding& _parameterBinding, Virus::Controller& _controller); + static constexpr auto kPartGroupId = 0x3FBBC; + private: + void changePart(uint8_t _part); + void setPlayMode(uint8_t _mode); + void timerCallback() override; + Virus::Controller &m_controller; + VirusParameterBinding &m_parameterBinding; + + Buttons::PartSelectButton m_partSelect[16]; + juce::Label m_partLabels[16]; + juce::TextButton m_presetNames[16]; + juce::TextButton m_nextPatch[16]; + juce::TextButton m_prevPatch[16]; + juce::Slider m_partVolumes[16]; + juce::Slider m_partPans[16]; + juce::TextButton m_btSingleMode; + juce::TextButton m_btMultiSingleMode; + juce::TextButton m_btMultiMode; }; \ No newline at end of file diff --git a/source/jucePlugin/ui/Virus_PatchBrowser.cpp b/source/jucePlugin/ui/Virus_PatchBrowser.cpp @@ -34,11 +34,13 @@ PatchBrowser::PatchBrowser(VirusParameterBinding & _parameterBinding, Virus::Con m_patchList.setBounds(m_bankList.getBounds().translated(m_bankList.getWidth(), 0)); - m_patchList.getHeader().addColumn("#", 0, 32); - m_patchList.getHeader().addColumn("Name", 1, 150); - m_patchList.getHeader().addColumn("Category1", 2, 100); - m_patchList.getHeader().addColumn("Category2", 3, 100); - m_patchList.getHeader().addColumn("Arp", 4, 32); + m_patchList.getHeader().addColumn("#", Columns::INDEX, 32); + m_patchList.getHeader().addColumn("Name", Columns::NAME, 140); + m_patchList.getHeader().addColumn("Category1", Columns::CAT1, 90); + m_patchList.getHeader().addColumn("Category2", Columns::CAT2, 90); + m_patchList.getHeader().addColumn("Arp", Columns::ARP, 32); + m_patchList.getHeader().addColumn("Uni", Columns::UNI, 32); + m_patchList.getHeader().addColumn("Ver", Columns::VER, 32); addAndMakeVisible(m_bankList); addAndMakeVisible(m_patchList); @@ -58,18 +60,32 @@ void PatchBrowser::fileClicked(const juce::File &file, const juce::MouseEvent &e m_patches.clear(); juce::MemoryBlock data; - file.loadFileAsData(data); - uint8_t index = 0; + if (!file.loadFileAsData(data)) { + return; + } + int index = 0; for (auto it = data.begin(); it != data.end(); it += 267) { - if ((it + 267) <= data.end()) + if ((uint8_t)*it == (uint8_t)0xf0 + && (uint8_t)*(it+1) == (uint8_t)0x00 + && (uint8_t)*(it+2) == (uint8_t)0x20 + && (uint8_t)*(it+3) == (uint8_t)0x33 + && (uint8_t)*(it+4) == (uint8_t)0x01 + && (uint8_t)*(it+6) == (uint8_t)virusLib::DUMP_SINGLE) { Patch patch; patch.progNumber = index; + if ((uint8_t)*(it + 266) != (uint8_t)0xf7) { + patch.model = VirusModel::TI; + } + else { + patch.model = VirusModel::B; + } data.copyTo(patch.data, 267*index + 9, 256); patch.name = parseAsciiText(patch.data, 128 + 112); patch.category1 = patch.data[251]; patch.category2 = patch.data[252]; + patch.unison = patch.data[97]; m_patches.add(patch); index++; } @@ -93,9 +109,13 @@ void PatchBrowser::fileClicked(const juce::File &file, const juce::MouseEvent &e for (auto it = ptr; it < end; it += 1) { - if ((uint8_t)*it == (uint8_t)0xf0 && (it + 267) < end) + if ((uint8_t)*it == (uint8_t)0xf0 && (it + 267) < end) // we don't check for sysex eof so we can load TI banks { - if ((uint8_t) * (it + 1) == (uint8_t)0x00) + if ((uint8_t) *(it+1) == (uint8_t)0x00 + && (uint8_t)*(it+2) == 0x20 + && (uint8_t)*(it+3) == 0x33 + && (uint8_t)*(it+4) == 0x01 + && (uint8_t)*(it+6) == virusLib::DUMP_SINGLE) { auto syx = Virus::SysEx(it, it + 267); syx[7] = 0x01; // force to bank a @@ -103,15 +123,26 @@ void PatchBrowser::fileClicked(const juce::File &file, const juce::MouseEvent &e Patch patch; patch.progNumber = index; + if ((uint8_t)*(it + 266) != (uint8_t)0xf7) { + patch.model = VirusModel::TI; + } + else { + patch.model = VirusModel::B; + } std::copy(syx.begin() + 9, syx.end() - 2, patch.data); patch.name = parseAsciiText(patch.data, 128 + 112); patch.category1 = patch.data[251]; patch.category2 = patch.data[252]; + patch.unison = patch.data[97]; m_patches.add(patch); index++; it += 266; } - else // some midi files have two bytes after the 0xf0 + else if((uint8_t)*(it+3) == 0x00 // some midi files have two bytes after the 0xf0 + && (uint8_t)*(it+4) == 0x20 + && (uint8_t)*(it+5) == 0x33 + && (uint8_t)*(it+6) == 0x01 + && (uint8_t)*(it+8) == virusLib::DUMP_SINGLE) { auto syx = Virus::SysEx(); syx.push_back(0xf0); @@ -123,11 +154,18 @@ void PatchBrowser::fileClicked(const juce::File &file, const juce::MouseEvent &e syx[266] = 0xf7; Patch patch; + if ((uint8_t)*(it + 2 + 266) != (uint8_t)0xf7) { // TI patches are 512 bytes + patch.model = VirusModel::TI; + } + else { + patch.model = VirusModel::B; + } std::memcpy(patch.data, syx.data()+9, 256); patch.progNumber = index; patch.name = parseAsciiText(patch.data, 128 + 112); patch.category1 = patch.data[251]; patch.category2 = patch.data[252]; + patch.unison = patch.data[97]; m_patches.add(patch); index++; @@ -137,6 +175,7 @@ void PatchBrowser::fileClicked(const juce::File &file, const juce::MouseEvent &e } } m_patchList.updateContent(); + m_patchList.deselectAllRows(); m_patchList.repaint(); } } @@ -164,23 +203,32 @@ void PatchBrowser::paintCell(Graphics &g, int rowNumber, int columnId, int width auto rowElement = m_patches[rowNumber]; //auto text = rowElement.name; juce::String text = ""; - if (columnId == 0) + if (columnId == Columns::INDEX) text = juce::String(rowElement.progNumber); - else if (columnId == 1) + else if (columnId == Columns::NAME) text = rowElement.name; - else if (columnId == 2) + else if (columnId == Columns::CAT1) text = categories[rowElement.category1]; - else if (columnId == 3) + else if (columnId == Columns::CAT2) text = categories[rowElement.category2]; - else if (columnId == 4) + else if (columnId == Columns::ARP) text = rowElement.data[129] != 0 ? "Y" : " "; + else if(columnId == Columns::UNI) + text = rowElement.unison == 0 ? " " : juce::String(rowElement.unison+1); + else if (columnId == Columns::VER) { + if(rowElement.model < ModelList.size()) + text = ModelList[rowElement.model]; + } g.drawText(text, 2, 0, width - 4, height, juce::Justification::centredLeft, true); // [6] g.setColour(getLookAndFeel().findColour(juce::ListBox::backgroundColourId)); g.fillRect(width - 1, 0, 1, height); // [7] } -void PatchBrowser::selectedRowsChanged(int lastRowSelected) { +void PatchBrowser::selectedRowsChanged(int lastRowSelected) { auto idx = m_patchList.getSelectedRow(); + if (idx == -1) { + return; + } uint8_t syxHeader[9] = {0xF0, 0x00, 0x20, 0x33, 0x01, 0x00, 0x10, 0x00, 0x00}; syxHeader[8] = m_controller.isMultiMode() ? m_controller.getCurrentPart() : virusLib::ProgramType::SINGLE; // set edit buffer const uint8_t syxEof = 0xF7; @@ -213,3 +261,52 @@ void PatchBrowser::cellDoubleClicked(int rowNumber, int columnId, const juce::Mo selectedRowsChanged(0); } } + +class PatchBrowser::PatchBrowserSorter +{ +public: + PatchBrowserSorter (int attributeToSortBy, bool forwards) + : attributeToSort (attributeToSortBy), + direction (forwards ? 1 : -1) + {} + + int compareElements (Patch first, Patch second) const + { + if(attributeToSort == Columns::INDEX) { + return direction * (first.progNumber - second.progNumber); + } + else if (attributeToSort == Columns::NAME) { + return direction * first.name.compareIgnoreCase(second.name); + } + else if (attributeToSort == Columns::CAT1) { + return direction * (first.category1 - second.category1); + } + else if (attributeToSort == Columns::CAT2) { + return direction * (first.category2 - second.category2); + } + else if (attributeToSort == Columns::ARP) { + return direction * (first.data[129]- second.data[129]); + } + else if (attributeToSort == Columns::UNI) { + return direction * (first.unison - second.unison); + } + else if (attributeToSort == Columns::VER) { + return direction * (first.model - second.model); + } + return direction * (first.progNumber - second.progNumber); + } + +private: + int attributeToSort; + int direction; +}; + +void PatchBrowser::sortOrderChanged(int newSortColumnId, bool isForwards) +{ + if (newSortColumnId != 0) + { + PatchBrowser::PatchBrowserSorter sorter (newSortColumnId, isForwards); + m_patches.sort(sorter); + m_patchList.updateContent(); + } +} +\ No newline at end of file diff --git a/source/jucePlugin/ui/Virus_PatchBrowser.h b/source/jucePlugin/ui/Virus_PatchBrowser.h @@ -5,6 +5,24 @@ #include <juce_gui_extra/juce_gui_extra.h> #include "../VirusController.h" class VirusParameterBinding; +enum VirusModel { + A, + B, + C, + TI +}; + +const juce::Array<juce::String> ModelList = {"A","B","C","TI"}; +struct Patch +{ + int progNumber; + juce::String name; + uint8_t category1; + uint8_t category2; + uint8_t data[256]; + VirusModel model; + uint8_t unison; +}; class PatchBrowser : public juce::Component, juce::FileBrowserListener, juce::TableListBoxModel { @@ -15,15 +33,6 @@ public: private: VirusParameterBinding &m_parameterBinding; Virus::Controller& m_controller; - struct Patch - { - uint8_t progNumber; - juce::String name; - uint8_t category1; - uint8_t category2; - uint8_t data[256]; - - }; template <typename T> juce::String parseAsciiText(const T &msg, const int start) const { char text[Virus::Controller::kNameLength + 1]; @@ -53,4 +62,15 @@ private: 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, + VER = 7 + }; }; diff --git a/source/virusLib/microcontroller.cpp b/source/virusLib/microcontroller.cpp @@ -492,7 +492,7 @@ bool Microcontroller::sendSysex(const std::vector<uint8_t>& _data, bool _cancelI const auto param = _data[8]; const auto value = _data[9]; - if(page == PAGE_C) + if(page == PAGE_C || (page == PAGE_B && param == CLOCK_TEMPO)) { applyToMultiEditBuffer(part, param, value); @@ -904,9 +904,12 @@ void Microcontroller::applyToSingleEditBuffer(TPreset& _single, const Page _page void Microcontroller::applyToMultiEditBuffer(const uint8_t _part, const uint8_t _param, const uint8_t _value) { // remap page C parameters into the multi edit buffer - if (_param >= PART_MIDI_CHANNEL && _param <= PART_OUTPUT_SELECT) { + if (_part == PAGE_C && _param >= PART_MIDI_CHANNEL && _param <= PART_OUTPUT_SELECT) { m_multiEditBuffer[MD_PART_MIDI_CHANNEL + ((_param-PART_MIDI_CHANNEL)*16) + _part] = _value; } + else if (_part == PAGE_B && _param == CLOCK_TEMPO) { + m_multiEditBuffer[MD_CLOCK_TEMPO] = _value; + } } } diff --git a/source/virusLib/microcontrollerTypes.h b/source/virusLib/microcontrollerTypes.h @@ -38,6 +38,8 @@ namespace virusLib MULTI_NAME_CHAR_8 = 13, MULTI_NAME_CHAR_9 = 14, + CLOCK_TEMPO = 16, // this is actually in page B + MULTI_DELAY_OUTPUT_SELECT = 22, UNK1a = 26,