#include "LFODisplay.h" namespace serum { void LFODisplay::setShapeData (const std::vector& data, int s) { const int newSteps = juce::jlimit (2, 64, s); const size_t dataSize = std::min (data.size(), size_t (64)); if (steps == newSteps && shapeData.size() == 64 && std::equal (data.begin(), data.begin() + dataSize, shapeData.begin()) && std::all_of (shapeData.begin() + dataSize, shapeData.end(), [] (float v) { return v == 0.0f; })) return; steps = newSteps; shapeData.assign (data.begin(), data.begin() + dataSize); shapeData.resize (64, 0.0f); repaint(); } float LFODisplay::valueAt (float phase) const noexcept { switch ((LfoShape) shape) { case LfoShape::Sine: return std::sin (phase * 6.2831853f); case LfoShape::Triangle: return 1.0f - 4.0f * std::abs (phase - 0.5f); case LfoShape::Saw: return 2.0f * phase - 1.0f; case LfoShape::Square: return (phase < 0.5f) ? 1.0f : -1.0f; case LfoShape::SampleHold: { const int step = juce::jlimit (0, 15, (int) (phase * 16.0f)); return ((step * 2654435761u) & 0xffffu) / 32768.0f - 1.0f; } case LfoShape::StepSeq: { const int idx = juce::jlimit (0, steps - 1, (int) (phase * steps)); return shapeData.empty() ? 0.0f : shapeData[(size_t) idx]; } case LfoShape::Freehand: { if (shapeData.empty()) return 0.0f; const float pos = phase * (float) (steps - 1); const int i0 = (int) pos; const int i1 = juce::jmin (i0 + 1, steps - 1); const float frac = pos - (float) i0; return shapeData[(size_t) i0] * (1.0f - frac) + shapeData[(size_t) i1] * frac; } default: return 0.0f; } } void LFODisplay::paint (juce::Graphics& g) { const auto b = getLocalBounds().toFloat(); g.setColour (juce::Colours::black.withAlpha (0.4f)); g.fillRoundedRectangle (b, 4.0f); g.setColour (theme::outline); g.drawRoundedRectangle (b.reduced (0.5f), 4.0f, 1.0f); const float midY = b.getCentreY(); g.setColour (theme::outline); g.drawLine (b.getX() + 4.0f, midY, b.getRight() - 4.0f, midY, 1.0f); // Editable shapes show step markers. if (shape == (int) LfoShape::StepSeq) { const float stepW = (b.getWidth() - 8.0f) / (float) steps; for (int i = 0; i < steps; ++i) { const float v = shapeData.empty() ? 0.0f : shapeData[(size_t) i]; const float x = b.getX() + 4.0f + stepW * i; const float y = midY - v * (b.getHeight() * 0.42f); g.setColour (theme::amber); g.fillRect (x + 1.0f, juce::jmin (y, midY), stepW - 2.0f, std::abs (midY - y)); } } juce::Path path; path.startNewSubPath (b.getX() + 4.0f, midY); const int n = 256; for (int i = 0; i <= n; ++i) { const float phase = (float) i / (float) n; const float v = valueAt (phase); const float x = b.getX() + 4.0f + phase * (b.getWidth() - 8.0f); const float y = midY - v * (b.getHeight() * 0.42f); if (i == 0) path.startNewSubPath (x, y); else path.lineTo (x, y); } g.setColour (theme::green); g.strokePath (path, juce::PathStrokeType (1.5f)); } void LFODisplay::editAt (float x, float y) { const auto b = getLocalBounds().toFloat(); const float midY = b.getCentreY(); const float normX = juce::jlimit (0.0f, 1.0f, (x - b.getX() - 4.0f) / (b.getWidth() - 8.0f)); const float v = juce::jlimit (-1.0f, 1.0f, (midY - y) / (b.getHeight() * 0.42f)); if (shapeData.empty()) shapeData.assign (64, 0.0f); if (shape == (int) LfoShape::StepSeq) { const int idx = juce::jlimit (0, steps - 1, (int) (normX * steps)); shapeData[(size_t) idx] = v; } else if (shape == (int) LfoShape::Freehand) { const int idx = juce::jlimit (0, steps - 1, (int) (normX * steps)); shapeData[(size_t) idx] = v; } else { return; } if (onEdited) onEdited (shapeData, steps); repaint(); } void LFODisplay::mouseDown (const juce::MouseEvent& e) { editAt (e.x, e.y); } void LFODisplay::mouseDrag (const juce::MouseEvent& e) { editAt (e.x, e.y); } } // namespace serum