Created
December 22, 2018 04:03
-
-
Save shidephen/37b997d16315a1eb7d1aa1c7331c58e4 to your computer and use it in GitHub Desktop.
EnvelopeEditor juce component
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class Envelope | |
| { | |
| public: | |
| //======================================================================================= | |
| //======================================================================================= | |
| using Breakpoint = std::pair<int, float>; | |
| enum class Interpolation | |
| { | |
| discrete, | |
| linear, | |
| smooth | |
| }; | |
| Envelope () | |
| { | |
| } | |
| int insertBreakpoint (const int samplePosition, const float value) | |
| { | |
| ScopedLock lock (threadLock); | |
| if (breakpoints.empty()) | |
| { | |
| breakpoints.emplace_back (std::make_pair (samplePosition, value)); | |
| return 0; | |
| } | |
| for (auto it = breakpoints.cbegin(); it <= breakpoints.cend(); ++it) | |
| { | |
| if (it->first > samplePosition) | |
| { | |
| return int (std::distance (breakpoints.begin(), | |
| breakpoints.insert (it, std::make_pair (samplePosition, value)))); | |
| } | |
| } | |
| breakpoints.emplace_back (std::make_pair (samplePosition, value)); | |
| return int (breakpoints.size() - 1); | |
| } | |
| void moveBreakPoint (const int index, const int samplePosition, const float value) | |
| { | |
| if (isPositiveAndBelow (index, breakpoints.size())) | |
| { | |
| auto& bp = breakpoints [index]; | |
| if (index > 0 && index < breakpoints.size() - 1) | |
| bp.first = std::max (breakpoints [index-1].first + 10, | |
| std::min (breakpoints [index+1].first - 10, | |
| samplePosition)); | |
| bp.second = value; | |
| } | |
| } | |
| void deleteBreakPoint (const int index) | |
| { | |
| if (index > 0 && index < breakpoints.size() - 1) | |
| breakpoints.erase (breakpoints.begin() + index); | |
| } | |
| /** Returns the interpolated value for a given position, by interpolating between the two closest breakpoints. | |
| */ | |
| float getValueForSamplePosition (int samplePosition) const | |
| { | |
| ScopedLock lock (threadLock); | |
| if (breakpoints.empty()) | |
| return 20; | |
| if (samplePosition <= breakpoints.front().first) | |
| return breakpoints.front().second; | |
| if (samplePosition >= breakpoints.back().first) | |
| return breakpoints.back().second; | |
| for (auto it = breakpoints.cbegin(); it <= breakpoints.cend(); ++it) | |
| { | |
| auto next = it; | |
| ++next; | |
| if (next == breakpoints.cend()) | |
| return it->second; | |
| if ( samplePosition >= it->first && samplePosition < next->first ) | |
| { | |
| if (interpolation == Interpolation::discrete || next->first == it->first) | |
| return it->second; | |
| //FIXME: add smooth interpolation | |
| return it->second + (samplePosition - it->first) * (next->second - it->second) / (next->first - it->first); | |
| } | |
| } | |
| return breakpoints.back().second; | |
| } | |
| const std::vector<Breakpoint>& getBreakpoints() const | |
| { | |
| return breakpoints; | |
| } | |
| void setInterpolation (Interpolation i) | |
| { | |
| ScopedLock lock (threadLock); | |
| interpolation = i; | |
| } | |
| Interpolation getInterpolation() const | |
| { | |
| ScopedLock lock (threadLock); | |
| return interpolation; | |
| } | |
| private: | |
| std::vector<Breakpoint> breakpoints; | |
| Interpolation interpolation = Interpolation::linear; | |
| CriticalSection threadLock; | |
| JUCE_DECLARE_WEAK_REFERENCEABLE (Envelope) | |
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Envelope) | |
| }; | |
| class EnvelopeEditor : public Component | |
| { | |
| public: | |
| EnvelopeEditor(); | |
| ~EnvelopeEditor(); | |
| void setReadOnly (bool readonly); | |
| void setTransformation (std::function<float(float)> transform); | |
| void setInversTransformation (std::function<float(float)> transform); | |
| void setMinMaxValue (float min, float max); | |
| void setCurveColour (Colour colour); | |
| void setEnvelope (const Envelope* envelope); | |
| void paint (Graphics&) override; | |
| bool hitTest (int x, int y) override; | |
| void mouseDown (const MouseEvent& e) override; | |
| void mouseDrag (const MouseEvent& e) override; | |
| void mouseUp (const MouseEvent& e) override; | |
| void mouseDoubleClick (const MouseEvent& e) override; | |
| private: | |
| int pixelFromTime (int sample) const; | |
| int timeFromPixel (int x) const; | |
| int pixelFromValue (float value) const; | |
| float valueFromPixel (int y) const; | |
| Point<int> pointFromBreakPoint (const Envelope::Breakpoint& breakpoint) const; | |
| int findClosestBreakPoint (const Point<int>& pos) const; | |
| WeakReference<Envelope> envelope; | |
| bool readonly = true; | |
| int dragging = -1; | |
| float minValue = 0.0f; | |
| float maxValue = 1.0f; | |
| std::function<float(float)> transform = nullptr; | |
| std::function<float(float)> inversTransform = nullptr; | |
| const int margin = 10; | |
| const int diameter = 16; | |
| Colour colour = { Colours::silver }; | |
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (EnvelopeEditor) | |
| }; | |
| #include "EnvelopeEditor.h" | |
| //============================================================================== | |
| EnvelopeEditor::EnvelopeEditor() | |
| { | |
| setOpaque (false); | |
| } | |
| EnvelopeEditor::~EnvelopeEditor() | |
| { | |
| } | |
| void EnvelopeEditor::setReadOnly (bool shouldBeReadonly) | |
| { | |
| readonly = shouldBeReadonly; | |
| repaint(); | |
| } | |
| void EnvelopeEditor::setTransformation (std::function<float(float)> transformToUse) | |
| { | |
| transform = transformToUse; | |
| } | |
| void EnvelopeEditor::setInversTransformation (std::function<float(float)> transformToUse) | |
| { | |
| inversTransform = transformToUse; | |
| } | |
| void EnvelopeEditor::setMinMaxValue (float min, float max) | |
| { | |
| minValue = min; | |
| maxValue = max; | |
| } | |
| void EnvelopeEditor::setCurveColour (Colour colourToUse) | |
| { | |
| colour = colourToUse; | |
| } | |
| void EnvelopeEditor::setEnvelope (const Envelope* envelopeToUse) | |
| { | |
| envelope = const_cast<Envelope*> (envelopeToUse); | |
| } | |
| void EnvelopeEditor::paint (Graphics& g) | |
| { | |
| if (envelope == nullptr) | |
| return; | |
| Path p; | |
| if (envelope->getInterpolation() == Envelope::Interpolation::discrete) | |
| { | |
| int pixel = pixelFromValue (envelope->getValueForSamplePosition (0)); | |
| p.startNewSubPath (margin, pixel); | |
| for (const auto& point : envelope->getBreakpoints()) | |
| { | |
| p.lineTo (pixelFromTime (point.first), pixel); | |
| pixel = pixelFromValue (point.second); | |
| p.lineTo (pixelFromTime (point.first), pixel); | |
| } | |
| p.lineTo (getRight() - margin, pixel); | |
| } | |
| else if (envelope->getInterpolation() == Envelope::Interpolation::linear) | |
| { | |
| p.startNewSubPath (margin, pixelFromValue (envelope->getValueForSamplePosition (0))); | |
| for (const auto& point : envelope->getBreakpoints()) | |
| p.lineTo (pixelFromTime (point.first), pixelFromValue (point.second)); | |
| } | |
| else if (envelope->getInterpolation() == Envelope::Interpolation::smooth) | |
| { | |
| const auto duration = envelope->getDurationSamples(); | |
| const auto step = 0.6 * duration / (getWidth() - 2.0 * margin); | |
| p.startNewSubPath (margin, pixelFromValue (envelope->getValueForSamplePosition (0))); | |
| for (double samplePos = step; samplePos <= duration; samplePos += step) | |
| p.lineTo (pixelFromTime (samplePos), | |
| pixelFromValue (envelope->getValueForSamplePosition (samplePos))); | |
| } | |
| g.setColour (colour); | |
| g.strokePath (p, PathStrokeType (1.0)); | |
| for (const auto& point : envelope->getBreakpoints()) | |
| { | |
| const auto rect = Rectangle<int> {0, 0, 10, 10}.withCentre ({ | |
| pixelFromTime (point.first), | |
| pixelFromValue (point.second)}).toFloat(); | |
| g.setColour (Colours::grey); | |
| g.fillEllipse (rect); | |
| g.setColour (Colours::green); | |
| g.drawEllipse (rect, 1.0f); | |
| } | |
| } | |
| void EnvelopeEditor::mouseDown (const MouseEvent& e) | |
| { | |
| if (envelope == nullptr) | |
| return; | |
| dragging = findClosestBreakPoint (e.getPosition()); | |
| if (dragging >= 0) | |
| { | |
| if (e.mods.isCtrlDown()) | |
| envelope->deleteBreakPoint (dragging); | |
| repaint(); | |
| return; | |
| } | |
| const auto sample = timeFromPixel (e.position.getX()); | |
| if (std::abs (pixelFromValue (envelope->getValueForSamplePosition (sample)) - e.position.getY()) < diameter / 2) | |
| { | |
| dragging = envelope->insertBreakpoint(sample, envelope->getValueForSamplePosition (sample)); | |
| repaint(); | |
| } | |
| } | |
| void EnvelopeEditor::mouseDrag (const MouseEvent& e) | |
| { | |
| if (envelope == nullptr || dragging < 0) | |
| return; | |
| envelope->moveBreakPoint (dragging, timeFromPixel (e.position.x), valueFromPixel (e.position.y)); | |
| repaint(); | |
| } | |
| void EnvelopeEditor::mouseUp (const MouseEvent& e) | |
| { | |
| dragging = -1; | |
| } | |
| void EnvelopeEditor::mouseDoubleClick (const MouseEvent& e) | |
| { | |
| if (envelope == nullptr) | |
| return; | |
| auto index = findClosestBreakPoint (e.getPosition()); | |
| if (index >= 0) | |
| { | |
| envelope->deleteBreakPoint (index); | |
| repaint(); | |
| } | |
| } | |
| bool EnvelopeEditor::hitTest (int x, int y) | |
| { | |
| if (readonly) | |
| return false; | |
| if (envelope) | |
| if (std::abs (pixelFromValue (envelope->getValueForSamplePosition (timeFromPixel (x))) - y) < diameter / 2) | |
| return true; | |
| return false; | |
| } | |
| int EnvelopeEditor::pixelFromTime (int sample) const | |
| { | |
| if (envelope != nullptr) | |
| return jlimit (margin, getRight() - margin, | |
| int (margin + (double (sample) / envelope->getDurationSamples()) * (getWidth() - 2.0 * margin))); | |
| return margin; | |
| } | |
| int EnvelopeEditor::timeFromPixel (int x) const | |
| { | |
| if (envelope != nullptr) | |
| return jlimit (0, envelope->getDurationSamples(), | |
| int ((x - margin) * envelope->getDurationSamples() / (getWidth() - 2.0 * margin))); | |
| return 0; | |
| } | |
| int EnvelopeEditor::pixelFromValue (float value) const | |
| { | |
| if (envelope != nullptr && minValue != maxValue) | |
| return jlimit (margin, getBottom() - margin, | |
| int (jmap ((transform != nullptr) ? transform (value) : value, | |
| minValue, maxValue, | |
| float (getBottom() - margin), float (getY() + margin)))); | |
| return margin; | |
| } | |
| float EnvelopeEditor::valueFromPixel (int y) const | |
| { | |
| if (envelope != nullptr) | |
| return jlimit (minValue, maxValue, | |
| jmap ((inversTransform != nullptr) ? inversTransform (y) : float (y), | |
| float (getBottom() - margin), float (getY() + margin), | |
| minValue, maxValue)); | |
| return 0.0; | |
| } | |
| Point<int> EnvelopeEditor::pointFromBreakPoint (const Envelope::Breakpoint& breakpoint) const | |
| { | |
| return { | |
| pixelFromTime (breakpoint.first), | |
| pixelFromValue (breakpoint.second) }; | |
| } | |
| int EnvelopeEditor::findClosestBreakPoint (const Point<int>& pos) const | |
| { | |
| if (envelope == nullptr) | |
| return -1; | |
| int index = 0; | |
| for (const auto& bp : envelope->getBreakpoints()) | |
| { | |
| if (pos.getDistanceFrom (pointFromBreakPoint (bp)) < diameter) | |
| return index; | |
| ++index; | |
| } | |
| return -1; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment