This commit is contained in:
Armin 2026-07-23 23:40:48 +02:00
commit d21bc831e1
178 changed files with 24136 additions and 0 deletions

377
Source/Components/Buttons.h Normal file
View file

@ -0,0 +1,377 @@
/*
==============================================================================
Buttons.h
Created: 30 Jul 2024 11:30:24pm
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#include "../Utilities/ComponentUtils.h"
/** Simple wrapper around JUCE's ShapeButton for transparent disabled styling */
class CustomShapeButton final : public juce::ShapeButton, public CustomHelpTextProvider, public EnabledMouseCursor
{
public:
explicit CustomShapeButton(juce::Colour buttonColor, const juce::Path& shape = {}, CustomHelpTextDisplay* helpTextDisplay = nullptr) :
ShapeButton("", buttonColor, buttonColor, buttonColor),
CustomHelpTextProvider(this, helpTextDisplay),
color(buttonColor)
{
ShapeButton::setShape(shape, false, true, false);
}
explicit CustomShapeButton(const juce::Path& shape = {}, CustomHelpTextDisplay* helpTextDisplay = nullptr) :
CustomShapeButton(juce::Colours::transparentBlack, shape, helpTextDisplay)
{
}
void setColor(juce::Colour buttonColor)
{
color = buttonColor;
setColours(buttonColor, buttonColor, buttonColor);
enablementChanged();
}
void setShape(const juce::Path& newShape)
{
ShapeButton::setShape(newShape, false, true, false);
repaint();
}
private:
void enablementChanged() override
{
auto adjustedColor = color.withMultipliedAlpha(isEnabled() ? 1.f : 0.5f);
setColours(adjustedColor, adjustedColor, adjustedColor);
EnabledMouseCursor::enablementChanged(*this);
repaint();
}
juce::Colour color;
};
/** A nicely styled button that can be toggled on and off. An owner button can be set to "own" this button,
meaning this button is only on when the owner button is on.
*/
class CustomToggleableButton final : public juce::Button, public juce::Button::Listener, public CustomHelpTextProvider, public EnabledMouseCursor
{
public:
/** Create a new button with the given colors.
altStyle keeps the text the same while changing the background, onBackground removes the shadow and sets a background when on
*/
CustomToggleableButton(juce::Colour offColor, juce::Colour onColor, bool altStyle = false, bool useOnBackground = false, CustomHelpTextDisplay* helpTextDisplay = nullptr) :
Button(""), CustomHelpTextProvider(this, helpTextDisplay),
offColor(offColor), onColor(onColor), altStyle(altStyle), onBackground(useOnBackground)
{
setClickingTogglesState(true);
}
CustomToggleableButton(bool altStyle = false, bool useOnBackground = false, CustomHelpTextDisplay* helpTextDisplay = nullptr) :
CustomToggleableButton(juce::Colours::transparentBlack, juce::Colours::transparentBlack, altStyle, useOnBackground, helpTextDisplay)
{
}
~CustomToggleableButton() override
{
if (ownerButton)
ownerButton->removeListener(this);
}
void setColors(juce::Colour off, juce::Colour on, juce::Colour shadow = defaultTheme.dark.withAlpha(0.25f))
{
offColor = off;
onColor = on;
onShadow.setColor(shadow);
repaint();
}
/** An owner button is a button that will be toggled on when this button is toggled but can also be toggled independently. */
void setOwnerButton(Button* owner)
{
ownerButton = owner;
owner->addListener(this);
if (!owner->getToggleState())
setClickingTogglesState(false);
}
/** Use x, y, width, height to set the left, top, right, and bottom bounds of the button */
void setBorder(float width, float rounding = 0.f, bool roundTopLeft = true, bool roundTopRight = true, bool roundBottomLeft = true, bool roundBottomRight = true)
{
borderWidth = width;
borderRounding = rounding;
topLeft = roundTopLeft;
topRight = roundTopRight;
bottomLeft = roundBottomLeft;
bottomRight = roundBottomRight;
}
void setPadding(float amount)
{
setPadding(amount, amount, amount, amount);
}
void setPadding(float paddingLeft, float paddingRight, float paddingTop, float paddingBottom)
{
padLeft = paddingLeft;
padRight = paddingRight;
padTop = paddingTop;
padBottom = paddingBottom;
}
void useShape(const juce::Path& shapePath, const juce::Path& offShapePath = {}, juce::Justification justification = juce::Justification::centred)
{
shape = shapePath;
offShape = offShapePath;
shapeJustification = justification;
}
private:
void buttonStateChanged(Button* button) override
{
if (button == ownerButton)
{
setClickingTogglesState(ownerButton->getToggleState());
if (getToggleState())
setState(ownerButton->getState());
repaint();
}
}
void buttonClicked(Button*) override {}
void clicked() override
{
if (ownerButton && !ownerButton->getToggleState())
{
ownerButton->setToggleState(true, juce::sendNotificationSync);
ownerButton->setState(buttonNormal);
setToggleState(true, juce::dontSendNotification);
repaint();
}
}
void buttonStateChanged() override
{
if (ownerButton && !ownerButton->getToggleState())
{
ownerButton->setState(getState());
ownerButton->repaint();
}
}
void paintButton(juce::Graphics& g, bool /*shouldDrawButtonAsHighlighted*/, bool shouldDrawButtonAsDown) override
{
bool toggledOn = getToggleState() && (!ownerButton || ownerButton->getToggleState());
bool drawAsOn = toggledOn != shouldDrawButtonAsDown;
auto bounds = getLocalBounds().toFloat();
if (drawAsOn)
{
juce::Path background;
auto pathBounds = bounds.reduced(borderWidth * 0.33f);
background.addRoundedRectangle(pathBounds.getX(), pathBounds.getY(), pathBounds.getWidth(), pathBounds.getHeight(),
borderRounding, borderRounding, topLeft, topRight, bottomLeft, bottomRight);
g.setColour(altStyle ? onColor : offColor);
g.fillPath(background);
if (!onBackground)
onShadow.render(g, background);
}
else if (!onBackground)
{
juce::Path border;
auto pathBounds = bounds.reduced(borderWidth * 0.66f);
border.addRoundedRectangle(pathBounds.getX(), pathBounds.getY(), pathBounds.getWidth(), pathBounds.getHeight(),
borderRounding, borderRounding, topLeft, topRight, bottomLeft, bottomRight);
g.setColour(offColor);
g.strokePath(border, juce::PathStrokeType(borderWidth));
}
bounds.removeFromLeft(padLeft);
bounds.removeFromRight(padRight);
bounds.removeFromTop(padTop);
bounds.removeFromBottom(padBottom);
auto useShape = !drawAsOn && !offShape.isEmpty() ? offShape : shape;
auto trans = useShape.getTransformToScaleToFit(bounds, true, shapeJustification);
g.setColour(drawAsOn && !altStyle ? onColor : offColor);
if (!useShape.isEmpty())
{
g.fillPath(useShape, trans);
}
else if (getButtonText().isNotEmpty())
{
g.setFont(getInterBold().withHeight(getHeight() * 0.381f));
g.drawText(getButtonText(), getLocalBounds(), juce::Justification::centred);
}
if (!isEnabled())
{
g.setColour(findColour(Colors::backgroundColorId, true).withAlpha(0.5f));
g.fillRect(getLocalBounds());
}
}
void enablementChanged() override
{
EnabledMouseCursor::enablementChanged(*this);
repaint();
}
//==============================================================================
juce::Colour offColor, onColor;
bool altStyle{ false }, onBackground{ false };
float borderWidth{ 1.f };
float borderRounding{ 0.f };
bool topLeft{ true }, topRight{ true }, bottomLeft{ true }, bottomRight{ true };
float padLeft{ 0.f }, padRight{ 0.f }, padTop{ 0.f }, padBottom{ 0.f };
juce::Path shape, offShape;
juce::Justification shapeJustification{ juce::Justification::centred };
melatonin::InnerShadow onShadow{ defaultTheme.dark.withAlpha(0.25f), 3, {0, 2} };
Button* ownerButton{ nullptr };
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomToggleableButton)
};
/** Two buttons that act as a nicely styled toggle. */
class CustomChoiceButton final : public CustomComponent
{
public:
CustomChoiceButton(const APVTS& apvts, const juce::Identifier& parameter, juce::Colour offColor, juce::Colour onColor,
const juce::String& firstButtonText, const juce::String& secondButtonText, CustomHelpTextDisplay* helpTextDisplay = nullptr) :
CustomComponent(helpTextDisplay),
firstButton(offColor, onColor), secondButton(offColor, onColor),
choiceAttachment(*apvts.getParameter(parameter), [this](float newValue) { valueChanged(newValue); }, apvts.undoManager),
offColor(offColor), onColor(onColor)
{
setPaintingIsUnclipped(true); // Stop border from clipping
firstButton.setButtonText(firstButtonText);
secondButton.setButtonText(secondButtonText);
firstButton.onClick = [this] { choiceAttachment.setValueAsCompleteGesture(0.f); };
secondButton.onClick = [this] { choiceAttachment.setValueAsCompleteGesture(1.f); };
firstButton.onStateChange = [this] { secondButton.setState(firstButton.getState()); };
secondButton.onStateChange = [this] { firstButton.setState(secondButton.getState()); };
addAndMakeVisible(firstButton);
addAndMakeVisible(secondButton);
choiceAttachment.sendInitialUpdate();
}
CustomChoiceButton(const APVTS& apvts, const juce::Identifier& parameter, const juce::String& firstButtonText, const juce::String& secondButtonText,
CustomHelpTextDisplay* helpTextDisplay = nullptr) :
CustomChoiceButton(apvts, parameter, juce::Colours::transparentBlack, juce::Colours::transparentBlack, firstButtonText, secondButtonText, helpTextDisplay)
{
}
void setColors(juce::Colour off, juce::Colour on, juce::Colour shadow = defaultTheme.dark.withAlpha(0.25f))
{
offColor = off;
onColor = on;
firstButton.setColors(off, on, shadow);
secondButton.setColors(off, on, shadow);
repaint();
}
void setBorder(float width, float rounding = 0.f)
{
borderWidth = width;
borderRounding = rounding;
repaint();
}
bool getChoice() const
{
return secondButton.getToggleState();
}
void setHelpText(const juce::String& firstHelpText, const juce::String& secondHelpText)
{
firstButton.setHelpText(firstHelpText);
secondButton.setHelpText(secondHelpText);
}
juce::String getCustomHelpText() override
{
return getChoice() ? secondButton.getHelpText() : firstButton.getHelpText();
}
private:
void paint(juce::Graphics& g) override
{
auto bounds = getLocalBounds().toFloat();
firstButton.setBorder(0.f, borderRounding, true, false, true, false);
secondButton.setBorder(0.f, borderRounding, false, true, false, true);
g.setColour(offColor);
g.drawRoundedRectangle(bounds.reduced(borderWidth / 2.f), borderRounding, borderWidth);
}
void resized() override
{
auto bounds = getLocalBounds();
firstButton.setBounds(bounds.removeFromLeft(int(bounds.getWidth() / 2.f)));
secondButton.setBounds(bounds);
}
void valueChanged(float newValue)
{
firstButton.setToggleState(newValue < 0.5f, juce::dontSendNotification);
secondButton.setToggleState(newValue > 0.5f, juce::dontSendNotification);
firstButton.setInterceptsMouseClicks(!firstButton.getToggleState(), false);
secondButton.setInterceptsMouseClicks(!secondButton.getToggleState(), false);
sendHelpTextUpdate(false);
}
void enablementChanged() override
{
if (!isEnabled())
{
firstButton.setInterceptsMouseClicks(false, false);
secondButton.setInterceptsMouseClicks(false, false);
}
else
{
choiceAttachment.sendInitialUpdate();
}
repaint();
}
//==============================================================================
CustomToggleableButton firstButton;
CustomToggleableButton secondButton;
juce::ParameterAttachment choiceAttachment;
juce::Colour offColor, onColor;
float borderWidth{ 1.f }, borderRounding{ 0.f };
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(CustomChoiceButton)
};

View file

@ -0,0 +1,62 @@
/*
==============================================================================
ChorusVisualizer.cpp
Created: 30 Jan 2024 6:12:49pm
Author: binya
==============================================================================
*/
#include <JuceHeader.h>
#include "ChorusVisualizer.h"
ChorusVisualizer::ChorusVisualizer(APVTS& apvts, int sampleRate) :
apvts(apvts), sampleRate(sampleRate),
rateAttachment(*apvts.getParameter(PluginParameters::CHORUS_RATE), [this](float newValue) { rate = newValue; repaint(); }, apvts.undoManager),
depthAttachment(*apvts.getParameter(PluginParameters::CHORUS_DEPTH), [this](float newValue) { depth = newValue; repaint(); }, apvts.undoManager),
centerDelayAttachment(*apvts.getParameter(PluginParameters::CHORUS_CENTER_DELAY), [this](float newValue) { centerDelay = newValue; repaint(); }, apvts.undoManager)
{
rateAttachment.sendInitialUpdate();
depthAttachment.sendInitialUpdate();
centerDelayAttachment.sendInitialUpdate();
}
void ChorusVisualizer::paint(juce::Graphics& g)
{
auto theme = getTheme();
juce::Path path;
constexpr int pointsPerPixel = 10;
for (int i = 0; i < getWidth() * pointsPerPixel; i++)
{
float pos = float(i) / pointsPerPixel;
float lfo = std::sin(WINDOW_LENGTH * rate * pos / getWidth() * 2 * juce::MathConstants<float>::pi);
float skewedDepth = logf(depth) + b;
float delay = centerDelay * 0.98f + skewedDepth * lfo * multiplier;
float loc = juce::jmap<float>(delay, -oscRange, oscRange, float(getHeight()), 0.f);
if (i == 0)
path.startNewSubPath(pos, loc);
else
path.lineTo(pos, loc);
}
auto strokeWidth = getWidth() * Layout::fxDisplayStrokeWidth;
g.setColour(theme.dark.withAlpha(0.2f));
g.drawRect(0.f, (getHeight() - strokeWidth) / 2.f, float(getWidth()), strokeWidth);
g.setColour(theme.dark);
g.strokePath(path, juce::PathStrokeType(strokeWidth));
if (!isEnabled())
g.fillAll(theme.background.withAlpha(0.5f));
}
void ChorusVisualizer::enablementChanged()
{
repaint();
}

View file

@ -0,0 +1,43 @@
/*
==============================================================================
ChorusVisualizer.h
Created: 30 Jan 2024 6:12:49pm
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#include "../../PluginParameters.h"
#include "../../Utilities/ComponentUtils.h"
/** A simple chorus visualizer, which displays a sine wave representing the delay line. */
class ChorusVisualizer final : public CustomComponent
{
public:
ChorusVisualizer(APVTS& apvts, int sampleRate);
private:
void paint(juce::Graphics&) override;
void enablementChanged() override;
//==============================================================================
static constexpr float WINDOW_LENGTH{ 1.5f };
static constexpr float b{ 5.f };
static constexpr float multiplier{ 25.f };
static constexpr float oscRange{ b * multiplier + PluginParameters::CHORUS_CENTER_DELAY_RANGE.getEnd() };
//==============================================================================
APVTS& apvts;
int sampleRate;
bool shouldRepaint{ false };
float rate{ 0.f }, depth{ 0.f }, centerDelay{ 0.f }; /*feedback{ 0.f }, mix{ 0.f }*/
juce::ParameterAttachment rateAttachment, depthAttachment, centerDelayAttachment;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ChorusVisualizer)
};

View file

@ -0,0 +1,61 @@
/*
==============================================================================
DistortionVisualizer.cpp
Created: 23 Jan 2024 9:12:52pm
Author: binya
==============================================================================
*/
#include <JuceHeader.h>
#include "DistortionVisualizer.h"
DistortionVisualizer::DistortionVisualizer(APVTS& apvts, int sampleRate) : apvts(apvts), inputBuffer(1, WINDOW_LENGTH),
densityAttachment(*apvts.getParameter(PluginParameters::DISTORTION_DENSITY), [this](float newValue) { distortionDensity = newValue; repaint(); }, apvts.undoManager),
mixAttachment(*apvts.getParameter(PluginParameters::DISTORTION_MIX), [this](float newValue) { distortionMix = newValue; repaint(); }, apvts.undoManager)
{
densityAttachment.sendInitialUpdate();
mixAttachment.sendInitialUpdate();
distortion.initialize(1, sampleRate);
}
void DistortionVisualizer::paint(juce::Graphics& g)
{
auto theme = getTheme();
// Fill the input buffer with a pre-chosen wave
for (int i = 0; i < WINDOW_LENGTH; i++)
inputBuffer.setSample(0, i, sinf(juce::MathConstants<float>::twoPi * i * SINE_HZ / WINDOW_LENGTH) + cosf(2.5f * juce::MathConstants<float>::pi * i * SINE_HZ / WINDOW_LENGTH));
distortion.updateParams(distortionDensity, 0.f, distortionMix);
distortion.process(inputBuffer, inputBuffer.getNumSamples());
auto range = inputBuffer.findMinMax(0, 0, inputBuffer.getNumSamples()).getLength() / 2.f;
// Draw the resulting waveform
juce::Path path;
int numPointsPerPixel = 10;
for (int i = 0; i < getWidth() * numPointsPerPixel; i++)
{
float pos = float(i) / numPointsPerPixel;
float sample = inputBuffer.getSample(0, int(pos * WINDOW_LENGTH / getWidth()));
float y = juce::jmap<float>(sample, -range, range, float(getHeight()) * 0.98f, getHeight() * 0.02f);
if (i == 0)
path.startNewSubPath(0, y);
else
path.lineTo(float(pos), y);
}
auto strokeWidth = getWidth() * Layout::fxDisplayStrokeWidth;
g.setColour(theme.dark);
g.strokePath(path, juce::PathStrokeType(strokeWidth));
if (!isEnabled())
g.fillAll(theme.background.withAlpha(0.5f));
}
void DistortionVisualizer::enablementChanged()
{
repaint();
}

View file

@ -0,0 +1,42 @@
/*
==============================================================================
DistortionVisualizer.h
Created: 23 Jan 2024 9:12:52pm
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#include "../../Sampler/Effects/Distortion.h"
#include "../../Utilities/ComponentUtils.h"
/** Visualizes the distortion effect by displaying its result on a sine wave. */
class DistortionVisualizer final : public CustomComponent
{
public:
DistortionVisualizer(APVTS& apvts, int sampleRate);
private:
void paint(juce::Graphics&) override;
void enablementChanged() override;
//==============================================================================
static constexpr int WINDOW_LENGTH{ 30000 };
static constexpr float SINE_HZ{ 13.f };
//==============================================================================
APVTS& apvts;
juce::AudioBuffer<float> inputBuffer;
Distortion distortion;
float distortionDensity{ 0.f }, distortionMix{ 0.f }; /*distortionHighpass{ 0.f }*/
juce::ParameterAttachment densityAttachment, mixAttachment;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DistortionVisualizer)
};

View file

@ -0,0 +1,207 @@
/*
==============================================================================
FilterResponse.cpp
Created: 2 Jan 2024 10:40:21pm
Author: binya
==============================================================================
*/
#include <JuceHeader.h>
#include "FilterResponse.h"
FilterResponse::FilterResponse(APVTS& apvts, int sampleRate) : apvts(apvts),
lowFreqAttachment(*apvts.getParameter(PluginParameters::EQ_LOW_FREQ), [&](float newValue) { lowFreq = newValue; repaint(); }, apvts.undoManager),
highFreqAttachment(*apvts.getParameter(PluginParameters::EQ_HIGH_FREQ), [&](float newValue) { highFreq = newValue; repaint(); }, apvts.undoManager),
lowGainAttachment(*apvts.getParameter(PluginParameters::EQ_LOW_GAIN), [&](float newValue) { lowGain = newValue; repaint(); }, apvts.undoManager),
midGainAttachment(*apvts.getParameter(PluginParameters::EQ_MID_GAIN), [&](float newValue) { midGain = newValue; repaint(); }, apvts.undoManager),
highGainAttachment(*apvts.getParameter(PluginParameters::EQ_HIGH_GAIN), [&](float newValue) { highGain = newValue; repaint(); }, apvts.undoManager)
{
eq.initialize(1, sampleRate);
lowFreqAttachment.sendInitialUpdate();
highFreqAttachment.sendInitialUpdate();
lowGainAttachment.sendInitialUpdate();
midGainAttachment.sendInitialUpdate();
highGainAttachment.sendInitialUpdate();
}
void FilterResponse::paint(juce::Graphics& g)
{
using namespace juce;
auto theme = getTheme();
auto bounds = getLocalBounds().toFloat();
Array<double> frequencies;
for (int i = 0; i < bounds.getWidth(); i++)
frequencies.add(posToFreq(bounds, float(i)));
eq.updateParams(lowFreq, highFreq, lowGain, midGain, highGain);
Array<double> magnitudes = eq.getMagnitudeForFrequencyArray(frequencies);
auto gainRange = PluginParameters::EQ_GAIN_RANGE;
Path path;
path.startNewSubPath(bounds.getBottomLeft());
for (int i = 0; i < bounds.getWidth(); i++)
{
float normalizedDecibel = jmap<float>(float(Decibels::gainToDecibels(magnitudes[i])), gainRange.start - 1.f, gainRange.end + 1.f, bounds.getHeight(), 0.f);
if (i == 0)
path.startNewSubPath(0, normalizedDecibel);
else
path.lineTo(bounds.getX() + i, normalizedDecibel);
}
auto borderWidth = getWidth() * Layout::fxDisplayStrokeWidth * 1.5f;
g.setColour(theme.dark);
g.strokePath(path, PathStrokeType{ borderWidth, PathStrokeType::curved });
auto boundsPad = getWidth() * Layout::fxDisplayStrokeWidth * 6.f;
int lowLoc = int(freqToPos(bounds, lowFreq));
float curveLowPos = jmap<float>(float(Decibels::gainToDecibels(magnitudes[lowLoc])), gainRange.start, gainRange.end, bounds.getHeight() * 0.98f, 0.02f);
g.setColour(dragging && draggingTarget == LOW_FREQ ? theme.slate.withAlpha(0.5f) : theme.slate);
if (float height = curveLowPos - boundsPad; height > 0.f)
g.fillRect(lowLoc - borderWidth / 2.f, 0.f, borderWidth, height);
if (float height = getHeight() - curveLowPos - boundsPad; height > 0.f)
g.fillRect(lowLoc - borderWidth / 2.f, curveLowPos + boundsPad, borderWidth, height);
int highLoc = int(freqToPos(bounds, highFreq));
float curveHighPos = jmap<float>(float(Decibels::gainToDecibels(magnitudes[highLoc])), gainRange.start, gainRange.end, bounds.getHeight() * 0.98f, 0.02f);
g.setColour(dragging && draggingTarget == HIGH_FREQ ? theme.slate.withAlpha(0.5f) : theme.slate);
if (float height = curveHighPos - boundsPad; height > 0.f)
g.fillRect(highLoc - borderWidth / 2.f, 0.f, borderWidth, height);
if (float height = getHeight() - curveHighPos - boundsPad; height > 0.f)
g.fillRect(highLoc - borderWidth / 2.f, curveHighPos + boundsPad, borderWidth, height);
if (!isEnabled())
g.fillAll(theme.background.withAlpha(0.5f));
}
void FilterResponse::enablementChanged()
{
repaint();
}
void FilterResponse::mouseMove(const juce::MouseEvent& event)
{
if (!isEnabled())
{
setMouseCursor(juce::MouseCursor::NormalCursor);
return;
}
FilterResponseParts part = getClosestPartInRange(event.x, event.y);
switch (part)
{
case LOW_FREQ:
case HIGH_FREQ:
setMouseCursor(juce::MouseCursor::LeftRightResizeCursor);
break;
case NONE:
setMouseCursor(juce::MouseCursor::NormalCursor);
break;
}
}
void FilterResponse::mouseDown(const juce::MouseEvent& event)
{
if (!isEnabled() || dragging)
return;
FilterResponseParts closest = getClosestPartInRange(event.getMouseDownX(), event.getMouseDownY());
if (closest == NONE)
return;
if (closest == LOW_FREQ)
lowFreqAttachment.beginGesture();
else if (closest == HIGH_FREQ)
highFreqAttachment.beginGesture();
dragging = true;
draggingTarget = closest;
repaint();
}
void FilterResponse::mouseUp(const juce::MouseEvent&)
{
if (!dragging)
return;
if (draggingTarget == LOW_FREQ)
lowFreqAttachment.endGesture();
else if (draggingTarget == HIGH_FREQ)
highFreqAttachment.endGesture();
dragging = false;
repaint();
}
void FilterResponse::mouseDrag(const juce::MouseEvent& event)
{
if (!dragging || !isEnabled())
return;
auto bounds = getLocalBounds().toFloat();
auto newFreq = posToFreq(bounds, float(event.getMouseDownX() + event.getOffsetFromDragStart().getX()));
auto freqStartBound = draggingTarget == LOW_FREQ ? PluginParameters::EQ_LOW_FREQ_RANGE.getStart() : PluginParameters::EQ_HIGH_FREQ_RANGE.getStart();
auto freqEndBound = draggingTarget == LOW_FREQ ? PluginParameters::EQ_LOW_FREQ_RANGE.getEnd() : PluginParameters::EQ_HIGH_FREQ_RANGE.getEnd();
newFreq = juce::jlimit<float>(freqStartBound, freqEndBound, newFreq);
switch (draggingTarget)
{
case LOW_FREQ:
lowFreqAttachment.setValueAsPartOfGesture(newFreq);
break;
case HIGH_FREQ:
highFreqAttachment.setValueAsPartOfGesture(newFreq);
break;
}
}
void FilterResponse::mouseDoubleClick(const juce::MouseEvent& event)
{
auto part = getClosestPartInRange(event.getMouseDownX(), event.getMouseDownY());
switch (part)
{
case LOW_FREQ:
lowFreqAttachment.setValueAsCompleteGesture(PluginParameters::EQ_LOW_FREQ_DEFAULT);
break;
case HIGH_FREQ:
highFreqAttachment.setValueAsCompleteGesture(PluginParameters::EQ_HIGH_FREQ_DEFAULT);
break;
}
}
FilterResponseParts FilterResponse::getClosestPartInRange(int x, int y) const
{
auto bounds = getLocalBounds().toFloat();
juce::Array targets = {
CompPart {LOW_FREQ, juce::Rectangle<float>(freqToPos(bounds, lowFreq), bounds.getY(), 0, bounds.getHeight()), 1},
CompPart {HIGH_FREQ, juce::Rectangle<float>(freqToPos(bounds, highFreq), bounds.getY(), 0, bounds.getHeight()), 1},
};
return CompPart<FilterResponseParts>::getClosestInRange(targets, x, y, Feel::DRAGGABLE_SNAP);
}
float FilterResponse::freqToPos(juce::Rectangle<float> bounds, float freq) const
{
return (bounds.getWidth() - 1) * logf(freq / startFreq) / logf(float(endFreq) / startFreq);
}
float FilterResponse::posToFreq(juce::Rectangle<float> bounds, float pos) const
{
return startFreq * powf(float(endFreq) / startFreq, float(pos) / (bounds.getWidth() - 1.f));
}
juce::String FilterResponse::getCustomHelpText()
{
auto part = dragging ? draggingTarget : getClosestPartInRange(getMouseXYRelative().getX(), getMouseXYRelative().getY());
switch (part)
{
case LOW_FREQ: return PluginParameters::EQ_LOW_FREQ + ": " + juce::String(int(lowFreq)) + " " + PluginParameters::FREQUENCY_UNIT;
case HIGH_FREQ: return PluginParameters::EQ_HIGH_FREQ + ": " + juce::String(int(highFreq)) + " " + PluginParameters::FREQUENCY_UNIT;
default: return "Drag to adjust filter cutoffs";
}
}

View file

@ -0,0 +1,61 @@
/*
==============================================================================
FilterResponse.h
Created: 2 Jan 2024 10:40:21pm
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#include "../../Sampler/Effects/BandEQ.h"
#include "../../Utilities/ComponentUtils.h"
enum FilterResponseParts
{
NONE,
LOW_FREQ,
HIGH_FREQ
};
class FilterResponse final : public CustomComponent
{
public:
FilterResponse(APVTS& apvts, int sampleRate);
private:
void paint(juce::Graphics&) override;
void enablementChanged() override;
void mouseMove(const juce::MouseEvent& event) override;
void mouseDown(const juce::MouseEvent& event) override;
void mouseUp(const juce::MouseEvent& event) override;
void mouseDrag(const juce::MouseEvent& event) override;
void mouseDoubleClick(const juce::MouseEvent& event) override;
FilterResponseParts getClosestPartInRange(int x, int y) const;
float freqToPos(juce::Rectangle<float> bounds, float freq) const;
float posToFreq(juce::Rectangle<float> bounds, float pos) const;
juce::String getCustomHelpText() override;
//==============================================================================
constexpr static int startFreq{ 20 };
constexpr static int endFreq{ 17500 };
//==============================================================================
APVTS& apvts;
BandEQ eq;
float lowFreq{ 0 }, highFreq{ 0 }, lowGain{ 0 }, midGain{ 0 }, highGain{ 0 };
juce::ParameterAttachment lowFreqAttachment, highFreqAttachment, lowGainAttachment, midGainAttachment, highGainAttachment;
bool dragging{ false };
FilterResponseParts draggingTarget{ NONE };
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FilterResponse)
};

View file

@ -0,0 +1,150 @@
/*
==============================================================================
ReverbResponse.cpp
Created: 17 Jan 2024 11:16:38pm
Author: binya
==============================================================================
*/
#include <JuceHeader.h>
#include "ReverbResponse.h"
ReverbResponse::ReverbResponse(APVTS& apvts) : apvts(apvts),
lowsAttachment(*apvts.getParameter(PluginParameters::REVERB_LOWS), [this](float newValue) { lows = newValue; repaint(); }, apvts.undoManager),
highsAttachment(*apvts.getParameter(PluginParameters::REVERB_HIGHS), [this](float newValue) { highs = newValue; repaint(); }, apvts.undoManager),
mixAttachment(*apvts.getParameter(PluginParameters::REVERB_MIX), [this](float newValue) { mix = newValue; repaint(); }, apvts.undoManager),
responseThread(apvts, sampleRate),
response(1, int(ReverbResponseThread::DISPLAY_TIME * sampleRate / ReverbResponseThread::SAMPLE_RATE_RATIO))
{
lowsAttachment.sendInitialUpdate();
highsAttachment.sendInitialUpdate();
mixAttachment.sendInitialUpdate();
response.clear();
responseThread.startThread();
setBufferedToImage(true);
startTimerHz(30);
}
void ReverbResponse::paint(juce::Graphics& g)
{
auto bounds = getLocalBounds().toFloat();
auto theme = getTheme();
// Calculate the values
float numPointsPerPixel = 0.5f;
int numPoints = int(numPointsPerPixel * getWidth());
int samplesPerPoint = int(response.getNumSamples() / numPoints);
juce::Array<float> responseValues;
for (int i = 0; i < numPoints; i++)
{
int sampleIndex = i * samplesPerPoint;
float rms = response.getRMSLevel(0, sampleIndex, juce::jmin(samplesPerPoint, response.getNumSamples() - sampleIndex));
float multiplier = 0.2f + 0.4f * lows + 0.4f * highs;
float yPos = juce::jmap<float>(rms * multiplier, 0.f, 0.7f, bounds.getHeight() / 2.f, getHeight() * 0.02f);
responseValues.add(yPos);
}
// Draw the response
juce::Path rmsPath;
for (int i = 0; i < numPoints; i++)
{
float x = float(i) / numPointsPerPixel;
float y = responseValues[i];
if (i == 0)
rmsPath.startNewSubPath(x, y);
else
rmsPath.lineTo(x, y);
}
for (int i = numPoints - 1; i >= 0; i--)
{
float x = float(i) / numPointsPerPixel;
float y = responseValues[i];
rmsPath.lineTo(x, getHeight() - y);
}
rmsPath.closeSubPath();
g.setColour(theme.dark);
g.fillRect(0.f, mix * getHeight() / 2.f, getWidth() * 0.01f, float(getHeight()) * (1 - mix));
g.fillPath(rmsPath);
if (!isEnabled())
g.fillAll(theme.background.withAlpha(0.5f));
}
void ReverbResponse::enablementChanged()
{
repaint();
}
void ReverbResponse::timerCallback()
{
// Process the response thread changes
bool changesMade{ false };
while (responseThread.getResponseChangeQueue().peek() != nullptr)
{
response = *responseThread.getResponseChangeQueue().peek();
responseThread.getResponseChangeQueue().pop();
changesMade = true;
}
if (changesMade)
repaint();
}
ReverbResponseThread::ReverbResponseThread(const APVTS& apvts, int sampleRate) : Thread("Reverb_Response_Thread"), sampleRate(sampleRate),
sizeAttachment(*apvts.getParameter(PluginParameters::REVERB_SIZE), [this](float value) { size = value; reverbChanged = true; notify(); }, apvts.undoManager),
dampingAttachment(*apvts.getParameter(PluginParameters::REVERB_DAMPING), [this](float value) { damping = value; reverbChanged = true; notify(); }, apvts.undoManager),
delayAttachment(*apvts.getParameter(PluginParameters::REVERB_PREDELAY), [this](float value) { delay = value; reverbChanged = true; notify(); }, apvts.undoManager),
mixAttachment(*apvts.getParameter(PluginParameters::REVERB_MIX), [this](float value) { mix = value; reverbChanged = true; notify(); }, apvts.undoManager),
impulse(1, int(DISPLAY_TIME * sampleRate / SAMPLE_RATE_RATIO))
{
sizeAttachment.sendInitialUpdate();
dampingAttachment.sendInitialUpdate();
delayAttachment.sendInitialUpdate();
mixAttachment.sendInitialUpdate();
initializeImpulse();
}
ReverbResponseThread::~ReverbResponseThread()
{
stopThread(5000);
}
void ReverbResponseThread::initializeImpulse()
{
// Exponential chirp (seems to provide most accurate response)
impulse.clear();
auto impulseSize = int(IMPULSE_TIME * sampleRate / SAMPLE_RATE_RATIO);
for (int i = 1; i <= impulseSize; i++)
{
impulse.setSample(0, i - 1,
sinf(juce::MathConstants<float>::twoPi * CHIRP_START * (powf(CHIRP_END / CHIRP_START, float(i) / impulseSize) - 1.f) /
(float(impulseSize) / i * logf(CHIRP_END / CHIRP_START)))
);
}
}
void ReverbResponseThread::run()
{
while (!threadShouldExit())
{
if (!reverbChanged)
wait(-1); // Wait until the RMS value needs to be updated, and notify() is called
reverbChanged = false;
initializeImpulse();
reverb.initialize(1, juce::jmax(1000, int(sampleRate / SAMPLE_RATE_RATIO)));
reverb.updateParams(size, damping, delay, 1.f, 1.f, mix);
reverb.process(impulse, impulse.getNumSamples());
responseChangeQueue.enqueue(impulse);
}
}

View file

@ -0,0 +1,74 @@
/*
==============================================================================
ReverbResponse.h
Created: 17 Jan 2024 11:16:38pm
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#include <readerwriterqueue.h>
#include "../../Utilities/ComponentUtils.h"
#include "../../Sampler/Effects/Reverb.h"
class ReverbResponseThread final : public juce::Thread
{
public:
explicit ReverbResponseThread(const APVTS& apvts, int sampleRate);
~ReverbResponseThread() override;
void run() override;
moodycamel::ReaderWriterQueue<juce::AudioBuffer<float>, 16384>& getResponseChangeQueue() { return responseChangeQueue; }
//==============================================================================
static constexpr int DISPLAY_TIME{ 10 };
static constexpr float SAMPLE_RATE_RATIO{ 10.f };
private:
static constexpr float IMPULSE_TIME{ 0.05f };
static constexpr float CHIRP_START{ 50.f };
static constexpr float CHIRP_END{ 18000.f };
void initializeImpulse();
//==============================================================================
int sampleRate;
Reverb reverb;
std::atomic<float> size{ 0.f }, damping{ 0.f }, delay{ 0.f }, mix{ 0.f };
std::atomic<bool> reverbChanged{ false };
juce::ParameterAttachment sizeAttachment, dampingAttachment, delayAttachment, mixAttachment;
juce::AudioBuffer<float> impulse;
#pragma warning(disable: 4324) // structure was padded due to __declspec(align())
moodycamel::ReaderWriterQueue<juce::AudioBuffer<float>, 16384> responseChangeQueue;
};
class ReverbResponse final : public CustomComponent, public juce::Timer
{
public:
ReverbResponse(APVTS& apvts);
void paint (juce::Graphics&) override;
void enablementChanged() override;
void timerCallback() override;
private:
APVTS& apvts;
constexpr static int sampleRate = 48000;
float lows{ 0.f }, highs{ 0.f }, mix{ 0.f };
juce::ParameterAttachment lowsAttachment, highsAttachment, mixAttachment;
ReverbResponseThread responseThread;
juce::AudioBuffer<float> response;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ReverbResponse)
};

View file

@ -0,0 +1,453 @@
/*
==============================================================================
SamplePainter.cpp
Created: 19 Sep 2023 3:02:14pm
Author: binya
==============================================================================
*/
#include <JuceHeader.h>
#include <algorithm>
#include "SamplePainter.h"
SamplePainter::SamplePainter(ListenableAtomic<int>& primaryVisibleChannel, float resolutionScale, UIDummyParam* uiDummyParam) :
resolutionScale(resolutionScale), sampleData(2, 0),
primaryChannel(primaryVisibleChannel), dummyParam(uiDummyParam)
{
primaryChannel.addListener(this);
setBufferedToImage(true);
}
SamplePainter::~SamplePainter()
{
primaryChannel.removeListener(this);
}
void SamplePainter::paint(juce::Graphics& g)
{
if (!sample || !sample->getNumChannels() || sample->getNumSamples() <= 1 || viewEnd <= viewStart || viewStart >= sample->getNumSamples() ||
viewEnd >= sample->getNumSamples() || sample->getNumSamples() != sampleSize || numPoints == 0)
return;
using namespace juce;
g.setColour(findColour(Colors::painterColorId, true));
// Draw a horizontal line at 0
float dividerHeight = getHeight() * 0.0035f;
g.fillRect(0.f, (getHeight() - dividerHeight) / 2.f, float(getWidth()), dividerHeight);
int start = viewStart;
int end = viewEnd;
int viewSize = end - start + 1;
// While we could do a more general solution with a variable amount of caches, let's just use two fixed caches
int cacheRatio = intervalWidth >= cache2Amount ? cache2Amount : intervalWidth >= cache1Amount ? cache1Amount : 1;
auto& cacheData = (mono && sample->getNumChannels() > 1)
? (cacheRatio == cache1Amount ? cache1DataMono : cache2DataMono)
: (cacheRatio == cache1Amount ? cache1Data : cache2Data);
auto strokeWidth = getWidth() / resolution * 1.25f;
// Regular display
if (!isSampleBySample())
{
// Sample the data
sampleData.setSize(sampleData.getNumChannels(), numPoints, false, false, true);
// To keep sampling more consistent, we round down to intervalWidth
float startX = std::floor(start / intervalWidth) * intervalWidth / cacheRatio;
for (auto i = 0; i < numPoints; i++)
{
int index = int(startX + intervalWidth / cacheRatio * i);
int numValues = int(startX + intervalWidth / cacheRatio * (i + 1)) - index;
float min;
float max;
if (cacheRatio > 1.f)
{
min = FloatVectorOperations::findMinimum(cacheData.getReadPointer(0, index), numValues);
max = FloatVectorOperations::findMaximum(cacheData.getReadPointer(1, index), numValues);
}
else if (mono)
{
downsample(*sample, true, index, numValues, min, max);
}
else
{
downsample(*sample, false, index, numValues, min, max);
}
sampleData.setSample(0, i, min * gain);
sampleData.setSample(1, i, max * gain);
}
// Create the path
float offset = float(startX * cacheRatio - viewStart) / viewSize * getWidth();
Path path;
path.preallocateSpace(numPoints * 2 * 3);
for (auto i = 0; i < numPoints; i++)
{
float x = offset + jmap<float>(float(i), 0.f, numPoints - 2.f, 0.f, float(getWidth()));
float yMax = jmap<float>(sampleData.getSample(1, i), -1.f, 1.f, float(getHeight()), 0.f);
float yMin = jmap<float>(sampleData.getSample(0, i), -1.f, 1.f, float(getHeight()), 0.f);
if (!i)
path.startNewSubPath(x, yMax);
else
path.lineTo(x, yMax);
path.lineTo(x, yMin);
}
g.strokePath(path, PathStrokeType(strokeWidth, PathStrokeType::beveled));
}
// Sample by sample display
else
{
// If mono is enabled, we average the channels and exit this loop on the first iteration
// Otherwise, we draw each channel separately with a different opacity
// Find the primary channel to display
int primary = primaryChannel;
if (selectingChannel >= 0)
primary = selectingChannel;
Path path;
path.preallocateSpace(3 * (viewSize + 1));
Path circles;
auto circRadius = 0.003125f * getWidth();
bool drawCircles = viewSize <= SAMPLE_BY_SAMPLE_THRESHOLD;
if (drawCircles)
path.preallocateSpace(4 * viewSize);
for (auto ch = 0; ch < sample->getNumChannels(); ch++)
{
for (auto i = 0; i < viewSize; i++)
{
float level = sample->getSample(ch, start + i);
// If mono is enabled, we average the channels
if (mono)
{
level = 0;
for (auto ch2 = 0; ch2 < sample->getNumChannels(); ch2++)
level += sample->getSample(ch2, start + i);
level /= sample->getNumChannels();
}
level *= gain;
float xPos = float(getWidth() * i) / (end - start);
float yPos = jmap<float>(level, -1, 1, float(getHeight()), 0);
if (!i)
path.startNewSubPath(0, yPos);
path.lineTo(xPos, yPos);
// Only draw the circles when we are further zoomed in
if (drawCircles)
circles.addEllipse(xPos - circRadius, yPos - circRadius, circRadius * 2.f, circRadius * 2.f);
}
int channelOrderNum = (primary - ch + sample->getNumChannels()) % sample->getNumChannels();
float opacity = std::pow(0.6f, float(channelOrderNum));
if (mono)
opacity = 1.f;
g.setColour(findColour(Colors::painterColorId, true).withMultipliedAlpha(opacity));
g.strokePath(path, PathStrokeType(strokeWidth, PathStrokeType::curved));
g.fillPath(circles);
if (mono)
break;
path.clear();
circles.clear();
}
path.clear();
circles.clear();
}
}
void SamplePainter::resized()
{
recalculateIntervals();
}
void SamplePainter::enablementChanged()
{
repaint();
}
void SamplePainter::colourChanged()
{
repaint();
}
juce::String SamplePainter::getCustomHelpText()
{
auto mousePos = getMouseXYRelative();
int hoveringChannel = getChannel(mousePos.x, mousePos.y);
if (hoveringChannel == -1 || sample->getNumChannels() == 1)
return "";
if (sample->getNumChannels() == 2)
return "Focus " + juce::String(hoveringChannel == 0 ? "left" : "right") + " channel";
return "Focus channel " + juce::String(hoveringChannel + 1);
}
bool SamplePainter::isSampleBySample() const
{
return intervalWidth <= 5.f;
}
int SamplePainter::getChannel(int x, int y) const
{
if (!sample || !sample->getNumChannels() || !isSampleBySample() || mono)
return -1;
int closestCh = -1;
float bestYDist = getHeight() / 6.f;
int start = viewStart;
int end = viewEnd;
int viewSize = end - start + 1;
// Estimate closest sample index from x
float normX = float(x) / getWidth();
int i = juce::jlimit(0, viewSize - 1, int(std::round(normX * (end - start))));
for (int ch = 0; ch < sample->getNumChannels(); ++ch)
{
float level = sample->getSample(ch, start + i) * gain;
float yPos = juce::jmap<float>(level, -1, 1, float(getHeight()), 0);
float yDist = std::abs(y - yPos);
if (yDist < bestYDist)
{
bestYDist = yDist;
closestCh = ch;
}
}
return closestCh;
}
void SamplePainter::mouseDown(const juce::MouseEvent& event)
{
if (!isSampleBySample() || mono)
return;
selectingChannel = getChannel(event.x, event.y);
repaint();
}
void SamplePainter::mouseUp(const juce::MouseEvent& event)
{
if (!isSampleBySample() || mono)
return;
int hoveringChannel = getChannel(event.x, event.y);
if (selectingChannel >= 0 && hoveringChannel == selectingChannel)
{
primaryChannel = selectingChannel;
if (dummyParam)
dummyParam->sendUIUpdate();
}
selectingChannel = -1;
repaint();
}
void SamplePainter::valueChanged(ListenableValue<int>&, int)
{
repaint();
}
void SamplePainter::downsample(const juce::AudioBuffer<float>& buffer, bool average, int start, int numSamples, float& outMin, float& outMax)
{
// Average channels for mono downsample
if (average)
{
downsampleBuffer.setSize(1, numSamples, false, false, true);
downsampleBuffer.clear();
for (int ch = 0; ch < buffer.getNumChannels(); ch++)
downsampleBuffer.addFrom(0, 0, buffer, ch, start, numSamples);
auto range = juce::FloatVectorOperations::findMinAndMax(downsampleBuffer.getReadPointer(0), numSamples);
outMin = range.getStart() / buffer.getNumChannels();
outMax = range.getEnd() / buffer.getNumChannels();
}
// Downsample across all channels
else
{
float min = std::numeric_limits<float>::max();
float max = std::numeric_limits<float>::lowest();
for (int ch = 0; ch < buffer.getNumChannels(); ch++)
{
auto range = juce::FloatVectorOperations::findMinAndMax(buffer.getReadPointer(ch, start), numSamples);
min = juce::jmin(range.getStart(), min);
max = juce::jmax(range.getEnd(), max);
}
outMin = min;
outMax = max;
}
}
void SamplePainter::updateCaches(int start, int end)
{
if (!sample || start < 0)
return;
float min;
float max;
int cacheRatio = int(cache2Amount / cache1Amount);
const int cache1Size = int(1 + std::ceil(float(sample->getNumSamples()) / cache1Amount));
int effectiveStart = (start / cache1Amount) * cache1Amount; // Round down to the nearest cache1Amount
cache1Data.setSize(2, cache1Size, true, false, false);
for (int i = effectiveStart; i < end; i += cache1Amount)
{
int numSamples = juce::jmin(cache1Amount, sample->getNumSamples() - i);
downsample(*sample, false, i, numSamples, min, max);
cache1Data.setSample(0, i / cache1Amount, min);
cache1Data.setSample(1, i / cache1Amount, max);
}
const int cache2Size = int(std::ceil(float(cache1Data.getNumSamples() + 1) / cacheRatio));
int effectiveCache2Start = (effectiveStart / cache1Amount / cacheRatio) * cacheRatio; // Round down to the nearest cacheRatio
cache2Data.setSize(2, cache2Size, true, false, false);
for (int i = effectiveCache2Start; i < end / cache1Amount; i += cacheRatio)
{
int numSamples = juce::jmin(cacheRatio, cache1Data.getNumSamples() - i);
min = juce::FloatVectorOperations::findMinimum(cache1Data.getReadPointer(0, i), numSamples);
max = juce::FloatVectorOperations::findMaximum(cache1Data.getReadPointer(1, i), numSamples);
cache2Data.setSample(0, i / cacheRatio, min);
cache2Data.setSample(1, i / cacheRatio, max);
}
if (sample->getNumChannels() > 1)
{
cache1DataMono.setSize(2, cache1Size, true, false, false);
for (int i = effectiveStart; i < end; i += cache1Amount)
{
int numSamples = juce::jmin(cache1Amount, sample->getNumSamples() - i);
downsample(*sample, true, i, numSamples, min, max);
cache1DataMono.setSample(0, i / cache1Amount, min);
cache1DataMono.setSample(1, i / cache1Amount, max);
}
cache2DataMono.setSize(2, cache2Size, true, false, false);
for (int i = effectiveCache2Start; i < end / cache1Amount; i += cacheRatio)
{
int numSamples = juce::jmin(cacheRatio, cache1DataMono.getNumSamples() - i);
min = juce::FloatVectorOperations::findMinimum(cache1DataMono.getReadPointer(0, i), numSamples);
max = juce::FloatVectorOperations::findMaximum(cache1DataMono.getReadPointer(1, i), numSamples);
cache2DataMono.setSample(0, i / cacheRatio, min);
cache2DataMono.setSample(1, i / cacheRatio, max);
}
}
}
void SamplePainter::appendToPath(int startSample, int endSample)
{
if (!sample)
return;
updateCaches(startSample, endSample);
recalculateIntervals();
repaint();
}
void SamplePainter::setSample(const juce::AudioBuffer<float>& sampleBuffer)
{
sample = &sampleBuffer;
sampleSize = sampleBuffer.getNumSamples();
viewStart = 0;
viewEnd = sampleBuffer.getNumSamples() - 1;
if (!sample)
return;
updateCaches(0, sample->getNumSamples());
recalculateIntervals();
repaint();
}
void SamplePainter::setSample(const juce::AudioBuffer<float>& sampleBuffer, int viewStartSample, int viewEndSample)
{
setSample(sampleBuffer);
viewStart = viewStartSample;
viewEnd = viewEndSample;
recalculateIntervals();
repaint();
}
void SamplePainter::setSampleView(int viewStartSample, int viewEndSample)
{
viewStart = viewStartSample;
viewEnd = viewEndSample;
recalculateIntervals();
repaint();
}
void SamplePainter::recalculateIntervals()
{
int viewSize = viewEnd - viewStart + 1;
// Set resolution as a constant factor of the screen width
int width = getWidth();
if (const juce::Displays::Display* screen = juce::Desktop::getInstance().getDisplays().getPrimaryDisplay())
width = screen->userArea.getWidth();
resolution = width * resolutionScale;
// We adjust numPoints in factors of 2 to keep the view smooth looking
float sampleDiv = viewSize / resolution;
float base = 2.f;
float nextPower = std::pow(base, std::ceil(std::log(sampleDiv) / std::log(base)));
intervalWidth = nextPower / base;
numPoints = int(viewSize / intervalWidth);
}
void SamplePainter::setGain(float newGain)
{
gain = newGain;
repaint();
}
void SamplePainter::setMono(bool isMono)
{
mono = isMono;
if (!isMono)
selectingChannel = -1;
repaint();
}

View file

@ -0,0 +1,96 @@
/*
==============================================================================
SamplePainter.h
Created: 19 Sep 2023 3:02:14pm
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#include "../../Sampler/CustomSamplerVoice.h"
#include "../../Utilities/ComponentUtils.h"
/** A custom component that paints a waveform of a sample. A cache is used to help render large
waveforms with minimal overhead. Additionally, a method is provided to append to the path for real-time recording.
*/
class SamplePainter final : public CustomComponent, public ValueListener<int>
{
public:
explicit SamplePainter(ListenableAtomic<int>& primaryVisibleChannel, float resolutionScale = 0.25f, UIDummyParam* dummyParam = nullptr);
~SamplePainter() override;
/** This adds (does not remove) to the path along the given start and end samples */
void appendToPath(int startSample, int endSample);
void setSample(const juce::AudioBuffer<float>& sampleBuffer);
void setSample(const juce::AudioBuffer<float>& sampleBuffer, int viewStartSample, int viewEndSample);
void setSampleView(int viewStartSample, int viewEndSample);
/** Change gain and repaint */
void setGain(float newGain);
/** Change mono display and repaint */
void setMono(bool isMono);
private:
void paint(juce::Graphics& g) override;
void resized() override;
void enablementChanged() override;
void colourChanged() override;
juce::String getCustomHelpText() override;
/** Whether sample by sample display is active */
bool isSampleBySample() const;
/** Get the channel number of the sample at the given x and y coordinates (within a snap amount) */
int getChannel(int x, int y) const;
void mouseDown(const juce::MouseEvent& event) override;
void mouseUp(const juce::MouseEvent& event) override;
void valueChanged(ListenableValue<int>& source, int newValue) override;
/** Downsample a range of a buffer. If average is true, the values are averaged, otherwise the max value is taken (across a
a single channel if channel is set, or across all channels if channel is -1).
*/
void downsample(const juce::AudioBuffer<float>& buffer, bool average, int start, int numSamples, float& outMin, float& outMax);
void updateCaches(int start, int end);
void recalculateIntervals();
//==============================================================================
const juce::AudioBuffer<float>* sample{ nullptr };
int sampleSize{ 0 };
int viewStart{ 0 }, viewEnd{ 0 };
float gain{ 1.f };
bool mono{ false };
float resolutionScale{ 1.f };
float resolution{ 0.f };
int numPoints{ 0 };
float intervalWidth{ 0.f };
/** An intermediate buffer */
juce::AudioBuffer<float> sampleData;
juce::AudioBuffer<float> downsampleBuffer;
/** The cache is a down-sampled version of the sample that is used to speed up rendering */
juce::AudioBuffer<float> cache1Data, cache2Data;
juce::AudioBuffer<float> cache1DataMono, cache2DataMono;
static constexpr int cache1Amount{ 100 }, cache2Amount{ 5000 };
const int SAMPLE_BY_SAMPLE_THRESHOLD{ 150 };
/** Which channel has full opacity */
ListenableAtomic<int>& primaryChannel;
UIDummyParam* dummyParam{ nullptr };
/** Set when a mouse down has occurred on a channel */
int selectingChannel{ -1 };
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SamplePainter)
};

View file

@ -0,0 +1,211 @@
/*
==============================================================================
FxChain.cpp
Created: 5 Jan 2024 3:26:37pm
Author: binya
==============================================================================
*/
#include <JuceHeader.h>
#include "FxChain.h"
void FxChainShadows::resized()
{
int innerShadowOffset = int(0.001f * getWidth());
innerShadow.setOffset({ 0, innerShadowOffset }, 0);
innerShadow.setOffset({ 0, -innerShadowOffset }, 1);
dragShadow.setOffset({ innerShadowOffset, innerShadowOffset });
}
void FxChainShadows::paint(juce::Graphics& g)
{
juce::Path innerShadowPath;
innerShadowPath.addRectangle(getLocalBounds());
innerShadow.render(g, innerShadowPath);
auto parent = dynamic_cast<FxChain*>(getParentComponent());
if (parent->isChainDragging())
{
juce::Path dragShadowPath;
dragShadowPath.addRectangle(parent->getDragComp()->getBounds());
dragShadow.render(g, dragShadowPath);
}
}
void FxChainShadows::lookAndFeelChanged()
{
auto colors = getTheme();
innerShadow.setColor(colors.slate.withAlpha(0.25f), 0);
innerShadow.setColor(colors.slate.withAlpha(0.25f), 1);
dragShadow.setColor(colors.slate.withAlpha(0.125f), 0);
}
FxChain::FxChain(JustaSampleAudioProcessor& processor) :
reverbDisplay(processor.APVTS()),
distortionDisplay(processor.APVTS(), int(processor.getSampleRate())),
eqDisplay(processor.APVTS(), int(processor.getSampleRate())),
chorusDisplay(processor.APVTS(), int(processor.getSampleRate())),
reverbModule(this, processor.APVTS(), "Reverb", PluginParameters::REVERB, PluginParameters::REVERB_ENABLED, PluginParameters::REVERB_MIX, reverbDisplay),
distortionModule(this, processor.APVTS(), "Distortion", PluginParameters::DISTORTION, PluginParameters::DISTORTION_ENABLED, PluginParameters::DISTORTION_MIX, distortionDisplay),
eqModule(this, processor.APVTS(), "Equalizer", PluginParameters::EQ, PluginParameters::EQ_ENABLED, eqDisplay),
chorusModule(this, processor.APVTS(), "Chorus", PluginParameters::CHORUS, PluginParameters::CHORUS_ENABLED, PluginParameters::CHORUS_MIX, chorusDisplay),
fxPermAttachment(*processor.APVTS().getParameter(PluginParameters::FX_PERM), [&](float newValue) { moduleOrder = PluginParameters::paramToPerm(int(newValue)); oldVal = int(newValue); resized(); }, &processor.getUndoManager())
{
reverbModule.addRotary(PluginParameters::REVERB_SIZE, "Size", { 11.4f, 82.f }, 87.f);
reverbModule.addRotary(PluginParameters::REVERB_DAMPING, "Damping", { 108.4f, 52.f }, 87.f);
reverbModule.addRotary(PluginParameters::REVERB_PREDELAY, "Delay", { 208.f, 82.f }, 87.f, PluginParameters::TIME_UNIT);
reverbModule.addRotary(PluginParameters::REVERB_LOWS, "Lows", { 302.f, 52.f }, 87.f);
reverbModule.addRotary(PluginParameters::REVERB_HIGHS, "Highs", { 399.4f, 82.f }, 87.f);
addAndMakeVisible(reverbModule);
distortionModule.addRotary(PluginParameters::DISTORTION_DENSITY, "Density", { 99.f, 46.f }, 110.f);
distortionModule.addRotary(PluginParameters::DISTORTION_HIGHPASS, "Highpass", { 289.f, 46.f }, 110.f);
addAndMakeVisible(distortionModule);
eqModule.addRotary(PluginParameters::EQ_LOW_GAIN, "Lows", { 44.5f, 46.f }, 110.f, PluginParameters::VOLUME_UNIT);
eqModule.addRotary(PluginParameters::EQ_MID_GAIN, "Mids", { 194.f, 46.f }, 110.f, PluginParameters::VOLUME_UNIT);
eqModule.addRotary(PluginParameters::EQ_HIGH_GAIN, "Highs", { 344.f, 46.f }, 110.f, PluginParameters::VOLUME_UNIT);
addAndMakeVisible(eqModule);
chorusModule.addRotary(PluginParameters::CHORUS_RATE, "Rate", { 40.f, 82.f }, 87.f, PluginParameters::FREQUENCY_UNIT);
chorusModule.addRotary(PluginParameters::CHORUS_DEPTH, "Depth", { 150.f, 52.f }, 87.f);
chorusModule.addRotary(PluginParameters::CHORUS_CENTER_DELAY, "Delay", { 260.6f, 82.f }, 87.f, PluginParameters::TIME_UNIT);
chorusModule.addRotary(PluginParameters::CHORUS_FEEDBACK, "Feedback", { 371.f, 52.f }, 87.f);
addAndMakeVisible(chorusModule);
shadows.setInterceptsMouseClicks(false, false);
addAndMakeVisible(shadows);
fxPermAttachment.sendInitialUpdate();
addMouseListener(this, true);
}
void FxChain::paint(juce::Graphics& g)
{
auto colors = getTheme();
g.setColour(colors.slate);
auto bounds = getLocalBounds();
auto dividerWidth = int(std::ceil(getWidth() * Layout::fxChainDivider));
auto moduleWidth = int(std::round((bounds.getWidth() - 3 * dividerWidth) / 4.f));
for (size_t i = 0; i < moduleOrder.size() - 1; i++)
{
bounds.removeFromLeft(moduleWidth);
auto divider = bounds.removeFromLeft(dividerWidth);
g.fillRect(divider);
}
}
void FxChain::resized()
{
auto bounds = getLocalBounds();
auto dividerWidth = int(std::ceil(getWidth() * Layout::fxChainDivider));
auto moduleWidth = int(std::round((bounds.getWidth() - 3 * dividerWidth) / 4.f));
for (const auto& fxType : moduleOrder)
{
auto& module = getModule(fxType);
auto moduleBounds = bounds.removeFromLeft(moduleWidth);
bounds.removeFromLeft(dividerWidth);
if (!(dragging && &module == dragComp))
module.setBounds(moduleBounds.toNearestInt());
}
shadows.setBounds(getLocalBounds());
if (dragging)
{
auto dragCompBounds = dragComp->getBounds();
dragCompBounds.setPosition(juce::jlimit<int>(0, getWidth() - dragComp->getWidth(), mouseX + dragOffset), 0);
dragComp->setBounds(dragCompBounds);
repaint();
}
}
void FxChain::mouseDrag(const juce::MouseEvent& event)
{
if (!dragging)
return;
auto localEvent = event.getEventRelativeTo(this);
mouseX = localEvent.x;
auto dragCompBounds = dragComp->getBounds();
dragCompBounds.setPosition(juce::jlimit<int>(0, getWidth() - dragComp->getWidth(), mouseX + dragOffset), 0);
// See if the chain's module order needs to be updated
auto bounds = getLocalBounds();
auto moduleWidth = bounds.getWidth() / 4;
for (int i = 0; i < 4; i++)
{
auto moduleBounds = bounds.removeFromLeft(moduleWidth);
if (i != dragCompIndex && moduleBounds.getX() <= dragCompBounds.getCentreX() && dragCompBounds.getCentreX() <= moduleBounds.getRight())
{
auto fxType = moduleOrder[i];
moduleOrder[i] = moduleOrder[dragCompIndex];
moduleOrder[dragCompIndex] = fxType;
dragCompIndex = i;
}
}
resized();
}
void FxChain::dragStarted(Component* comp, const juce::MouseEvent& event)
{
dragging = true;
dragComp = comp;
dragTarget = dynamic_cast<FxModule*>(comp)->getEffectType();
for (int i = 0; i < 4; i++)
if (&getModule(moduleOrder[i]) == dragComp)
dragCompIndex = i;
shadows.toFront(true);
dragComp->toFront(true);
mouseX = dragComp->getX();
dragOffset = dragComp->getX() - event.getEventRelativeTo(this).x;
}
void FxChain::dragEnded()
{
dragging = false;
int newVal = permToParam(moduleOrder);
if (newVal != oldVal)
fxPermAttachment.setValueAsCompleteGesture(float(newVal));
dragComp->toBehind(&shadows);
resized();
repaint();
}
void FxChain::enablementChanged()
{
reverbModule.setEnabled(isEnabled());
distortionModule.setEnabled(isEnabled());
eqModule.setEnabled(isEnabled());
chorusModule.setEnabled(isEnabled());
}
FxModule& FxChain::getModule(PluginParameters::FxTypes type)
{
switch (type)
{
case PluginParameters::REVERB:
return reverbModule;
case PluginParameters::DISTORTION:
return distortionModule;
case PluginParameters::EQ:
return eqModule;
case PluginParameters::CHORUS:
default:
return chorusModule;
}
}

View file

@ -0,0 +1,82 @@
/*
==============================================================================
FxChain.h
Created: 5 Jan 2024 3:26:37pm
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#include "../PluginProcessor.h"
#include "../PluginParameters.h"
#include "../Utilities/ComponentUtils.h"
#include "FxModule.h"
#include "FxDragTarget.h"
#include "Displays/FilterResponse.h"
#include "Displays/ReverbResponse.h"
#include "Displays/DistortionVisualizer.h"
#include "Displays/ChorusVisualizer.h"
class FxChainShadows final : public CustomComponent
{
void resized() override;
void paint(juce::Graphics& g) override;
void lookAndFeelChanged() override;
melatonin::InnerShadow innerShadow{
{defaultTheme.slate.withAlpha(0.25f), 3, {0, 2}},
{defaultTheme.slate.withAlpha(0.25f), 3, {0, -2}}
};
melatonin::DropShadow dragShadow{ defaultTheme.slate.withAlpha(0.125f), 3, {2, 2} };
};
/** The FX chain allows for drag and drop reordering of the effects modules. */
class FxChain final : public CustomComponent, public FxDragTarget
{
public:
explicit FxChain(JustaSampleAudioProcessor& processor);
~FxChain() override = default;
bool isChainDragging() const { return dragging; }
Component* getDragComp() const { return dragComp; }
private:
void paint(juce::Graphics& g) override;
void resized() override;
void mouseDrag(const juce::MouseEvent& event) override;
void dragStarted(juce::Component* comp, const juce::MouseEvent& event) override;
void dragEnded() override;
void enablementChanged() override;
FxModule& getModule(PluginParameters::FxTypes type);
//==============================================================================
ReverbResponse reverbDisplay;
DistortionVisualizer distortionDisplay;
FilterResponse eqDisplay;
ChorusVisualizer chorusDisplay;
FxModule reverbModule, distortionModule, eqModule, chorusModule;
FxChainShadows shadows;
// Dragging functionality
juce::ParameterAttachment fxPermAttachment;
int oldVal{ 0 };
std::array<PluginParameters::FxTypes, 4> moduleOrder{};
bool dragging{ false };
PluginParameters::FxTypes dragTarget{ PluginParameters::REVERB };
Component* dragComp{ nullptr };
int dragCompIndex{ 0 };
int mouseX{ 0 };
int dragOffset{ 0 };
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FxChain)
};

View file

@ -0,0 +1,20 @@
/*
==============================================================================
FxDragTarget.h
Created: 7 Jan 2024 12:21:57pm
Author: binya
==============================================================================
*/
#pragma once
/** This interface mostly exists separately to avoid some circular reference problems */
class FxDragTarget
{
public:
virtual ~FxDragTarget() = default;
virtual void dragStarted(juce::Component* component, const juce::MouseEvent& event) = 0;
virtual void dragEnded() = 0;
};

View file

@ -0,0 +1,219 @@
/*
==============================================================================
FXModule.cpp
Created: 28 Dec 2023 8:09:18pm
Author: binya
==============================================================================
*/
#include <JuceHeader.h>
#include "FxModule.h"
FxModule::FxModule(FxDragTarget* fxChain, APVTS& apvts, const juce::String& fxName, PluginParameters::FxTypes effectType, const juce::String& fxEnabledParameter, Component& displayComponent) :
fxChain(fxChain), apvts(apvts), fxType(effectType), nameLabel("", fxName), mixControl(juce::Slider::RotaryVerticalDrag, juce::Slider::NoTextBox),
enablementAttachment(apvts, fxEnabledParameter, fxEnabled), display(displayComponent),
dragIcon(getOutlineFromSVG(BinaryData::IconDrag_svg))
{
nameLabel.setInterceptsMouseClicks(false, false);
addAndMakeVisible(&nameLabel);
fxEnabled.onStateChange = [&] {
display.setEnabled(fxEnabled.getToggleState());
for (auto rotary : rotaryReferences)
rotary->setEnabled(fxEnabled.getToggleState());
for (auto& label : labels)
label->setEnabled(fxEnabled.getToggleState());
};
fxEnabled.onStateChange(); // Set the initial state
fxEnabled.setHelpText("Toggle " + fxName.toLowerCase());
addAndMakeVisible(&fxEnabled);
addAndMakeVisible(&displayComponent);
setRepaintsOnMouseActivity(true);
addMouseListener(this, true);
setHelpText(fxName + " module");
setBufferedToImage(true);
}
FxModule::FxModule(FxDragTarget* fxChain, APVTS& apvts, const juce::String& fxName, PluginParameters::FxTypes effectType, const juce::String& fxEnabledParameter, const juce::String& mixControlParameter, Component& displayComponent) :
FxModule(fxChain, apvts, fxName, effectType, fxEnabledParameter, displayComponent)
{
setupRotary(mixControl, false);
mixControl.getProperties().set(ComponentProps::ROTARY_UNIT, "%");
mixControlAttachment = std::make_unique<CustomRotaryAttachment>(apvts, mixControlParameter, mixControl);
}
juce::Slider* FxModule::addRotary(const juce::String& parameter, const juce::String& label, juce::Point<float> position, float width, const juce::String& unit)
{
auto rotary = std::make_unique<CustomRotary>();
CustomRotary* rawRotaryPtr = rotary.get();
auto attachment = std::make_unique<CustomRotaryAttachment>(apvts, parameter, *rawRotaryPtr);
attachments.push_back(std::move(attachment));
setupRotary(*rotary);
rotary->getProperties().set(ComponentProps::ROTARY_UNIT, unit);
addAndMakeVisible(rawRotaryPtr);
rotaries.push_back(std::move(rotary));
auto rotaryLabel = std::make_unique<juce::Label>("", label);
rotaryLabel->setInterceptsMouseClicks(false, false);
rotaryLabel->setJustificationType(juce::Justification::centredBottom);
rotaryLabel->setEnabled(fxEnabled.getToggleState());
addAndMakeVisible(rotaryLabel.get());
labels.push_back(std::move(rotaryLabel));
rotaryPositions.push_back(position);
rotaryWidths.push_back(width);
return rawRotaryPtr;
}
void FxModule::setupRotary(CustomRotary& rotary, bool useTextbox)
{
rotary.setSliderStyle(juce::Slider::RotaryVerticalDrag);
rotary.setMouseDragSensitivity(150);
if (useTextbox)
rotary.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 0, 0);
rotary.addMouseListener(this, true); // We need this to pass drag events through the labels
rotary.setEnabled(fxEnabled.getToggleState());
addAndMakeVisible(rotary);
rotaryReferences.push_back(&rotary);
}
void FxModule::paint(juce::Graphics& g)
{
auto colors = getTheme();
auto bounds = getLocalBounds();
g.setColour(colors.background);
g.fillAll();
// Draw the drag icon
auto header = bounds.toFloat().removeFromTop(scalef(Layout::fxModuleHeader));
if (header.contains(getMouseXYRelative().toFloat()))
{
g.setColour(colors.darkerSlate);
header.removeFromTop(scalef(25.f));
auto iconBounds = header.removeFromTop(scalef(28.f));
g.fillPath(dragIcon, dragIcon.getTransformToScaleToFit(iconBounds.toFloat(), true));
}
}
void FxModule::resized()
{
auto bounds = getLocalBounds().toFloat();
// Header
auto mixPad = scale(43.f) * Layout::rotaryPadding;
auto header = bounds.removeFromTop(scale(Layout::fxModuleHeader)).reduced(scale(21.f), scale(15.f) - mixPad);
fxEnabled.setBounds(header.removeFromRight(scale(43.f)).reduced(0.f, mixPad).toNearestInt());
header.removeFromRight(scale(20.f) - mixPad);
if (mixControlAttachment)
mixControl.setBounds(header.removeFromRight(scale(43.f) + 2 * mixPad).toNearestInt());
header.removeFromRight(scale(20.f) - mixPad);
nameLabel.setBounds(header.reduced(0.f, mixPad).toNearestInt());
nameLabel.setFont(getInriaSans().withHeight(scalef(41.4f)));
// Controls
display.setBounds(bounds.removeFromTop(scale(103.f)).toNearestInt());
bounds.reduce(0.f, scale(27.5f));
for (size_t i = 0; i < rotaries.size(); i++)
{
auto* rotary = rotaries[i].get();
auto [xPos, yPos] = rotaryPositions[i];
auto width = rotaryWidths[i];
auto padding = Layout::rotaryPadding * scale(width);
rotary->setBounds(juce::Rectangle<float>(bounds.getX() + scale(xPos), bounds.getY() + scale(yPos), scale(width), scale(width) * Layout::rotaryHeightRatio).expanded(padding).toNearestInt());
auto* label = labels[i].get();
auto labelBounds = juce::Rectangle<float>(bounds.getX() + scale(xPos), bounds.getY() + scale(yPos) - scale(width) * 0.45f, scale(width), scale(width * 0.356f)).expanded(width, 0.f);
label->setFont(getInriaSans().withHeight(scale(width) * 0.356f));
label->setBounds(labelBounds.toNearestInt());
}
if (mixControlAttachment)
mixControl.sendLookAndFeelChange();
for (auto& rotary : rotaries)
rotary->sendLookAndFeelChange();
}
void FxModule::enablementChanged()
{
setInterceptsMouseClicks(isEnabled(), isEnabled());
repaint();
}
void FxModule::mouseDown(const juce::MouseEvent& event)
{
// We pass the events on the rotary number labels through to the rotary
auto parent = event.originalComponent->getParentComponent();
for (auto rotary : rotaryReferences)
if (parent == rotary)
rotary->mouseDown(event.getEventRelativeTo(rotary));
}
//==============================================================================
void FxModule::mouseUp(const juce::MouseEvent& event)
{
if (dragging)
{
dragging = false;
fxChain->dragEnded();
repaint();
}
auto parent = event.originalComponent->getParentComponent();
for (auto rotary : rotaryReferences)
if (parent == rotary)
rotary->mouseUp(event.getEventRelativeTo(rotary));
}
void FxModule::mouseMove(const juce::MouseEvent& /*event*/)
{
auto header = getLocalBounds().toFloat().removeFromTop(scale(Layout::fxModuleHeader));
auto mouseInHeader = header.contains(getMouseXYRelative().toFloat());
if (mouseInHeader)
setMouseCursor(juce::MouseCursor::DraggingHandCursor);
else
setMouseCursor(juce::MouseCursor::NormalCursor);
}
void FxModule::mouseDrag(const juce::MouseEvent& event)
{
auto header = getLocalBounds().toFloat().removeFromTop(scale(Layout::fxModuleHeader));
auto mouseInHeader = header.contains(getMouseXYRelative().toFloat());
if (!dragging && event.eventComponent == this && mouseInHeader)
{
dragging = true;
fxChain->dragStarted(this, event);
repaint();
}
auto parent = event.originalComponent->getParentComponent();
for (auto rotary : rotaryReferences)
if (parent == rotary)
rotary->mouseDrag(event.getEventRelativeTo(rotary));
}
void FxModule::mouseEnter(const juce::MouseEvent&)
{
repaint();
}
void FxModule::mouseExit(const juce::MouseEvent&)
{
repaint();
}

View file

@ -0,0 +1,78 @@
/*
==============================================================================
FXModule.h
Created: 28 Dec 2023 8:09:18pm
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#include "FxDragTarget.h"
#include "../PluginParameters.h"
#include "../Utilities/ComponentUtils.h"
/** An FX module within the FX chain. */
class FxModule final : public CustomComponent
{
public:
FxModule(FxDragTarget* fxChain, APVTS& apvts, const juce::String& fxName, PluginParameters::FxTypes effectType, const juce::String& fxEnabledParameter, Component& displayComponent);
/** Constructor for a module with a mix control. */
FxModule(FxDragTarget* fxChain, APVTS& apvts, const juce::String& fxName, PluginParameters::FxTypes effectType, const juce::String& fxEnabledParameter, const juce::String& mixControlParameter, Component& displayComponent);
juce::Slider* addRotary(const juce::String& parameter, const juce::String& label, juce::Point<float> position, float width, const juce::String& unit = "");
PluginParameters::FxTypes getEffectType() const { return fxType; }
private:
void paint(juce::Graphics&) override;
void resized() override;
void enablementChanged() override;
void mouseDown(const juce::MouseEvent& event) override;
void mouseUp(const juce::MouseEvent& event) override;
void mouseMove(const juce::MouseEvent& event) override;
void mouseDrag(const juce::MouseEvent& event) override;
void mouseEnter(const juce::MouseEvent& event) override;
void mouseExit(const juce::MouseEvent& event) override;
void setupRotary(CustomRotary& rotary, bool useTextbox = true);
float scale(float value) const { return std::round(value * getWidth() / (Layout::figmaWidth / 4.f)); }
float scalef(float value) const { return value * getWidth() / (Layout::figmaWidth / 4.f); }
//==============================================================================
FxDragTarget* fxChain;
APVTS& apvts;
PluginParameters::FxTypes fxType;
// Header controls
juce::Label nameLabel;
CustomRotary mixControl;
std::unique_ptr<APVTS::SliderAttachment> mixControlAttachment;
juce::ToggleButton fxEnabled;
APVTS::ButtonAttachment enablementAttachment;
// Controls
Component& display;
std::vector<std::unique_ptr<juce::Label>> labels;
std::vector<std::unique_ptr<CustomRotary>> rotaries;
std::vector<std::unique_ptr<CustomRotaryAttachment>> attachments;
std::vector<juce::Point<float>> rotaryPositions; // Relative to the area
std::vector<float> rotaryWidths;
std::vector<CustomRotary*> rotaryReferences;
bool dragging{ false };
juce::Path dragIcon;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FxModule)
};

View file

@ -0,0 +1,352 @@
/*
==============================================================================
InputDeviceSelector.cpp
Created: 18 Aug 2024 8:20:19pm
Author: binya
==============================================================================
*/
#include "InputDeviceSelector.h"
ChannelSelectorListBox::ChannelSelectorListBox(juce::AudioDeviceManager& audioDeviceManager, int minNumInputChannels, int maxNumInputChannels, bool useStereoPairs)
: manager(audioDeviceManager), minInputChannels(minNumInputChannels), maxInputChannels(maxNumInputChannels), useStereoPairs(useStereoPairs)
{
setModel(this);
audioDeviceManager.addChangeListener(this);
changeListenerCallback(nullptr);
}
ChannelSelectorListBox::~ChannelSelectorListBox()
{
manager.removeChangeListener(this);
}
void ChannelSelectorListBox::changeListenerCallback(juce::ChangeBroadcaster*)
{
repaint();
auto* currentDevice = manager.getCurrentAudioDevice();
if (device == currentDevice)
return;
channels.clear();
if (!currentDevice)
return;
channels = currentDevice->getInputChannelNames();
device = currentDevice;
if (useStereoPairs)
{
juce::StringArray pairs;
int numPairs = channels.size() / 2;
for (int i = 0; i < numPairs; i++)
pairs.add(getNameForChannelPair(channels[i * 2], channels[i * 2 + 1]));
// Interestingly, StringArray->end() caused an issue on Mac
if (channels.size() % 2)
pairs.add(channels[channels.size() - 1].trim());
channels = pairs;
}
updateContent();
}
int ChannelSelectorListBox::getNumRows()
{
return channels.size();
}
void ChannelSelectorListBox::listBoxItemClicked(int row, const juce::MouseEvent& /*e*/)
{
if (row < channels.size())
flipEnablement(row);
}
void ChannelSelectorListBox::paintListBoxItem(int row, juce::Graphics& g, int width, int height, bool)
{
using namespace juce;
if (row >= channels.size())
return;
auto item = channels[row];
auto inputChannels = manager.getAudioDeviceSetup().inputChannels;
// We need to actually check the device for enablement
bool enabled = useStereoPairs ? inputChannels[row * 2] || inputChannels[row * 2 + 1] : inputChannels[row];
Rectangle<float> bounds{ float(width), float(height) };
bounds = bounds.reduced(bounds.getHeight() * 0.04f);
// To draw the tick box, we create a temporary ToggleButton (due to how I implemented the lnf function)
ToggleButton tempComp;
tempComp.setBounds(bounds.removeFromLeft(bounds.getHeight()).toNearestInt());
getLookAndFeel().drawTickBox(g, tempComp, 0, 0, 0, 0, enabled, true, true, false);
bounds.removeFromLeft(bounds.getHeight() * 0.3f);
g.setColour(findColour(textColourId));
g.setFont(getInter().withHeight(bounds.getHeight() * 0.85f));
g.drawText(channels[row], bounds, Justification::centredLeft);
}
void ChannelSelectorListBox::flipEnablement(int row) const
{
auto config = manager.getAudioDeviceSetup();
config.useDefaultInputChannels = false;
if (useStereoPairs)
{
bool enabled = config.inputChannels[row * 2] || config.inputChannels[row * 2 + 1];
setBit(config.inputChannels, row * 2 + 1, !enabled);
setBit(config.inputChannels, row * 2, !enabled);
}
else
{
setBit(config.inputChannels, row, !config.inputChannels[row]);
}
manager.setAudioDeviceSetup(config, true);
}
void ChannelSelectorListBox::setBit(juce::BigInteger& ch, int index, bool set) const
{
auto numActive = ch.countNumberOfSetBits();
if (ch[index] == set || (!set && numActive <= minInputChannels))
return;
// Clear a bit if necessary
if (set && numActive >= maxInputChannels)
{
auto firstActiveChan = ch.findNextSetBit(0);
ch.clearBit(index > firstActiveChan ? firstActiveChan : ch.getHighestBit());
}
ch.setBit(index, set);
}
juce::String ChannelSelectorListBox::getNameForChannelPair(const juce::String& name1, const juce::String& name2) const
{
juce::String commonBit;
for (int j = 0; j < name1.length(); ++j)
if (name1.substring(0, j).equalsIgnoreCase(name2.substring(0, j)))
commonBit = name1.substring(0, j);
// Make sure we only split the name at a space, because otherwise, things like "input 11" + "input 12" would become "input 11 + 2"
while (commonBit.isNotEmpty() && !juce::CharacterFunctions::isWhitespace(commonBit.getLastCharacter()))
commonBit = commonBit.dropLastCharacters(1);
return name1.trim() + " + " + name2.substring(commonBit.length()).trim();
}
//==============================================================================
InputDeviceSelector::InputDeviceSelector(juce::AudioDeviceManager& deviceManager, int minInputChannelsToUse, int maxInputChannelsToUse, bool showChannelsAsStereoPairs) :
manager(deviceManager),
titleLabel("", "Input Device Settings"),
warningLabel("", "No device opened, check DAW audio settings if issues persist"),
deviceTypeLabel("", "Type: "),
deviceLabel("", "Device: "),
channelsLabel("", "Channels: "),
sampleRateLabel("", "Sample rate: "),
bufferSizeLabel("", "Buffer size: "),
levelMeter(deviceManager),
channelSelector(deviceManager, minInputChannelsToUse, maxInputChannelsToUse, showChannelsAsStereoPairs)
{
jassert(minInputChannelsToUse >= 0 && minInputChannelsToUse <= maxInputChannelsToUse);
titleLabel.setJustificationType(juce::Justification::centred);
juce::Array labels = { &titleLabel, &deviceTypeLabel, &deviceLabel, &channelsLabel, &sampleRateLabel, &bufferSizeLabel };
for (auto* label : labels)
addAndMakeVisible(label);
warningLabel.setJustificationType(juce::Justification::centred);
addChildComponent(warningLabel);
const auto& types= manager.getAvailableDeviceTypes();
for (int i = 0; i < types.size(); ++i)
deviceTypeChooser.addItem(types.getUnchecked(i)->getTypeName(), i + 1);
deviceTypeChooser.setTextWhenNoChoicesAvailable("No devices available");
deviceTypeChooser.setSelectedId(1);
juce::Array comboBoxes = { &deviceTypeChooser, &deviceChooser, &sampleRateChooser, &bufferSizeChooser };
for (auto* box : comboBoxes)
{
box->onChange = [this] { settingsChanged(); };
addAndMakeVisible(box);
}
addAndMakeVisible(levelMeter);
addAndMakeVisible(channelSelector);
manager.addChangeListener(this);
settingsChanged();
}
InputDeviceSelector::~InputDeviceSelector()
{
manager.removeChangeListener(this);
}
void InputDeviceSelector::paint(juce::Graphics& g)
{
auto colors = getTheme();
g.fillAll(colors.prompt);
g.setColour(colors.foreground);
g.fillRoundedRectangle(getLocalBounds().toFloat(), 5.f);
}
void InputDeviceSelector::resized()
{
auto bounds = getLocalBounds().toFloat();
auto w = bounds.getWidth();
const auto row = bounds.getHeight() * 0.07f;
const auto padding = bounds.getHeight() * 0.035f;
constexpr auto labelProp = 0.25f;
bounds.reduce(w * 0.12f, w * 0.05f);
auto titleBounds = bounds.removeFromTop(bounds.proportionOfHeight(0.1f));
titleLabel.setFont(getInter().withHeight(titleBounds.getHeight()));
titleLabel.setBounds(titleBounds.toNearestInt());
bounds.removeFromTop(padding / 4);
auto warningLabelHeight = std::round(row * 0.665f);
warningLabel.setFont(getInter().withHeight(warningLabelHeight));
warningLabel.setBounds(bounds.removeFromTop(warningLabelHeight).toNearestInt());
bounds.removeFromTop(padding);
auto deviceTypeBounds = bounds.removeFromTop(row);
deviceTypeLabel.setFont(getInter().withHeight(std::round(row * 0.95f)));
deviceTypeLabel.setBounds(deviceTypeBounds.removeFromLeft(deviceTypeBounds.proportionOfWidth(labelProp)).toNearestInt());
deviceTypeChooser.setBounds(deviceTypeBounds.withHeight(row * 1.2f).toNearestInt());
bounds.removeFromTop(padding);
auto deviceBounds = bounds.removeFromTop(row);
deviceLabel.setFont(getInter().withHeight(std::round(row * 0.95f)));
deviceLabel.setBounds(deviceBounds.removeFromLeft(deviceBounds.proportionOfWidth(labelProp)).toNearestInt());
deviceChooser.setBounds(deviceBounds.removeFromLeft(bounds.proportionOfWidth(0.56f)).withHeight(row * 1.2f).toNearestInt());
levelMeter.setBounds(deviceBounds.removeFromRight(bounds.proportionOfWidth(0.15f)).toNearestInt());
bounds.removeFromTop(padding);
auto channelsBounds = bounds.removeFromTop(5 * row);
channelsLabel.setFont(getInter().withHeight(std::round(row * 0.95f)));
channelsLabel.setBounds(channelsBounds.removeFromLeft(channelsBounds.proportionOfWidth(labelProp)).removeFromTop(row).toNearestInt());
channelSelector.setBounds(channelsBounds.toNearestInt());
channelSelector.setRowHeight(int(std::round(row)));
bounds.removeFromTop(padding);
auto lastRow = row * 0.7f;
auto sampleRateBounds = bounds.removeFromTop(lastRow);
auto bufferSizeBounds = sampleRateBounds.removeFromRight(sampleRateBounds.proportionOfWidth(0.55f));
auto sampleRateLabelBounds = sampleRateBounds.removeFromLeft(sampleRateBounds.proportionOfWidth(0.5f));
sampleRateLabel.setFont(getInter().withHeight(std::round(lastRow * 0.95f)));
sampleRateLabel.setBounds(sampleRateLabelBounds.toNearestInt());
sampleRateChooser.setBounds(sampleRateBounds.withHeight(lastRow * 1.2f).toNearestInt());
bufferSizeBounds.removeFromLeft(bounds.getWidth() * 0.05f);
bufferSizeLabel.setFont(getInter().withHeight(std::round(lastRow * 0.95f)));
bufferSizeLabel.setBounds(bufferSizeBounds.removeFromLeft(bufferSizeBounds.proportionOfWidth(0.5f)).toNearestInt());
bufferSizeChooser.setBounds(bufferSizeBounds.withHeight(lastRow * 1.2f).toNearestInt());
}
void InputDeviceSelector::lookAndFeelChanged()
{
auto colors = getTheme();
warningLabel.setColour(juce::Label::textColourId, colors.highlight);
juce::Array comboBoxes = { &deviceTypeChooser, &deviceChooser, &sampleRateChooser, &bufferSizeChooser };
for (auto* box : comboBoxes)
box->setColour(Colors::backgroundColorId, colors.foreground);
}
void InputDeviceSelector::changeListenerCallback(juce::ChangeBroadcaster*)
{
auto* device = manager.getCurrentAudioDevice();
warningLabel.setVisible(!device);
if (!device)
return;
inputDevice = device;
deviceChooser.setText(device->getName());
sampleRateChooser.clear();
for (auto rate : device->getAvailableSampleRates())
sampleRateChooser.addItem(juce::String(rate) + " hz", int(rate));
sampleRateChooser.setText(juce::String(int(device->getCurrentSampleRate())) + " hz");
bufferSizeChooser.clear();
for (auto size : device->getAvailableBufferSizes())
bufferSizeChooser.addItem(juce::String(size), int(size));
bufferSizeChooser.setText(juce::String(device->getCurrentBufferSizeSamples()));
updateEnablement();
}
void InputDeviceSelector::settingsChanged()
{
juce::AudioDeviceManager::AudioDeviceSetup settings = manager.getAudioDeviceSetup();
auto deviceType = manager.getCurrentAudioDeviceType();
auto* selectedDeviceType = manager.getAvailableDeviceTypes()[deviceTypeChooser.getSelectedId() - 1];
bool newDeviceSelected = false;
if (deviceType != selectedDeviceType->getTypeName() || !bool(deviceChooser.getNumItems()))
{
manager.setCurrentAudioDeviceType(selectedDeviceType->getTypeName(), true);
deviceChooser.clear();
auto devices = selectedDeviceType->getDeviceNames(true);
for (int i = 0; i < devices.size(); i++)
deviceChooser.addItem(devices[i], i + 1);
deviceChooser.setSelectedId(1);
newDeviceSelected = true;
}
auto device = settings.inputDeviceName;
auto selectedDevice = deviceChooser.getItemText(deviceChooser.getSelectedItemIndex());
if (device != selectedDevice || newDeviceSelected)
{
settings.inputDeviceName = selectedDevice;
settings.useDefaultInputChannels = true;
}
auto sampleRate = int(settings.sampleRate);
auto selectedSampleRate = sampleRateChooser.getSelectedId();
if (sampleRate != selectedSampleRate && !newDeviceSelected && selectedSampleRate)
settings.sampleRate = selectedSampleRate;
auto bufferSize = settings.bufferSize;
auto selectedBufferSize = bufferSizeChooser.getSelectedId();
if (bufferSize != selectedBufferSize && !newDeviceSelected && selectedBufferSize)
settings.bufferSize = selectedBufferSize;
manager.setAudioDeviceSetup(settings, true);
updateEnablement();
}
void InputDeviceSelector::updateEnablement()
{
deviceChooser.setEnabled(bool(deviceChooser.getNumItems()));
sampleRateChooser.setEnabled(bool(sampleRateChooser.getNumItems()) && inputDevice && manager.getAudioDeviceSetup().inputChannels.countNumberOfSetBits());
bufferSizeChooser.setEnabled(bool(bufferSizeChooser.getNumItems()) && inputDevice && manager.getAudioDeviceSetup().inputChannels.countNumberOfSetBits());
levelMeter.setEnabled(bool(inputDevice));
channelSelector.setVisible(bool(inputDevice));
}

View file

@ -0,0 +1,114 @@
/*
==============================================================================
InputDeviceSelector.h
Created: 18 Aug 2024 8:20:19pm
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#include "../Utilities/ComponentUtils.h"
/** A simple meter that displays the input level of the active audio device. */
struct DeviceLevelMeter final : CustomComponent, juce::Timer
{
explicit DeviceLevelMeter(juce::AudioDeviceManager& m) : manager(m)
{
startTimerHz(20);
inputLevelGetter = manager.getInputLevelGetter();
}
void timerCallback() override
{
if (!isVisible())
return;
auto newLevel = float(inputLevelGetter->getCurrentLevel());
if (std::abs(level - newLevel) > 0.005f)
level = newLevel;
auto* device = manager.getCurrentAudioDevice();
if (!device || !manager.getAudioDeviceSetup().inputChannels.countNumberOfSetBits())
level = 0;
repaint();
}
void paint(juce::Graphics& g) override
{
// Add a bit of skew
getLookAndFeel().drawLevelMeter(g, getWidth(), getHeight(), float(std::exp(std::log(level) / 3.0)));
}
juce::AudioDeviceManager& manager;
juce::AudioDeviceManager::LevelMeter::Ptr inputLevelGetter;
float level = 0;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DeviceLevelMeter)
};
/** A simple component that allows the user to select the input channels of the active audio device. */
class ChannelSelectorListBox final : public juce::ListBox, juce::ListBoxModel, public juce::ChangeListener
{
public:
ChannelSelectorListBox(juce::AudioDeviceManager& audioDeviceManager, int minNumInputChannels, int maxNumInputChannels, bool useStereoPairs);
~ChannelSelectorListBox() override;
void changeListenerCallback(juce::ChangeBroadcaster*) override;
int getNumRows() override;
void listBoxItemClicked(int row, const juce::MouseEvent& e) override;
void paintListBoxItem(int row, juce::Graphics& g, int width, int height, bool) override;
private:
void flipEnablement(int row) const;
void setBit(juce::BigInteger& ch, int index, bool set) const;
juce::String getNameForChannelPair(const juce::String& name1, const juce::String& name2) const;
//==============================================================================
juce::AudioDeviceManager& manager;
int minInputChannels, maxInputChannels;
bool useStereoPairs;
juce::AudioIODevice* device{ nullptr };
juce::StringArray channels;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ChannelSelectorListBox)
};
/** A cleaner version of JUCE's AudioDeviceSelectorComponent */
class InputDeviceSelector final : public CustomComponent, public juce::ChangeListener
{
public:
InputDeviceSelector(juce::AudioDeviceManager& deviceManager, int minInputChannelsToUse, int maxInputChannelsToUse, bool showChannelsAsStereoPairs);
~InputDeviceSelector() override;
private:
void paint(juce::Graphics& g) override;
void resized() override;
void lookAndFeelChanged() override;
/** Update the choosers once the device updates */
void changeListenerCallback(juce::ChangeBroadcaster* source) override;
/** Update the device manager when a user selects something */
void settingsChanged();
void updateEnablement();
//==============================================================================
juce::AudioDeviceManager& manager;
juce::AudioIODevice* inputDevice{ nullptr };
juce::Label titleLabel, warningLabel, settingsLabel, deviceTypeLabel, deviceLabel, channelsLabel, sampleRateLabel, bufferSizeLabel;
juce::ComboBox deviceTypeChooser, deviceChooser, sampleRateChooser, bufferSizeChooser;
DeviceLevelMeter levelMeter;
ChannelSelectorListBox channelSelector;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(InputDeviceSelector)
};

122
Source/Components/Prompt.h Normal file
View file

@ -0,0 +1,122 @@
/*
==============================================================================
Prompt.h
Created: 15 May 2024 5:09:33pm
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#include "../Utilities/ComponentUtils.h"
/** A prompt that can be opened and closed with a set of components to be kept "active" */
class Prompt final : public CustomComponent
{
public:
Prompt()
{
closePrompt();
}
~Prompt() override = default;
/** Opens a prompt with showComponents made visible with the prompt and highlightComponents placed in front
of the prompt background (although their visibility is never changed). onClose is called when the prompt is closed.
*/
void openPrompt(const juce::Array<Component*>& visibleComponents, const std::function<void()>& onClose = []() -> void {})
{
if (visible)
closePrompt();
onCloseCallback = onClose;
shownComponents = visibleComponents;
setVisible(true);
setInterceptsMouseClicks(true, true);
setWantsKeyboardFocus(true);
grabKeyboardFocus();
repaint();
visible = true;
}
/** Closes the prompt */
void closePrompt()
{
setVisible(false);
setInterceptsMouseClicks(false, false);
setWantsKeyboardFocus(false);
visible = false;
if (onCloseCallback)
onCloseCallback();
onCloseCallback = nullptr;
}
/** Returns whether the prompt is visible */
bool isPromptVisible() const { return visible; }
private:
/** Paint in all but the shown components */
void paint(juce::Graphics& g) override
{
if (!visible)
return;
auto colors = getTheme();
for (auto* comp : shownComponents)
g.excludeClipRegion(comp->getBounds().translated(-getX(), -getY()));
g.fillAll(colors.prompt);
auto bounds = getLocalBounds().toFloat();
auto xBounds = bounds.removeFromTop(getWidth() * 0.035f).removeFromRight(getWidth() * 0.035f).reduced(getWidth() * 0.01f);
auto thickness = getWidth() * 0.003f;
g.setColour(colors.dark);
g.drawLine({ xBounds.getTopLeft(), xBounds.getBottomRight() }, thickness);
g.drawLine({ xBounds.getTopRight(), xBounds.getBottomLeft() }, thickness);
}
/** Close the prompt when escape or space is pressed */
bool keyPressed(const juce::KeyPress& key) override
{
if (visible)
{
if (key == juce::KeyPress::escapeKey || key == juce::KeyPress::spaceKey)
{
closePrompt();
return true;
}
}
return false;
}
bool hitTest(int x, int y) override
{
return visible && !std::ranges::any_of(shownComponents, [this, x, y](const Component* comp) {
return comp->getBounds().translated(-getX(), -getY()).contains(x, y);
});
}
void mouseDown(const juce::MouseEvent&) override
{
if (visible)
closePrompt();
}
//==============================================================================
bool visible{ false };
juce::Array<juce::Component*> shownComponents;
std::function<void()> onCloseCallback;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Prompt)
};

View file

@ -0,0 +1,125 @@
/*
==============================================================================
RangeSelection.h
Created: 16 May 2024 8:28:47pm
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#include "../Utilities/ComponentUtils.h"
/** A component that allows the user to select a range of a display */
class RangeSelector final : public CustomComponent
{
public:
RangeSelector()
{
cancelRangeSelect();
}
/** Begin the range selection prompt */
void promptRangeSelect(const std::function<void(int startPos, int endPos)>& onCloseCallback = {})
{
isSelecting = true;
draggingStarted = false;
onSelectedCallback = onCloseCallback;
setVisible(true);
setInterceptsMouseClicks(true, false);
resized();
}
/** Cancel the prompt */
void cancelRangeSelect()
{
isSelecting = false;
setVisible(false);
setInterceptsMouseClicks(false, false);
resized();
}
bool isSelectingRange() const
{
return isSelecting;
}
private:
void paint(juce::Graphics& g) override
{
if (!isSelecting)
return;
auto colors = getTheme();
if (draggingStarted)
{
int x = juce::jmin(startLoc, endLoc);
int width = juce::jmax(startLoc, endLoc) - x;
g.setColour(colors.slate.withAlpha(0.15f));
g.fillRect(x, 0, width, getHeight());
g.setColour(colors.slate);
g.fillRect(float(startLoc), 0.f, getWidth() * Layout::boundsWidth / 2.f, float(getHeight()));
g.fillRect(float(endLoc), 0.f, getWidth() * Layout::boundsWidth / 2.f, float(getHeight()));
}
else
{
int mousePos = getMouseXYRelative().getX();
g.setColour(colors.slate);
g.fillRect(float(mousePos), 0.f, getWidth() * Layout::boundsWidth / 2.f, float(getHeight()));
}
}
void mouseDown(const juce::MouseEvent& event) override
{
if (isSelecting && !draggingStarted)
{
draggingStarted = true;
startLoc = endLoc = juce::jlimit<int>(getX(), getRight(), event.x);
}
}
void mouseDrag(const juce::MouseEvent& event) override
{
if (isSelecting && draggingStarted)
{
endLoc = juce::jlimit<int>(getX(), getRight(), event.x);
repaint();
}
}
void mouseUp(const juce::MouseEvent& event) override
{
if (isSelecting && draggingStarted)
{
endLoc = juce::jlimit<int>(getX(), getRight(), event.x);
int leftLoc = juce::jmin(startLoc, endLoc);
int rightLoc = juce::jmax(startLoc, endLoc);
cancelRangeSelect(); // To hide the component
onSelectedCallback(leftLoc, rightLoc);
}
}
void mouseMove(const juce::MouseEvent&) override
{
repaint();
}
//==============================================================================
bool isSelecting{ false };
bool draggingStarted{ false };
int startLoc{ 0 }, endLoc{ 0 };
std::function<void(int, int)> onSelectedCallback;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RangeSelector)
};

View file

@ -0,0 +1,556 @@
/*
==============================================================================
SampleEditorOverlay.cpp
Created: 19 Sep 2023 2:03:29pm
Author: binya
==============================================================================
*/
#include <JuceHeader.h>
#include "SampleEditor.h"
SampleEditorOverlay::SampleEditorOverlay(const APVTS& apvts, PluginParameters::State& pluginState, const juce::OwnedArray<CustomSamplerVoice>& synthVoices, UIDummyParam& dummy, CustomComponent* forwardEventsTo) :
synthVoices(synthVoices), dummyParam(dummy),
viewStart(pluginState.viewStart),
viewEnd(pluginState.viewEnd),
sampleStart(pluginState.sampleStart),
sampleEnd(pluginState.sampleEnd),
loopStart(pluginState.loopStart),
loopEnd(pluginState.loopEnd),
pinnedBounds(pluginState.pinView),
isWavetableModeDisabled(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::DISABLE_WAVETABLE_MODE))),
isLooping(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::IS_LOOPING))),
loopingHasStart(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::LOOPING_HAS_START))),
loopingHasEnd(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::LOOPING_HAS_END))),
isWavetableModeDisabledAttachment(*isWavetableModeDisabled, [this](bool) { repaint(); }, apvts.undoManager),
isLoopingAttachment(*isLooping, [this](bool) { repaint(); }, apvts.undoManager),
loopingHasStartAttachment(*loopingHasStart, [this](bool) { repaint(); }, apvts.undoManager),
loopingHasEndAttachment(*loopingHasEnd, [this](bool) { repaint(); }, apvts.undoManager),
forwardMouseEvents(forwardEventsTo)
{
viewStart.addListener(this);
viewEnd.addListener(this);
sampleStart.addListener(this);
sampleEnd.addListener(this);
loopStart.addListener(this);
loopEnd.addListener(this);
}
SampleEditorOverlay::~SampleEditorOverlay()
{
viewStart.removeListener(this);
viewEnd.removeListener(this);
sampleStart.removeListener(this);
sampleEnd.removeListener(this);
loopStart.removeListener(this);
loopEnd.removeListener(this);
}
//==============================================================================
void SampleEditorOverlay::valueChanged(ListenableValue<int>&, int)
{
safeRepaint();
}
void SampleEditorOverlay::paint(juce::Graphics& g)
{
if (viewStart == 0 && viewEnd == 0)
return;
using namespace juce;
auto colors = getTheme();
// Draw voice positions
auto waveformMode = isWaveformMode();
auto gain = 0.f;
for (auto& voice : synthVoices)
{
if (voice->isPlaying())
{
int location = int(std::ceil(voice->getPosition()));
auto pos = sampleToPosition(location) + getWidth() * Layout::boundsWidth;
Path voicePosition;
voicePosition.addLineSegment(Line<float>(pos, 0.f, pos, float(getHeight())), 1);
if (!waveformMode)
{
g.setColour(colors.light.withAlpha(voice->getEnvelopeGain()));
g.strokePath(voicePosition, PathStrokeType(Layout::playheadWidth * getWidth()));
}
gain += voice->getEnvelopeGain() / synthVoices.size();
}
}
float boundsWidth = getBoundsWidth();
float boundsSeparation = jmax(5 * boundsWidth, sampleToPosition(viewStart + Feel::MINIMUM_BOUNDS_DISTANCE) + 2 * boundsWidth);
float handleStrokeWidth = Layout::handleWidth * getWidth();
float startPos = sampleToPosition(sampleStart);
float endPos = sampleToPosition(sampleEnd) + boundsWidth;
float loopStartPos = sampleToPosition(loopStart);
float loopEndPos = sampleToPosition(loopEnd) + boundsWidth;
auto looping = isLooping->get() && !waveformMode;
auto loopingWithStart = isLooping->get() && loopingHasStart->get() && !waveformMode;
auto loopingWithEnd = isLooping->get() && loopingHasEnd->get() && !waveformMode;
// Paint the backgrounds
if (looping || waveformMode)
{
auto color = looping ? colors.loop.withAlpha(0.07f) : colors.highlight.withAlpha(jmin(gain * 0.5f, 0.2f));
g.setColour(disabled(color));
g.fillRect(Rectangle(startPos, 0.f, endPos - startPos, float(getHeight())));
}
g.setColour(disabled(colors.slate.withAlpha(0.15f)));
g.fillRect(Rectangle(0.f, 0.f, loopingWithStart ? loopStartPos : startPos, float(getHeight())));
g.fillRect(Rectangle(loopingWithEnd ? loopEndPos : endPos, 0.f, float(getWidth()) - endPos, float(getHeight())));
// Paint the start bound
Path startPosPath;
startPosPath.addRectangle(startPos, 0.f, boundsWidth, float(getHeight()));
(looping ? loopBoundsShadow : boundsShadow).render(g, startPosPath);
auto color = looping ? colors.loop : waveformMode ? colors.highlight : colors.slate;
g.setColour(dragging && draggingTarget == EditorParts::SAMPLE_START ? color.withAlpha(0.5f) : disabled(color));
g.fillPath(startPosPath);
if (isMouseOverOrDragging() && (!loopingWithStart || startPos - loopStartPos > boundsSeparation))
g.strokePath(handleLeft, PathStrokeType(handleStrokeWidth), AffineTransform::translation(startPos, 0.f));
if (isMouseOverOrDragging() && endPos - startPos > boundsSeparation)
g.strokePath(handleRight, PathStrokeType(handleStrokeWidth), AffineTransform::translation(startPos, 0.f));
// Paint the end bound
Path endPosPath;
endPosPath.addRectangle(endPos, 0.f, boundsWidth, float(getHeight()));
(looping ? loopBoundsShadow : boundsShadow).render(g, endPosPath);
g.setColour(dragging && draggingTarget == EditorParts::SAMPLE_END ? color.withAlpha(0.5f) : disabled(color));
g.fillPath(endPosPath);
if (isMouseOverOrDragging() && endPos - startPos > boundsSeparation)
g.strokePath(handleLeft, PathStrokeType(handleStrokeWidth), AffineTransform::translation(endPos, 0.f));
if (isMouseOverOrDragging() && (!loopingWithEnd || loopEndPos - endPos > boundsSeparation))
g.strokePath(handleRight, PathStrokeType(handleStrokeWidth), AffineTransform::translation(endPos, 0.f));
// Paint the loop bounds
if (looping)
{
if (loopingWithStart)
{
Path loopStartPath;
loopStartPath.addRectangle(loopStartPos, 0.f, boundsWidth, float(getHeight()));
boundsShadow.render(g, loopStartPath);
g.setColour(dragging && draggingTarget == EditorParts::LOOP_START ? colors.slate.withAlpha(0.5f) : disabled(colors.slate));
g.fillPath(loopStartPath);
if (isMouseOverOrDragging())
g.strokePath(handleLeft, PathStrokeType(handleStrokeWidth), AffineTransform::translation(loopStartPos, 0.f));
if (isMouseOverOrDragging() && startPos - loopStartPos > boundsSeparation)
g.strokePath(handleRight, PathStrokeType(handleStrokeWidth), AffineTransform::translation(loopStartPos, 0.f));
}
if (loopingWithEnd)
{
Path loopEndPath;
loopEndPath.addRectangle(loopEndPos, 0.f, boundsWidth, float(getHeight()));
boundsShadow.render(g, loopEndPath);
g.setColour(dragging && draggingTarget == EditorParts::LOOP_END ? colors.slate.withAlpha(0.5f) : disabled(colors.slate));
g.fillPath(loopEndPath);
if (isMouseOverOrDragging() && loopEndPos - endPos > boundsSeparation)
g.strokePath(handleLeft, PathStrokeType(handleStrokeWidth, PathStrokeType::curved, PathStrokeType::rounded), AffineTransform::translation(loopEndPos, 0.f));
if (isMouseOverOrDragging())
g.strokePath(handleRight, PathStrokeType(handleStrokeWidth, PathStrokeType::curved, PathStrokeType::rounded), AffineTransform::translation(loopEndPos, 0.f));
}
}
Path innerShadowPath;
innerShadowPath.addRectangle(getLocalBounds());
innerShadow.render(g, innerShadowPath);
}
void SampleEditorOverlay::resized()
{
int offset = int(0.0005f * getWidth());
boundsShadow.setOffset({ offset, 0 }, 0);
boundsShadow.setOffset({ -offset, 0 }, 1);
loopBoundsShadow.setOffset({ offset, 0 }, 0);
loopBoundsShadow.setOffset({ -offset, 0 }, 1);
int innerShadowOffset = int(0.001f * getWidth());
innerShadow.setOffset({ 0, innerShadowOffset }, 0);
innerShadow.setOffset({ 0, -innerShadowOffset }, 1);
float bounds = getBoundsWidth();
float handleHeight = 0.04f * getWidth();
handleLeft.clear();
handleLeft.startNewSubPath(-bounds / 2.f, (getHeight() + handleHeight) / 2.f);
handleLeft.lineTo(-bounds * 4.f / 3.f, getHeight() / 2.f);
handleLeft.lineTo(-bounds / 2.f, (getHeight() - handleHeight) / 2.f);
handleRight.clear();
handleRight.startNewSubPath(bounds + bounds / 2.f, (getHeight() + handleHeight) / 2.f);
handleRight.lineTo(bounds + bounds * 4.f / 3.f, getHeight() / 2.f);
handleRight.lineTo(bounds + bounds / 2.f, (getHeight() - handleHeight) / 2.f);
}
void SampleEditorOverlay::enablementChanged()
{
setInterceptsMouseClicks(isEnabled(), isEnabled());
repaint();
}
void SampleEditorOverlay::lookAndFeelChanged()
{
auto colors = getTheme();
boundsShadow.setColor(colors.slate.withAlpha(0.25f), 0);
boundsShadow.setColor(colors.slate.withAlpha(0.25f), 1);
loopBoundsShadow.setColor(colors.loop.withAlpha(0.25f), 0);
loopBoundsShadow.setColor(colors.loop.withAlpha(0.25f), 1);
innerShadow.setColor(colors.slate.withAlpha(0.25f), 0);
innerShadow.setColor(colors.slate.withAlpha(0.25f), 1);
}
//==============================================================================
void SampleEditorOverlay::mouseMove(const juce::MouseEvent& event)
{
EditorParts editorPart = getClosestPartInRange(event.x, event.y);
switch (editorPart)
{
case EditorParts::SAMPLE_START:
case EditorParts::SAMPLE_END:
case EditorParts::LOOP_START:
case EditorParts::LOOP_END:
setMouseCursor(juce::MouseCursor::LeftRightResizeCursor);
break;
case EditorParts::NONE:
setMouseCursor(juce::MouseCursor::NormalCursor);
}
}
void SampleEditorOverlay::mouseDown(const juce::MouseEvent& event)
{
EditorParts closest = getClosestPartInRange(event.getMouseDownX(), event.getMouseDownY());
switch (closest)
{
case EditorParts::NONE:
if (forwardMouseEvents)
forwardMouseEvents->mouseDown(event.getEventRelativeTo(forwardMouseEvents));
break;
default:
dragging = true;
draggingTarget = closest;
repaint();
break;
}
}
void SampleEditorOverlay::mouseUp(const juce::MouseEvent& event)
{
if (dragging)
dummyParam.sendUIUpdate();
dragging = false;
EditorParts closest = getClosestPartInRange(event.getMouseDownX(), event.getMouseDownY());
if (closest == EditorParts::NONE && forwardMouseEvents)
forwardMouseEvents->mouseUp(event.getEventRelativeTo(forwardMouseEvents));
repaint();
}
void SampleEditorOverlay::mouseDrag(const juce::MouseEvent& event)
{
using namespace juce;
if (!dragging)
return;
auto newSample = positionToSample(event.getMouseDownX() + event.getOffsetFromDragStart().getX() - getBoundsWidth());
auto loopHasStart = isLooping->get() && loopingHasStart->get() && !isWaveformMode();
auto loopHasEnd = isLooping->get() && loopingHasEnd->get() && !isWaveformMode();
auto pin = pinnedBounds.load();
auto minBufferSize = (1 + int(loopHasStart) + int(loopHasEnd)) * Feel::MINIMUM_BOUNDS_DISTANCE;
if (sampleBuffer->getNumSamples() < minBufferSize)
return;
// An intricate way to handle the dragging of the bounds, hopefully for a user-friendly experience
// Note that jmin(jmax(value, lower), upper) is equivalent to jlimit but uses the upper bound if min > max, while jmax(jmin(value, upper), lower) does the opposite
switch (draggingTarget)
{
case EditorParts::LOOP_START:
viewStart = jmax<int>(jmin<int>(viewStart, newSample), 0);
viewEnd = jmin<int>(jmax<int>(viewEnd, newSample + (2 + int(loopHasEnd)) * Feel::MINIMUM_BOUNDS_DISTANCE), sampleBuffer->getNumSamples() - 1);
if (newSample < loopStart)
{
loopStart = jmax(0, newSample);
}
else
{
loopEnd = jmin<int>(jmax<int>(loopEnd, newSample + 3 * Feel::MINIMUM_BOUNDS_DISTANCE), loopHasEnd && pin && loopEnd <= viewEnd ? viewEnd.load() : sampleBuffer->getNumSamples() - 1);
sampleEnd = jmin<int>(jmax<int>(sampleEnd, newSample + 2 * Feel::MINIMUM_BOUNDS_DISTANCE), jmin<int>(loopHasEnd ? loopEnd - Feel::MINIMUM_BOUNDS_DISTANCE : loopEnd.load(), pin ? viewEnd.load() : sampleBuffer->getNumSamples() - 1));
sampleStart = jmin<int>(jmax<int>(sampleStart, newSample + Feel::MINIMUM_BOUNDS_DISTANCE), sampleEnd - Feel::MINIMUM_BOUNDS_DISTANCE);
loopStart = jmin<int>(newSample, sampleStart - Feel::MINIMUM_BOUNDS_DISTANCE);
}
break;
case EditorParts::SAMPLE_START:
viewStart = jmax<int>(jmin<int>(viewStart, newSample - int(loopHasStart) * Feel::MINIMUM_BOUNDS_DISTANCE), 0);
viewEnd = jmin<int>(jmax<int>(viewEnd, newSample + (1 + int(loopHasEnd)) * Feel::MINIMUM_BOUNDS_DISTANCE), sampleBuffer->getNumSamples() - 1);
if (newSample < sampleStart)
{
loopStart = jmax<int>(jmin<int>(loopStart, newSample - Feel::MINIMUM_BOUNDS_DISTANCE), loopHasStart && pin && loopStart >= viewStart ? viewStart.load() : 0);
sampleStart = jmax<int>(newSample, loopStart + int(loopHasStart) * Feel::MINIMUM_BOUNDS_DISTANCE);
}
else
{
loopEnd = jmin<int>(jmax<int>(loopEnd, newSample + 2 * Feel::MINIMUM_BOUNDS_DISTANCE), loopHasEnd && pin && loopEnd <= viewEnd ? viewEnd.load() : sampleBuffer->getNumSamples() - 1);
sampleEnd = jmin<int>(jmax<int>(sampleEnd, newSample + Feel::MINIMUM_BOUNDS_DISTANCE), jmin<int>(loopHasEnd ? loopEnd - Feel::MINIMUM_BOUNDS_DISTANCE : loopEnd.load(), pin ? viewEnd.load() : sampleBuffer->getNumSamples() - 1));
sampleStart = jmin<int>(newSample, sampleEnd - Feel::MINIMUM_BOUNDS_DISTANCE);
}
break;
case EditorParts::SAMPLE_END:
viewStart = jmax<int>(jmin<int>(viewStart, newSample - (1 + int(loopHasStart)) * Feel::MINIMUM_BOUNDS_DISTANCE), 0);
viewEnd = jmin<int>(jmax<int>(viewEnd, newSample + int(loopHasEnd) * Feel::MINIMUM_BOUNDS_DISTANCE), sampleBuffer->getNumSamples() - 1);
if (newSample > sampleEnd)
{
loopEnd = jmin<int>(jmax<int>(loopEnd, newSample + Feel::MINIMUM_BOUNDS_DISTANCE), loopHasEnd && pin && loopEnd <= viewEnd ? viewEnd.load() : sampleBuffer->getNumSamples() - 1);
sampleEnd = jmin<int>(newSample, loopEnd - int(loopHasEnd) * Feel::MINIMUM_BOUNDS_DISTANCE);
}
else
{
loopStart = jmax<int>(jmin<int>(loopStart, newSample - 2 * Feel::MINIMUM_BOUNDS_DISTANCE), loopHasStart && pin && loopStart >= viewStart ? viewStart.load() : 0);
sampleStart = jmax<int>(jmin<int>(sampleStart, newSample - Feel::MINIMUM_BOUNDS_DISTANCE), jmax<int>(loopHasStart ? loopStart + Feel::MINIMUM_BOUNDS_DISTANCE : loopStart.load(), pin ? viewStart.load() : 0));
sampleEnd = jmax<int>(newSample, sampleStart + Feel::MINIMUM_BOUNDS_DISTANCE);
}
break;
case EditorParts::LOOP_END:
viewStart = jmax<int>(jmin<int>(viewStart, newSample - (2 + int(loopHasStart)) * Feel::MINIMUM_BOUNDS_DISTANCE), 0);
viewEnd = jmin<int>(jmax<int>(viewEnd, newSample), sampleBuffer->getNumSamples() - 1);
if (newSample > loopEnd)
{
loopEnd = jmin(sampleBuffer->getNumSamples() - 1, newSample);
}
else
{
loopStart = jmax<int>(jmin<int>(loopStart, newSample - 3 * Feel::MINIMUM_BOUNDS_DISTANCE), loopHasStart && pin && loopStart >= viewStart ? viewStart.load() : 0);
sampleStart = jmax<int>(jmin<int>(sampleStart, newSample - 2 * Feel::MINIMUM_BOUNDS_DISTANCE), jmax<int>(loopHasStart ? loopStart + Feel::MINIMUM_BOUNDS_DISTANCE : loopStart.load(), pin ? viewStart.load() : 0));
sampleEnd = jmax<int>(jmin<int>(sampleEnd, newSample - Feel::MINIMUM_BOUNDS_DISTANCE), sampleStart + Feel::MINIMUM_BOUNDS_DISTANCE);
loopEnd = jmax<int>(newSample, sampleEnd + Feel::MINIMUM_BOUNDS_DISTANCE);
}
break;
}
}
//==============================================================================
void SampleEditorOverlay::setSample(const juce::AudioBuffer<float>& sample, float bufferSampleRate)
{
sampleBuffer = &sample;
sampleRate = bufferSampleRate;
}
juce::String SampleEditorOverlay::getCustomHelpText()
{
auto closest = dragging ? draggingTarget : getClosestPartInRange(getMouseXYRelative().getX(), getMouseXYRelative().getY());
switch (closest)
{
case EditorParts::SAMPLE_START: return "Adjust sample start";
case EditorParts::SAMPLE_END: return "Adjust sample end";
case EditorParts::LOOP_START: return "Adjust loop start portion";
case EditorParts::LOOP_END: return "Adjust loop release portion";
default:
if (forwardMouseEvents)
{
const auto& helpText = forwardMouseEvents->getCustomHelpText();
if (helpText.isNotEmpty())
return helpText;
}
return "Scroll to navigate";
}
}
//==============================================================================
EditorParts SampleEditorOverlay::getClosestPartInRange(int x, int y) const
{
auto startPos = sampleToPosition(sampleStart);
auto endPos = sampleToPosition(sampleEnd);
juce::Array targets = {
CompPart {EditorParts::SAMPLE_START, juce::Rectangle(startPos + getBoundsWidth() / 2.f, 0.f, 1.f, float(getHeight())), 1},
CompPart {EditorParts::SAMPLE_END, juce::Rectangle(endPos + 3 * getBoundsWidth() / 2.f, 0.f, 1.f, float(getHeight())), 1},
};
if (isLooping->get() && !isWaveformMode())
{
if (loopingHasStart->get())
targets.add(CompPart{ EditorParts::LOOP_START, juce::Rectangle(sampleToPosition(loopStart) + getBoundsWidth() / 2.f, 0.f, 1.f, float(getHeight())), 1 });
if (loopingHasEnd->get())
targets.add(CompPart{ EditorParts::LOOP_END, juce::Rectangle(sampleToPosition(loopEnd) + 3 * getBoundsWidth() / 2.f, 0.f, 1.f, float(getHeight())), 1 });
}
return CompPart<EditorParts>::getClosestInRange(targets, x, y, Feel::DRAGGABLE_SNAP);
}
float SampleEditorOverlay::getBoundsWidth() const
{
return Layout::boundsWidth * getWidth();
}
float SampleEditorOverlay::sampleToPosition(int sampleIndex) const
{
return juce::jmap<float>(float(sampleIndex - viewStart), 0.f, float(viewEnd - viewStart), 0.f, float(getWidth() - 2 * getBoundsWidth()));
}
int SampleEditorOverlay::positionToSample(float position) const
{
return viewStart + int(std::round(juce::jmap<float>(position, 0.f, float(getWidth() - 2 * getBoundsWidth()), 0.f, float(viewEnd - viewStart))));
}
bool SampleEditorOverlay::isWaveformMode() const
{
return CustomSamplerVoice::isWavetableModeAvailable(sampleRate, sampleStart, sampleEnd) && !isWavetableModeDisabled->get();
}
/*
==============================================================================
SampleEditor.cpp
Created: 19 Sep 2023 2:03:29pm
Author: binya
==============================================================================
*/
SampleEditor::SampleEditor(APVTS& apvts, PluginParameters::State& pluginState, const juce::OwnedArray<CustomSamplerVoice>& synthVoices, const std::function<void(const juce::MouseWheelDetails& details, int centerSample)>& navScrollFunc) :
apvts(apvts), pluginState(pluginState), dummyParam(apvts, PluginParameters::State::UI_DUMMY_PARAM),
painter(pluginState.primaryChannel, 0.25f, &dummyParam),
gainAttachment(*apvts.getParameter(PluginParameters::SAMPLE_GAIN), [this](float newValue) { painter.setGain(juce::Decibels::decibelsToGain(newValue)); }, apvts.undoManager),
monoAttachment(*apvts.getParameter(PluginParameters::MONO_OUTPUT), [this](bool newValue) { painter.setMono(newValue); }, apvts.undoManager),
overlay(apvts, pluginState, synthVoices, dummyParam, &painter), scrollFunc(navScrollFunc)
{
pluginState.viewStart.addListener(this);
pluginState.viewEnd.addListener(this);
painter.setGain(juce::Decibels::decibelsToGain(float(apvts.getParameterAsValue(PluginParameters::SAMPLE_GAIN).getValue())));
painter.setMono(bool(apvts.getParameterAsValue(PluginParameters::MONO_OUTPUT).getValue()));
addAndMakeVisible(&painter);
overlay.toFront(true);
addAndMakeVisible(&overlay);
boundsSelector.toFront(true);
addAndMakeVisible(&boundsSelector);
}
SampleEditor::~SampleEditor()
{
pluginState.viewStart.removeListener(this);
pluginState.viewEnd.removeListener(this);
}
//==============================================================================
void SampleEditor::valueChanged(ListenableValue<int>& source, int /*newValue*/)
{
if ((&source == &pluginState.viewStart || &source == &pluginState.viewEnd) && pluginState.viewStart < pluginState.viewEnd)
{
painter.setSampleView(pluginState.viewStart, pluginState.viewEnd);
}
}
//==============================================================================
void SampleEditor::resized()
{
auto bounds = getLocalBounds();
overlay.setBounds(bounds);
bounds.reduce(int(Layout::boundsWidth * bounds.getWidth()), 0);
painter.setBounds(bounds.reduced(0, int(getWidth() * 0.01f)));
boundsSelector.setBounds(bounds);
}
void SampleEditor::enablementChanged()
{
overlay.setEnabled(isEnabled());
painter.setEnabled(isEnabled());
}
void SampleEditor::mouseWheelMove(const juce::MouseEvent& event, const juce::MouseWheelDetails& wheel)
{
if (!sampleBuffer || recordingMode)
return;
int sample = positionToSample(event.position.getX());
scrollFunc(wheel, sample);
}
int SampleEditor::positionToSample(float position) const
{
if (!sampleBuffer)
return 0;
return juce::jlimit<int>(0, sampleBuffer->getNumSamples() - 1, overlay.positionToSample(position - Layout::boundsWidth * getWidth()));
}
//==============================================================================
void SampleEditor::setSample(const juce::AudioBuffer<float>& sample, float bufferSampleRate, bool resetView)
{
sampleBuffer = &sample;
sampleRate = bufferSampleRate;
if (resetView || recordingMode)
painter.setSample(sample);
else
painter.setSample(sample, pluginState.viewStart, pluginState.viewEnd);
overlay.setSample(sample, bufferSampleRate);
}
void SampleEditor::setRecordingMode(bool recording)
{
recordingMode = recording;
overlay.setVisible(!recording);
}
bool SampleEditor::isRecordingMode() const
{
return recordingMode;
}
void SampleEditor::sampleUpdated(int oldSize, int newSize)
{
painter.appendToPath(oldSize, newSize - 1);
}
//==============================================================================
void SampleEditor::promptBoundsSelection(const std::function<void(int, int)>& callback)
{
overlay.setEnabled(false);
boundsSelector.promptRangeSelect([this, callback](int startPos, int endPos) -> void {
return callback(positionToSample(float(startPos)), positionToSample(float(endPos)));
});
}
void SampleEditor::cancelBoundsSelection()
{
overlay.setEnabled(true);
boundsSelector.cancelRangeSelect();
}
bool SampleEditor::isInBoundsSelection() const
{
return boundsSelector.isSelectingRange();
}

View file

@ -0,0 +1,168 @@
/*
==============================================================================
SampleEditorOverlay.h
Created: 19 Sep 2023 2:03:29pm
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#include "../Sampler/CustomSamplerVoice.h"
#include "../Utilities/ComponentUtils.h"
#include "RangeSelector.h"
#include "Displays/SamplePainter.h"
/** An enum of selectable parts of the editor overlay. */
enum class EditorParts
{
NONE,
SAMPLE_START,
SAMPLE_END,
LOOP_START,
LOOP_END,
};
/** The overlay draws the bounds and active voices, to be placed over the main editor object */
class SampleEditorOverlay final : public CustomComponent, public ValueListener<int>
{
public:
SampleEditorOverlay(const APVTS& apvts, PluginParameters::State& pluginState, const juce::OwnedArray<CustomSamplerVoice>& synthVoices, UIDummyParam& dummy, CustomComponent* forwardEventsTo = nullptr);
~SampleEditorOverlay() override;
void setSample(const juce::AudioBuffer<float>& sample, float bufferSampleRate);
/** Utility functions */
float sampleToPosition(int sampleIndex) const;
int positionToSample(float position) const;
bool isWaveformMode() const;
private:
void valueChanged(ListenableValue<int>& source, int newValue) override;
void paint(juce::Graphics&) override;
void resized() override;
void enablementChanged() override;
void lookAndFeelChanged() override;
void mouseMove(const juce::MouseEvent& event) override;
void mouseDown(const juce::MouseEvent& event) override;
void mouseUp(const juce::MouseEvent& event) override;
void mouseDrag(const juce::MouseEvent& event) override;
void mouseEnter(const juce::MouseEvent&) override { repaint(); };
void mouseExit(const juce::MouseEvent&) override { repaint(); };
juce::String getCustomHelpText() override;
EditorParts getClosestPartInRange(int x, int y) const;
float getBoundsWidth() const;
//==============================================================================
const juce::AudioBuffer<float>* sampleBuffer{ nullptr };
float sampleRate{ 0.f };
const juce::OwnedArray<CustomSamplerVoice>& synthVoices;
UIDummyParam& dummyParam;
ListenableAtomic<int>& viewStart, & viewEnd, & sampleStart, & sampleEnd, & loopStart, & loopEnd;
ListenableAtomic<bool>& pinnedBounds;
juce::AudioParameterBool* isWavetableModeDisabled, * isLooping, * loopingHasStart, * loopingHasEnd;
juce::ParameterAttachment isWavetableModeDisabledAttachment, isLoopingAttachment, loopingHasStartAttachment, loopingHasEndAttachment;
bool dragging{ false };
EditorParts draggingTarget{ EditorParts::NONE };
melatonin::DropShadow boundsShadow{
{defaultTheme.slate.withAlpha(0.25f), 2, {1, 0}},
{defaultTheme.slate.withAlpha(0.25f), 2, {-1, 0}}
};
melatonin::DropShadow loopBoundsShadow{
{defaultTheme.loop.withAlpha(0.25f), 2, {1, 0}},
{defaultTheme.loop.withAlpha(0.25f), 2, {-1, 0}}
};
melatonin::InnerShadow innerShadow{
{defaultTheme.slate.withAlpha(0.25f), 3, {0, 2}},
{defaultTheme.slate.withAlpha(0.25f), 3, {0, -2}}
};
juce::Path handleLeft, handleRight;
/** The overlay can be provided with a component to forward unhandled mouse events to.
This currently only forwards particular mouse events, but can be expanded if we need.
*/
CustomComponent* forwardMouseEvents{ nullptr };
};
/*
==============================================================================
SampleEditor.h
Created: 19 Sep 2023 2:03:29pm
Author: binya
==============================================================================
*/
/** The SampleEditor is the main component responsible for editing a sample. It allows for setting
the bounds for playback and looping. It also allows for bounds selection for operations that need it.
It reacts to viewport changes from the sample navigator. Like the navigator, it also displays active voices.
*/
class SampleEditor final : public CustomComponent, public ValueListener<int>
{
public:
/** The SampleEditor requires reference to the apvts, pluginState, synthVoices, and a
function reference to scroll the navigator (navigator.scrollView).
*/
SampleEditor(APVTS& apvts, PluginParameters::State& pluginState, const juce::OwnedArray<CustomSamplerVoice>& synthVoices,
const std::function<void(const juce::MouseWheelDetails& details, int centerSample)>& navScrollFunc);
~SampleEditor() override;
//==============================================================================
void setSample(const juce::AudioBuffer<float>& sample, float bufferSampleRate, bool resetView);
/** Recording mode hides the bounds selection and turns the editor into a view only
display while a recording is in progress.
*/
void setRecordingMode(bool recording);
bool isRecordingMode() const;
void sampleUpdated(int oldSize, int newSize); // Currently used for recording
//==============================================================================
/** Prompts the user to select a range of samples within the current viewport. */
void promptBoundsSelection(const std::function<void(int startSample, int endSample)>& callback);
void cancelBoundsSelection();
bool isInBoundsSelection() const;
private:
void valueChanged(ListenableValue<int>& source, int newValue) override;
void resized() override;
void enablementChanged() override;
void mouseWheelMove(const juce::MouseEvent& event, const juce::MouseWheelDetails& wheel) override;
int positionToSample(float position) const;
//==============================================================================
APVTS& apvts;
PluginParameters::State& pluginState;
UIDummyParam dummyParam;
const juce::AudioBuffer<float>* sampleBuffer{ nullptr };
float sampleRate{ 0.f };
SamplePainter painter;
juce::ParameterAttachment gainAttachment;
juce::ParameterAttachment monoAttachment;
SampleEditorOverlay overlay;
RangeSelector boundsSelector;
bool recordingMode{ false };
std::function<void(const juce::MouseWheelDetails& details, int centerSample)> scrollFunc;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SampleEditor)
};

View file

@ -0,0 +1,525 @@
/*
==============================================================================
SampleNavigator.cpp
Created: 19 Sep 2023 4:41:12pm
Author: binya
==============================================================================
*/
#include <JuceHeader.h>
#include "SampleNavigator.h"
SampleNavigator::SampleNavigator(APVTS& apvts, PluginParameters::State& pluginState, const juce::OwnedArray<CustomSamplerVoice>& synthVoices) :
apvts(apvts), state(pluginState), dummyParam(apvts, PluginParameters::State::UI_DUMMY_PARAM),
painter(pluginState.primaryChannel, 0.2f),
gainAttachment(*apvts.getParameter(PluginParameters::SAMPLE_GAIN), [this](float newValue) { painter.setGain(juce::Decibels::decibelsToGain(newValue)); }, apvts.undoManager),
monoAttachment(*apvts.getParameter(PluginParameters::MONO_OUTPUT), [this](bool newValue) { painter.setMono(newValue); }, apvts.undoManager),
synthVoices(synthVoices),
isWavetableModeDisabled(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::DISABLE_WAVETABLE_MODE))),
isLooping(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::IS_LOOPING))),
loopHasStart(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::LOOPING_HAS_START))),
loopHasEnd(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::LOOPING_HAS_END))),
isWavetableModeDisabledAttachment(*this->isWavetableModeDisabled, [this](bool) { repaint(); }, apvts.undoManager),
loopAttachment(*isLooping, [this](bool newValue) { loopHasStartUpdate(newValue && loopHasStart->get()); loopHasEndUpdate(newValue && loopHasEnd->get()); }, apvts.undoManager),
loopStartAttachment(*loopHasStart, [this](bool newValue) { loopHasStartUpdate(newValue); }, apvts.undoManager),
loopEndAttachment(*loopHasEnd, [this](bool newValue) { loopHasEndUpdate(newValue); }, apvts.undoManager)
{
state.viewStart.addListener(this);
state.viewEnd.addListener(this);
state.sampleStart.addListener(this);
state.sampleEnd.addListener(this);
state.loopStart.addListener(this);
state.loopEnd.addListener(this);
state.pinView.addListener(this);
updatePinnedPositions();
painter.setGain(juce::Decibels::decibelsToGain(float(apvts.getParameterAsValue(PluginParameters::SAMPLE_GAIN).getValue())));
addAndMakeVisible(&painter);
painter.setInterceptsMouseClicks(false, false);
}
SampleNavigator::~SampleNavigator()
{
state.viewStart.removeListener(this);
state.viewEnd.removeListener(this);
state.sampleStart.removeListener(this);
state.sampleEnd.removeListener(this);
state.loopStart.removeListener(this);
state.loopEnd.removeListener(this);
state.pinView.removeListener(this);
}
void SampleNavigator::valueChanged(ListenableValue<int>& source, int /*newValue*/)
{
auto& sourceA = dynamic_cast<ListenableAtomic<int>&>(source);
if (!navigatorUpdate && (sourceA == state.loopStart || sourceA == state.loopEnd || sourceA == state.sampleStart || sourceA == state.sampleEnd))
updatePinnedPositions();
safeRepaint();
}
void SampleNavigator::valueChanged(ListenableValue<bool>& source, bool newValue)
{
// When the pin is enabled, we move the bounds to within the viewport
updatePinnedPositions();
bool loopingHasStart = isLooping->get() && loopHasStart->get() && !isWaveformMode();
bool loopingHasEnd = isLooping->get() && loopHasEnd->get() && !isWaveformMode();
if (state.pinView && dynamic_cast<ListenableAtomic<bool>&>(source) == state.pinView && newValue &&
((loopingHasStart && state.loopStart < state.viewStart) || (loopingHasEnd && state.loopEnd > state.viewEnd) ||
state.sampleStart < state.viewStart || state.sampleEnd > state.viewEnd))
{
int currentViewStart = state.viewStart;
int currentViewEnd = state.viewEnd;
int newViewStart = loopingHasStart ? state.loopStart.load() : state.sampleStart.load();
int newViewEnd = loopingHasEnd ? state.loopEnd.load() : state.sampleEnd.load();
state.viewStart = newViewStart;
state.viewEnd = newViewEnd;
updatePinnedPositions();
state.viewStart = currentViewStart;
state.viewEnd = currentViewEnd;
moveBoundsToPinnedPositions(newViewStart > currentViewStart);
}
}
//==============================================================================
void SampleNavigator::setSample(const juce::AudioBuffer<float>& sampleBuffer, float bufferSampleRate, bool resetView)
{
painter.setSample(sampleBuffer);
sample = &sampleBuffer;
sampleRate = bufferSampleRate;
if (resetView || recordingMode)
{
state.viewStart = 0;
state.viewEnd = sampleBuffer.getNumSamples() - 1;
updatePinnedPositions();
}
repaint();
}
void SampleNavigator::sampleUpdated(int oldSize, int newSize)
{
painter.appendToPath(oldSize, newSize - 1);
}
void SampleNavigator::setRecordingMode(bool recording)
{
recordingMode = recording;
}
//==============================================================================
void SampleNavigator::paintOverChildren(juce::Graphics& g)
{
if (!sample || !sample->getNumSamples())
return;
using namespace juce;
auto colors = getTheme();
// Paints the voice positions
if (!recordingMode && !isWaveformMode())
{
for (auto& voice : synthVoices)
{
if (voice->isPlaying())
{
int location = int(std::ceil(voice->getPosition()));
auto pos = sampleToPosition(location);
Path voicePath{};
voicePath.addLineSegment(Line<float>(pos, 0.f, pos, float(getHeight())), 1.f);
g.setColour(colors.light.withAlpha(voice->getEnvelopeGain()));
g.strokePath(voicePath, PathStrokeType(Layout::playheadWidth * getWidth()));
}
}
}
float boundsThickness = getWidth() * Layout::navigatorBoundsWidth * 0.66f;
float sampleStartPos = sampleToPosition(state.sampleStart);
float sampleEndPos = sampleToPosition(state.sampleEnd);
float loopStartPos = sampleToPosition(state.loopStart);
float loopEndPos = sampleToPosition(state.loopEnd);
auto boundColor = isWaveformMode() ? colors.highlight : isLooping->get() ? colors.loop : colors.slate;
g.setColour(disabled(boundColor));
g.fillRect(sampleStartPos - boundsThickness, getHeight() * 0.1f, boundsThickness, getHeight() * 0.8f);
g.setColour(disabled(boundColor));
g.fillRect(sampleEndPos, getHeight() * 0.1f, boundsThickness, getHeight() * 0.8f);
g.setColour(disabled(colors.slate));
if (isLooping->get() && loopHasStart->get() && !isWaveformMode())
g.fillRect(loopStartPos - boundsThickness, getHeight() * 0.1f, boundsThickness, getHeight() * 0.8f);
if (isLooping->get() && loopHasEnd->get() && !isWaveformMode())
g.fillRect(loopEndPos, getHeight() * 0.1f, boundsThickness, getHeight() * 0.8f);
// Paints the start and stop
float startPos = sampleToPosition(recordingMode ? 0 : int(state.viewStart));
float stopPos = sampleToPosition(recordingMode ? sample->getNumSamples() - 1 : int(state.viewEnd));
g.setColour(disabled(colors.slate.withAlpha(0.15f)));
g.fillRect(startPos, 0.f, stopPos - startPos + 1.f, float(getHeight()));
float lineThickness = getWidth() * Layout::navigatorBoundsWidth;
g.setColour(disabled(colors.slate));
g.fillRect(startPos - lineThickness, 0.f, lineThickness, float(getHeight()));
g.fillRect(stopPos, 0.f, lineThickness, float(getHeight()));
}
void SampleNavigator::resized()
{
auto bounds = getLocalBounds().toFloat();
float lineThickness = bounds.getWidth() * Layout::navigatorBoundsWidth;
bounds.reduce(lineThickness, 0.f);
painter.setBounds(bounds.toNearestInt());
}
void SampleNavigator::lookAndFeelChanged()
{
auto colors = getTheme();
painter.setColour(Colors::painterColorId, colors.darkerSlate);
}
void SampleNavigator::enablementChanged()
{
painter.setEnabled(isEnabled());
painter.repaint();
}
//==============================================================================
void SampleNavigator::mouseDown(const juce::MouseEvent& event)
{
if (!sample || !isEnabled() || recordingMode)
return;
draggingTarget = getDraggingTarget(event.getMouseDownX(), event.getMouseDownY());
if (draggingTarget == Drag::SAMPLE_FULL)
dragSelectOffset = event.getMouseDownX() - sampleToPosition(state.viewStart);
dragging = draggingTarget != Drag::NONE;
lastDragOffset = 0;
if (draggingTarget == Drag::SAMPLE_START || draggingTarget == Drag::SAMPLE_END || draggingTarget == Drag::SAMPLE_FULL)
juce::Desktop::getInstance().getMainMouseSource().enableUnboundedMouseMovement(true, false);
repaint();
}
void SampleNavigator::mouseUp(const juce::MouseEvent&)
{
if (!sample || recordingMode || !dragging)
return;
if (dragging)
dummyParam.sendUIUpdate();
dragging = false;
juce::Desktop::getInstance().getMainMouseSource().enableUnboundedMouseMovement(false);
auto screenPos = getScreenBounds().toFloat();
juce::Point<float> newMousePos;
if (draggingTarget == Drag::SAMPLE_START)
newMousePos = juce::Point(screenPos.getX() + sampleToPosition(state.viewStart) * screenPos.getWidth() / getWidth(), screenPos.getCentreY());
else if (draggingTarget == Drag::SAMPLE_END)
newMousePos = juce::Point(screenPos.getX() + sampleToPosition(state.viewEnd) * screenPos.getWidth() / getWidth(), screenPos.getCentreY());
else if (draggingTarget == Drag::SAMPLE_FULL)
newMousePos = juce::Point(screenPos.getX() + (sampleToPosition(state.viewStart) + dragSelectOffset) * screenPos.getWidth() / getWidth(), screenPos.getCentreY());
juce::Desktop::getInstance().getMainMouseSource().setScreenPosition(newMousePos);
repaint();
}
void SampleNavigator::mouseMove(const juce::MouseEvent& event)
{
if (!sample || !isEnabled() || recordingMode)
{
setMouseCursor(juce::MouseCursor::NormalCursor);
return;
}
if (dragging) // Don't change while dragging
return;
Drag currentTarget = getDraggingTarget(event.getMouseDownX(), event.getMouseDownY());
switch (currentTarget)
{
case Drag::SAMPLE_START:
case Drag::SAMPLE_END:
setMouseCursor(juce::MouseCursor::LeftRightResizeCursor);
break;
case Drag::SAMPLE_FULL:
setMouseCursor(juce::MouseCursor::DraggingHandCursor);
break;
case Drag::NONE:
setMouseCursor(juce::MouseCursor::NormalCursor);
break;
}
}
void SampleNavigator::mouseDrag(const juce::MouseEvent& event)
{
if (!sample || !dragging || !isEnabled() || recordingMode)
return;
// The goal is to keep the positions within their normal constraints
bool secondary = juce::ModifierKeys::currentModifiers.isAnyModifierKeyDown();
float edgeSensitivity = getDragSensitivity(secondary);
float fullSensitivity = getDragSensitivity(!secondary);
switch (draggingTarget)
{
case Drag::SAMPLE_START:
{
int difference = event.getOffsetFromDragStart().getX() - lastDragOffset;
moveStart(difference / 2.f, edgeSensitivity);
moveEnd(-difference / 2.f, edgeSensitivity);
break;
}
case Drag::SAMPLE_END:
{
int difference = event.getOffsetFromDragStart().getX() - lastDragOffset;
moveEnd(difference / 2.f, edgeSensitivity);
moveStart(-difference / 2.f, edgeSensitivity);
break;
}
case Drag::SAMPLE_FULL:
{
int change = event.getOffsetFromDragStart().getX() - lastDragOffset;
moveBoth(float(change), fullSensitivity);
break;
}
case Drag::NONE:
break;
}
lastDragOffset = event.getOffsetFromDragStart().getX();
}
void SampleNavigator::mouseWheelMove(const juce::MouseEvent& event, const juce::MouseWheelDetails& wheel)
{
if (!sample || dragging || !isEnabled() || recordingMode)
return;
int sampleCenter = positionToSample(event.position.getX());
scrollView(wheel, sampleCenter);
}
void SampleNavigator::mouseDoubleClick(const juce::MouseEvent&)
{
if (!sample || recordingMode)
return;
state.viewStart = 0;
state.viewEnd = sample->getNumSamples() - 1;
}
void SampleNavigator::scrollView(const juce::MouseWheelDetails& wheel, int sampleCenter, bool centerZoomOut)
{
if (!sample)
return;
float changeY = wheel.deltaY * Feel::MOUSE_SENSITIVITY;
float changeX = wheel.deltaX * Feel::MOUSE_SENSITIVITY;
// MacOS sensitivity is a little different
if (wheel.isSmooth && ((juce::SystemStats::getOperatingSystemType() & juce::SystemStats::MacOSX) == 0))
changeY *= 0.75f;
bool trackHorizontal = std::abs(wheel.deltaX) > std::abs(wheel.deltaY);
bool modifier = juce::ModifierKeys::currentModifiers.isAnyModifierKeyDown();
if (modifier || trackHorizontal)
{
float sensitivity = getDragSensitivity(false);
moveBoth(trackHorizontal ? changeX : changeY, sensitivity);
}
else if (!modifier)
{
float sensitivity = getDragSensitivity(false);
float startRatio = 1.f;
float endRatio = 1.f;
if (sampleCenter >= state.viewStart && sampleCenter <= state.viewEnd &&
((centerZoomOut && state.viewStart > 0 && state.viewEnd < sample->getNumSamples() - 1) || changeY > 0))
{
endRatio = float(state.viewEnd - sampleCenter) / (state.viewEnd - state.viewStart);
startRatio = 1.f - endRatio;
}
// The order must change depending on the direction of the wheel, since both maintain constraints and there's a minimum view size
if (changeY > 0)
{
moveEnd(-changeY * endRatio, sensitivity);
moveStart(changeY * startRatio, sensitivity);
}
else
{
moveStart(changeY * startRatio, sensitivity);
moveEnd(-changeY * endRatio, sensitivity);
}
}
}
void SampleNavigator::fitView()
{
if (!sample)
return;
int viewStart = isLooping->get() && loopHasStart->get() && !isWaveformMode() ? state.loopStart.load() : state.sampleStart.load();
int viewEnd = isLooping->get() && loopHasEnd->get() && !isWaveformMode() ? state.loopEnd.load() : state.sampleEnd.load();
// Keep the view size larger than the minimum
if (viewEnd - viewStart + 1 < Feel::MINIMUM_VIEW)
{
viewEnd = juce::jmin<int>(sample->getNumSamples() - 1, viewStart + Feel::MINIMUM_VIEW - 1);
viewStart = juce::jmax<int>(0, viewEnd - Feel::MINIMUM_VIEW + 1);
}
state.viewStart = viewStart;
state.viewEnd = viewEnd;
updatePinnedPositions();
repaint();
}
void SampleNavigator::moveStart(float change, float sensitivity)
{
int oldViewStart = state.viewStart;
auto viewStart = juce::jmax<int>(juce::jmin<int>(int(std::round(state.viewStart + change * sensitivity)), state.viewEnd - Feel::MINIMUM_VIEW), 0);
state.viewStart = viewStart;
if (state.pinView)
moveBoundsToPinnedPositions(viewStart < oldViewStart);
}
void SampleNavigator::moveEnd(float change, float sensitivity)
{
int oldViewEnd = state.viewEnd;
auto viewEnd = juce::jmin<int>(juce::jmax<int>(int(std::round(state.viewEnd + change * sensitivity)), state.viewStart + Feel::MINIMUM_VIEW), sample->getNumSamples() - 1);
state.viewEnd = viewEnd;
if (state.pinView)
moveBoundsToPinnedPositions(viewEnd < oldViewEnd);
}
void SampleNavigator::moveBoth(float change, float sensitivity)
{
int difference = juce::jlimit<int>(-state.viewStart, sample->getNumSamples() - 1 - state.viewEnd, int(change * sensitivity));
state.viewStart = state.viewStart + difference;
state.viewEnd = state.viewEnd + difference;
if (state.pinView)
moveBoundsToPinnedPositions(difference < 0);
}
//==============================================================================
NavigatorParts SampleNavigator::getDraggingTarget(int x, int y) const
{
auto bounds = getLocalBounds().toFloat();
auto startPos = sampleToPosition(state.viewStart);
auto stopPos = sampleToPosition(state.viewEnd);
// A little trick to make the full sample always possible to drag
auto offset = juce::jmax<float>(30 - (stopPos - startPos), 0) / 2;
juce::Array targets = {
CompPart {Drag::SAMPLE_START, juce::Rectangle<float>{startPos - offset, bounds.getY(), 0, bounds.getHeight()}, 2},
CompPart {Drag::SAMPLE_END, juce::Rectangle<float>{stopPos + offset, bounds.getY(), 0, bounds.getHeight()}, 2},
CompPart {Drag::SAMPLE_FULL, juce::Rectangle<float>{startPos, bounds.getY(), stopPos - startPos, bounds.getHeight()}, 1}
};
return CompPart<Drag>::getClosestInRange(targets, x, y, Feel::DRAGGABLE_SNAP);
}
float SampleNavigator::sampleToPosition(int sampleIndex) const
{
float boundsWidth = getWidth() * Layout::navigatorBoundsWidth;
return juce::jmap<float>(float(sampleIndex), 0.f, float(sample->getNumSamples() - 1), 0.f, float(painter.getWidth())) + boundsWidth;
}
int SampleNavigator::positionToSample(float position) const
{
return int(juce::jmap<float>(position - getWidth() * Layout::navigatorBoundsWidth, 0.f, float(painter.getWidth()), 0.f, float(sample->getNumSamples())));
}
float SampleNavigator::getDragSensitivity(bool constant) const
{
int viewSize = state.viewEnd - state.viewStart + 1;
if (constant)
return float(sample->getNumSamples()) / getWidth();
else
return -std::log(float(viewSize) / sample->getNumSamples() / juce::MathConstants<float>::euler) * viewSize / getWidth();
}
void SampleNavigator::loopHasStartUpdate(bool newValue)
{
repaint();
if (!bool(newValue))
return;
if (state.viewStart > state.loopStart && (state.loopStart == 0 || state.pinView))
state.loopStart = state.viewStart.load();
}
void SampleNavigator::loopHasEndUpdate(bool newValue)
{
repaint();
if (!bool(newValue))
return;
if (state.viewEnd < state.loopEnd && (state.loopEnd == sample->getNumSamples() - 1 || state.pinView))
state.loopEnd = state.viewEnd.load();
}
void SampleNavigator::updatePinnedPositions()
{
int viewStart = state.viewStart;
int viewEnd = state.viewEnd;
pinnedSampleStart = juce::jlimit<float>(0.f, 1.f, float(state.sampleStart - viewStart) / (viewEnd - viewStart));
pinnedSampleEnd = juce::jlimit<float>(0.f, 1.f, float(state.sampleEnd - viewStart) / (viewEnd - viewStart));
pinnedLoopStart = juce::jlimit<float>(0.f, 1.f, float(state.loopStart - viewStart) / (viewEnd - viewStart));
pinnedLoopEnd = juce::jlimit<float>(0.f, 1.f, float(state.loopEnd - viewStart) / (viewEnd - viewStart));
}
void SampleNavigator::moveBoundsToPinnedPositions(bool startToEnd)
{
// To maintain constraints, we change the bounds in order depending on the direction of the change
navigatorUpdate = true;
if (startToEnd)
{
state.loopStart = juce::jmax(int(std::round(pinnedLoopStart * (state.viewEnd - state.viewStart))) + state.viewStart, 0);
state.sampleStart = int(std::round(pinnedSampleStart * (state.viewEnd - state.viewStart))) + state.viewStart;
state.sampleEnd = int(std::round(pinnedSampleEnd * (state.viewEnd - state.viewStart))) + state.viewStart;
state.loopEnd = juce::jmin(int(std::round(pinnedLoopEnd * (state.viewEnd - state.viewStart))) + state.viewStart, sample->getNumSamples() - 1);
}
else
{
state.loopEnd = juce::jmin(int(std::round(pinnedLoopEnd * (state.viewEnd - state.viewStart))) + state.viewStart, sample->getNumSamples() - 1);
state.sampleEnd = int(std::round(pinnedSampleEnd * (state.viewEnd - state.viewStart))) + state.viewStart;
state.sampleStart = int(std::round(pinnedSampleStart * (state.viewEnd - state.viewStart))) + state.viewStart;
state.loopStart = juce::jmax(int(std::round(pinnedLoopStart * (state.viewEnd - state.viewStart))) + state.viewStart, 0);
}
navigatorUpdate = false;
}
bool SampleNavigator::isWaveformMode() const
{
return CustomSamplerVoice::isWavetableModeAvailable(sampleRate, state.sampleStart, state.sampleEnd) && !isWavetableModeDisabled->get();
}

View file

@ -0,0 +1,118 @@
/*
==============================================================================
SampleNavigator.h
Created: 19 Sep 2023 4:41:12pm
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#include "../Sampler/CustomSamplerVoice.h"
#include "../Utilities/ComponentUtils.h"
#include "Displays/SamplePainter.h"
enum class NavigatorParts
{
NONE,
SAMPLE_START,
SAMPLE_END,
SAMPLE_FULL
};
/** A navigator control for the viewing window of the sample editor */
class SampleNavigator final : public CustomComponent, public ValueListener<int>, public ValueListener<bool>
{
using Drag = NavigatorParts;
public:
SampleNavigator(APVTS& apvts, PluginParameters::State& pluginState, const juce::OwnedArray<CustomSamplerVoice>& synthVoices);
~SampleNavigator() override;
//==============================================================================
void setSample(const juce::AudioBuffer<float>& sampleBuffer, float bufferSampleRate, bool resetView);
/** Call this while recording when more samples have been filled in the sampleBuffer */
void sampleUpdated(int oldSize, int newSize);
void setRecordingMode(bool recording);
/** Scrolling can be centered on a sample */
void scrollView(const juce::MouseWheelDetails& wheel, int sampleCenter, bool centerZoomOut = false);
/** Set the view to fit the sample play bounds */
void fitView();
private:
/** React to view changes */
void valueChanged(ListenableValue<int>& source, int newValue) override;
void valueChanged(ListenableValue<bool>& source, bool newValue) override;
void paintOverChildren(juce::Graphics& g) override;
void resized() override;
void lookAndFeelChanged() override;
void enablementChanged() override;
//==============================================================================
void mouseDown(const juce::MouseEvent& event) override;
void mouseUp(const juce::MouseEvent& event) override;
void mouseMove(const juce::MouseEvent& event) override;
void mouseDrag(const juce::MouseEvent& event) override;
void mouseWheelMove(const juce::MouseEvent& event, const juce::MouseWheelDetails& wheel) override;
void mouseDoubleClick(const juce::MouseEvent& event) override;
/** Move the view start, maintaining constraints */
void moveStart(float change, float sensitivity);
void moveEnd(float change, float sensitivity);
void moveBoth(float change, float sensitivity);
NavigatorParts getDraggingTarget(int x, int y) const;
/** Relative to the bounds of the painter */
float sampleToPosition(int sampleIndex) const;
int positionToSample(float position) const;
/** constant sensitivity means that the drag distance is the same regardless of zoom level */
float getDragSensitivity(bool constant) const;
/** In some cases we'd like to adjust the loop start/end positions when they are enabled */
void loopHasStartUpdate(bool newValue);
void loopHasEndUpdate(bool newValue);
/** Update the stored position ratios, for use when pinned */
void updatePinnedPositions();
void moveBoundsToPinnedPositions(bool startToEnd);
bool isWaveformMode() const;
//==============================================================================
APVTS& apvts;
PluginParameters::State& state;
UIDummyParam dummyParam;
SamplePainter painter;
juce::ParameterAttachment gainAttachment;
juce::ParameterAttachment monoAttachment;
const juce::AudioBuffer<float>* sample{ nullptr };
float sampleRate;
const juce::OwnedArray<CustomSamplerVoice>& synthVoices;
juce::AudioParameterBool* isWavetableModeDisabled, * isLooping, * loopHasStart, * loopHasEnd;
juce::ParameterAttachment isWavetableModeDisabledAttachment, loopAttachment, loopStartAttachment, loopEndAttachment;
bool dragging{ false };
NavigatorParts draggingTarget{ NavigatorParts::NONE };
float dragSelectOffset{ 0 }; // When dragging full, this is the offset from the view start where the drag started
int lastDragOffset{ 0 };
bool recordingMode{ false };
// To keep the bounds in a consistent location when pinned, we need to store the ratios (necessary because zooming in more
// causes rounding issues and the position moves drastically).
long double pinnedSampleStart{ 0.f }, pinnedSampleEnd{ 0.f }, pinnedLoopStart{ 0.f }, pinnedLoopEnd{ 0.f };
bool navigatorUpdate{ false }; // This flag allows the navigator to avoid updating the pinned positions when they are being used to update the bounds
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SampleNavigator)
};