mirror of
https://codeberg.org/armin/justasample.git
synced 2026-09-01 12:20:48 +02:00
init
This commit is contained in:
commit
d21bc831e1
178 changed files with 24136 additions and 0 deletions
377
Source/Components/Buttons.h
Normal file
377
Source/Components/Buttons.h
Normal 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)
|
||||
};
|
||||
62
Source/Components/Displays/ChorusVisualizer.cpp
Normal file
62
Source/Components/Displays/ChorusVisualizer.cpp
Normal 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();
|
||||
}
|
||||
43
Source/Components/Displays/ChorusVisualizer.h
Normal file
43
Source/Components/Displays/ChorusVisualizer.h
Normal 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)
|
||||
};
|
||||
61
Source/Components/Displays/DistortionVisualizer.cpp
Normal file
61
Source/Components/Displays/DistortionVisualizer.cpp
Normal 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();
|
||||
}
|
||||
42
Source/Components/Displays/DistortionVisualizer.h
Normal file
42
Source/Components/Displays/DistortionVisualizer.h
Normal 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)
|
||||
};
|
||||
207
Source/Components/Displays/FilterResponse.cpp
Normal file
207
Source/Components/Displays/FilterResponse.cpp
Normal 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";
|
||||
}
|
||||
}
|
||||
61
Source/Components/Displays/FilterResponse.h
Normal file
61
Source/Components/Displays/FilterResponse.h
Normal 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)
|
||||
};
|
||||
150
Source/Components/Displays/ReverbResponse.cpp
Normal file
150
Source/Components/Displays/ReverbResponse.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
74
Source/Components/Displays/ReverbResponse.h
Normal file
74
Source/Components/Displays/ReverbResponse.h
Normal 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)
|
||||
};
|
||||
453
Source/Components/Displays/SamplePainter.cpp
Normal file
453
Source/Components/Displays/SamplePainter.cpp
Normal 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();
|
||||
}
|
||||
96
Source/Components/Displays/SamplePainter.h
Normal file
96
Source/Components/Displays/SamplePainter.h
Normal 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)
|
||||
};
|
||||
211
Source/Components/FxChain.cpp
Normal file
211
Source/Components/FxChain.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
82
Source/Components/FxChain.h
Normal file
82
Source/Components/FxChain.h
Normal 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)
|
||||
};
|
||||
20
Source/Components/FxDragTarget.h
Normal file
20
Source/Components/FxDragTarget.h
Normal 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;
|
||||
};
|
||||
219
Source/Components/FxModule.cpp
Normal file
219
Source/Components/FxModule.cpp
Normal 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();
|
||||
}
|
||||
78
Source/Components/FxModule.h
Normal file
78
Source/Components/FxModule.h
Normal 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)
|
||||
};
|
||||
352
Source/Components/InputDeviceSelector.cpp
Normal file
352
Source/Components/InputDeviceSelector.cpp
Normal 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));
|
||||
}
|
||||
114
Source/Components/InputDeviceSelector.h
Normal file
114
Source/Components/InputDeviceSelector.h
Normal 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
122
Source/Components/Prompt.h
Normal 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)
|
||||
};
|
||||
125
Source/Components/RangeSelector.h
Normal file
125
Source/Components/RangeSelector.h
Normal 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)
|
||||
};
|
||||
556
Source/Components/SampleEditor.cpp
Normal file
556
Source/Components/SampleEditor.cpp
Normal 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();
|
||||
}
|
||||
168
Source/Components/SampleEditor.h
Normal file
168
Source/Components/SampleEditor.h
Normal 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)
|
||||
};
|
||||
525
Source/Components/SampleNavigator.cpp
Normal file
525
Source/Components/SampleNavigator.cpp
Normal 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();
|
||||
}
|
||||
118
Source/Components/SampleNavigator.h
Normal file
118
Source/Components/SampleNavigator.h
Normal 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)
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue