#include "LFODisplay.h" #include "ExactCompare.h" namespace serum { void LFODisplay::setShapeData (const std::vector& data, int s) { const int newSteps = juce::jlimit (2, LFO::kShapePoints, s); const auto dataSize = (std::ptrdiff_t) std::min (data.size(), size_t (LFO::kShapePoints)); if (steps == newSteps && (int) shapeData.size() == LFO::kShapePoints && std::equal (data.begin(), data.begin() + dataSize, shapeData.begin()) && std::all_of (shapeData.begin() + dataSize, shapeData.end(), [] (float v) { return exactly (v, 0.0f); })) return; steps = newSteps; shapeData.assign (data.begin(), data.begin() + dataSize); shapeData.resize ((size_t) LFO::kShapePoints, 0.0f); repaint(); } float LFODisplay::valueAt (float phase) const noexcept { return lfoShapeValue (phase, (LfoShape) shape, steps, shapeData.data(), (int) shapeData.size(), kSampleHoldLevel); } 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)); } } // Sample and hold is stateful in the audio path, so the preview draws the // held level as a flat schematic with its hold steps marked, and labels it, // rather than inventing a sequence the synth will never play. if (shape == (int) LfoShape::SampleHold) { const float stepW = (b.getWidth() - 8.0f) / (float) steps; g.setColour (theme::outline); for (int i = 1; i < steps; ++i) { const float x = b.getX() + 4.0f + stepW * (float) i; g.drawLine (x, midY - b.getHeight() * 0.28f, x, midY + b.getHeight() * 0.28f, 1.0f); } g.setColour (theme::textDim); g.setFont (10.0f); g.drawText ("S&H schematic", juce::Rectangle (b.getX(), b.getY(), b.getWidth(), 14.0f).toNearestInt(), juce::Justification::centred, false); } 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 ((size_t) LFO::kShapePoints, 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