From 885d9e9f4c4de13884e9c79eae642395bf413e22 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 30 Mar 2022 10:17:41 -0400 Subject: [PATCH] A visual Mod and Pitch wheel on the virtual keyboard (#6001) Addresses #5746 Adds a pitch wheel and mod wheel control to the virtual keyboard Definitely needs some rendering work but all the plumbing drawing and layout is in place --- src/surge-xt/SurgeSynthEditor.cpp | 108 ++++++++++++++++++++++++++- src/surge-xt/SurgeSynthEditor.h | 1 + src/surge-xt/SurgeSynthProcessor.cpp | 19 ++++- src/surge-xt/SurgeSynthProcessor.h | 12 ++- 4 files changed, 133 insertions(+), 7 deletions(-) diff --git a/src/surge-xt/SurgeSynthEditor.cpp b/src/surge-xt/SurgeSynthEditor.cpp index 755f32f764a..fc4c73eb7f3 100644 --- a/src/surge-xt/SurgeSynthEditor.cpp +++ b/src/surge-xt/SurgeSynthEditor.cpp @@ -18,6 +18,83 @@ #include "RuntimeFont.h" #include +struct VKeyboardWheel : public juce::Component +{ + std::function onValueChanged = [](int f) {}; + bool snapBack{false}; + bool unipolar{true}; + int range{127}; + int value{0}; + void paint(juce::Graphics &g) override + { + auto wheelSz = getLocalBounds().reduced(1, 2); + g.setColour(juce::Colour(15, 15, 15)); + g.fillRect(wheelSz); + g.setColour(juce::Colour(80, 80, 80)); + g.drawRect(wheelSz); + + float p = 1.0 * value / range; + if (!unipolar) + p = 1.0 * (value + range) / (2 * range); + + // y direction is flipped + p = 1 - p; + + int nTicks = 10; + float shift = 1.0 * p / nTicks; + + for (int i = 0; i < nTicks; ++i) + { + int lev = 150 - (i + p) * 50.0 / (nTicks); + g.setColour(juce::Colour(lev, lev, lev)); + float yp = (i + p * nTicks - floor(p * nTicks)) * wheelSz.getHeight() / nTicks + + wheelSz.getY(); + g.drawLine(wheelSz.getX(), yp, wheelSz.getRight(), yp); + } + + float cp = wheelSz.getY() + p * wheelSz.getHeight(); + auto r = wheelSz.withHeight(3).translated(0, cp - 3); + g.setColour(juce::Colours::yellow); + g.fillRect(r); + } + + void valueFromY(float y) + { + auto wheelSz = getLocalBounds().reduced(1, 2); + auto py = std::clamp(y, 1.f * wheelSz.getY(), 1.f * wheelSz.getY() + wheelSz.getHeight()); + py = (py - wheelSz.getY()) / wheelSz.getHeight(); + py = 1 - py; + + if (unipolar) + value = py * range; + else + value = 2 * py * range - range; + onValueChanged(value); + } + + void mouseDown(const juce::MouseEvent &event) override + { + valueFromY(event.position.y); + repaint(); + } + + void mouseDrag(const juce::MouseEvent &event) override + { + valueFromY(event.position.y); + repaint(); + } + + void mouseUp(const juce::MouseEvent &event) override + { + if (snapBack) + { + value = 0; + onValueChanged(value); + } + repaint(); + } +}; + //============================================================================== SurgeSynthEditor::SurgeSynthEditor(SurgeSynthProcessor &p) : juce::AudioProcessorEditor(&p), processor(p) @@ -42,6 +119,23 @@ SurgeSynthEditor::SurgeSynthEditor(SurgeSynthProcessor &p) // this makes VKB always receive keyboard input (except when we focus on any typeins, of course) keyboard->setWantsKeyboardFocus(false); + auto w = std::make_unique(); + w->snapBack = true; + w->unipolar = false; + w->range = 8196; + w->onValueChanged = [this](auto f) { + processor.midiFromGUI.push( + SurgeSynthProcessor::midiR(SurgeSynthProcessor::midiR::PITCHWHEEL, f)); + }; + pitchwheel = std::move(w); + + auto m = std::make_unique(); + m->onValueChanged = [this](auto f) { + processor.midiFromGUI.push( + SurgeSynthProcessor::midiR(SurgeSynthProcessor::midiR::MODWHEEL, f)); + }; + modwheel = std::move(m); + tempoTypein = std::make_unique("Tempo"); tempoTypein->setFont(Surge::GUI::getFontManager()->getLatoAtSize(11)); tempoTypein->setInputRestrictions(3, "0123456789"); @@ -57,6 +151,8 @@ SurgeSynthEditor::SurgeSynthEditor(SurgeSynthProcessor &p) tempoLabel = std::make_unique("Tempo", "Tempo"); addChildComponent(*keyboard); + addChildComponent(*pitchwheel); + addChildComponent(*modwheel); addChildComponent(*tempoLabel); addChildComponent(*tempoTypein); @@ -184,12 +280,13 @@ void SurgeSynthEditor::resized() { auto y = adapter->getWindowSizeY(); auto x = addTempo ? 50 : 0; + auto wheels = 32; int tempoHeight = 14, typeinHeight = 18, yOffset = -2; int tempoBlockHeight = tempoHeight + typeinHeight; int tempoBlockYPos = ((extraYSpaceForVirtualKeyboard - tempoBlockHeight) / 2) + yOffset; auto xf = juce::AffineTransform().scaled(applyZoomFactor); - auto r = juce::Rectangle(x, y, adapter->getWindowSizeX() - x, + auto r = juce::Rectangle(x + wheels, y, adapter->getWindowSizeX() - x - wheels, extraYSpaceForVirtualKeyboard); // std::cout << "B4 " << r.toString() << std::endl; // r = r.transformedBy(xf); @@ -198,6 +295,15 @@ void SurgeSynthEditor::resized() keyboard->setTransform(xf); // juce::AffineTransform().scaled(1.05)); keyboard->setVisible(true); + auto pmr = juce::Rectangle(x, y, wheels / 2, extraYSpaceForVirtualKeyboard); + pitchwheel->setBounds(pmr); + pitchwheel->setTransform(xf); // juce::AffineTransform().scaled(1.05)); + pitchwheel->setVisible(true); + pmr = pmr.translated(wheels / 2, 0); + modwheel->setBounds(pmr); + modwheel->setTransform(xf); // juce::AffineTransform().scaled(1.05)); + modwheel->setVisible(true); + if (addTempo) { tempoLabel->setBounds(4, y + tempoBlockYPos, x - 8, tempoHeight); diff --git a/src/surge-xt/SurgeSynthEditor.h b/src/surge-xt/SurgeSynthEditor.h index 446442374a2..1f97dfebc60 100644 --- a/src/surge-xt/SurgeSynthEditor.h +++ b/src/surge-xt/SurgeSynthEditor.h @@ -79,6 +79,7 @@ class SurgeSynthEditor : public juce::AudioProcessorEditor, bool drawExtendedControls{false}; int midiKeyboardOctave{5}; float midiKeyboardVelocity{96.f / 127.f}; + std::unique_ptr pitchwheel, modwheel; std::unique_ptr keyboard; std::unique_ptr tempoLabel; std::unique_ptr tempoTypein; diff --git a/src/surge-xt/SurgeSynthProcessor.cpp b/src/surge-xt/SurgeSynthProcessor.cpp index 4946782254b..cd2d30e6761 100644 --- a/src/surge-xt/SurgeSynthProcessor.cpp +++ b/src/surge-xt/SurgeSynthProcessor.cpp @@ -228,10 +228,21 @@ void SurgeSynthProcessor::processBlock(juce::AudioBuffer &buffer, midiR rec; while (midiFromGUI.pop(rec)) { - if (rec.on) - surge->playNote(rec.ch, rec.note, rec.vel, 0); - else - surge->releaseNote(rec.ch, rec.note, rec.vel); + if (rec.type == midiR::NOTE) + { + if (rec.on) + surge->playNote(rec.ch, rec.note, rec.vel, 0); + else + surge->releaseNote(rec.ch, rec.note, rec.vel); + } + if (rec.type == midiR::PITCHWHEEL) + { + surge->pitchBend(rec.ch, rec.cval); + } + if (rec.type == midiR::MODWHEEL) + { + surge->channelController(rec.ch, 1, rec.cval); + } } // Make sure we have a main output diff --git a/src/surge-xt/SurgeSynthProcessor.h b/src/surge-xt/SurgeSynthProcessor.h index feeaf378ab7..10ea2f3196c 100644 --- a/src/surge-xt/SurgeSynthProcessor.h +++ b/src/surge-xt/SurgeSynthProcessor.h @@ -184,10 +184,18 @@ class SurgeSynthProcessor : public juce::AudioProcessor, std::atomic standaloneTempo{120}; struct midiR { - midiR() {} - midiR(int c, int n, int v, bool o) : ch(c), note(n), vel(v), on(o) {} + enum Type + { + NOTE, + MODWHEEL, + PITCHWHEEL, + } type{NOTE}; int ch{0}, note{0}, vel{0}; bool on{false}; + int cval{0}; + midiR() {} + midiR(int c, int n, int v, bool o) : type(NOTE), ch(c), note(n), vel(v), on(o) {} + midiR(Type type, int cval) : type(type), cval(cval) {} }; LockFreeStack midiFromGUI; bool isAddingFromMidi{false};