Skip to content

Instantly share code, notes, and snippets.

@shidephen
Created December 22, 2018 04:03
Show Gist options
  • Select an option

  • Save shidephen/37b997d16315a1eb7d1aa1c7331c58e4 to your computer and use it in GitHub Desktop.

Select an option

Save shidephen/37b997d16315a1eb7d1aa1c7331c58e4 to your computer and use it in GitHub Desktop.
EnvelopeEditor juce component
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