mirror of
https://codeberg.org/armin/justasample.git
synced 2026-09-01 04:10: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)
|
||||
};
|
||||
563
Source/CustomLookAndFeel.cpp
Normal file
563
Source/CustomLookAndFeel.cpp
Normal file
|
|
@ -0,0 +1,563 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
LookAndFeel.cpp
|
||||
Created: 19 Sep 2023 4:48:33pm
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#include "CustomLookAndFeel.h"
|
||||
|
||||
#include "Components/Buttons.h"
|
||||
#include "Sampler/CustomSamplerVoice.h"
|
||||
|
||||
CustomLookAndFeel::CustomLookAndFeel()
|
||||
{
|
||||
setTheme(defaultTheme);
|
||||
setUsingNativeAlertWindows(true);
|
||||
}
|
||||
|
||||
void CustomLookAndFeel::setTheme(const Colors& theme)
|
||||
{
|
||||
colors = theme;
|
||||
|
||||
setColour(juce::Label::textColourId, colors.dark);
|
||||
setColour(juce::Label::textWhenEditingColourId, colors.dark);
|
||||
setColour(juce::Label::outlineWhenEditingColourId, juce::Colours::transparentWhite);
|
||||
setColour(juce::TextEditor::highlightColourId, colors.slate.withAlpha(0.15f));
|
||||
setColour(juce::TextEditor::highlightedTextColourId, colors.dark);
|
||||
setColour(juce::CaretComponent::caretColourId, colors.dark);
|
||||
setColour(juce::ComboBox::textColourId, colors.dark);
|
||||
setColour(juce::Slider::thumbColourId, colors.highlight);
|
||||
setColour(juce::ResizableWindow::backgroundColourId, colors.foreground);
|
||||
setColour(juce::ListBox::backgroundColourId, colors.foreground.withAlpha(0.f));
|
||||
setColour(juce::ListBox::outlineColourId, colors.dark.withAlpha(0.f));
|
||||
setColour(juce::ListBox::textColourId, colors.dark);
|
||||
setColour(juce::ScrollBar::thumbColourId, colors.dark);
|
||||
setColour(Colors::backgroundColorId, colors.background);
|
||||
setColour(Colors::painterColorId, colors.dark);
|
||||
}
|
||||
|
||||
juce::Slider::SliderLayout CustomLookAndFeel::getSliderLayout(juce::Slider& slider)
|
||||
{
|
||||
auto bounds = slider.getLocalBounds().toFloat();
|
||||
bounds = bounds.reduced(bounds.getWidth() * Layout::rotaryPadding);
|
||||
|
||||
float textSize = bounds.getWidth() * Layout::rotaryTextSize;
|
||||
|
||||
juce::Slider::SliderLayout layout;
|
||||
layout.sliderBounds = slider.getLocalBounds();
|
||||
if (slider.getTextBoxPosition() != juce::Slider::NoTextBox)
|
||||
layout.textBoxBounds = juce::Rectangle(bounds.getX(), (bounds.getHeight() - textSize / 3.5f) / 2.f, bounds.getWidth(), textSize).reduced(bounds.getWidth() * 0.1f, 0.f).getLargestIntegerWithin();
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
juce::Label* CustomLookAndFeel::createSliderTextBox(juce::Slider& slider)
|
||||
{
|
||||
using namespace juce;
|
||||
|
||||
auto bounds = slider.getLocalBounds().toFloat();
|
||||
bounds = bounds.reduced(bounds.getWidth() * Layout::rotaryPadding);
|
||||
|
||||
auto label = new CustomLabel();
|
||||
label->setColour(TextEditor::highlightColourId, Colours::transparentWhite);
|
||||
|
||||
label->setJustificationType(Justification::centredBottom);
|
||||
label->setFont(getInriaSansBold().withHeight(bounds.getWidth() * Layout::rotaryTextSize));
|
||||
label->setHasFocusOutline(false);
|
||||
label->setKeyboardType(TextInputTarget::decimalKeyboard);
|
||||
label->setEditable(false, true, false);
|
||||
label->setEnabledMouseCursor(MouseCursor::IBeamCursor);
|
||||
|
||||
label->onEditorShow = [label]
|
||||
{
|
||||
auto editor = label->getCurrentTextEditor();
|
||||
editor->setJustification(Justification::centredBottom);
|
||||
editor->setIndents(0, 0);
|
||||
editor->moveCaretToEnd();
|
||||
editor->selectAll();
|
||||
|
||||
// Some compensation to keep the text in a constant position
|
||||
auto editorBounds = editor->getBounds().toFloat();
|
||||
float offsetAmount = editorBounds.getHeight() * 0.125f;
|
||||
editorBounds.setY(editorBounds.getY() - offsetAmount);
|
||||
editorBounds.translate(offsetAmount, 0);
|
||||
editorBounds.expand(0, offsetAmount);
|
||||
editor->setBounds(editorBounds.toNearestInt());
|
||||
};
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
void CustomLookAndFeel::drawRotarySlider(juce::Graphics& g, int x, int y, int width, int height, float sliderPosProportional, float, float, juce::Slider& slider)
|
||||
{
|
||||
using namespace juce;
|
||||
|
||||
auto bounds = Rectangle(x, y, width, height).toFloat();
|
||||
bounds = bounds.reduced(bounds.getWidth() * Layout::rotaryPadding / (1 + Layout::rotaryPadding));
|
||||
bounds.setHeight(bounds.getWidth() * Layout::rotaryHeightRatio);
|
||||
|
||||
auto radius = bounds.getWidth() / 2.f;
|
||||
auto lineWidth = radius * 0.2f;
|
||||
|
||||
constexpr float rotaryStartAngle = MathConstants<float>::pi * (1 + 5.f / 26.f);
|
||||
constexpr float rotaryEndAngle = rotaryStartAngle + MathConstants<float>::pi * (2.f - 5.f / 13.f);
|
||||
auto toAngle = rotaryStartAngle + sliderPosProportional * (rotaryEndAngle - rotaryStartAngle);
|
||||
|
||||
// Draw the background arc
|
||||
const float lineOffset = lineWidth * 0.5f;
|
||||
Path backgroundArc;
|
||||
backgroundArc.addCentredArc(bounds.getCentreX(), bounds.getY() + radius + lineOffset / 2.f, radius - lineOffset, radius - lineOffset / 2.f,
|
||||
0.0f, rotaryStartAngle, rotaryEndAngle, true);
|
||||
|
||||
g.setColour(colors.dark);
|
||||
g.strokePath(backgroundArc, PathStrokeType(lineWidth));
|
||||
|
||||
// Draw the value arc
|
||||
float baseAngle = rotaryStartAngle;
|
||||
if (approximatelyEqual(slider.getRange().getStart(), -slider.getRange().getEnd()))
|
||||
baseAngle = MathConstants<float>::twoPi;
|
||||
|
||||
Path valueArc;
|
||||
valueArc.addCentredArc(bounds.getCentreX(), bounds.getY() + radius + lineOffset / 2.f, radius - lineOffset, radius - lineOffset / 2.f,
|
||||
0.0f, baseAngle, toAngle, true);
|
||||
|
||||
g.setColour(colors.highlight);
|
||||
g.strokePath(valueArc, PathStrokeType(lineWidth));
|
||||
|
||||
// Draw the unit label
|
||||
g.setColour(colors.dark);
|
||||
|
||||
if (slider.getProperties().contains(ComponentProps::ROTARY_ICON))
|
||||
{
|
||||
auto iconPath = dynamic_cast<ReferenceCountedPath*>(slider.getProperties()[ComponentProps::ROTARY_ICON].getObject())->path;
|
||||
g.fillPath(iconPath, iconPath.getTransformToScaleToFit(Rectangle{ bounds.getX() + 0.025f * bounds.getWidth(), bounds.getY() + 0.78f * bounds.getHeight(), bounds.getWidth(), 12.f * bounds.getWidth() / Layout::standardRotarySize}, true));
|
||||
}
|
||||
else if (slider.getTextBoxPosition() != Slider::NoTextBox)
|
||||
{
|
||||
var unit = slider.getProperties().getWithDefault(ComponentProps::ROTARY_UNIT, "");
|
||||
if (slider.getValue() >= 1000)
|
||||
unit = slider.getProperties().getWithDefault(ComponentProps::ROTARY_GREATER_UNIT, unit);
|
||||
g.setFont(getInriaSansBold().withHeight(20.f * bounds.getWidth() / Layout::standardRotarySize));
|
||||
g.drawText(unit, Rectangle{ bounds.getX(), bounds.getY() + 0.78f * bounds.getHeight(), bounds.getWidth(), g.getCurrentFont().getAscent() }, Justification::centredBottom);
|
||||
}
|
||||
|
||||
// Draw % if there is no textbox
|
||||
if (slider.getTextBoxPosition() == Slider::NoTextBox)
|
||||
{
|
||||
g.setFont(getInriaSansBold().withHeight(0.68f * bounds.getWidth()));
|
||||
bounds.removeFromTop(bounds.getHeight() * 0.08f);
|
||||
g.drawText("%", bounds, Justification::centred);
|
||||
}
|
||||
|
||||
if (!slider.isEnabled())
|
||||
{
|
||||
g.setColour(slider.findColour(colors.backgroundColorId, true).withAlpha(0.5f));
|
||||
g.fillRect(slider.getLocalBounds());
|
||||
}
|
||||
}
|
||||
|
||||
void CustomLookAndFeel::drawLabel(juce::Graphics& g, juce::Label& label)
|
||||
{
|
||||
using namespace juce;
|
||||
|
||||
if (!label.isBeingEdited())
|
||||
{
|
||||
auto textArea = label.getLocalBounds();
|
||||
|
||||
// Draw the background (if the colors are set)
|
||||
float cornerSize = textArea.getHeight() * 0.2f;
|
||||
float borderSize = textArea.getHeight() * 0.05f;
|
||||
|
||||
g.setColour(label.findColour(Label::backgroundColourId));
|
||||
g.fillRoundedRectangle(textArea.toFloat().reduced(borderSize * 0.33f), cornerSize);
|
||||
|
||||
g.setColour(label.findColour(Label::outlineColourId));
|
||||
g.drawRoundedRectangle(textArea.toFloat().reduced(borderSize * 0.48f), cornerSize, borderSize);
|
||||
|
||||
// Draw the text
|
||||
const Font font(getLabelFont(label));
|
||||
|
||||
g.setColour(label.findColour(Label::textColourId).withMultipliedAlpha(label.isEnabled() ? 1.f : 0.5f));
|
||||
g.setFont(font);
|
||||
g.drawText(label.getText(), textArea, label.getJustificationType(), label.getProperties().contains(ComponentProps::LABEL_ELLIPSES));
|
||||
}
|
||||
}
|
||||
|
||||
juce::Button* CustomLookAndFeel::createFilenameComponentBrowseButton(const juce::String& /*text*/)
|
||||
{
|
||||
return new CustomShapeButton(colors.dark, getOutlineFromSVG(BinaryData::IconAdd_svg));
|
||||
}
|
||||
|
||||
void CustomLookAndFeel::layoutFilenameComponent(juce::FilenameComponent& filenameComp, juce::ComboBox* filenameBox, juce::Button* browseButton)
|
||||
{
|
||||
if (browseButton == nullptr || filenameBox == nullptr)
|
||||
return;
|
||||
|
||||
auto compBounds = filenameComp.getLocalBounds().toFloat();
|
||||
|
||||
auto addButtonBounds = compBounds.removeFromRight(filenameComp.getHeight() * 0.945f).reduced(0.158f * compBounds.getHeight(), 0.18f * compBounds.getHeight());
|
||||
browseButton->setBounds(addButtonBounds.toNearestInt());
|
||||
|
||||
filenameBox->setBounds(compBounds.toNearestInt());
|
||||
}
|
||||
|
||||
juce::Font CustomLookAndFeel::getComboBoxFont(juce::ComboBox& comboBox)
|
||||
{
|
||||
return getInter().withHeight(comboBox.getHeight() * 0.73f);
|
||||
}
|
||||
|
||||
juce::Label* CustomLookAndFeel::createComboBoxTextBox(juce::ComboBox& comboBox)
|
||||
{
|
||||
using namespace juce;
|
||||
|
||||
auto bounds = comboBox.getLocalBounds().toFloat();
|
||||
auto label = new CustomLabel();
|
||||
|
||||
label->setFont(getInter().withHeight(bounds.getWidth() * Layout::rotaryTextSize));
|
||||
label->setJustificationType(Justification::centredLeft);
|
||||
label->setHasFocusOutline(false);
|
||||
label->setKeyboardType(TextInputTarget::decimalKeyboard);
|
||||
label->setEditable(false, true, false);
|
||||
label->getProperties().set(ComponentProps::LABEL_ELLIPSES, true);
|
||||
label->setEnabledMouseCursor(MouseCursor::IBeamCursor);
|
||||
|
||||
label->onEditorShow = [label]
|
||||
{
|
||||
auto editor = label->getCurrentTextEditor();
|
||||
editor->setJustification(Justification::centredLeft);
|
||||
editor->setIndents(0, 0);
|
||||
editor->moveCaretToEnd();
|
||||
|
||||
// Some compensation to keep the text in a constant position
|
||||
auto editorBounds = editor->getBounds().toFloat();
|
||||
float offset = editorBounds.getHeight() * 0.125f;
|
||||
editorBounds.translate(-1, 0);
|
||||
editorBounds.expand(0, offset);
|
||||
editor->setBounds(editorBounds.toNearestInt());
|
||||
};
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
void CustomLookAndFeel::positionComboBoxText(juce::ComboBox& box, juce::Label& label)
|
||||
{
|
||||
auto bounds = box.getLocalBounds().toFloat();
|
||||
bounds.removeFromRight(Layout::expandWidth * box.getHeight());
|
||||
|
||||
label.setBounds(bounds.toNearestInt());
|
||||
label.setFont(getComboBoxFont(box));
|
||||
}
|
||||
|
||||
void CustomLookAndFeel::drawComboBox(juce::Graphics& g, int /*width*/, int /*height*/, bool isButtonDown, int /*buttonX*/,
|
||||
int /*buttonY*/, int /*buttonW*/, int /*buttonH*/, juce::ComboBox& box)
|
||||
{
|
||||
using namespace juce;
|
||||
|
||||
auto bounds = box.getLocalBounds().toFloat();
|
||||
auto expandBounds = bounds.removeFromRight(Layout::expandWidth * bounds.getHeight()).reduced(0.272f * bounds.getHeight(), 0.321f * bounds.getHeight());
|
||||
|
||||
if (isButtonDown)
|
||||
expandBounds.reduce(expandBounds.getHeight() * 0.05f, expandBounds.getHeight() * 0.05f);
|
||||
|
||||
Path path;
|
||||
path.startNewSubPath(expandBounds.getTopLeft());
|
||||
path.lineTo(expandBounds.getCentreX(), expandBounds.getBottom());
|
||||
path.lineTo(expandBounds.getTopRight());
|
||||
|
||||
g.setColour(colors.dark);
|
||||
g.strokePath(path, PathStrokeType(0.1f * box.getHeight(), PathStrokeType::curved, PathStrokeType::rounded));
|
||||
|
||||
if (!box.isEnabled())
|
||||
{
|
||||
g.setColour(box.findColour(colors.backgroundColorId, true).withAlpha(0.5f));
|
||||
g.fillRect(box.getLocalBounds());
|
||||
}
|
||||
}
|
||||
|
||||
void CustomLookAndFeel::drawComboBoxTextWhenNothingSelected(juce::Graphics& g, juce::ComboBox& box,
|
||||
juce::Label& label)
|
||||
{
|
||||
auto textArea = getLabelBorderSize(label).subtractedFrom(label.getLocalBounds());
|
||||
auto font = label.getLookAndFeel().getLabelFont(label);
|
||||
|
||||
g.setColour(findColour(juce::ComboBox::textColourId));
|
||||
g.setFont(font);
|
||||
g.drawText(box.getTextWhenNothingSelected(), textArea, label.getJustificationType());
|
||||
|
||||
if (!box.isEnabled())
|
||||
{
|
||||
g.setColour(box.findColour(colors.backgroundColorId, true).withAlpha(0.5f));
|
||||
g.fillRect(box.getLocalBounds());
|
||||
}
|
||||
}
|
||||
|
||||
juce::PopupMenu::Options CustomLookAndFeel::getOptionsForComboBoxPopupMenu(juce::ComboBox& box, juce::Label& label)
|
||||
{
|
||||
return juce::PopupMenu::Options().withTargetComponent(&box)
|
||||
.withPreferredPopupDirection(juce::PopupMenu::Options::PopupDirection::downwards)
|
||||
.withItemThatMustBeVisible(box.getSelectedId())
|
||||
.withInitiallySelectedItem(box.getSelectedId())
|
||||
.withMinimumWidth(box.getWidth())
|
||||
.withMaximumNumColumns(1)
|
||||
.withStandardItemHeight(label.getHeight());
|
||||
}
|
||||
|
||||
juce::Font CustomLookAndFeel::getPopupMenuFont()
|
||||
{
|
||||
return getInter().withHeight(17.f);
|
||||
}
|
||||
|
||||
void CustomLookAndFeel::drawPopupMenuBackground(juce::Graphics& graphics, int, int)
|
||||
{
|
||||
graphics.fillAll(colors.slate);
|
||||
}
|
||||
|
||||
void CustomLookAndFeel::drawPopupMenuItem(juce::Graphics& g, const juce::Rectangle<int>& area, bool /*isSeparator*/,
|
||||
bool isActive, bool isHighlighted, bool isTicked, bool /*hasSubMenu*/, const juce::String& text,
|
||||
const juce::String& /*shortcutKeyText*/, const juce::Drawable* /*icon*/, const Colour* /*textColour*/)
|
||||
{
|
||||
using namespace juce;
|
||||
|
||||
auto bounds = area.toFloat();
|
||||
|
||||
if (isHighlighted && isActive)
|
||||
{
|
||||
g.setColour(colors.darkerSlate);
|
||||
g.fillRect(bounds);
|
||||
}
|
||||
|
||||
float padding = area.getWidth() * 0.02f;
|
||||
bounds.reduce(padding, 0);
|
||||
|
||||
auto font = getPopupMenuFont();
|
||||
auto maxFontHeight = bounds.getHeight() * 0.77f;
|
||||
if (font.getHeight() > maxFontHeight)
|
||||
font.setHeight(maxFontHeight);
|
||||
|
||||
g.setColour(colors.light);
|
||||
g.setFont(font);
|
||||
|
||||
auto iconArea = bounds.removeFromLeft(bounds.getHeight()).toFloat().reduced(bounds.getHeight() * 0.29f);
|
||||
if (isTicked)
|
||||
{
|
||||
auto tick = getTickShape(1.0f);
|
||||
g.strokePath(tick, PathStrokeType(iconArea.getWidth() * 0.18f, PathStrokeType::curved, PathStrokeType::rounded), tick.getTransformToScaleToFit(iconArea, true));
|
||||
}
|
||||
|
||||
bounds.removeFromRight(3);
|
||||
g.drawText(text, bounds, Justification::centredLeft, true);
|
||||
}
|
||||
|
||||
void CustomLookAndFeel::drawPopupMenuUpDownArrow(juce::Graphics& g, int width, int height, bool isScrollUpArrow)
|
||||
{
|
||||
using namespace juce;
|
||||
|
||||
auto hw = float(width * 0.5f);
|
||||
auto arrowW = float(height * 0.3f);
|
||||
auto y1 = float(height) * (isScrollUpArrow ? 0.6f : 0.3f);
|
||||
auto y2 = float(height) * (isScrollUpArrow ? 0.3f : 0.6f);
|
||||
|
||||
Path p;
|
||||
p.addTriangle(hw - arrowW, y1,
|
||||
hw + arrowW, y1,
|
||||
hw, y2);
|
||||
|
||||
g.setColour(colors.dark);
|
||||
g.fillPath(p);
|
||||
}
|
||||
|
||||
juce::Path CustomLookAndFeel::getTickShape(float height)
|
||||
{
|
||||
juce::Path tick;
|
||||
tick.startNewSubPath(height, 0.f);
|
||||
tick.lineTo(height * 0.4f, height);
|
||||
tick.lineTo(0.f, height * 0.5f);
|
||||
return tick;
|
||||
}
|
||||
|
||||
void CustomLookAndFeel::drawTickBox(juce::Graphics& g, juce::Component& component, float /*x*/, float /*y*/, float /*w*/,
|
||||
float /*h*/, bool ticked, bool /*isEnabled*/, bool /*shouldDrawButtonAsHighlighted*/, bool /*shouldDrawButtonAsDown*/)
|
||||
{
|
||||
using namespace juce;
|
||||
|
||||
auto bounds = component.getLocalBounds().toFloat();
|
||||
auto size = jmin(bounds.getWidth(), bounds.getHeight());
|
||||
bounds.reduce((bounds.getWidth() - size) / 2.f, (bounds.getHeight() - size) / 2.f);
|
||||
|
||||
float lineThickness = 0.093f * size;
|
||||
|
||||
if (ticked)
|
||||
{
|
||||
g.setColour(colors.highlight);
|
||||
g.fillRect(bounds.reduced(lineThickness * 0.75f));
|
||||
}
|
||||
|
||||
g.setColour(colors.dark);
|
||||
g.drawRoundedRectangle(bounds.reduced(lineThickness * 0.66f), 0.14f * size, lineThickness);
|
||||
|
||||
if (!component.isEnabled())
|
||||
{
|
||||
g.setColour(component.findColour(colors.backgroundColorId, true).withAlpha(0.5f));
|
||||
g.fillRect(component.getLocalBounds());
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
template <EnvelopeSlider Direction>
|
||||
void EnvelopeSliderLookAndFeel<Direction>::drawRotarySlider(juce::Graphics& g, int x, int y, int width, int height, float, float, float, juce::Slider& slider)
|
||||
{
|
||||
using namespace juce;
|
||||
|
||||
auto bounds = Rectangle(x, y, width, height).toFloat();
|
||||
bounds = bounds.reduced(bounds.getWidth() * Layout::rotaryPadding);
|
||||
float exp = float(slider.getValue());
|
||||
|
||||
Path curve;
|
||||
for (int i = 0; i < width; i++)
|
||||
{
|
||||
float xPos = float(i) / width;
|
||||
float yPos = CustomSamplerVoice::exponentialCurve(exp, xPos);
|
||||
|
||||
if constexpr (Direction == EnvelopeSlider::release)
|
||||
yPos = 1 - yPos;
|
||||
|
||||
if (i == 0)
|
||||
curve.startNewSubPath(float(i), bounds.getHeight() * (1 - yPos));
|
||||
else
|
||||
curve.lineTo(float(i), bounds.getHeight() * (1 - yPos));
|
||||
}
|
||||
|
||||
g.setColour(colors.dark.withAlpha(slider.isEnabled() ? 1.f : 0.5f));
|
||||
g.strokePath(curve, PathStrokeType(0.08f * width, PathStrokeType::curved, PathStrokeType::rounded), curve.getTransformToScaleToFit(bounds, false));
|
||||
|
||||
slider.setMouseCursor(slider.isEnabled() ? MouseCursor::UpDownResizeCursor : MouseCursor::NormalCursor);
|
||||
}
|
||||
|
||||
void CustomLookAndFeel::drawCornerResizer(juce::Graphics& g, int w, int h, bool isMouseOver,
|
||||
bool isMouseDragging)
|
||||
{
|
||||
auto lineThickness = juce::jmin(float(w), float(h)) * 0.075f;
|
||||
|
||||
auto d = 0.6f;
|
||||
for (auto i = 0; i < 1 + int(isMouseOver || isMouseDragging); i++)
|
||||
{
|
||||
g.setColour(colors.slate);
|
||||
g.drawLine(float(w) * d, float(h) + 1.0f, float(w) + 1.0f, float(h) * d, lineThickness);
|
||||
|
||||
g.setColour(colors.darkerSlate);
|
||||
g.drawLine(float(w) * d + lineThickness, float(h) + 1.0f, float(w) + 1.0f, float(h) * d + lineThickness, lineThickness);
|
||||
|
||||
d -= 0.3f;
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
juce::Slider::SliderLayout VolumeSliderLookAndFeel::getSliderLayout(juce::Slider& slider)
|
||||
{
|
||||
auto borderSize = slider.getWidth() * 0.020f;
|
||||
|
||||
juce::Slider::SliderLayout layout;
|
||||
layout.sliderBounds = slider.getLocalBounds();
|
||||
layout.sliderBounds.removeFromLeft(int(std::round(slider.getWidth() * 0.12f)));
|
||||
layout.sliderBounds.removeFromRight(int(std::round(borderSize + slider.getWidth() * 0.09f)));
|
||||
|
||||
layout.textBoxBounds = slider.getLocalBounds().removeFromTop(int(std::round(0.21f * slider.getWidth())));
|
||||
layout.textBoxBounds.removeFromLeft(int(std::round(slider.getWidth() * 0.1f)));
|
||||
layout.textBoxBounds.removeFromRight(int(std::round(slider.getWidth() * 0.25f)));
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
juce::Label* VolumeSliderLookAndFeel::createSliderTextBox(juce::Slider& slider)
|
||||
{
|
||||
auto label = CustomLookAndFeel::createSliderTextBox(slider);
|
||||
label->setFont(getInriaSansBold().withHeight(slider.getWidth() * 0.225f));
|
||||
label->setJustificationType(juce::Justification::bottomRight);
|
||||
auto superOnEditorShow = label->onEditorShow;
|
||||
label->onEditorShow = [label, superOnEditorShow]
|
||||
{
|
||||
superOnEditorShow();
|
||||
|
||||
auto editor = label->getCurrentTextEditor();
|
||||
auto editorBounds = editor->getBounds();
|
||||
editorBounds.translate(3, 0);
|
||||
editor->setBounds(editorBounds);
|
||||
editor->setJustification(juce::Justification::bottomRight);
|
||||
};
|
||||
return label;
|
||||
}
|
||||
|
||||
void VolumeSliderLookAndFeel::drawLinearSlider(juce::Graphics& g, int /*x*/, int /*y*/, int /*width*/, int /*height*/, float sliderPos, float, float, juce::Slider::SliderStyle, juce::Slider& slider)
|
||||
{
|
||||
using namespace juce;
|
||||
|
||||
auto bounds = slider.getBounds().withZeroOrigin().toFloat();
|
||||
|
||||
auto borderSize = bounds.getWidth() * 0.020f;
|
||||
bounds = bounds.reduced(borderSize);
|
||||
|
||||
// Get thumb dimensions
|
||||
auto thumbWidth = bounds.getWidth() * 0.203f;
|
||||
auto thumbHeight = thumbWidth * 0.5f;
|
||||
auto thumbRound = bounds.getWidth() * 0.032f;
|
||||
auto thumb = Rectangle(sliderPos - thumbWidth / 2.f, bounds.getHeight() - thumbHeight, thumbWidth, thumbHeight);
|
||||
|
||||
// Draw fader shape
|
||||
Path sliderShape;
|
||||
float bottom = bounds.getHeight() - thumbHeight / 2.f;
|
||||
float right = bounds.getRight() - bounds.getWidth() * 0.08f;
|
||||
sliderShape.addTriangle(bounds.getX(), bottom, right, bottom, right, bottom - bounds.getWidth() * 0.4f);
|
||||
sliderShape = sliderShape.createPathWithRoundedCorners(2 * borderSize);
|
||||
|
||||
g.setColour(colors.highlight);
|
||||
float scale = sliderPos / (right - bounds.getX()) * 0.985f;
|
||||
g.fillPath(sliderShape, AffineTransform::scale(scale, scale * 0.98f, bounds.getX(), bottom));
|
||||
g.setColour(colors.dark);
|
||||
g.strokePath(sliderShape, PathStrokeType(borderSize, PathStrokeType::JointStyle::curved));
|
||||
|
||||
if (!slider.isEnabled())
|
||||
{
|
||||
g.setColour(slider.findColour(colors.backgroundColorId, true).withAlpha(0.5f));
|
||||
g.fillRect(slider.getLocalBounds());
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
const juce::Font& getInriaSans()
|
||||
{
|
||||
static juce::Font inriaSans{ juce::FontOptions(juce::Typeface::createSystemTypefaceFor(BinaryData::InriaSansRegular_ttf, BinaryData::InriaSansRegular_ttfSize)) };
|
||||
|
||||
return inriaSans;
|
||||
}
|
||||
|
||||
const juce::Font& getInriaSansBold()
|
||||
{
|
||||
static juce::Font inriaSansBold{ juce::FontOptions(juce::Typeface::createSystemTypefaceFor(BinaryData::InriaSansBold_ttf, BinaryData::InriaSansBold_ttfSize)) };
|
||||
|
||||
return inriaSansBold;
|
||||
}
|
||||
|
||||
const juce::Font& getInter()
|
||||
{
|
||||
static juce::Font inter{ juce::FontOptions(juce::Typeface::createSystemTypefaceFor(BinaryData::InterRegular_ttf, BinaryData::InterRegular_ttfSize)) };
|
||||
|
||||
return inter;
|
||||
}
|
||||
|
||||
const juce::Font& getInterBold()
|
||||
{
|
||||
static juce::Font interBold{ juce::FontOptions(juce::Typeface::createSystemTypefaceFor(BinaryData::InterBold_ttf, BinaryData::InterBold_ttfSize)) };
|
||||
|
||||
return interBold;
|
||||
}
|
||||
|
||||
juce::Path getOutlineFromSVG(const char* data)
|
||||
{
|
||||
auto xml = juce::parseXML(data);
|
||||
jassert(xml != nullptr);
|
||||
return juce::Drawable::createFromSVG(*xml)->getOutlineAsPath();
|
||||
}
|
||||
227
Source/CustomLookAndFeel.h
Normal file
227
Source/CustomLookAndFeel.h
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
LookAndFeel.h
|
||||
Created: 19 Sep 2023 4:48:33pm
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
/** This struct contains the plugin's colors. */
|
||||
struct Colors
|
||||
{
|
||||
/** Background for the editor */
|
||||
juce::Colour background;
|
||||
|
||||
/** Foreground for the controls */
|
||||
juce::Colour foreground;
|
||||
|
||||
/** Dark color */
|
||||
juce::Colour dark;
|
||||
|
||||
/** Highlight color for controls */
|
||||
juce::Colour highlight;
|
||||
|
||||
/** Loop color */
|
||||
juce::Colour loop;
|
||||
|
||||
/** Lighter slate for some buttons */
|
||||
juce::Colour slate;
|
||||
|
||||
/** Darker slate for waveform and some buttons */
|
||||
juce::Colour darkerSlate;
|
||||
|
||||
/** Background neutral color (outside editor) */
|
||||
juce::Colour light;
|
||||
|
||||
/** Prompt background color */
|
||||
juce::Colour prompt;
|
||||
|
||||
static constexpr int backgroundColorId{ -1 };
|
||||
static constexpr int painterColorId{ -2 };
|
||||
};
|
||||
|
||||
static const Colors defaultTheme
|
||||
{
|
||||
.background = juce::Colour{0xFFFDF6C3},
|
||||
.foreground = juce::Colour{0xFFEAFFF7},
|
||||
.dark = juce::Colour{0xFF171614},
|
||||
.highlight = juce::Colour{0xFFFF595E},
|
||||
.loop = juce::Colour{0xFFFFDA22},
|
||||
.slate = juce::Colour{0xFF6E7894},
|
||||
.darkerSlate = juce::Colour{0xFF403D37},
|
||||
.light = juce::Colour{0xFFFFFFFF},
|
||||
.prompt = juce::Colour{0xFF171614}.withAlpha(0.3f)
|
||||
};
|
||||
|
||||
static const Colors darkTheme
|
||||
{
|
||||
.background = juce::Colour{0xFF0F0F1A},
|
||||
.foreground = juce::Colour{0xFF1A1A30},
|
||||
.dark = juce::Colour{0xFF8888AA},
|
||||
.highlight = juce::Colour{0xFF3A73E5},
|
||||
.loop = juce::Colour{0xFFEE8833},
|
||||
.slate = juce::Colour{0xFF2A2A4A},
|
||||
.darkerSlate = juce::Colour{0xFF1A192D},
|
||||
.light = juce::Colour{0xFFFFFFFF},
|
||||
.prompt = juce::Colour{0xFF0F0F1A}.withAlpha(0.8f)
|
||||
};
|
||||
|
||||
/** This struct contains layout constants. */
|
||||
struct Layout
|
||||
{
|
||||
// Toolbar values should all be scaled according to the window width
|
||||
static constexpr int figmaWidth{ 1999 };
|
||||
|
||||
static constexpr int controlsHeight{ 159 };
|
||||
static constexpr int controlsPaddingX{ 16 };
|
||||
static constexpr int moduleGap{ 36 };
|
||||
|
||||
static constexpr int moduleLabelHeight{ 42 };
|
||||
static constexpr int moduleLabelPadding{ 3 };
|
||||
static constexpr int moduleLabelGap{ 3 };
|
||||
static constexpr int moduleControlsGap{ 22 };
|
||||
|
||||
static constexpr int tuningWidth{ 320 }; // As ratios of the window width
|
||||
static constexpr int attackWidth{ 204 };
|
||||
static constexpr int releaseWidth{ 204 };
|
||||
static constexpr int playbackWidth{ 534 };
|
||||
static constexpr int loopWidth{ 217 };
|
||||
static constexpr int masterWidth{ 295 };
|
||||
|
||||
static constexpr int standardRotarySize{ 87 }; // Not including padding
|
||||
static constexpr float rotaryHeightRatio{ 0.965f }; // The height of the rotary slider as a ratio of its width
|
||||
static constexpr float rotaryPadding{ 0.09f };
|
||||
static constexpr float rotaryTextSize{ 0.40f };
|
||||
|
||||
static constexpr float boundsWidth{ 0.0045f };
|
||||
static constexpr float handleWidth{ boundsWidth / 2.f };
|
||||
static constexpr float playheadWidth{ handleWidth * 0.66f };
|
||||
|
||||
static constexpr juce::Point<int> sampleControlsMargin{ 46, 25 };
|
||||
static constexpr int sampleControlsHeight{ 45 };
|
||||
static constexpr int fileControlsWidth{ 1179 };
|
||||
static constexpr int waveformModeWidth{ 238 };
|
||||
static constexpr int playbackControlsWidth{ 137 };
|
||||
static constexpr float expandWidth{ 1.185f }; // Ratio of combobox height
|
||||
|
||||
static constexpr int sampleNavigatorHeight{ 93 };
|
||||
static constexpr float navigatorBoundsWidth{ 0.002f };
|
||||
static constexpr juce::Point<int> navigatorControlsSize{ 124, 52 };
|
||||
|
||||
static constexpr int fxChainHeight{ 384 };
|
||||
static constexpr float fxChainDivider{ 0.001f };
|
||||
static constexpr float fxModuleHeader{ 73.f };
|
||||
static constexpr float fxDisplayStrokeWidth{ 0.004f };
|
||||
|
||||
static constexpr int footerHeight{ 64 };
|
||||
};
|
||||
|
||||
/** This struct contains certain UX constants */
|
||||
struct Feel
|
||||
{
|
||||
static constexpr int MOUSE_SENSITIVITY{ 60 };
|
||||
static constexpr int DRAGGABLE_SNAP{ 10 };
|
||||
|
||||
static constexpr int MINIMUM_BOUNDS_DISTANCE{ 10 }; // This needs to be at some minimum value
|
||||
static constexpr int MINIMUM_VIEW{ 6 * MINIMUM_BOUNDS_DISTANCE }; // Minimum view size in samples
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** Some utilities */
|
||||
struct ComponentProps
|
||||
{
|
||||
inline static const juce::String& ROTARY_UNIT{ "props_unit" }; // The rotary unit
|
||||
inline static const juce::String& ROTARY_GREATER_UNIT{ "greater_unit" }; // A larger unit (e.g. s instead of ms)
|
||||
inline static const juce::String& ROTARY_ICON{ "label_icon" }; // An icon to display in place of the unit
|
||||
|
||||
inline static const juce::String& ROTARY_PARAMETER_NAME{ "parameter_name" }; // For use in a custom rotaries help text
|
||||
|
||||
inline static const juce::String& LABEL_ELLIPSES{ "label_ellipses" }; // Use ellipses for the label
|
||||
};
|
||||
|
||||
class ReferenceCountedPath final : public juce::ReferenceCountedObject
|
||||
{
|
||||
public:
|
||||
explicit ReferenceCountedPath(juce::Path path) : path(std::move(path)) {}
|
||||
|
||||
juce::Path path;
|
||||
};
|
||||
|
||||
const juce::Font& getInriaSans();
|
||||
const juce::Font& getInriaSansBold();
|
||||
const juce::Font& getInter();
|
||||
const juce::Font& getInterBold();
|
||||
juce::Path getOutlineFromSVG(const char* data);
|
||||
|
||||
//==============================================================================
|
||||
class CustomLookAndFeel : public juce::LookAndFeel_V4
|
||||
{
|
||||
using Colour = juce::Colour;
|
||||
|
||||
public:
|
||||
CustomLookAndFeel();
|
||||
void setTheme(const Colors& theme);
|
||||
|
||||
juce::Slider::SliderLayout getSliderLayout(juce::Slider& slider) override;
|
||||
juce::Label* createSliderTextBox(juce::Slider& slider) override;
|
||||
void drawRotarySlider(juce::Graphics& g, int x, int y, int width, int height, float sliderPosProportional, float rotaryStartAngle, float rotaryEndAngle, juce::Slider& slider) override;
|
||||
|
||||
void drawLabel(juce::Graphics&, juce::Label&) override;
|
||||
|
||||
juce::Button* createFilenameComponentBrowseButton(const juce::String& text) override;
|
||||
void layoutFilenameComponent(juce::FilenameComponent&, juce::ComboBox* filenameBox, juce::Button* browseButton) override;
|
||||
|
||||
juce::Font getComboBoxFont(juce::ComboBox&) override;
|
||||
juce::Label* createComboBoxTextBox(juce::ComboBox&) override;
|
||||
void positionComboBoxText(juce::ComboBox&, juce::Label& label) override;
|
||||
void drawComboBox(juce::Graphics&, int width, int height, bool isButtonDown, int buttonX, int buttonY, int buttonW, int buttonH, juce::ComboBox&) override;
|
||||
void drawComboBoxTextWhenNothingSelected(juce::Graphics&, juce::ComboBox&, juce::Label&) override;
|
||||
juce::PopupMenu::Options getOptionsForComboBoxPopupMenu(juce::ComboBox&, juce::Label&) override;
|
||||
|
||||
juce::Font getPopupMenuFont() override;
|
||||
void drawPopupMenuBackground(juce::Graphics&, int width, int height) override;
|
||||
void drawPopupMenuItem(juce::Graphics&, const juce::Rectangle<int>& area, bool isSeparator, bool isActive, bool isHighlighted, bool isTicked, bool hasSubMenu, const juce::String& text, const juce::String& shortcutKeyText, const juce::Drawable* icon, const Colour* textColour) override;
|
||||
void drawPopupMenuUpDownArrow(juce::Graphics&, int width, int height, bool isScrollUpArrow) override;
|
||||
juce::Path getTickShape(float height) override;
|
||||
|
||||
void drawTickBox(juce::Graphics&, juce::Component&, float x, float y, float w, float h, bool ticked, bool isEnabled, bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override;
|
||||
|
||||
juce::Font getAlertWindowTitleFont() override { return getInterBold().withHeight(18.f); }
|
||||
juce::Font getAlertWindowMessageFont() override { return getInter().withHeight(16.f); }
|
||||
juce::Font getAlertWindowFont() override { return getInter().withHeight(14.f); }
|
||||
|
||||
void drawCornerResizer(juce::Graphics&, int w, int h, bool isMouseOver, bool isMouseDragging) override;
|
||||
|
||||
Colors colors;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** Here's a custom look and feel for the envelope sliders. A template was not the only way to do this, but I thought I'd try it. */
|
||||
enum class EnvelopeSlider : std::uint8_t
|
||||
{
|
||||
attack,
|
||||
release
|
||||
};
|
||||
|
||||
template <EnvelopeSlider Direction>
|
||||
class EnvelopeSliderLookAndFeel final : public CustomLookAndFeel
|
||||
{
|
||||
void drawRotarySlider(juce::Graphics& g, int x, int y, int width, int height, float sliderPos, float rotaryStartAngle, float rotaryEndAngle, juce::Slider& slider) override;
|
||||
};
|
||||
|
||||
template class EnvelopeSliderLookAndFeel<EnvelopeSlider::attack>;
|
||||
template class EnvelopeSliderLookAndFeel<EnvelopeSlider::release>;
|
||||
|
||||
//==============================================================================
|
||||
/** Here's a custom look and feel for the volume slider. */
|
||||
class VolumeSliderLookAndFeel final : public CustomLookAndFeel
|
||||
{
|
||||
juce::Slider::SliderLayout getSliderLayout(juce::Slider& slider) override;
|
||||
juce::Label* createSliderTextBox(juce::Slider& slider) override;
|
||||
void drawLinearSlider(juce::Graphics& g, int x, int y, int width, int height, float sliderPos, float minSliderPos, float maxSliderPos, juce::Slider::SliderStyle, juce::Slider&) override;
|
||||
};
|
||||
1204
Source/PluginEditor.cpp
Normal file
1204
Source/PluginEditor.cpp
Normal file
File diff suppressed because it is too large
Load diff
250
Source/PluginEditor.h
Normal file
250
Source/PluginEditor.h
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
PluginProcessor.cpp
|
||||
Created: 5 Sep 2023
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
#include "PluginProcessor.h"
|
||||
#include "CustomLookAndFeel.h"
|
||||
#include "Components/Buttons.h"
|
||||
#include "Components/InputDeviceSelector.h"
|
||||
#include "Components/SampleEditor.h"
|
||||
#include "Components/FxChain.h"
|
||||
#include "Components/Prompt.h"
|
||||
#include "Components/SampleNavigator.h"
|
||||
|
||||
/** Painting ordering requires a separate component... */
|
||||
class EditorOverlay final : public CustomComponent
|
||||
{
|
||||
public:
|
||||
void setWaveformMode(bool isWaveformMode)
|
||||
{
|
||||
if (waveformModeAvailable != isWaveformMode)
|
||||
{
|
||||
waveformModeAvailable = isWaveformMode;
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
void setWaveformModeDisabled(bool disabled)
|
||||
{
|
||||
if (waveformModeDisabled != disabled)
|
||||
{
|
||||
waveformModeDisabled = disabled;
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void paint(juce::Graphics& g) override;
|
||||
void resized() override;
|
||||
void lookAndFeelChanged() override;
|
||||
|
||||
float scale(float value) const { return value * getWidth() / Layout::figmaWidth; }
|
||||
float scale(int value) const { return scale(float(value)); }
|
||||
|
||||
//==============================================================================
|
||||
melatonin::DropShadow sampleControlShadow{ defaultTheme.slate.withAlpha(0.125f), 3, {2, 2} };
|
||||
melatonin::DropShadow navControlShadow{ defaultTheme.slate.withAlpha(0.125f), 3, {-2, -2} };
|
||||
|
||||
bool waveformModeAvailable{ false };
|
||||
bool waveformModeDisabled{ false };
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class JustaSampleAudioProcessorEditor final : public juce::AudioProcessorEditor, public juce::Timer, public juce::FileDragAndDropTarget,
|
||||
public juce::FilenameComponentListener, public CustomHelpTextDisplay, public ThemeProvider
|
||||
{
|
||||
public:
|
||||
explicit JustaSampleAudioProcessorEditor(JustaSampleAudioProcessor& audioProcessor);
|
||||
~JustaSampleAudioProcessorEditor() override;
|
||||
|
||||
private:
|
||||
void timerCallback() override;
|
||||
void paint(juce::Graphics&) override;
|
||||
void resized() override;
|
||||
void lookAndFeelChanged() override;
|
||||
|
||||
void mouseDown(const juce::MouseEvent& event) override;
|
||||
void mouseUp(const juce::MouseEvent& event) override;
|
||||
void mouseDrag(const juce::MouseEvent& event) override;
|
||||
|
||||
void mouseWheelMove(const juce::MouseEvent& event, const juce::MouseWheelDetails& wheel) override;
|
||||
|
||||
void helpTextChanged(const juce::String&) override {}
|
||||
|
||||
void setTheme(Colors newTheme) override;
|
||||
Colors getTheme() const override;
|
||||
|
||||
juce::Rectangle<int> getConstrainedBounds() const;
|
||||
|
||||
//==============================================================================
|
||||
/** Update the Editor to fit with the processor's sample. On the initial load, the
|
||||
SampleNavigator will not update the viewing bounds.
|
||||
*/
|
||||
void loadSample();
|
||||
|
||||
/** Handles the incoming recording messages from the recorder queue */
|
||||
void handleActiveRecording();
|
||||
|
||||
/** If permitted, toggles whether the plugin state will use a file reference or store the
|
||||
samples directly. If necessary, this will prompt the user to save the current sample.
|
||||
*/
|
||||
void toggleLinkSample();
|
||||
|
||||
/** Signals to the processor to start recording, or first opens the device settings prompt if
|
||||
no valid input device is selected. Note that no editor changes occur here, instead it polls
|
||||
for changes in the timer callback (simpler than registering as a listener to the recorder).
|
||||
*/
|
||||
void startRecording(bool promptSettings = true);
|
||||
|
||||
/** Starts a pitch detection prompt */
|
||||
void promptPitchDetection();
|
||||
|
||||
/** Opens the device settings prompt */
|
||||
void promptDeviceSettings(bool recordOnClose = false);
|
||||
|
||||
/** We use this as a place to update the enablement and display of our plugin controls. */
|
||||
void enablementChanged() override;
|
||||
void fxEnablementChanged();
|
||||
|
||||
//==============================================================================
|
||||
/** Whether the editor is interested in a file */
|
||||
bool isInterestedInFile(const juce::String& file) const;
|
||||
bool isInterestedInFileDrag(const juce::StringArray& files) override;
|
||||
void filesDropped(const juce::StringArray& files, int x, int y) override;
|
||||
void filenameComponentChanged(juce::FilenameComponent* fileComponentThatHasChanged) override;
|
||||
|
||||
void fileDragEnter(const juce::StringArray& files, int x, int y) override;
|
||||
void fileDragExit(const juce::StringArray& files) override;
|
||||
|
||||
void updateLabel(const juce::String& text = "");
|
||||
|
||||
//==============================================================================
|
||||
/** Scaling the sizes in our Figma demo to percentages of width.
|
||||
This rounding operation is important to ensure consistent spacing with JUCE's integer component bounds.
|
||||
Otherwise, we'd just do a simple division.
|
||||
*/
|
||||
float scalei(int value) const { return scalei(float(value)); }
|
||||
float scalei(float value) const { return std::roundf(value * prompt.getWidth() / Layout::figmaWidth); }
|
||||
float scalef(float value) const { return value * prompt.getWidth() / Layout::figmaWidth; }
|
||||
|
||||
//==============================================================================
|
||||
JustaSampleAudioProcessor& p;
|
||||
PluginParameters::State& pluginState;
|
||||
UIDummyParam dummyParam;
|
||||
const juce::OwnedArray<CustomSamplerVoice>& synthVoices;
|
||||
bool currentlyPlaying{ false };
|
||||
|
||||
/** Some thought is needed to keep the editor synchronized when changes to the sample occur
|
||||
or files are loaded. I decided the easiest way is to have the processor and editor both
|
||||
keep track of the buffer's hash.
|
||||
*/
|
||||
juce::String expectedHash{ 0 };
|
||||
|
||||
bool userDraggedSample{ false }; // We use this to reset the UI only when a sample loads as result of a user selection
|
||||
|
||||
//==============================================================================
|
||||
juce::AudioBuffer<float> pendingRecordingBuffer;
|
||||
int recordingBufferSize{ 0 };
|
||||
|
||||
//==============================================================================
|
||||
// Modules
|
||||
juce::Label tuningLabel, attackLabel, releaseLabel, playbackLabel, loopingLabel, masterLabel;
|
||||
juce::Array<juce::Slider*> rotaries;
|
||||
|
||||
// Tuning module
|
||||
CustomRotary semitoneRotary, centRotary, waveformSemitoneRotary, waveformCentRotary;
|
||||
CustomRotaryAttachment semitoneRotaryAttachment, centRotaryAttachment, waveformSemitoneRotaryAttachment, waveformCentRotaryAttachment;
|
||||
|
||||
juce::Label tuningDetectLabel;
|
||||
CustomShapeButton tuningDetectButton;
|
||||
|
||||
// Attack and release modules
|
||||
CustomRotary attackTimeRotary, attackCurve, releaseTimeRotary, releaseCurve;
|
||||
CustomRotaryAttachment attackTimeAttachment, attackCurveAttachment, releaseTimeAttachment, releaseCurveAttachment;
|
||||
EnvelopeSliderLookAndFeel<EnvelopeSlider::attack> attackCurveLNF;
|
||||
EnvelopeSliderLookAndFeel<EnvelopeSlider::release> releaseCurveLNF;
|
||||
|
||||
// Playback module
|
||||
CustomToggleableButton lofiModeButton;
|
||||
APVTS::ButtonAttachment lofiModeAttachment;
|
||||
CustomChoiceButton playbackModeButton;
|
||||
juce::ParameterAttachment playbackModeAttachment;
|
||||
CustomRotary playbackSpeedRotary;
|
||||
CustomRotaryAttachment playbackSpeedAttachment;
|
||||
|
||||
// Loop module
|
||||
CustomToggleableButton loopButton, loopStartButton, loopEndButton;
|
||||
APVTS::ButtonAttachment loopAttachment, loopStartAttachment, loopEndAttachment;
|
||||
|
||||
// Mixing module
|
||||
CustomToggleableButton monoOutputButton;
|
||||
APVTS::ButtonAttachment monoOutputAttachment;
|
||||
CustomRotary gainSlider;
|
||||
CustomRotaryAttachment gainSliderAttachment;
|
||||
VolumeSliderLookAndFeel gainSliderLNF;
|
||||
|
||||
// Sample controls
|
||||
EditorOverlay editorOverlay;
|
||||
juce::FilenameComponent filenameComponent;
|
||||
CustomToggleableButton linkSampleToggle; // Whether the sample should be stored in the plugin state
|
||||
|
||||
CustomToggleableButton waveformModeLabel;
|
||||
juce::ButtonParameterAttachment waveformModeLabelAttachment;
|
||||
|
||||
CustomShapeButton playStopButton;
|
||||
juce::Path playPath, stopPath;
|
||||
const juce::String playHelpText{ "Play sample" }, stopHelpText{ "Halt voices" };
|
||||
CustomShapeButton recordButton;
|
||||
CustomShapeButton deviceSettingsButton;
|
||||
InputDeviceSelector audioDeviceSettings;
|
||||
|
||||
// Nav controls
|
||||
CustomShapeButton fitButton;
|
||||
CustomToggleableButton pinButton;
|
||||
ToggleButtonAttachment pinButtonAttachment;
|
||||
|
||||
// Main components
|
||||
SampleEditor sampleEditor;
|
||||
SampleNavigator sampleNavigator; // Note that SampleNavigator manages ViewStart and ViewEnd
|
||||
FxChain fxChain;
|
||||
|
||||
juce::Label statusLabel;
|
||||
bool fileDragging{ false };
|
||||
|
||||
// Some variables to help detect scroll gestures
|
||||
const int maxCallbacksSinceInZoomGesture{ 20 };
|
||||
int zoomGestureEndCallbacks{ 0 };
|
||||
bool mouseWheelDetected{ false };
|
||||
int lastViewStart{ 0 }, lastViewEnd{ 0 };
|
||||
|
||||
// Footer
|
||||
juce::Label versionInfo;
|
||||
CustomToggleableButton preFXButton;
|
||||
APVTS::ButtonAttachment preFXAttachment;
|
||||
CustomToggleableButton showFXButton;
|
||||
ToggleButtonAttachment showFXAttachment;
|
||||
juce::ParameterAttachment eqEnablementAttachment, reverbEnablementAttachment, distortionEnablementAttachment, chorusEnablementAttachment;
|
||||
bool eqEnabled{ false }, reverbEnabled{ false }, distortionEnabled{ false }, chorusEnabled{ false };
|
||||
|
||||
Prompt prompt;
|
||||
|
||||
CustomLookAndFeel& lnf;
|
||||
Colors theme{ defaultTheme };
|
||||
juce::OpenGLContext openGLContext;
|
||||
juce::PluginHostType hostType;
|
||||
|
||||
#if JUCE_DEBUG
|
||||
melatonin::Inspector inspector{ *this, false };
|
||||
#endif
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (JustaSampleAudioProcessorEditor)
|
||||
};
|
||||
438
Source/PluginParameters.h
Normal file
438
Source/PluginParameters.h
Normal file
|
|
@ -0,0 +1,438 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
PluginParameters.h
|
||||
Created: 4 Oct 2023 10:40:47am
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
#include "Utilities/ListenableValue.h"
|
||||
|
||||
#ifndef JAS_DARKMODE_DEFAULT
|
||||
#define JAS_DARKMODE_DEFAULT true
|
||||
#endif
|
||||
|
||||
#ifndef JAS_VST3_REAPER_INTEGRATION
|
||||
#define JAS_VST3_REAPER_INTEGRATION false
|
||||
#endif
|
||||
|
||||
/** This namespace contains all APVTS parameter IDs, the other plugin state, various plugin configuration settings, and the parameter layout */
|
||||
namespace PluginParameters
|
||||
{
|
||||
using String = juce::String;
|
||||
using StringArray = juce::StringArray;
|
||||
using NormalisableRange = juce::NormalisableRange<float>;
|
||||
using Range = juce::Range<float>;
|
||||
|
||||
/** Stores the non-parameter state of the plugin */
|
||||
struct State
|
||||
{
|
||||
ListenableAtomic<int> width{ 0 };
|
||||
inline static const String WIDTH{ "Width" };
|
||||
ListenableAtomic<int> height{ 0 };
|
||||
inline static const String HEIGHT{ "Height" };
|
||||
|
||||
ListenableMutex<String> filePath{ "" };
|
||||
inline static const String FILE_PATH{ "File Path" };
|
||||
ListenableMutex<String> sampleHash{ "" };
|
||||
inline static const String SAMPLE_HASH{ "Sample Hash" };
|
||||
ListenableAtomic<bool> usingFileReference{ false };
|
||||
inline static const String USING_FILE_REFERENCE{ "Using File Reference" };
|
||||
ListenableMutex<StringArray> recentFiles;
|
||||
inline static const String RECENT_FILES{ "Recent Files" };
|
||||
inline static const String SAVED_DEVICE_SETTINGS{ "Saved Device Settings" };
|
||||
|
||||
ListenableAtomic<int> viewStart{ 0 };
|
||||
inline static const String UI_VIEW_START{ "UI View Start" };
|
||||
ListenableAtomic<int> viewEnd{ 0 };
|
||||
inline static const String UI_VIEW_END{ "UI View End" };
|
||||
ListenableAtomic<bool> pinView{ false };
|
||||
inline static const String PIN_VIEW{ "Pin View" };
|
||||
ListenableAtomic<int> primaryChannel{ 0 };
|
||||
inline static const String PRIMARY_CHANNEL{ "Primary Channel" };
|
||||
|
||||
ListenableAtomic<int> sampleStart{ 0 };
|
||||
inline static const String SAMPLE_START{ "Sample Start" };
|
||||
ListenableAtomic<int> sampleEnd{ 0 };
|
||||
inline static const String SAMPLE_END{ "Sample End" };
|
||||
ListenableAtomic<int> loopStart{ 0 };
|
||||
inline static const String LOOP_START{ "Loop Start" };
|
||||
ListenableAtomic<int> loopEnd{ 0 };
|
||||
inline static const String LOOP_END{ "Loop End" };
|
||||
|
||||
ListenableAtomic<bool> showFX{ false };
|
||||
inline static const String SHOW_FX{ "Show FX" };
|
||||
ListenableAtomic<bool> darkMode{ JAS_DARKMODE_DEFAULT };
|
||||
inline static const String DARK_MODE{ "Dark Mode" };
|
||||
|
||||
inline static const String UI_DUMMY_PARAM{ "UI Update" };
|
||||
};
|
||||
|
||||
// Misc
|
||||
inline static constexpr int FRAME_RATE{ 60 };
|
||||
|
||||
inline static constexpr bool REAPER_INTEGRATION_ENABLED{ JAS_VST3_REAPER_INTEGRATION };
|
||||
|
||||
// Sample storage
|
||||
inline static constexpr bool USE_FILE_REFERENCE{ true };
|
||||
inline static constexpr int STORED_BITRATE{ 16 };
|
||||
inline static constexpr double MAX_FILE_SIZE{ 320000000.0 }; // in bits, 40MB
|
||||
|
||||
// Tuning
|
||||
inline static const String SEMITONE_TUNING{ "Semitone Tuning" };
|
||||
inline static const String CENT_TUNING{ "Cent Tuning" };
|
||||
inline static const String PITCH_WHEEL_RANGE{ "Pitch Wheel Range" };
|
||||
inline static const String WIDE_TUNING{ "Wide Tuning" };
|
||||
|
||||
inline static const String A4_HZ{ "A4 Frequency" };
|
||||
inline static const String WAVEFORM_SEMITONE_TUNING{ "Waveform Semitone Tuning" };
|
||||
inline static const String WAVEFORM_CENT_TUNING{ "Waveform Cent Tuning" };
|
||||
|
||||
// Sample envelope
|
||||
inline static const String ATTACK{ "Attack Time" };
|
||||
inline static const String RELEASE{ "Release Time" };
|
||||
inline static const NormalisableRange ENVELOPE_TIME_RANGE{ 0.f, 5000.f, 1.f };
|
||||
inline static const String ATTACK_SHAPE{ "Attack Curve Shape" };
|
||||
inline static const String RELEASE_SHAPE{ "Release Curve Shape" };
|
||||
inline static const String CROSSFADE_SAMPLES{"Crossfade Samples"};
|
||||
|
||||
// Sample playback
|
||||
inline static const String PLAY_UNTIL_END{ "Play Until End" };
|
||||
inline static const String IS_LOOPING{ "Loop" };
|
||||
inline static const String LOOPING_HAS_START{ "Loop With Start" };
|
||||
inline static const String LOOPING_HAS_END{ "Loop With End" };
|
||||
|
||||
inline static const String PLAYBACK_MODE{ "Playback Mode" };
|
||||
inline static const StringArray PLAYBACK_MODE_LABELS{ "Basic", "Bungee" }; // for IDs and display
|
||||
|
||||
enum PLAYBACK_MODES : std::uint8_t
|
||||
{
|
||||
BASIC,
|
||||
BUNGEE,
|
||||
};
|
||||
|
||||
/** Returns an enum representation of a playback mode given a float */
|
||||
inline PLAYBACK_MODES getPlaybackMode(int value) { return static_cast<PLAYBACK_MODES>(value); }
|
||||
|
||||
/** Skipping antialiasing can be an interesting effect */
|
||||
inline static const String SKIP_ANTIALIASING{ "Lo-fi Resampling" };
|
||||
|
||||
// Some controls for advanced playback
|
||||
inline static const String SPEED_FACTOR{ "Playback Speed" };
|
||||
inline static const String OCTAVE_SPEED_FACTOR{ "Octave Speed Factor" };
|
||||
|
||||
inline static constexpr float WAVETABLE_CUTOFF_HZ{ 20 }; // The cutoff frequency for "wavetable mode"
|
||||
inline static const String DISABLE_WAVETABLE_MODE{ "Disable Waveform Mode" };
|
||||
|
||||
inline static const String SAMPLE_GAIN{ "Sample Gain" };
|
||||
inline static const String MONO_OUTPUT{ "Mono Output" };
|
||||
|
||||
inline static const String DISABLE_VELOCITY{ "Disable Velocity" };
|
||||
|
||||
inline static constexpr int MAX_VOICES{ 256 };
|
||||
inline static const String NUM_VOICES{ "Voice Count" };
|
||||
|
||||
inline static constexpr juce::Range MIDI_NOTE_RANGE{ 0, 127 };
|
||||
inline static const String MIDI_START{ "MIDI Range Start" };
|
||||
inline static const String MIDI_END{ "MIDI Range End" };
|
||||
inline static const String MIDI_ROOT{ "MIDI Root Note" };
|
||||
inline static const String FOLLOW_MIDI_PITCH{ "Follow MIDI Pitch" };
|
||||
|
||||
// FX parameters
|
||||
inline static const String REVERB_ENABLED{ "Reverb Enabled" };
|
||||
inline static const String REVERB_MIX{ "Reverb Mix" };
|
||||
inline static const String REVERB_SIZE{ "Reverb Size" };
|
||||
inline static constexpr Range REVERB_SIZE_RANGE{ 5.f, 100.f};
|
||||
inline static const String REVERB_DAMPING{ "Reverb Damping" };
|
||||
inline static constexpr Range REVERB_DAMPING_RANGE{ 0.f, 95.f };
|
||||
inline static const String REVERB_LOWS{ "Reverb Lows" }; // These controls map to filters built into Gin's SimpleVerb
|
||||
inline static constexpr Range REVERB_LOWS_RANGE{ 0.f, 1.f };
|
||||
inline static const String REVERB_HIGHS{ "Reverb Highs" };
|
||||
inline static constexpr Range REVERB_HIGHS_RANGE{ 0.f, 1.f };
|
||||
inline static const String REVERB_PREDELAY{ "Reverb Predelay" };
|
||||
|
||||
inline static const String DISTORTION_ENABLED{ "Distortion Enabled" };
|
||||
inline static const String DISTORTION_DENSITY{ "Distortion Density" };
|
||||
inline static constexpr Range DISTORTION_DENSITY_RANGE{ -0.5f, 1.f };
|
||||
inline static const String DISTORTION_HIGHPASS{ "Distortion Highpass" };
|
||||
inline static constexpr Range DISTORTION_HIGHPASS_RANGE{ 0.f, 0.999f };
|
||||
inline static const String DISTORTION_MIX{ "Distortion Mix" };
|
||||
|
||||
inline static const String EQ_ENABLED{ "EQ Enabled" };
|
||||
inline static const String EQ_LOW_GAIN{ "EQ Low Gain" };
|
||||
inline static const NormalisableRange EQ_GAIN_RANGE{ -18.f, 12.f, 0.1f }; // Decibels
|
||||
inline static const String EQ_MID_GAIN{ "EQ Mid Gain" };
|
||||
inline static const String EQ_HIGH_GAIN{ "EQ High Gain" };
|
||||
inline static const String EQ_LOW_FREQ{ "EQ Low Cutoff" };
|
||||
inline static constexpr Range EQ_LOW_FREQ_RANGE{ 25.f, 600.f };
|
||||
inline static constexpr float EQ_LOW_FREQ_DEFAULT{ 200.f };
|
||||
inline static const String EQ_HIGH_FREQ{ "EQ High Cutoff" };
|
||||
inline static constexpr Range EQ_HIGH_FREQ_RANGE{ 700.f, 15500.f };
|
||||
inline static constexpr float EQ_HIGH_FREQ_DEFAULT{ 2000.f };
|
||||
|
||||
inline static const String CHORUS_ENABLED{ "Chorus Enabled" };
|
||||
inline static const String CHORUS_RATE{ "Chorus Rate" };
|
||||
inline static const NormalisableRange CHORUS_RATE_RANGE{ 0.1f, 20.f, 0.1f, 0.7f }; // in hz, upper range could be extended to 100hz
|
||||
inline static const String CHORUS_DEPTH{ "Chorus Depth" };
|
||||
inline static const NormalisableRange CHORUS_DEPTH_RANGE{ 0.01f, 1.f, 0.01f, 0.5f };
|
||||
inline static const String CHORUS_FEEDBACK{ "Chorus Feedback" };
|
||||
inline static constexpr Range CHORUS_FEEDBACK_RANGE{ -0.95f, 0.95f };
|
||||
inline static const String CHORUS_CENTER_DELAY{ "Chorus Center Delay" };
|
||||
inline static constexpr Range CHORUS_CENTER_DELAY_RANGE{ 0.f, 100.f }; // in ms
|
||||
inline static const String CHORUS_MIX{ "Chorus Mix" };
|
||||
|
||||
inline static const String FX_PERM{ "FX Ordering" };
|
||||
|
||||
enum FxTypes : std::uint8_t
|
||||
{
|
||||
DISTORTION,
|
||||
CHORUS,
|
||||
REVERB,
|
||||
EQ
|
||||
};
|
||||
|
||||
inline static const String PRE_FX{ "FX Before Envelope" };
|
||||
inline static constexpr float FX_TAIL_OFF_MAX{ 0.0001f }; // The cutoff RMS value for tailing off effects
|
||||
|
||||
//==============================================================================
|
||||
/** Returns a permutation of FxTypes, given a representative integer */
|
||||
inline std::array<FxTypes, 4> paramToPerm(int fxParam)
|
||||
{
|
||||
std::array types{ DISTORTION, CHORUS, REVERB, EQ };
|
||||
std::array<FxTypes, 4> perm{};
|
||||
int factorial = 6; // 3!
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
int index = fxParam / factorial;
|
||||
perm[i] = types[index];
|
||||
for (int j = index; j < 3; j++)
|
||||
types[j] = types[j + 1];
|
||||
fxParam %= factorial;
|
||||
if (i < 3)
|
||||
factorial /= 3 - i;
|
||||
}
|
||||
return perm;
|
||||
}
|
||||
|
||||
/** Returns a representative integer, given a permutation of FxTypes */
|
||||
static int permToParam(std::array<FxTypes, 4> fxPerm)
|
||||
{
|
||||
std::array types{ DISTORTION, CHORUS, REVERB, EQ };
|
||||
int result = 0;
|
||||
int factorial = 6; // 3!
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
int type = fxPerm[i];
|
||||
int index;
|
||||
for (index = 0; index < types.size(); index++)
|
||||
if (type == types[index])
|
||||
break;
|
||||
result += factorial * index;
|
||||
for (int j = index; j < 3 - i; j++)
|
||||
types[j] = types[j + 1];
|
||||
if (i < 3)
|
||||
factorial /= 3 - i;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Units
|
||||
inline static const String SEMITONE_UNIT{ "sm" };
|
||||
inline static const String CENT_UNIT{ "%" };
|
||||
inline static const String TIME_UNIT{ "ms" };
|
||||
inline static const String TIME_UNIT_LONG{ "sec" };
|
||||
inline static const String SPEED_UNIT{ "x" };
|
||||
inline static const String VOLUME_UNIT{ "dB" };
|
||||
inline static const String FREQUENCY_UNIT{ "Hz" };
|
||||
|
||||
//==============================================================================
|
||||
namespace Version
|
||||
{
|
||||
// For DAW compatibility, these must not change once released
|
||||
constexpr int V1 = 100;
|
||||
constexpr int V1_1 = 101;
|
||||
constexpr int V1_2 = 102;
|
||||
constexpr int V1_3 = 1030;
|
||||
constexpr int V1_3_2 = 1032;
|
||||
}
|
||||
|
||||
/** Utility to add an integer parameter to the layout */
|
||||
inline void addInt(juce::AudioProcessorValueTreeState::ParameterLayout& layout, const juce::String& identifier,
|
||||
int defaultValue, const juce::NormalisableRange<int>& range, int versionNum, const std::function<String(int value, int maximumStringLength)>& formatFunc = nullptr)
|
||||
{
|
||||
layout.add(std::make_unique<juce::AudioParameterInt>(
|
||||
juce::ParameterID{ identifier, versionNum }, identifier, range.start, range.end, defaultValue, juce::AudioParameterIntAttributes{}.withStringFromValueFunction(formatFunc)
|
||||
));
|
||||
}
|
||||
|
||||
/** Utility to add a float parameter to the layout */
|
||||
inline void addFloat(juce::AudioProcessorValueTreeState::ParameterLayout& layout, const juce::String& identifier,
|
||||
float defaultValue, const juce::NormalisableRange<float>& range, int versionNum, const std::function<String(float value, int maximumStringLength)>& formatFunc = nullptr)
|
||||
{
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{ identifier, versionNum }, identifier, range, defaultValue, juce::AudioParameterFloatAttributes{}.withStringFromValueFunction(formatFunc)
|
||||
));
|
||||
}
|
||||
|
||||
/** Utility to add a boolean parameter to the layout */
|
||||
inline void addBool(juce::AudioProcessorValueTreeState::ParameterLayout& layout, const juce::String& identifier, bool defaultValue, int versionNum, const std::function<String(bool value, int maximumStringLength)>& formatFunc = nullptr)
|
||||
{
|
||||
layout.add(std::make_unique<juce::AudioParameterBool>(
|
||||
juce::ParameterID{ identifier, versionNum }, identifier, defaultValue, juce::AudioParameterBoolAttributes{}.withStringFromValueFunction(formatFunc)
|
||||
));
|
||||
};
|
||||
|
||||
/** Utility to add a choice parameter to the layout */
|
||||
inline void addChoice(juce::AudioProcessorValueTreeState::ParameterLayout& layout, const juce::String& identifier, int defaultIndex,
|
||||
const juce::StringArray& choicesToUse, int versionNum, const std::function<String(int value, int maximumStringLength)>& formatFunc = nullptr)
|
||||
{
|
||||
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||||
juce::ParameterID{ identifier, versionNum }, identifier, choicesToUse, defaultIndex, juce::AudioParameterChoiceAttributes{}.withStringFromValueFunction(formatFunc)
|
||||
));
|
||||
}
|
||||
|
||||
template <typename T> juce::NormalisableRange<T> addSkew(juce::NormalisableRange<T> range, T skewCenter)
|
||||
{
|
||||
auto r = range.getRange();
|
||||
range.skew = std::log(0.5f) / std::log((float(skewCenter) - r.getStart()) / r.getLength());
|
||||
return range;
|
||||
}
|
||||
|
||||
/** This inverts the proportions, such that "increasing" a slider value will decrease the parameter value */
|
||||
template <typename T> juce::NormalisableRange<T> invertProportions(juce::NormalisableRange<T> range)
|
||||
{
|
||||
auto convertFrom0To1Function = [](T rangeStart, T rangeEnd, T normalised) { return juce::jmap<float>(normalised, rangeEnd, rangeStart); };
|
||||
auto convertTo0From1Function = [](T rangeStart, T rangeEnd, T value) { return juce::jmap<float>(value, rangeEnd, rangeStart, 0.0f, 1.0f); };
|
||||
auto newRange = juce::NormalisableRange<T>{ range.start, range.end, convertFrom0To1Function, convertTo0From1Function };
|
||||
newRange.interval = range.interval;
|
||||
return newRange;
|
||||
}
|
||||
|
||||
/** Returns a function that appends a suffix to an integer */
|
||||
inline std::function<String(int, int)> suffixI(const juce::String& suffix)
|
||||
{
|
||||
return [suffix](int value, int) { return juce::String(value) + suffix; };
|
||||
}
|
||||
|
||||
/** Returns a function that appends a suffix to a float. */
|
||||
inline std::function<String(float, int)> suffixF(const juce::String& suffix, float interval)
|
||||
{
|
||||
|
||||
return [suffix, interval](float value, int maxLength)
|
||||
{
|
||||
int numDecimalPlaces = String{ int(1.f / interval) }.length() - 1;
|
||||
auto asText{ String{value, numDecimalPlaces} + suffix };
|
||||
return maxLength > 0 ? asText.substring(0, maxLength) : asText;
|
||||
};
|
||||
}
|
||||
|
||||
const auto FORMAT_MIDI_NOTE = [](int value, int) -> String { return juce::MidiMessage::getMidiNoteName(value, true, true, 4); };
|
||||
|
||||
const auto FORMAT_PERM_VALUE = [](int value, int) -> String
|
||||
{
|
||||
std::array<FxTypes, 4> perm = paramToPerm(value);
|
||||
String result;
|
||||
for (auto fx : perm)
|
||||
{
|
||||
switch (fx)
|
||||
{
|
||||
case DISTORTION:
|
||||
result += "D";
|
||||
break;
|
||||
case CHORUS:
|
||||
result += "C";
|
||||
break;
|
||||
case REVERB:
|
||||
result += "R";
|
||||
break;
|
||||
case EQ:
|
||||
result += "E";
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
inline juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout()
|
||||
{
|
||||
juce::AudioProcessorValueTreeState::ParameterLayout layout;
|
||||
|
||||
addInt(layout, SEMITONE_TUNING, 0, { -18, 18 }, Version::V1, suffixI(" " + SEMITONE_UNIT));
|
||||
addInt(layout, CENT_TUNING, 0, { -100, 100 }, Version::V1, suffixI(CENT_UNIT));
|
||||
addFloat(layout, PITCH_WHEEL_RANGE, 1.f, { 0.f, 12.f, 0.1f }, Version::V1_3, suffixF(" " + SEMITONE_UNIT, 0.1f));
|
||||
addFloat(layout, WIDE_TUNING, 0.f, { -48.f, 48.f, 0.01f }, Version::V1_3, suffixF(" " + SEMITONE_UNIT, 0.01f));
|
||||
|
||||
addFloat(layout, A4_HZ, 440.f, { 400.f, 480.f, 0.1f }, Version::V1_3);
|
||||
addInt(layout, WAVEFORM_SEMITONE_TUNING, 0, { -24, 24 }, Version::V1, suffixI(" " + SEMITONE_UNIT));
|
||||
addInt(layout, WAVEFORM_CENT_TUNING, 0, { -100, 100 }, Version::V1, suffixI(CENT_UNIT));
|
||||
|
||||
addBool(layout, SKIP_ANTIALIASING, false, Version::V1);
|
||||
addChoice(layout, PLAYBACK_MODE, 0, PLAYBACK_MODE_LABELS, Version::V1);
|
||||
addFloat(layout, SPEED_FACTOR, 1.f, addSkew({ 0.01f, 5.f, 0.01f }, 1.f), Version::V1, suffixF(SPEED_UNIT, 0.01f));
|
||||
addFloat(layout, OCTAVE_SPEED_FACTOR, 0.f, { 0.f, 0.6f, 0.15f }, Version::V1, suffixF(SPEED_UNIT, 0.15f));
|
||||
addBool(layout, DISABLE_WAVETABLE_MODE, false, Version::V1_3);
|
||||
|
||||
addBool(layout, PLAY_UNTIL_END, false, Version::V1_3);
|
||||
addBool(layout, LOOPING_HAS_START, false, Version::V1);
|
||||
addBool(layout, IS_LOOPING, false, Version::V1);
|
||||
addBool(layout, LOOPING_HAS_END, false, Version::V1);
|
||||
|
||||
addFloat(layout, SAMPLE_GAIN, 0.f, addSkew({ -32.f, 16.f, 0.1f }, 0.f), Version::V1, suffixF(" " + VOLUME_UNIT, 0.1f));
|
||||
addBool(layout, MONO_OUTPUT, false, Version::V1);
|
||||
|
||||
addBool(layout, DISABLE_VELOCITY, false, Version::V1_3_2);
|
||||
|
||||
addInt(layout, NUM_VOICES, 88, { 1, MAX_VOICES }, Version::V1_2, suffixI(" v"));
|
||||
|
||||
addInt(layout, MIDI_START, 0, MIDI_NOTE_RANGE, Version::V1_1, FORMAT_MIDI_NOTE);
|
||||
addInt(layout, MIDI_END, 127, MIDI_NOTE_RANGE, Version::V1_1, FORMAT_MIDI_NOTE);
|
||||
addInt(layout, MIDI_ROOT, 69, MIDI_NOTE_RANGE, Version::V1_3, FORMAT_MIDI_NOTE);
|
||||
addBool(layout, FOLLOW_MIDI_PITCH, true, Version::V1_3);
|
||||
|
||||
addInt(layout, FX_PERM, permToParam({ DISTORTION, CHORUS, REVERB, EQ }), { 0, 23 }, Version::V1, FORMAT_PERM_VALUE);
|
||||
addBool(layout, PRE_FX, false, Version::V1);
|
||||
|
||||
addFloat(layout, ATTACK, 1, addSkew(ENVELOPE_TIME_RANGE, 1000.f), Version::V1, suffixF(" " + TIME_UNIT, ENVELOPE_TIME_RANGE.interval));
|
||||
addFloat(layout, RELEASE, 1, addSkew(ENVELOPE_TIME_RANGE, 1000.f), Version::V1, suffixF(" " + TIME_UNIT, ENVELOPE_TIME_RANGE.interval));
|
||||
addFloat(layout, ATTACK_SHAPE, 0.f, invertProportions(NormalisableRange{ -10.f, 10.f, 0.1f }), Version::V1);
|
||||
addFloat(layout, RELEASE_SHAPE, 2.f, { -10.f, 10.f, 0.1f }, Version::V1);
|
||||
addInt(layout, CROSSFADE_SAMPLES, 1000, { 0, 50000 }, Version::V1_2);
|
||||
|
||||
addBool(layout, REVERB_ENABLED, false, Version::V1);
|
||||
addFloat(layout, REVERB_MIX, 0.5f, { 0.f, 1.f, 0.01f }, Version::V1);
|
||||
addFloat(layout, REVERB_SIZE, 0.5f, { REVERB_SIZE_RANGE, 1.f }, Version::V1);
|
||||
addFloat(layout, REVERB_DAMPING, 0.5f, { REVERB_DAMPING_RANGE, 1.f }, Version::V1);
|
||||
addFloat(layout, REVERB_LOWS, 0.5f, { REVERB_LOWS_RANGE, 0.01f }, Version::V1);
|
||||
addFloat(layout, REVERB_HIGHS, 0.5f, { REVERB_HIGHS_RANGE, 0.01f }, Version::V1);
|
||||
addFloat(layout, REVERB_PREDELAY, 0.5f, { 0.f, 500.f, 1.f, 0.5f }, Version::V1, suffixF(" " + TIME_UNIT, 0.5f));
|
||||
|
||||
addBool(layout, DISTORTION_ENABLED, false, Version::V1);
|
||||
addFloat(layout, DISTORTION_MIX, 1.f, { 0.f, 1.f, 0.01f }, Version::V1);
|
||||
addFloat(layout, DISTORTION_HIGHPASS, 0.f, { DISTORTION_HIGHPASS_RANGE, 0.01f }, Version::V1);
|
||||
addFloat(layout, DISTORTION_DENSITY, 0.f, addSkew({ DISTORTION_DENSITY_RANGE, 0.01f }, 0.f), Version::V1);
|
||||
|
||||
addBool(layout, EQ_ENABLED, false, Version::V1);
|
||||
addFloat(layout, EQ_LOW_GAIN, 0.f, addSkew(EQ_GAIN_RANGE, 0.f), Version::V1, suffixF(" " + VOLUME_UNIT, EQ_GAIN_RANGE.interval));
|
||||
addFloat(layout, EQ_MID_GAIN, 0.f, addSkew(EQ_GAIN_RANGE, 0.f), Version::V1, suffixF(" " + VOLUME_UNIT, EQ_GAIN_RANGE.interval));
|
||||
addFloat(layout, EQ_HIGH_GAIN, 0.f, addSkew(EQ_GAIN_RANGE, 0.f), Version::V1, suffixF(" " + VOLUME_UNIT, EQ_GAIN_RANGE.interval));
|
||||
addFloat(layout, EQ_LOW_FREQ, EQ_LOW_FREQ_DEFAULT, EQ_LOW_FREQ_RANGE, Version::V1, suffixF(" " + FREQUENCY_UNIT, 1.f));
|
||||
addFloat(layout, EQ_HIGH_FREQ, EQ_HIGH_FREQ_DEFAULT, EQ_HIGH_FREQ_RANGE, Version::V1, suffixF(" " + FREQUENCY_UNIT, 1.f));
|
||||
|
||||
addBool(layout, CHORUS_ENABLED, false, Version::V1);
|
||||
addFloat(layout, CHORUS_RATE, 1.f, CHORUS_RATE_RANGE, Version::V1, suffixF(" " + FREQUENCY_UNIT, 1.f));
|
||||
addFloat(layout, CHORUS_DEPTH, 0.25f, CHORUS_DEPTH_RANGE, Version::V1);
|
||||
addFloat(layout, CHORUS_FEEDBACK, 0.f, { CHORUS_FEEDBACK_RANGE, 0.01f }, Version::V1);
|
||||
addFloat(layout, CHORUS_CENTER_DELAY, 7.f, { CHORUS_CENTER_DELAY_RANGE, 1.f }, Version::V1, suffixF(" " + TIME_UNIT, 1.f));
|
||||
addFloat(layout, CHORUS_MIX, 0.5f, { 0.f, 1.f, 0.01f }, Version::V1);
|
||||
|
||||
// This is a dummy parameter to notify the host of state changes
|
||||
addBool(layout, State::UI_DUMMY_PARAM, true, Version::V1, [](bool, int) -> String { return "Dummy Param"; });
|
||||
|
||||
return layout;
|
||||
}
|
||||
} // namespace PluginParameters
|
||||
584
Source/PluginProcessor.cpp
Normal file
584
Source/PluginProcessor.cpp
Normal file
|
|
@ -0,0 +1,584 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
PluginProcessor.cpp
|
||||
Created: 5 Sep 2023
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#include "PluginProcessor.h"
|
||||
#include "PluginEditor.h"
|
||||
#include "PluginParameters.h"
|
||||
#include "Sampler/CustomSynthesizer.h"
|
||||
|
||||
#if JUCE_DEBUG
|
||||
#include "Utilities/BufferUtils.h"
|
||||
#endif
|
||||
|
||||
JustaSampleAudioProcessor::JustaSampleAudioProcessor()
|
||||
#ifndef JucePlugin_PreferredChannelConfigurations
|
||||
: AudioProcessor(BusesProperties()
|
||||
#if ! JucePlugin_IsMidiEffect
|
||||
#if ! JucePlugin_IsSynth
|
||||
.withInput("Input", juce::AudioChannelSet::stereo(), true)
|
||||
#endif
|
||||
.withOutput("Output", juce::AudioChannelSet::stereo(), true)
|
||||
#endif
|
||||
),
|
||||
apvts(*this, &undoManager, "Parameters", PluginParameters::createParameterLayout()),
|
||||
samplerSound(apvts, pluginState, sampleBuffer, int(bufferSampleRate)),
|
||||
fileFilter("", {}, {}),
|
||||
deviceRecorder(deviceManager)
|
||||
#endif
|
||||
{
|
||||
deviceRecorder.addListener(this);
|
||||
pitchDetector.addListener(this);
|
||||
|
||||
formatManager.registerBasicFormats();
|
||||
fileFilter = juce::WildcardFileFilter(formatManager.getWildcardForAllFormats(), {}, {});
|
||||
|
||||
mtsClient = MTS_RegisterClient();
|
||||
}
|
||||
|
||||
JustaSampleAudioProcessor::~JustaSampleAudioProcessor()
|
||||
{
|
||||
for (int i = synth.getNumVoices() - 1; i >= 0; i--)
|
||||
synth.removeVoiceWithoutDeleting(i);
|
||||
|
||||
MTS_DeregisterClient(mtsClient);
|
||||
}
|
||||
|
||||
#ifndef JucePlugin_PreferredChannelConfigurations
|
||||
bool JustaSampleAudioProcessor::isBusesLayoutSupported(const BusesLayout& layouts) const
|
||||
{
|
||||
#if JucePlugin_IsMidiEffect
|
||||
juce::ignoreUnused(layouts);
|
||||
return true;
|
||||
#else
|
||||
// This is the place where you check if the layout is supported.
|
||||
// In this template code we only support mono or stereo.
|
||||
// Some plugin hosts, such as certain GarageBand versions, will only
|
||||
// load plugins that support stereo bus layouts.
|
||||
if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono()
|
||||
&& layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
|
||||
return false;
|
||||
|
||||
// This checks if the input layout matches the output layout
|
||||
#if ! JucePlugin_IsSynth
|
||||
if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet())
|
||||
return false;
|
||||
#endif
|
||||
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
bool JustaSampleAudioProcessor::acceptsMidi() const
|
||||
{
|
||||
#if JucePlugin_WantsMidiInput
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool JustaSampleAudioProcessor::producesMidi() const
|
||||
{
|
||||
#if JucePlugin_ProducesMidiOutput
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool JustaSampleAudioProcessor::isMidiEffect() const
|
||||
{
|
||||
#if JucePlugin_IsMidiEffect
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
juce::AudioProcessorEditor* JustaSampleAudioProcessor::createEditor()
|
||||
{
|
||||
// A bit of a hack to set a default look and feel for the editor as it's created without sharing the object globally
|
||||
juce::LookAndFeel::setDefaultLookAndFeel(&lookAndFeel);
|
||||
auto* editor = new JustaSampleAudioProcessorEditor(*this);
|
||||
editor->setLookAndFeel(&lookAndFeel);
|
||||
juce::LookAndFeel::setDefaultLookAndFeel(nullptr);
|
||||
|
||||
return editor;
|
||||
}
|
||||
|
||||
juce::VST3ClientExtensions* JustaSampleAudioProcessor::getVST3ClientExtensions()
|
||||
{
|
||||
if (PluginParameters::REAPER_INTEGRATION_ENABLED && hostType.isReaper() && wrapperType == wrapperType_VST3)
|
||||
return &reaperExtensions;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void JustaSampleAudioProcessor::prepareToPlay(double sampleRate, int /*maximumExpectedSamplesPerBlock*/)
|
||||
{
|
||||
juce::ScopedLock lock(voiceLock);
|
||||
|
||||
synth.clearSounds();
|
||||
synth.addSound(new BlankSynthesizerSound());
|
||||
|
||||
for (int i = synth.getNumVoices() - 1; i >= 0; i--)
|
||||
synth.removeVoiceWithoutDeleting(i);
|
||||
samplerVoices.clear();
|
||||
|
||||
synth.setCurrentPlaybackSampleRate(sampleRate);
|
||||
|
||||
for (int i = 0; i < PluginParameters::MAX_VOICES; i++)
|
||||
{
|
||||
const bool initializeSample = samplerSound.sampleRate > 0 && samplerSound.sample.getNumSamples() > 0;
|
||||
auto* voice = new CustomSamplerVoice(samplerSound, mtsClient, sampleRate, getBlockSize(), initializeSample);
|
||||
samplerVoices.add(voice);
|
||||
}
|
||||
}
|
||||
|
||||
void JustaSampleAudioProcessor::releaseResources()
|
||||
{
|
||||
// When playback stops, this is a place to clean up resources
|
||||
}
|
||||
|
||||
void JustaSampleAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
|
||||
{
|
||||
juce::ScopedNoDenormals noDenormals;
|
||||
auto totalNumInputChannels = getTotalNumInputChannels();
|
||||
auto totalNumOutputChannels = getTotalNumOutputChannels();
|
||||
|
||||
for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
|
||||
buffer.clear(i, 0, buffer.getNumSamples());
|
||||
|
||||
// Handle VST3 Reaper extensions functionality
|
||||
if (PluginParameters::REAPER_INTEGRATION_ENABLED && hostType.isReaper() && wrapperType == wrapperType_VST3)
|
||||
{
|
||||
auto file = reaperExtensions.getNamedConfigParam(REAPER_FILE_PATH);
|
||||
juce::File filePath{ file };
|
||||
auto currentFile = lastLoadAttempt.isNotEmpty() ? lastLoadAttempt : juce::String(pluginState.filePath);
|
||||
juce::File currentFilePath{ currentFile };
|
||||
if (file.isNotEmpty() && filePath.getFileIdentifier() != currentFilePath.getFileIdentifier())
|
||||
{
|
||||
loadedFromReaper = true;
|
||||
loadSampleFromPath(file, true, "", false, [&](bool loaded) -> void
|
||||
{
|
||||
if (!loaded)
|
||||
loadedFromReaper = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (sampleBuffer.getNumSamples() == 0 || sampleBuffer.getNumChannels() == 0)
|
||||
return;
|
||||
|
||||
juce::ScopedTryLock lock(voiceLock);
|
||||
|
||||
if (lock.isLocked())
|
||||
{
|
||||
adjustVoiceCount();
|
||||
|
||||
synth.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples());
|
||||
|
||||
#if JUCE_DEBUG
|
||||
for (int ch = 0; ch < buffer.getNumChannels(); ch++)
|
||||
{
|
||||
protectYourEars(buffer.getWritePointer(ch), buffer.getNumSamples());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void JustaSampleAudioProcessor::adjustVoiceCount(int count)
|
||||
{
|
||||
int numVoices = juce::jmax<int>(1, p(PluginParameters::NUM_VOICES));
|
||||
if (count >= 0)
|
||||
numVoices = count;
|
||||
int currentVoices = synth.getNumVoices();
|
||||
|
||||
// We never need to instantiate new voices
|
||||
if (currentVoices > numVoices)
|
||||
{
|
||||
for (int i = currentVoices - 1; i >= numVoices; --i)
|
||||
{
|
||||
synth.removeVoiceWithoutDeleting(i);
|
||||
samplerVoices[i]->immediateHalt();
|
||||
}
|
||||
}
|
||||
|
||||
if (currentVoices < numVoices)
|
||||
{
|
||||
for (int i = currentVoices; i < numVoices; ++i)
|
||||
{
|
||||
synth.addVoice(samplerVoices[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void JustaSampleAudioProcessor::getStateInformation(juce::MemoryBlock& destData)
|
||||
{
|
||||
// All state properties should be saved at this point
|
||||
spv(PluginParameters::State::WIDTH) = pluginState.width.load();
|
||||
spv(PluginParameters::State::HEIGHT) = pluginState.height.load();
|
||||
spv(PluginParameters::State::FILE_PATH) = pluginState.filePath.load();
|
||||
spv(PluginParameters::State::SAMPLE_HASH) = pluginState.sampleHash.load();
|
||||
spv(PluginParameters::State::USING_FILE_REFERENCE) = pluginState.usingFileReference.load();
|
||||
spv(PluginParameters::State::RECENT_FILES) = pluginState.recentFiles.load();
|
||||
if (const auto stateXml = deviceManager.createStateXml())
|
||||
spv(PluginParameters::State::SAVED_DEVICE_SETTINGS) = stateXml->toString();
|
||||
spv(PluginParameters::State::UI_VIEW_START) = pluginState.viewStart.load();
|
||||
spv(PluginParameters::State::UI_VIEW_END) = pluginState.viewEnd.load();
|
||||
spv(PluginParameters::State::PIN_VIEW) = pluginState.pinView.load();
|
||||
spv(PluginParameters::State::PRIMARY_CHANNEL) = pluginState.primaryChannel.load();
|
||||
spv(PluginParameters::State::SAMPLE_START) = pluginState.sampleStart.load();
|
||||
spv(PluginParameters::State::SAMPLE_END) = pluginState.sampleEnd.load();
|
||||
spv(PluginParameters::State::LOOP_START) = pluginState.loopStart.load();
|
||||
spv(PluginParameters::State::LOOP_END) = pluginState.loopEnd.load();
|
||||
spv(PluginParameters::State::SHOW_FX) = pluginState.showFX.load();
|
||||
spv(PluginParameters::State::DARK_MODE) = pluginState.darkMode.load();
|
||||
|
||||
// Then, write empty "header" information to the stream
|
||||
size_t initialSize{ 0 };
|
||||
size_t apvtsSize{ 0 };
|
||||
{
|
||||
auto apvtsMos = juce::MemoryOutputStream{ destData, true };
|
||||
apvtsMos.writeInt(0); // apvts size
|
||||
apvtsMos.writeInt(0); // sample size
|
||||
initialSize = apvtsMos.getDataSize();
|
||||
|
||||
apvts.state.writeToStream(apvtsMos);
|
||||
apvtsSize = apvtsMos.getDataSize() - initialSize;
|
||||
}
|
||||
|
||||
size_t sampleSize = 0;
|
||||
|
||||
// If we're not using a file reference, write the sample buffer to the stream
|
||||
if (!pluginState.usingFileReference && sampleBuffer.getNumSamples())
|
||||
{
|
||||
juce::WavAudioFormat wavFormat;
|
||||
auto options = juce::AudioFormatWriterOptions{}
|
||||
.withSampleRate(bufferSampleRate)
|
||||
.withNumChannels(sampleBuffer.getNumChannels())
|
||||
.withBitsPerSample(PluginParameters::STORED_BITRATE);
|
||||
|
||||
std::unique_ptr<juce::OutputStream> stream = std::make_unique<juce::MemoryOutputStream>(destData, true);
|
||||
auto formatWriter = wavFormat.createWriterFor(stream, options);
|
||||
formatWriter->writeFromAudioSampleBuffer(sampleBuffer, 0, sampleBuffer.getNumSamples());
|
||||
formatWriter.reset();
|
||||
sampleSize = destData.getSize() - apvtsSize - initialSize;
|
||||
}
|
||||
|
||||
// Write the header
|
||||
auto headerMos = juce::MemoryOutputStream{ destData, true };
|
||||
headerMos.setPosition(0);
|
||||
headerMos.writeInt(int(apvtsSize));
|
||||
headerMos.writeInt(int(sampleSize));
|
||||
}
|
||||
|
||||
void JustaSampleAudioProcessor::setStateInformation(const void* data, int sizeInBytes)
|
||||
{
|
||||
// First, read the header information which includes the sizes of the APVTS and sample buffer
|
||||
juce::MemoryInputStream mis(data, sizeInBytes, false);
|
||||
size_t apvtsSize = mis.readInt();
|
||||
size_t sampleSize = mis.readInt();
|
||||
|
||||
if (size_t(sizeInBytes) != apvtsSize + sampleSize + 8)
|
||||
return; // format issue
|
||||
|
||||
// Read the APVTS
|
||||
juce::SubregionStream apvtsStream{ &mis, mis.getPosition(), juce::int64(apvtsSize), false };
|
||||
auto tree = juce::ValueTree::readFromStream(apvtsStream);
|
||||
if (tree.isValid())
|
||||
{
|
||||
apvts.replaceState(tree);
|
||||
|
||||
// Load the plugin state struct
|
||||
pluginState.width = sp(PluginParameters::State::WIDTH);
|
||||
pluginState.height = sp(PluginParameters::State::HEIGHT);
|
||||
pluginState.filePath = sp(PluginParameters::State::FILE_PATH);
|
||||
pluginState.usingFileReference = sp(PluginParameters::State::USING_FILE_REFERENCE);
|
||||
pluginState.pinView = sp(PluginParameters::State::PIN_VIEW);
|
||||
pluginState.primaryChannel = sp(PluginParameters::State::PRIMARY_CHANNEL);
|
||||
pluginState.showFX = sp(PluginParameters::State::SHOW_FX);
|
||||
pluginState.darkMode = sp(PluginParameters::State::DARK_MODE);
|
||||
|
||||
// We'd rather wait to update certain fields until the sample is actually loaded. This is usually irrelevant, but if the DAW
|
||||
// allows undo and redo then a previous sample will likely be loaded while the new one is loading, so the new info will not make sense.
|
||||
auto sampleHash = sp(PluginParameters::State::SAMPLE_HASH);
|
||||
auto updateFileInfo = [this, sampleHash,
|
||||
viewStart = sp(PluginParameters::State::UI_VIEW_START), viewEnd = sp(PluginParameters::State::UI_VIEW_END),
|
||||
sampleStart = sp(PluginParameters::State::SAMPLE_START), sampleEnd = sp(PluginParameters::State::SAMPLE_END),
|
||||
loopStart = sp(PluginParameters::State::LOOP_START), loopEnd = sp(PluginParameters::State::LOOP_END)]
|
||||
{
|
||||
pluginState.sampleHash = sampleHash;
|
||||
pluginState.viewStart = viewStart;
|
||||
pluginState.viewEnd = viewEnd;
|
||||
pluginState.sampleStart = sampleStart;
|
||||
pluginState.sampleEnd = sampleEnd;
|
||||
pluginState.loopStart = loopStart;
|
||||
pluginState.loopEnd = loopEnd;
|
||||
};
|
||||
|
||||
bool newFile = pluginState.sampleHash != sp(PluginParameters::State::SAMPLE_HASH).toString();
|
||||
if (!newFile)
|
||||
updateFileInfo();
|
||||
|
||||
auto recentFiles{ sp(PluginParameters::State::RECENT_FILES) };
|
||||
juce::StringArray fileArray{};
|
||||
for (int i = recentFiles.size() - 1; i >= 0; i--)
|
||||
fileArray.add(recentFiles[i].toString());
|
||||
pluginState.recentFiles = fileArray;
|
||||
|
||||
juce::XmlDocument deviceSettingsDocument{ sp(PluginParameters::State::SAVED_DEVICE_SETTINGS) };
|
||||
deviceManagerLoadedState = deviceSettingsDocument.getDocumentElement();
|
||||
auto currentState = deviceManager.createStateXml();
|
||||
deviceManagerLoaded = currentState && currentState->isEquivalentTo(&*deviceManagerLoadedState, true);
|
||||
|
||||
// Either load the sample from the file reference or from the stream directly
|
||||
juce::String filePath = pluginState.filePath;
|
||||
if (pluginState.usingFileReference && filePath.isNotEmpty() && newFile)
|
||||
{
|
||||
loadSampleFromPath(filePath, false, sampleHash, false, [this, filePath, updateFileInfo, sampleHash](bool fileLoaded) -> void
|
||||
{
|
||||
if (!fileLoaded) // Either the file was not found, loaded incorrectly, or the hash was incorrect
|
||||
{
|
||||
openFileChooser("File was not found. Please locate " + juce::File(filePath).getFileName(),
|
||||
juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectFiles, [this, updateFileInfo, sampleHash](const juce::FileChooser& chooser)
|
||||
{
|
||||
auto file = chooser.getResult();
|
||||
loadSampleFromPath(file.getFullPathName(), false, sampleHash, true,
|
||||
[updateFileInfo](bool loaded) { if (loaded) updateFileInfo(); });
|
||||
});
|
||||
}
|
||||
else updateFileInfo();
|
||||
});
|
||||
}
|
||||
else if (sampleSize && newFile)
|
||||
{
|
||||
// A copy must be made to allow reading in another thread, outside the lifetime of this function
|
||||
auto sampleData = std::make_shared<juce::MemoryBlock>(sampleSize);
|
||||
mis.read(sampleData->getData(), int(sampleSize));
|
||||
auto wavStream = new juce::MemoryInputStream(*sampleData, false);
|
||||
|
||||
juce::WavAudioFormat wavFormat;
|
||||
std::unique_ptr<juce::AudioFormatReader> wavFormatReader{ wavFormat.createReaderFor(wavStream, true) };
|
||||
if (wavFormatReader)
|
||||
{
|
||||
lastLoadAttempt = "";
|
||||
reaperExtensions.setNamedConfigParam(REAPER_FILE_PATH, "");
|
||||
sampleLoader.loadSample(std::move(wavFormatReader), [this, sampleData /* necessary capture */, updateFileInfo, sampleHash]
|
||||
(const std::unique_ptr<juce::AudioBuffer<float>>& loadedSample, const juce::String&, const std::unique_ptr<juce::AudioFormatReader>& reader) -> void
|
||||
{
|
||||
if (!reader)
|
||||
return;
|
||||
|
||||
loadSample(*loadedSample, int(reader->sampleRate), false, sampleHash);
|
||||
|
||||
updateFileInfo();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void JustaSampleAudioProcessor::loadSample(juce::AudioBuffer<float>& sample, int sampleRate, bool resetParameters, const juce::String& precomputedHash)
|
||||
{
|
||||
juce::ScopedLock lock(voiceLock);
|
||||
|
||||
haltVoices();
|
||||
|
||||
sampleBuffer = std::move(sample);
|
||||
bufferSampleRate = float(sampleRate);
|
||||
|
||||
if (precomputedHash.isNotEmpty())
|
||||
pluginState.sampleHash = precomputedHash;
|
||||
else
|
||||
pluginState.sampleHash = getSampleHash(sampleBuffer);
|
||||
|
||||
if (resetParameters)
|
||||
{
|
||||
pluginState.usingFileReference = sampleBufferNeedsReference();
|
||||
|
||||
pluginState.sampleStart = 0;
|
||||
pluginState.sampleEnd = sampleBuffer.getNumSamples() - 1;
|
||||
|
||||
// The order here is actually important, because the start and end buttons can otherwise enable the main loop button
|
||||
pv(PluginParameters::LOOPING_HAS_START) = false;
|
||||
pv(PluginParameters::LOOPING_HAS_END) = false;
|
||||
pv(PluginParameters::IS_LOOPING) = false;
|
||||
|
||||
pluginState.loopStart = 0;
|
||||
pluginState.loopEnd = sampleBuffer.getNumSamples() - 1;
|
||||
}
|
||||
|
||||
samplerSound.sampleChanged(int(bufferSampleRate));
|
||||
for (const auto& voice : samplerVoices)
|
||||
voice->initializeSample();
|
||||
}
|
||||
|
||||
void JustaSampleAudioProcessor::loadSampleFromPath(const juce::String& path, bool resetParameters, const juce::String& expectedHash, bool continueWithWrongHash, const std::function<void(bool)>& callback)
|
||||
{
|
||||
const juce::File file{ path };
|
||||
std::unique_ptr<juce::AudioFormatReader> formatReader{ formatManager.createReaderFor(file) };
|
||||
|
||||
if (!formatReader || !formatReader->lengthInSamples)
|
||||
return callback(false);
|
||||
|
||||
lastLoadAttempt = path;
|
||||
reaperExtensions.setNamedConfigParam(REAPER_FILE_PATH, "");
|
||||
|
||||
// Load the file and check the hash
|
||||
sampleLoader.loadSample(std::move(formatReader), [this, callback, path, expectedHash, continueWithWrongHash, resetParameters]
|
||||
(const std::unique_ptr<juce::AudioBuffer<float>>& loadedSample, const juce::String& sampleHash, const std::unique_ptr<juce::AudioFormatReader>& reader) -> void
|
||||
{
|
||||
if (expectedHash.isNotEmpty() && sampleHash != expectedHash && !continueWithWrongHash)
|
||||
return callback(false);
|
||||
|
||||
pluginState.filePath = path;
|
||||
loadSample(*loadedSample, int(reader->sampleRate), resetParameters || (sampleHash != expectedHash && continueWithWrongHash), sampleHash);
|
||||
|
||||
return callback(true);
|
||||
});
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool JustaSampleAudioProcessor::canLoadFileExtension(const juce::String& filePath) const
|
||||
{
|
||||
return fileFilter.isFileSuitable(filePath);
|
||||
}
|
||||
|
||||
bool JustaSampleAudioProcessor::sampleBufferNeedsReference(const juce::AudioBuffer<float>& buffer) const
|
||||
{
|
||||
double totalFileBits = double(buffer.getNumSamples()) * buffer.getNumChannels() * PluginParameters::STORED_BITRATE;
|
||||
return totalFileBits > PluginParameters::MAX_FILE_SIZE;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void JustaSampleAudioProcessor::openFileChooser(const juce::String& message, int flags, const std::function<void(const juce::FileChooser&)>& callback, bool wavOnly)
|
||||
{
|
||||
// DAW specific exceptions
|
||||
bool useNative = !hostType.isArdour();
|
||||
fileChooser = std::make_unique<juce::FileChooser>(message, juce::File::getSpecialLocation(juce::File::userDesktopDirectory), wavOnly ? "*.wav" : formatManager.getWildcardForAllFormats(), useNative);
|
||||
juce::MessageManager::callAsync([this, flags, callback] { fileChooser->launchAsync(flags, callback); });
|
||||
}
|
||||
|
||||
void JustaSampleAudioProcessor::haltVoices()
|
||||
{
|
||||
for (auto voice : samplerVoices)
|
||||
{
|
||||
voice->immediateHalt();
|
||||
}
|
||||
}
|
||||
|
||||
void JustaSampleAudioProcessor::playVoice()
|
||||
{
|
||||
synth.noteOn(0, 70, 1.0f);
|
||||
}
|
||||
|
||||
void JustaSampleAudioProcessor::initializeDeviceManager()
|
||||
{
|
||||
if (!deviceManagerLoaded)
|
||||
{
|
||||
deviceManager.initialise(2, 0, &*deviceManagerLoadedState, true);
|
||||
deviceManagerLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
void JustaSampleAudioProcessor::recordingFinished(juce::AudioBuffer<float> recordingBuffer, int recordingSampleRate)
|
||||
{
|
||||
// If the recording is too large, we prompt to save it to a file, otherwise load it into the plugin simply
|
||||
if (sampleBufferNeedsReference(recordingBuffer))
|
||||
{
|
||||
openFileChooser("Recording too large for plugin state, save to a file",
|
||||
juce::FileBrowserComponent::saveMode | juce::FileBrowserComponent::canSelectFiles,
|
||||
[this, recordingBuffer, recordingSampleRate](const juce::FileChooser& chooser) -> void {
|
||||
juce::File file = chooser.getResult();
|
||||
auto fileStream = std::make_unique<juce::FileOutputStream>(file);
|
||||
if (file.hasWriteAccess() && fileStream->openedOk())
|
||||
{
|
||||
fileStream->setPosition(0);
|
||||
fileStream->truncate();
|
||||
|
||||
juce::WavAudioFormat wavFormat;
|
||||
auto options = juce::AudioFormatWriterOptions{}
|
||||
.withSampleRate(recordingSampleRate)
|
||||
.withNumChannels(recordingBuffer.getNumChannels())
|
||||
.withBitsPerSample(PluginParameters::STORED_BITRATE);
|
||||
|
||||
std::unique_ptr<juce::OutputStream> outputStream = std::move(fileStream);
|
||||
auto formatWriter = wavFormat.createWriterFor(outputStream, options);
|
||||
formatWriter->writeFromAudioSampleBuffer(recordingBuffer, 0, recordingBuffer.getNumSamples());
|
||||
outputStream.release();
|
||||
|
||||
loadSampleFromPath(file.getFullPathName(), true);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
pluginState.filePath = "";
|
||||
lastLoadAttempt = "";
|
||||
reaperExtensions.setNamedConfigParam(REAPER_FILE_PATH, "");
|
||||
loadSample(recordingBuffer, recordingSampleRate, true);
|
||||
}
|
||||
}
|
||||
|
||||
bool JustaSampleAudioProcessor::startPitchDetectionRoutine(int startSample, int endSample)
|
||||
{
|
||||
if (!sampleBuffer.getNumSamples())
|
||||
return false;
|
||||
|
||||
// Truth be told, this does not need to be in a separate thread, but the algorithm I was using before was much slower
|
||||
pitchDetector.setData(sampleBuffer, startSample, endSample, bufferSampleRate);
|
||||
pitchDetector.startThread();
|
||||
return true;
|
||||
}
|
||||
|
||||
void JustaSampleAudioProcessor::exitSignalSent()
|
||||
{
|
||||
double pitch = pitchDetector.getPitch();
|
||||
if (pitch > 0)
|
||||
{
|
||||
float a4_hz = p(PluginParameters::A4_HZ);
|
||||
if (a4_hz <= 0)
|
||||
a4_hz = 440.0f;
|
||||
|
||||
// Large pitch adjustments result in worse quality, so we limit to +/- 6 semitones for this feature
|
||||
float tuningAmount = 12.f * std::log2(a4_hz / pitch);
|
||||
float rangeMin = -6.f;
|
||||
float rangeMax = 6.f;
|
||||
float range = rangeMax - rangeMin;
|
||||
tuningAmount = std::fmod(tuningAmount - rangeMin, range);
|
||||
if (tuningAmount < 0)
|
||||
tuningAmount += range;
|
||||
tuningAmount += rangeMin;
|
||||
|
||||
int semitones = int(tuningAmount);
|
||||
int cents = int(100 * (tuningAmount - semitones));
|
||||
|
||||
apvts.getParameterAsValue(PluginParameters::SEMITONE_TUNING) = semitones;
|
||||
apvts.getParameterAsValue(PluginParameters::CENT_TUNING) = cents;
|
||||
apvts.getParameterAsValue(PluginParameters::WIDE_TUNING) = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
int JustaSampleAudioProcessor::visibleSamples() const
|
||||
{
|
||||
return pluginState.viewEnd.load() - pluginState.viewStart.load() + 1;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// This creates new instances of the plugin...
|
||||
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
|
||||
{
|
||||
return new JustaSampleAudioProcessor();
|
||||
}
|
||||
196
Source/PluginProcessor.h
Normal file
196
Source/PluginProcessor.h
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
PluginProcessor.h
|
||||
Created: 5 Sep 2023
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
|
||||
Welcome to the Just a Sample source code! This is my first plugin, and I am
|
||||
excited to share it as a free, open-source project. I hope this can serve
|
||||
as a learning tool for other programmers and a useful plugin for musicians.
|
||||
|
||||
Even a "simple" plugin like this one requires some architecture considerations.
|
||||
Care was taken to make PluginProcessor self-contained with a clear API exposed
|
||||
to the PluginEditor.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
|
||||
#include "CustomLookAndFeel.h"
|
||||
#include "Sampler/CustomSamplerVoice.h"
|
||||
#include "Sampler/CustomSynthesizer.h"
|
||||
#include "Utilities/PitchDetector.h"
|
||||
#include "Utilities/DeviceRecorder.h"
|
||||
#include "Utilities/Reaper/ReaperVST3Extensions.h"
|
||||
#include "Utilities/SampleLoader.h"
|
||||
#include <libMTSClient.h>
|
||||
|
||||
class JustaSampleAudioProcessor final : public juce::AudioProcessor, public juce::Thread::Listener, public DeviceRecorderListener
|
||||
#if JucePlugin_Enable_ARA
|
||||
, public juce::AudioProcessorARAExtension
|
||||
#endif
|
||||
{
|
||||
public:
|
||||
JustaSampleAudioProcessor();
|
||||
~JustaSampleAudioProcessor() override;
|
||||
|
||||
/** Returns whether the sample buffer is too large to be stored in the plugin data */
|
||||
bool sampleBufferNeedsReference() const { return sampleBufferNeedsReference(sampleBuffer); }
|
||||
bool sampleBufferNeedsReference(const juce::AudioBuffer<float>& buffer) const;
|
||||
|
||||
/** Whether the processor can handle a filePath's extension */
|
||||
bool canLoadFileExtension(const juce::String& filePath) const;
|
||||
|
||||
//==============================================================================
|
||||
/** Asynchronously loads an audio buffer from a file path, returning (in a callback) whether the sample
|
||||
was loaded successfully. If an expected hash is provided and continueWithWrongHash = false, then the
|
||||
sample will only be loaded if the hash matches. If continueWithWrongHash = true, then the sample will
|
||||
be loaded and parameters will be reset.
|
||||
*/
|
||||
void loadSampleFromPath(const juce::String& path, bool resetParameters = true, const juce::String& expectedHash = "", bool continueWithWrongHash = false,
|
||||
const std::function<void(bool loadedSuccessfully)>& callback = [](bool) -> void {});
|
||||
|
||||
/** Open a file chooser */
|
||||
void openFileChooser(const juce::String& message, int flags, const std::function<void(const juce::FileChooser&)>& callback, bool wavOnly = false);
|
||||
|
||||
/** Detects the pitch of the sample between the supplied bounds and adjusts the tuning parameters accordingly */
|
||||
bool startPitchDetectionRoutine(int startSample, int endSample);
|
||||
|
||||
/** Stop all the voices from playing */
|
||||
void haltVoices();
|
||||
|
||||
/** Plays an A5 note */
|
||||
void playVoice();
|
||||
|
||||
//==============================================================================
|
||||
const juce::AudioBuffer<float>& getSampleBuffer() const { return sampleBuffer; }
|
||||
float getBufferSampleRate() const { return bufferSampleRate; }
|
||||
const juce::OwnedArray<CustomSamplerVoice>& getSamplerVoices() const { return samplerVoices; }
|
||||
|
||||
/** The APVTS is the central object storing plugin state and audio processing parameters. See PluginParameters.h. */
|
||||
juce::AudioProcessorValueTreeState& APVTS() { return apvts; }
|
||||
juce::UndoManager& getUndoManager() { return undoManager; }
|
||||
/** The plugin state object stores non-parameter data, to be used in a thread-safe way */
|
||||
PluginParameters::State& getPluginState() { return pluginState; }
|
||||
|
||||
DeviceRecorder& getRecorder() { return deviceRecorder; }
|
||||
juce::AudioDeviceManager& getDeviceManager() { return deviceManager; }
|
||||
/** We initialize the device manager lazily from the UI thread, so that we only block the plugin when necessary. Call this
|
||||
from the Editor when you need the device manager configured appropriately.
|
||||
*/
|
||||
void initializeDeviceManager();
|
||||
|
||||
juce::String getWildcardFilter() const { return formatManager.getWildcardForAllFormats(); }
|
||||
const SampleLoader& getSampleLoader() const { return sampleLoader; }
|
||||
|
||||
/** Use to determine whether the editor should reset UI parameters */
|
||||
bool hasLoadedFromReaper() const { return loadedFromReaper.load(); }
|
||||
void setLoadedFromReaper(const bool loaded) { loadedFromReaper.store(loaded); }
|
||||
|
||||
//==============================================================================
|
||||
juce::var p(const juce::Identifier& identifier) const { return apvts.getParameterAsValue(identifier).getValue(); }
|
||||
juce::Value pv(const juce::Identifier& identifier) const { return apvts.getParameterAsValue(identifier); }
|
||||
|
||||
private:
|
||||
const juce::var& sp(const juce::Identifier& identifier) const { return apvts.state.getProperty(identifier); }
|
||||
juce::Value spv(const juce::Identifier& identifier) { return apvts.state.getPropertyAsValue(identifier, apvts.undoManager); }
|
||||
|
||||
//==============================================================================
|
||||
#ifndef JucePlugin_PreferredChannelConfigurations
|
||||
bool isBusesLayoutSupported(const juce::AudioProcessor::BusesLayout& layouts) const override;
|
||||
#endif
|
||||
const juce::String getName() const override { return JucePlugin_Name; }
|
||||
bool acceptsMidi() const override;
|
||||
bool producesMidi() const override;
|
||||
bool isMidiEffect() const override;
|
||||
double getTailLengthSeconds() const override { return 0.0; }
|
||||
int getNumPrograms() override { return 1; }
|
||||
int getCurrentProgram() override { return 0; }
|
||||
void setCurrentProgram(int) override {}
|
||||
const juce::String getProgramName(int) override { return "Program"; }
|
||||
void changeProgramName(int, const juce::String&) override {}
|
||||
bool hasEditor() const override { return true; }
|
||||
juce::AudioProcessorEditor* createEditor() override;
|
||||
juce::VST3ClientExtensions* getVST3ClientExtensions() override;
|
||||
|
||||
//==============================================================================
|
||||
void prepareToPlay(double sampleRate, int maximumExpectedSamplesPerBlock) override;
|
||||
void releaseResources() override;
|
||||
void processBlock(juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
|
||||
|
||||
/** Add or subtract voices if necessary */
|
||||
void adjustVoiceCount(int count = -1);
|
||||
|
||||
//==============================================================================
|
||||
/** The plugin's state information includes the full APVTS (with non-parameter values) and audio data if a file
|
||||
reference is not being used.
|
||||
*/
|
||||
void getStateInformation(juce::MemoryBlock& destData) override;
|
||||
void setStateInformation(const void* data, int sizeInBytes) override;
|
||||
|
||||
//==============================================================================
|
||||
/** Loads an audio buffer into the synth, preparing everything for playback. If
|
||||
resetParameters is true, the plugin's parameters are reset to default, matching
|
||||
the new sample. Otherwise, the assumption is that the parameters are in a valid state.
|
||||
Note that this method uses std::move on the sample.
|
||||
Also, if necessary, set pluginState.filePath before calling this method, so that the editor syncs correctly.
|
||||
*/
|
||||
void loadSample(juce::AudioBuffer<float>& sample, int sampleRate, bool resetParameters = true, const juce::String& precomputedHash = "");
|
||||
|
||||
//==============================================================================
|
||||
void recordingStarted() override {}
|
||||
void recordingFinished(juce::AudioBuffer<float> recordingBuffer, int recordingSampleRate) override;
|
||||
|
||||
/** This runs when the pitch detection thread finishes. */
|
||||
void exitSignalSent() override;
|
||||
|
||||
//==============================================================================
|
||||
int visibleSamples() const;
|
||||
|
||||
//==============================================================================
|
||||
juce::AudioProcessorValueTreeState apvts;
|
||||
juce::UndoManager undoManager;
|
||||
PluginParameters::State pluginState;
|
||||
|
||||
CustomSynthesizer synth;
|
||||
|
||||
/** Note that this is referenced directly by the Editor. As such, it should only be modified in the Message Thread. */
|
||||
juce::AudioBuffer<float> sampleBuffer;
|
||||
float bufferSampleRate{ 0.f };
|
||||
SamplerParameters samplerSound;
|
||||
/** We manage MAX_VOICES for the duration of the plugin and control how many the synth has access to */
|
||||
juce::OwnedArray<CustomSamplerVoice> samplerVoices;
|
||||
juce::CriticalSection voiceLock;
|
||||
|
||||
juce::PluginHostType hostType;
|
||||
std::unique_ptr<juce::FileChooser> fileChooser;
|
||||
juce::AudioFormatManager formatManager;
|
||||
juce::WildcardFileFilter fileFilter;
|
||||
|
||||
SampleLoader sampleLoader;
|
||||
juce::String lastLoadAttempt;
|
||||
/** We use this to notify the editor that the sample was loaded from Reaper, since this should be treated as a user load */
|
||||
std::atomic<bool> loadedFromReaper{ false };
|
||||
|
||||
juce::AudioDeviceManager deviceManager; // Spent an hour debugging because I put this after the DeviceRecorder, and it crashes without a trace. C++ is fun!
|
||||
std::unique_ptr<juce::XmlElement> deviceManagerLoadedState;
|
||||
bool deviceManagerLoaded{ false };
|
||||
DeviceRecorder deviceRecorder;
|
||||
|
||||
PitchDetector pitchDetector;
|
||||
|
||||
CustomLookAndFeel lookAndFeel;
|
||||
|
||||
ReaperVST3Extensions reaperExtensions;
|
||||
const juce::String REAPER_FILE_PATH{ "P_EXT:FILE" };
|
||||
|
||||
MTSClient* mtsClient{ nullptr };
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(JustaSampleAudioProcessor)
|
||||
};
|
||||
635
Source/Sampler/CustomSamplerVoice.cpp
Normal file
635
Source/Sampler/CustomSamplerVoice.cpp
Normal file
|
|
@ -0,0 +1,635 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
CustomSamplerVoice.cpp
|
||||
Created: 5 Sep 2023 3:35:03pm
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#include "CustomSamplerVoice.h"
|
||||
|
||||
#include "../Utilities/BufferUtils.h"
|
||||
#include "Effects/BandEQ.h"
|
||||
#include "Effects/Chorus.h"
|
||||
#include "Effects/Distortion.h"
|
||||
#include "Effects/Reverb.h"
|
||||
|
||||
CustomSamplerVoice::CustomSamplerVoice(const SamplerParameters& samplerSound, MTSClient* client, double applicationSampleRate, int expectedBlockSize, bool initSample) :
|
||||
expectedBlockSize(expectedBlockSize), sampleSound(samplerSound),
|
||||
mainStretcher(samplerSound.sample, samplerSound.sampleRate),
|
||||
loopStretcher(samplerSound.sample, samplerSound.sampleRate),
|
||||
endStretcher(samplerSound.sample, samplerSound.sampleRate),
|
||||
mtsClient(client)
|
||||
{
|
||||
SynthesiserVoice::setCurrentPlaybackSampleRate(applicationSampleRate);
|
||||
|
||||
if (expectedBlockSize <= 0)
|
||||
this->expectedBlockSize = 512; // In case a DAW reports this incorrectly at the time of prepareToPlay
|
||||
|
||||
if (initSample)
|
||||
initializeSample();
|
||||
}
|
||||
|
||||
void CustomSamplerVoice::initializeSample()
|
||||
{
|
||||
if (sampleSound.sample.getNumChannels() <= 0)
|
||||
return;
|
||||
|
||||
mainStretcher = BungeeStretcher(sampleSound.sample, sampleSound.sampleRate);
|
||||
loopStretcher = BungeeStretcher(sampleSound.sample, sampleSound.sampleRate);
|
||||
endStretcher = BungeeStretcher(sampleSound.sample, sampleSound.sampleRate);
|
||||
|
||||
const int sampleRate = int(getSampleRate());
|
||||
if (sampleRate > 0)
|
||||
{
|
||||
mainStretcher.preallocateStretcher(sampleRate);
|
||||
loopStretcher.preallocateStretcher(sampleRate);
|
||||
endStretcher.preallocateStretcher(sampleRate);
|
||||
}
|
||||
|
||||
tempOutputBuffer.setSize(sampleSound.sample.getNumChannels(), expectedBlockSize * 2);
|
||||
envelopeBuffer.setSize(sampleSound.sample.getNumChannels(), expectedBlockSize * 2);
|
||||
|
||||
tailOffBuffer.setSize(2, TAIL_OFF, false, true);
|
||||
mainStretcherBuffer.setSize(sampleSound.sample.getNumChannels(), expectedBlockSize * 2);
|
||||
loopStretcherBuffer.setSize(sampleSound.sample.getNumChannels() - 1, expectedBlockSize * 2);
|
||||
endStretcherBuffer.setSize(sampleSound.sample.getNumChannels() - 1, expectedBlockSize * 2);
|
||||
|
||||
mainLowpass.clear();
|
||||
loopLowpass.clear();
|
||||
endLowpass.clear();
|
||||
for (int i = 0; i < sampleSound.sample.getNumChannels(); i++)
|
||||
{
|
||||
mainLowpass.emplace_back(std::make_unique<LowpassStream>(2 * LANCZOS_WINDOW_SIZE));
|
||||
loopLowpass.emplace_back(std::make_unique<LowpassStream>(2 * LANCZOS_WINDOW_SIZE));
|
||||
endLowpass.emplace_back(std::make_unique<LowpassStream>(2 * LANCZOS_WINDOW_SIZE));
|
||||
}
|
||||
}
|
||||
|
||||
void CustomSamplerVoice::startNote(int midiNoteNumber, float velocity, juce::SynthesiserSound* sound, int currentPitchWheelPosition)
|
||||
{
|
||||
if (midiNoteNumber < sampleSound.midiStart->get() || midiNoteNumber > sampleSound.midiEnd->get() || MTS_ShouldFilterNote(mtsClient, char(midiNoteNumber), -1))
|
||||
return;
|
||||
|
||||
if (!sampleSound.disableVelocity->get())
|
||||
noteVelocity = velocity;
|
||||
else
|
||||
noteVelocity = 1.f;
|
||||
|
||||
if (sound)
|
||||
{
|
||||
sampleRateConversion = float(sampleSound.sampleRate / getSampleRate());
|
||||
|
||||
sampleStart = sampleSound.sampleStart; // Note this implicitly loads the atomic value
|
||||
sampleEnd = sampleSound.sampleEnd;
|
||||
|
||||
wavetableMode = isWavetableModeAvailable(float(sampleSound.sampleRate), sampleStart, sampleEnd) && !sampleSound.disableWavetableMode->get();
|
||||
|
||||
playbackMode = wavetableMode ? PluginParameters::BASIC : sampleSound.getPlaybackMode();
|
||||
|
||||
playUntilEnd = sampleSound.playUntilEnd->get();
|
||||
isLooping = sampleSound.isLooping->get() || wavetableMode;
|
||||
loopingHasStart = isLooping && sampleSound.loopingHasStart->get() && sampleSound.loopStart < sampleSound.sampleStart && !wavetableMode;
|
||||
loopStart = sampleSound.loopStart;
|
||||
loopingHasEnd = isLooping && sampleSound.loopingHasEnd->get() && sampleSound.loopEnd > sampleSound.sampleEnd && !wavetableMode;
|
||||
loopEnd = sampleSound.loopEnd;
|
||||
|
||||
effectiveStart = loopingHasStart ? loopStart : sampleStart;
|
||||
effectiveEnd = loopingHasEnd ? loopEnd : sampleEnd;
|
||||
|
||||
// While release can be applied before or after the FX, a minimum number of attack smoothing always needs to be applied to the sample before FX
|
||||
attackSmoothing = sampleSound.attack->get() * float(getSampleRate()) / 1000.f;
|
||||
releaseSmoothing = sampleSound.release->get() * float(getSampleRate()) / 1000.f;
|
||||
attackShape = sampleSound.attackShape->get();
|
||||
releaseShape = sampleSound.releaseShape->get();
|
||||
if (loopingHasEnd) // Keep release smoothing within end portion
|
||||
releaseSmoothing = juce::jmin<float>(releaseSmoothing, float(loopEnd - sampleEnd));
|
||||
crossfade = juce::jmin<float>(float(sampleSound.crossfadeSamples->get()), (sampleEnd - sampleStart + 1) / 2.f + 1);
|
||||
|
||||
vc = VoiceContext();
|
||||
midiReleased = false;
|
||||
|
||||
doLowpass = false;
|
||||
vc.currentPosition = effectiveStart;
|
||||
updateSpeedAndPitch(midiNoteNumber, currentPitchWheelPosition);
|
||||
|
||||
if (playbackMode == PluginParameters::BUNGEE)
|
||||
mainStretcher.initialize(effectiveStart, tuning, speedFactor);
|
||||
|
||||
effects.clear();
|
||||
initializeFx();
|
||||
for (auto& effect : effects)
|
||||
{
|
||||
effect.fx->initialize(sampleSound.sample.getNumChannels(), int(getSampleRate()));
|
||||
effect.fx->updateParams(sampleSound);
|
||||
}
|
||||
|
||||
updateFXParamsTimer = 0;
|
||||
|
||||
// Set the initial state (vc.currentPosition is set before updateSpeedAndPitch)
|
||||
vc.state = PLAYING;
|
||||
vc.isSmoothingAttack = attackSmoothing > 0;
|
||||
}
|
||||
}
|
||||
|
||||
void CustomSamplerVoice::stopNote(float /*velocity*/, bool allowTailOff)
|
||||
{
|
||||
if (allowTailOff)
|
||||
{
|
||||
if (!playUntilEnd || isLooping)
|
||||
midiReleased = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We render a quick tail-off to avoid clicks
|
||||
juce::AudioBuffer<float> temp{ tailOffBuffer.getNumChannels(), tailOffBuffer.getNumSamples() };
|
||||
temp.clear();
|
||||
renderNextBlock(temp, 0, TAIL_OFF);
|
||||
tailOffBuffer = temp;
|
||||
tailOff = 0;
|
||||
|
||||
vc.state = STOPPED;
|
||||
clearCurrentNote();
|
||||
}
|
||||
}
|
||||
|
||||
void CustomSamplerVoice::immediateHalt()
|
||||
{
|
||||
vc.state = STOPPED;
|
||||
clearCurrentNote();
|
||||
}
|
||||
|
||||
void CustomSamplerVoice::pitchWheelMoved(int newPitchWheelValue)
|
||||
{
|
||||
updateSpeedAndPitch(getCurrentlyPlayingNote(), newPitchWheelValue);
|
||||
}
|
||||
|
||||
void CustomSamplerVoice::updateSpeedAndPitch(int currentNote, int pitchWheelPosition)
|
||||
{
|
||||
pitchWheel = pitchWheelPosition;
|
||||
|
||||
// Account for tuning adjustments
|
||||
float tuningRatio = 1.f;
|
||||
|
||||
if (playbackMode == PluginParameters::BASIC && wavetableMode)
|
||||
tuningRatio *= std::pow(2.f, (sampleSound.waveformSemitoneTuning->get() + sampleSound.waveformCentTuning->get() / 100.f) / 12.f);
|
||||
else
|
||||
tuningRatio *= std::pow(2.f, (sampleSound.semitoneTuning->get() + sampleSound.centTuning->get() / 100.f) / 12.f);
|
||||
|
||||
float wheelRange = sampleSound.pitchWheelRange->get();
|
||||
tuningRatio *= std::pow(2.f, juce::jmap<float>(float(pitchWheelPosition), 0.f, 16383.f, -wheelRange, wheelRange) / 12.f);
|
||||
|
||||
tuningRatio *= std::pow(2.f, sampleSound.wideTuningControl->get() / 12.f);
|
||||
|
||||
// Account for MIDI note
|
||||
if (sampleSound.followMidiPitch->get())
|
||||
{
|
||||
int rootNote = sampleSound.midiRoot->get();
|
||||
tuningRatio *= std::pow(2.f, (currentNote - rootNote) / 12.f);
|
||||
tuningRatio *= float(MTS_RetuningAsRatio(mtsClient, char(currentNote), -1));
|
||||
}
|
||||
|
||||
tuning = tuningRatio;
|
||||
speedFactor = sampleSound.speedFactor->get();
|
||||
speedFactor *= 1.f + (tuning - 1.f) * sampleSound.octaveSpeedFactor->get();
|
||||
|
||||
if (playbackMode == PluginParameters::BASIC)
|
||||
{
|
||||
// In wavetable mode, the sample is treated as a single cycle
|
||||
// Otherwise, the sample is simply sped up according to the tuning
|
||||
float a4_hz = sampleSound.a4_freq->get();
|
||||
speed = wavetableMode ? (tuning * a4_hz) * (sampleEnd - sampleStart + 1 - crossfade) / float(getSampleRate()) / 2.f
|
||||
: tuning * sampleRateConversion;
|
||||
|
||||
// Configure the filters
|
||||
auto frequency = sampleSound.sampleRate / 2.f / speed;
|
||||
auto filterLimit = sampleSound.sampleRate / 2.f - 10.f; // We've run into some issues when the filter is too close to the Nyquist frequency
|
||||
|
||||
bool wasLowpass = doLowpass;
|
||||
doLowpass = speed > 1.f && frequency < filterLimit;
|
||||
|
||||
if (!wasLowpass && doLowpass)
|
||||
{
|
||||
for (int ch = 0; ch < sampleSound.sample.getNumChannels(); ch++)
|
||||
mainLowpass[ch]->resetProcessing(int(vc.currentPosition));
|
||||
}
|
||||
|
||||
if (doLowpass)
|
||||
{
|
||||
for (int ch = 0; ch < sampleSound.sample.getNumChannels(); ch++)
|
||||
{
|
||||
mainLowpass[ch]->setCoefficients(sampleSound.sampleRate, frequency);
|
||||
loopLowpass[ch]->setCoefficients(sampleSound.sampleRate, frequency);
|
||||
endLowpass[ch]->setCoefficients(sampleSound.sampleRate, frequency);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update the stretchers
|
||||
mainStretcher.setPitchAndSpeed(tuning, speedFactor);
|
||||
loopStretcher.setPitchAndSpeed(tuning, speedFactor);
|
||||
endStretcher.setPitchAndSpeed(tuning, speedFactor);
|
||||
|
||||
speed = speedFactor * sampleRateConversion;
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void CustomSamplerVoice::renderNextBlock(juce::AudioBuffer<float>& outputBuffer, int startSample, int numSamples)
|
||||
{
|
||||
if (vc.state == STOPPED && (!doFxTailOff || !getCurrentlyPlayingSound()))
|
||||
{
|
||||
clearCurrentNote();
|
||||
return;
|
||||
}
|
||||
|
||||
// These resizes will happen rarely, if at all
|
||||
if (tempOutputBuffer.getNumSamples() < numSamples)
|
||||
{
|
||||
tempOutputBuffer.setSize(tempOutputBuffer.getNumChannels(), numSamples);
|
||||
envelopeBuffer.setSize(envelopeBuffer.getNumChannels(), numSamples);
|
||||
}
|
||||
tempOutputBuffer.clear();
|
||||
envelopeBuffer.clear();
|
||||
|
||||
if (playbackMode == PluginParameters::BUNGEE && loopStretcherBuffer.getNumSamples() < numSamples)
|
||||
{
|
||||
mainStretcherBuffer.setSize(mainStretcherBuffer.getNumChannels(), numSamples);
|
||||
loopStretcherBuffer.setSize(loopStretcherBuffer.getNumChannels(), numSamples);
|
||||
endStretcherBuffer.setSize(endStretcherBuffer.getNumChannels(), numSamples);
|
||||
}
|
||||
|
||||
updateSpeedAndPitch(getCurrentlyPlayingNote(), pitchWheel);
|
||||
|
||||
bool someFXEnabled{ false };
|
||||
for (const auto& effect : effects)
|
||||
{
|
||||
someFXEnabled = someFXEnabled || effect.enablementSource->get();
|
||||
}
|
||||
|
||||
// Main processing loop
|
||||
VoiceContext con;
|
||||
for (auto ch = 0; ch < sampleSound.sample.getNumChannels(); ch++)
|
||||
{
|
||||
// This struct is used to easily process channel by channel
|
||||
con = vc;
|
||||
for (auto i = 0; i < numSamples; i++)
|
||||
{
|
||||
if (con.state == STOPPED)
|
||||
{
|
||||
con.samplesSinceStopped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fetch the sample according to the playback mode
|
||||
float sample = playbackMode == PluginParameters::BASIC ?
|
||||
fetchSample(ch, con.currentPosition, mainLowpass) :
|
||||
nextSample(ch, &mainStretcher, mainStretcherBuffer, i);
|
||||
|
||||
// Crossfading
|
||||
if (con.isCrossfadingLoop)
|
||||
{
|
||||
double crossfadePosition = con.currentPosition - sampleStart;
|
||||
if (crossfadePosition >= crossfade)
|
||||
{
|
||||
con.isCrossfadingLoop = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Power preserving crossfade (https://www.youtube.com/watch?v=-5cB3rec2T0)
|
||||
float crossfadeIncrease = float(std::sqrt(0.5f + 0.5f * std::cos(juce::MathConstants<float>::pi * crossfadePosition / crossfade + juce::MathConstants<float>::pi)));
|
||||
float crossfadeDecrease = float(std::sqrt(0.5f + 0.5f * std::cos(juce::MathConstants<float>::pi * crossfadePosition / crossfade)));
|
||||
float next = playbackMode == PluginParameters::BASIC ?
|
||||
fetchSample(ch, con.currentPosition + sampleEnd - sampleStart - crossfade, loopLowpass) :
|
||||
nextSample(ch, &loopStretcher, loopStretcherBuffer, i);
|
||||
sample = sample * crossfadeIncrease + next * crossfadeDecrease;
|
||||
}
|
||||
}
|
||||
|
||||
if (con.isCrossfadingEnd)
|
||||
{
|
||||
double crossfadePosition = con.currentPosition - sampleEnd;
|
||||
if (crossfadePosition >= crossfade)
|
||||
{
|
||||
con.isCrossfadingEnd = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
float crossfadeIncrease = float(std::sqrt(0.5f + 0.5f * std::cos(juce::MathConstants<float>::pi * crossfadePosition / crossfade + juce::MathConstants<float>::pi)));
|
||||
float crossfadeDecrease = float(std::sqrt(0.5f + 0.5f * std::cos(juce::MathConstants<float>::pi * crossfadePosition / crossfade)));
|
||||
float next = playbackMode == PluginParameters::BASIC ?
|
||||
fetchSample(ch, con.crossfadeEndPosition, endLowpass) :
|
||||
nextSample(ch, &endStretcher, endStretcherBuffer, i);
|
||||
sample = sample * crossfadeIncrease + next * crossfadeDecrease;
|
||||
con.crossfadeEndPosition += speed;
|
||||
}
|
||||
}
|
||||
|
||||
// Attack and release envelopes
|
||||
envelopeBuffer.setSample(ch, i, 1.f);
|
||||
|
||||
if (con.isSmoothingAttack)
|
||||
{
|
||||
if (con.speedMovedSinceStart >= attackSmoothing)
|
||||
con.isSmoothingAttack = false;
|
||||
else
|
||||
envelopeBuffer.setSample(ch, i, exponentialCurve(attackShape, con.speedMovedSinceStart / attackSmoothing));
|
||||
|
||||
}
|
||||
|
||||
if (con.isReleasing)
|
||||
envelopeBuffer.setSample(ch, i, envelopeBuffer.getSample(ch, i) * exponentialCurve(releaseShape, 1 - con.speedMovedSinceRelease / releaseSmoothing));
|
||||
|
||||
// Update the position
|
||||
con.currentPosition += speed;
|
||||
con.speedMovedSinceStart += 1;
|
||||
if (con.isReleasing)
|
||||
con.speedMovedSinceRelease += 1;
|
||||
|
||||
// Handle transitions
|
||||
if (con.state == PLAYING && isLooping && con.currentPosition >= sampleEnd - crossfade) // Loop crossfade
|
||||
{
|
||||
con.currentPosition -= (sampleEnd - sampleStart + 1) - crossfade;
|
||||
con.isCrossfadingLoop = true;
|
||||
|
||||
if (playbackMode == PluginParameters::BUNGEE && ch == 0)
|
||||
{
|
||||
std::swap(mainStretcher, loopStretcher);
|
||||
mainStretcher.initialize(con.currentPosition, tuning, speedFactor); // This could also be done at note start
|
||||
}
|
||||
else
|
||||
{
|
||||
std::swap(mainLowpass[ch], loopLowpass[ch]);
|
||||
mainLowpass[ch]->resetProcessing(int(con.currentPosition));
|
||||
}
|
||||
}
|
||||
|
||||
if (midiReleased && !con.isReleasing && con.state == PLAYING) // Midi release, end crossfade
|
||||
{
|
||||
if (loopingHasEnd)
|
||||
{
|
||||
con.crossfadeEndPosition = con.currentPosition;
|
||||
con.currentPosition = sampleEnd + 1;
|
||||
con.state = PLAYING_END;
|
||||
con.isCrossfadingEnd = true;
|
||||
|
||||
if (playbackMode == PluginParameters::BUNGEE && ch == 0)
|
||||
{
|
||||
std::swap(mainStretcher, endStretcher);
|
||||
mainStretcher.initialize(con.currentPosition, tuning, speedFactor); // This could also be done at note start
|
||||
}
|
||||
else
|
||||
{
|
||||
std::swap(mainLowpass[ch], endLowpass[ch]);
|
||||
mainLowpass[ch]->resetProcessing(int(con.currentPosition));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
con.isReleasing = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (((con.state == PLAYING && !isLooping) || con.state == PLAYING_END) && !con.isReleasing &&
|
||||
con.currentPosition > effectiveEnd - releaseSmoothing * speed) // Release smoothing
|
||||
{
|
||||
con.isReleasing = true;
|
||||
}
|
||||
|
||||
if (con.currentPosition > effectiveEnd || (con.isReleasing && con.speedMovedSinceRelease >= releaseSmoothing)) // End of playback reached
|
||||
con.state = STOPPED;
|
||||
|
||||
// Scale with standard velocity curve
|
||||
sample *= juce::Decibels::decibelsToGain(40 * log10(noteVelocity));
|
||||
sample *= juce::Decibels::decibelsToGain(float(sampleSound.gain->get()));
|
||||
|
||||
tempOutputBuffer.setSample(ch, i, sample);
|
||||
}
|
||||
}
|
||||
vc = con;
|
||||
|
||||
// Check for updated FX order
|
||||
if (updateFXParamsTimer == UPDATE_PARAMS_LENGTH)
|
||||
initializeFx();
|
||||
|
||||
// Apply envelope here or after FX if PRE_FX is enabled
|
||||
if (!sampleSound.applyFXPre->get())
|
||||
for (int ch = 0; ch < tempOutputBuffer.getNumChannels(); ch++)
|
||||
juce::FloatVectorOperations::multiply(tempOutputBuffer.getWritePointer(ch), tempOutputBuffer.getReadPointer(ch), envelopeBuffer.getReadPointer(ch), numSamples);
|
||||
|
||||
// Apply FX
|
||||
int reverbSampleDelay = int(1000.f + sampleSound.reverbPredelay->get() * float(getSampleRate()) / 1000.f); // the 1000.f is approximate
|
||||
someFXEnabled = false;
|
||||
for (auto& effect : effects)
|
||||
{
|
||||
// Check for updated enablement
|
||||
bool enablement = effect.enablementSource->get();
|
||||
if (!effect.enabled && enablement)
|
||||
{
|
||||
effect.fx->initialize(sampleSound.sample.getNumChannels(), int(getSampleRate()));
|
||||
effect.fx->updateParams(sampleSound, false);
|
||||
}
|
||||
effect.enabled = enablement;
|
||||
someFXEnabled = someFXEnabled || effect.enabled;
|
||||
|
||||
if (effect.enabled && !effect.locallyDisabled)
|
||||
{
|
||||
// Update params every UPDATE_PARAMS_LENGTH calls to process
|
||||
if (updateFXParamsTimer == UPDATE_PARAMS_LENGTH)
|
||||
effect.fx->updateParams(sampleSound, true);
|
||||
effect.fx->process(tempOutputBuffer, numSamples);
|
||||
|
||||
// Check if an effect should be locally disabled. Note that reverb can only be disabled after a certain delay
|
||||
if (con.state == STOPPED && numSamples > 10 && !(effect.fxType == PluginParameters::REVERB && con.samplesSinceStopped <= reverbSampleDelay))
|
||||
{
|
||||
bool disable{ true };
|
||||
for (int ch = 0; ch < tempOutputBuffer.getNumChannels(); ch++)
|
||||
{
|
||||
float level = tempOutputBuffer.getRMSLevel(ch, 0, numSamples);
|
||||
if (level > 0)
|
||||
{
|
||||
disable = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (disable)
|
||||
effect.locallyDisabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sampleSound.applyFXPre->get())
|
||||
for (int ch = 0; ch < tempOutputBuffer.getNumChannels(); ch++)
|
||||
juce::FloatVectorOperations::multiply(tempOutputBuffer.getWritePointer(ch), tempOutputBuffer.getReadPointer(ch), envelopeBuffer.getReadPointer(ch), numSamples);
|
||||
|
||||
updateFXParamsTimer--;
|
||||
if (updateFXParamsTimer <= 0)
|
||||
updateFXParamsTimer = UPDATE_PARAMS_LENGTH;
|
||||
doFxTailOff = !sampleSound.applyFXPre->get() && someFXEnabled && !effects.empty();
|
||||
|
||||
// Check RMS level to see if a voice should be ended despite tailing off effects
|
||||
if (con.state == STOPPED && someFXEnabled && numSamples > 10 && con.samplesSinceStopped > reverbSampleDelay)
|
||||
{
|
||||
bool end{ true }; // Whether all channels are below the threshold
|
||||
for (int ch = 0; ch < tempOutputBuffer.getNumChannels(); ch++)
|
||||
{
|
||||
float level = tempOutputBuffer.getRMSLevel(ch, 0, numSamples);
|
||||
if (level >= PluginParameters::FX_TAIL_OFF_MAX)
|
||||
{
|
||||
end = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (end)
|
||||
{
|
||||
clearCurrentNote();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
mixToBuffer(tempOutputBuffer, outputBuffer, startSample, numSamples, sampleSound.monoOutput->get());
|
||||
|
||||
// Add the previous tail-off samples to the output buffer
|
||||
tailOffBuffer.setSize(tempOutputBuffer.getNumChannels(), tailOffBuffer.getNumSamples(), true, true);
|
||||
int i = 0;
|
||||
for (; tailOff < TAIL_OFF; tailOff++)
|
||||
{
|
||||
if (i >= numSamples)
|
||||
break;
|
||||
|
||||
for (int ch = 0; ch < tailOffBuffer.getNumChannels(); ch++)
|
||||
{
|
||||
float sample = tailOffBuffer.getSample(ch, tailOff) * (TAIL_OFF - tailOff) / TAIL_OFF;
|
||||
outputBuffer.addSample(ch, startSample + i, sample);
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
float CustomSamplerVoice::fetchSample(int channel, double position, std::vector<std::unique_ptr<LowpassStream>>& lowpassStreams) const
|
||||
{
|
||||
if (0 > position || position >= float(sampleSound.sample.getNumSamples()))
|
||||
return 0.f;
|
||||
|
||||
if (sampleSound.skipAntialiasing->get())
|
||||
{
|
||||
return sampleSound.sample.getSample(channel, int(position));
|
||||
}
|
||||
else
|
||||
{
|
||||
return lanczosInterpolate(channel, position, lowpassStreams);
|
||||
}
|
||||
}
|
||||
|
||||
float CustomSamplerVoice::nextSample(int channel, BungeeStretcher* stretcher, juce::AudioBuffer<float>& channelBuffer, int i) const
|
||||
{
|
||||
if (channel == 0)
|
||||
{
|
||||
for (int ch = 1; ch < sampleSound.sample.getNumChannels(); ch++)
|
||||
channelBuffer.setSample(ch - 1, i, stretcher->nextSample(ch, false));
|
||||
return stretcher->nextSample(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
return channelBuffer.getSample(channel - 1, i);
|
||||
}
|
||||
}
|
||||
|
||||
float CustomSamplerVoice::getEnvelopeGain() const
|
||||
{
|
||||
float gain = 1.f;
|
||||
if (vc.isSmoothingAttack)
|
||||
gain *= exponentialCurve(attackShape, vc.speedMovedSinceStart / attackSmoothing);
|
||||
if (vc.isReleasing)
|
||||
gain *= exponentialCurve(releaseShape, 1 - vc.speedMovedSinceRelease / releaseSmoothing);
|
||||
return gain;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void CustomSamplerVoice::initializeFx()
|
||||
{
|
||||
auto fxOrder = sampleSound.getFxOrder();
|
||||
bool changed = false;
|
||||
for (size_t i = 0; i < fxOrder.size(); i++)
|
||||
{
|
||||
if (effects.size() <= i || effects[i].fxType != fxOrder[i])
|
||||
{
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (changed)
|
||||
{
|
||||
effects.clear();
|
||||
for (auto& fxType : fxOrder)
|
||||
{
|
||||
switch (fxType)
|
||||
{
|
||||
case PluginParameters::DISTORTION:
|
||||
effects.emplace_back(PluginParameters::DISTORTION, std::make_unique<Distortion>(), sampleSound.distortionEnabled);
|
||||
break;
|
||||
case PluginParameters::REVERB:
|
||||
effects.emplace_back(PluginParameters::REVERB, std::make_unique<Reverb>(), sampleSound.reverbEnabled);
|
||||
break;
|
||||
case PluginParameters::CHORUS:
|
||||
effects.emplace_back(PluginParameters::CHORUS, std::make_unique<Chorus>(expectedBlockSize), sampleSound.chorusEnabled);
|
||||
break;
|
||||
case PluginParameters::EQ:
|
||||
effects.emplace_back(PluginParameters::EQ, std::make_unique<BandEQ>(), sampleSound.eqEnabled);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
/** Thank god for Wikipedia, I don't really know why this works. https://en.wikipedia.org/wiki/Lanczos_resampling
|
||||
The technical details of resampling elude me, but JUCE's filters seem to work well enough for this...
|
||||
*/
|
||||
float CustomSamplerVoice::lanczosInterpolate(int channel, double position, std::vector<std::unique_ptr<LowpassStream>>& lowpassStreams) const
|
||||
{
|
||||
// First, process the lowpass filter
|
||||
auto& lowpassStream = *lowpassStreams[channel];
|
||||
if (doLowpass && lowpassStream.getNextSample() < sampleSound.sample.getNumSamples())
|
||||
{
|
||||
int lastWindowSample = juce::jmin(int(std::floor(position)) + LANCZOS_WINDOW_SIZE, sampleSound.sample.getNumSamples() - 1);
|
||||
lowpassStream.processSamples(sampleSound.sample.getReadPointer(channel, lowpassStream.getNextSample()), lastWindowSample - lowpassStream.getNextSample() + 1);
|
||||
}
|
||||
|
||||
// Then, interpolate
|
||||
int floorIndex = int(std::floor(position));
|
||||
|
||||
float result = 0.f;
|
||||
for (int i = -LANCZOS_WINDOW_SIZE + 1; i <= LANCZOS_WINDOW_SIZE; i++)
|
||||
{
|
||||
int iPlus = i + floorIndex;
|
||||
|
||||
float sample = 0.f;
|
||||
if (0 <= iPlus && iPlus < sampleSound.sample.getNumSamples()) // Bounds checking is a bit awkward here but handles some edge cases
|
||||
{
|
||||
if (doLowpass)
|
||||
{
|
||||
if (iPlus >= lowpassStream.getStartSample())
|
||||
sample = lowpassStream.getProcessedSample(iPlus);
|
||||
}
|
||||
else
|
||||
{
|
||||
sample = sampleSound.sample.getSample(channel, iPlus);
|
||||
}
|
||||
}
|
||||
|
||||
float window = lanczosWindow(position - floorIndex - i);
|
||||
result += sample * window;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
float CustomSamplerVoice::lanczosWindow(double x)
|
||||
{
|
||||
return x == 0.f ? 1.f : float(LANCZOS_WINDOW_SIZE * std::sin(juce::MathConstants<float>::pi * x) * std::sin(juce::MathConstants<float>::pi * x / LANCZOS_WINDOW_SIZE) * INVERSE_SIN_SQUARED / (x * x));
|
||||
}
|
||||
262
Source/Sampler/CustomSamplerVoice.h
Normal file
262
Source/Sampler/CustomSamplerVoice.h
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
CustomSamplerVoice.h
|
||||
Created: 5 Sep 2023 3:35:03pm
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
#include "SamplerParameters.h"
|
||||
#include "Effects/Effect.h"
|
||||
#include "Stretcher.h"
|
||||
#include <libMTSClient.h>
|
||||
|
||||
/** This enum includes the different states a voice can be in */
|
||||
enum VoiceState
|
||||
{
|
||||
PLAYING, // The voice is still before or during the loop
|
||||
PLAYING_END, // The voice is continuing after the loop
|
||||
STOPPED
|
||||
};
|
||||
|
||||
/** The context information for sample by sample processing is stored in its own struct. This
|
||||
is primarily to allow for easy multichannel processing but also encapsulates the state nicely.
|
||||
Note that the smoothing variables are an important part of the state transition logic.
|
||||
*/
|
||||
struct VoiceContext
|
||||
{
|
||||
VoiceState state{ STOPPED };
|
||||
double currentPosition{ 0 }; // Fractional positions are necessary
|
||||
|
||||
bool isSmoothingAttack{ false }; // The initial attack curve
|
||||
bool isCrossfadingLoop{ false };
|
||||
bool isCrossfadingEnd{ false }; // Crossfading between looping and the end part of the sample
|
||||
bool isReleasing{ false }; // Active when the note is released or when it nears the end of the sample
|
||||
|
||||
double crossfadeEndPosition{ 0 }; // The current position of the end crossfade
|
||||
float speedMovedSinceStart{ 0 }; // Used to time the attack envelope, note this is in terms of time passed, not position
|
||||
float speedMovedSinceRelease{ 0 }; // Used to time the release envelope
|
||||
int samplesSinceStopped{ 0 }; // This is needed to time the RMS measurements for reverb tail off (since it has a delay)
|
||||
};
|
||||
|
||||
/** This class is used to store the state of the lowpass filter for a channel / stream
|
||||
Because of our use case, a circular buffer is used to store past samples, large enough for the size of the lanczos window.
|
||||
*/
|
||||
class LowpassStream
|
||||
{
|
||||
public:
|
||||
explicit LowpassStream(int bufferSize) : intermediateBuffer(1, bufferSize) {}
|
||||
|
||||
/** Reset the processing state of the stream to a new sample position */
|
||||
void resetProcessing(int nextSampleToProcess)
|
||||
{
|
||||
filter1.reset();
|
||||
filter2.reset();
|
||||
filter3.reset();
|
||||
filter4.reset();
|
||||
|
||||
bufferLoc = 0;
|
||||
startSample = nextSampleToProcess;
|
||||
nextSample = nextSampleToProcess;
|
||||
}
|
||||
|
||||
/** Process a block of samples, storing the recent result in the intermediate buffer.
|
||||
nextSample is the index of the next sample that should be processed.
|
||||
*/
|
||||
void processSamples(const float* samples, int numSamples)
|
||||
{
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
float processedSample = filter1.processSingleSampleRaw(samples[i]);
|
||||
processedSample = filter2.processSingleSampleRaw(processedSample);
|
||||
processedSample = filter3.processSingleSampleRaw(processedSample);
|
||||
processedSample = filter4.processSingleSampleRaw(processedSample);
|
||||
|
||||
intermediateBuffer.setSample(0, bufferLoc, processedSample);
|
||||
nextSample++;
|
||||
bufferLoc = (bufferLoc + 1) % intermediateBuffer.getNumSamples();
|
||||
}
|
||||
}
|
||||
|
||||
/** Get the processed sample at a given index. Asserts the sample is contained. */
|
||||
float getProcessedSample(int sampleIndex) const
|
||||
{
|
||||
jassert(sampleIndex >= startSample && sampleIndex < nextSample && sampleIndex >= nextSample - intermediateBuffer.getNumSamples());
|
||||
|
||||
int bufferIndex = (sampleIndex - startSample) % intermediateBuffer.getNumSamples();
|
||||
return intermediateBuffer.getSample(0, bufferIndex);
|
||||
}
|
||||
|
||||
int getNextSample() const { return nextSample; }
|
||||
|
||||
/** Following juce::dsp::FilterDesign::designIIRLowpassHighOrderButterworthMethod(), this is theoretically -48db above 20khz */
|
||||
void setCoefficients(int sampleRate, float frequency)
|
||||
{
|
||||
float order = 8.f;
|
||||
filter1.setCoefficients(juce::IIRCoefficients::makeLowPass(sampleRate, frequency, 1.f / (2.f * std::cos(1.f * juce::MathConstants<float>::pi / (order * 2.f)))));
|
||||
filter2.setCoefficients(juce::IIRCoefficients::makeLowPass(sampleRate, frequency, 1.f / (2.f * std::cos(3.f * juce::MathConstants<float>::pi / (order * 2.f)))));
|
||||
filter3.setCoefficients(juce::IIRCoefficients::makeLowPass(sampleRate, frequency, 1.f / (2.f * std::cos(5.f * juce::MathConstants<float>::pi / (order * 2.f)))));
|
||||
filter4.setCoefficients(juce::IIRCoefficients::makeLowPass(sampleRate, frequency, 1.f / (2.f * std::cos(7.f * juce::MathConstants<float>::pi / (order * 2.f)))));
|
||||
}
|
||||
|
||||
int getStartSample() const { return startSample; }
|
||||
|
||||
private:
|
||||
juce::SingleThreadedIIRFilter filter1;
|
||||
juce::SingleThreadedIIRFilter filter2;
|
||||
juce::SingleThreadedIIRFilter filter3;
|
||||
juce::SingleThreadedIIRFilter filter4;
|
||||
|
||||
juce::AudioBuffer<float> intermediateBuffer;
|
||||
int startSample{ 0 };
|
||||
int bufferLoc{ 0 }; // Location in the buffer to write to (circular buffer)
|
||||
int nextSample{ 0 }; // The next sample to be processed
|
||||
};
|
||||
|
||||
/** This struct serves to separate per instance enablement of effects from the effect classes themselves */
|
||||
struct Fx
|
||||
{
|
||||
Fx(PluginParameters::FxTypes fxType, std::unique_ptr<Effect> fx, juce::AudioParameterBool* enablementSource) :
|
||||
fxType(fxType), fx(std::move(fx)), enablementSource(enablementSource) {}
|
||||
|
||||
PluginParameters::FxTypes fxType;
|
||||
std::unique_ptr<Effect> fx;
|
||||
juce::AudioParameterBool* enablementSource;
|
||||
bool enabled{ false };
|
||||
bool locallyDisabled{ false }; // used to avoid empty processing
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** The CustomSamplerVoice is the main DSP logic of this plugin. It can pitch shift directly or integrate with a 3rd party
|
||||
algorithm. It supports antialiasing, an FX chain, different looping modes, attack and release, and smooth crossfading.
|
||||
*/
|
||||
class CustomSamplerVoice final : public juce::SynthesiserVoice
|
||||
{
|
||||
public:
|
||||
CustomSamplerVoice(const SamplerParameters& samplerSound, MTSClient* client, double applicationSampleRate, int expectedBlockSize, bool initSample = true);
|
||||
|
||||
/** For general convenience, we'd like to be able to initialize all voices at plugin start */
|
||||
void initializeSample();
|
||||
|
||||
/** Updates the speed and pitch, setting stretchers and filter cutoffs correctly.
|
||||
Before calling this the first time, set doLowpass = false so that it resets the lowpass filters.
|
||||
*/
|
||||
void updateSpeedAndPitch(int currentNote, int pitchWheelPosition);
|
||||
|
||||
//==============================================================================
|
||||
/** Returns whether the voice is actively playing (not stopped or tailing off) */
|
||||
bool isPlaying() const { return getCurrentlyPlayingSound() && vc.state != STOPPED; }
|
||||
|
||||
/** This is the condition for wavetable mode */
|
||||
static bool isWavetableModeAvailable(float sampleRate, int sampleStart, int sampleEnd)
|
||||
{
|
||||
return float(sampleRate) / (sampleEnd - sampleStart + 1) > PluginParameters::WAVETABLE_CUTOFF_HZ;
|
||||
}
|
||||
|
||||
/** Get the effective location of the sampler voice relative to the original sample, not precise in ADVANCED mode */
|
||||
double getPosition() const { return vc.currentPosition; }
|
||||
|
||||
/** Get the current gain of the voice in the attack and release envelopes, for visualization */
|
||||
float getEnvelopeGain() const;
|
||||
|
||||
/** x should be [0, 1] */
|
||||
static const float exponentialCurve(float a, float x) { return juce::approximatelyEqual(a, 0.f, juce::Tolerance<float>().withAbsolute(0.001f)) ? x : (std::exp(a * x) - 1) / (std::exp(a) - 1); }
|
||||
|
||||
void stopNote(float velocity, bool allowTailOff) override;
|
||||
void immediateHalt();
|
||||
|
||||
private:
|
||||
bool canPlaySound(juce::SynthesiserSound*) override { return true; }
|
||||
void startNote(int midiNoteNumber, float velocity, juce::SynthesiserSound* sound, int currentPitchWheelPosition) override;
|
||||
void pitchWheelMoved(int newPitchWheelValue) override;
|
||||
void controllerMoved(int, int) override {}
|
||||
void renderNextBlock(juce::AudioBuffer<float>& outputBuffer, int startSample, int numSamples) override;
|
||||
|
||||
//==============================================================================
|
||||
/** Fetch a sample at a given position, in BASIC mode.
|
||||
Provide a vector of lowpass streams to apply lowpass filtering before interpolation (if doLowpass).
|
||||
This is necessary to avoid frequencies going above the Nyquist frequency.
|
||||
*/
|
||||
float fetchSample(int channel, double position, std::vector<std::unique_ptr<LowpassStream>>& lowpassStreams) const;
|
||||
|
||||
/** Fetches the next sample from a stretcher, in ADVANCED mode. Note that on channel 0, the stretcher
|
||||
advances and stores the other channels' output in the channel buffer at index i. Then it's fetched
|
||||
from there when nextSample is called with the later channel.
|
||||
*/
|
||||
float nextSample(int channel, BungeeStretcher* stretcher, juce::AudioBuffer<float>& channelBuffer, int i) const;
|
||||
|
||||
/** Use a Lanczos kernel to calculate fractional sample indices. Applies a lowpass filter beforehand, if doLowpass. */
|
||||
float lanczosInterpolate(int channel, double position, std::vector<std::unique_ptr<LowpassStream>>& lowpassStreams) const;
|
||||
|
||||
inline static float lanczosWindow(double x);
|
||||
static constexpr int LANCZOS_WINDOW_SIZE{ 5 };
|
||||
|
||||
/** Initialize or updates (by reinitializing) the effect chain. This is not real-time safe, but I don't think reordering needs to be. */
|
||||
void initializeFx();
|
||||
|
||||
//==============================================================================
|
||||
int expectedBlockSize;
|
||||
|
||||
const SamplerParameters& sampleSound;
|
||||
float sampleRateConversion{ 0 }; // Loaded sample rate / application sample rate
|
||||
float speed{ 0 }; // Used in BASIC mode
|
||||
int effectiveStart{ 0 };
|
||||
int effectiveEnd{ 0 };
|
||||
|
||||
/** "Wavetable mode" activates when the bounds are very short and can act as a waveform cycle. */
|
||||
bool wavetableMode{ false };
|
||||
|
||||
// Unchanging sampler sound parameters
|
||||
PluginParameters::PLAYBACK_MODES playbackMode{ PluginParameters::PLAYBACK_MODES::BASIC };
|
||||
float tuning{ 0.f };
|
||||
int pitchWheel{ 0 };
|
||||
float speedFactor{ 0.f }; // Used in ADVANCED mode
|
||||
float noteVelocity{ 0.f };
|
||||
|
||||
bool playUntilEnd{ false };
|
||||
bool isLooping{ false }, loopingHasStart{ false }, loopingHasEnd{ false };
|
||||
int sampleStart{ 0 }, sampleEnd{ 0 }, loopStart{ 0 }, loopEnd{ 0 };
|
||||
|
||||
/** We call this "smoothing" but it's a pretty normal attack/release envelope. */
|
||||
float attackSmoothing{ 0.f }, releaseSmoothing{ 0.f };
|
||||
float attackShape{ 0.f }, releaseShape{ 0.f };
|
||||
float crossfade{ 0.f };
|
||||
|
||||
VoiceContext vc;
|
||||
bool midiReleased{ false };
|
||||
juce::AudioBuffer<float> tempOutputBuffer;
|
||||
juce::AudioBuffer<float> envelopeBuffer; // To enable the PRE_FX option, we store the envelope gain here before applying
|
||||
|
||||
static constexpr int TAIL_OFF = 50;
|
||||
int tailOff{ 0 };
|
||||
juce::AudioBuffer<float> tailOffBuffer; // To avoid clicks on voice-stealing, we render a tail
|
||||
|
||||
BungeeStretcher mainStretcher;
|
||||
BungeeStretcher loopStretcher;
|
||||
BungeeStretcher endStretcher;
|
||||
|
||||
// Since the stretchers process channels together, buffers are needed to store the output
|
||||
juce::AudioBuffer<float> mainStretcherBuffer;
|
||||
juce::AudioBuffer<float> loopStretcherBuffer;
|
||||
juce::AudioBuffer<float> endStretcherBuffer;
|
||||
|
||||
bool doLowpass{ false };
|
||||
std::vector<std::unique_ptr<LowpassStream>> mainLowpass;
|
||||
std::vector<std::unique_ptr<LowpassStream>> loopLowpass;
|
||||
std::vector<std::unique_ptr<LowpassStream>> endLowpass;
|
||||
|
||||
//==============================================================================
|
||||
bool doFxTailOff{ false };
|
||||
static constexpr int UPDATE_PARAMS_LENGTH{ 4 }; // After how many process calls should we query for FX params
|
||||
int updateFXParamsTimer{ 0 };
|
||||
std::vector<Fx> effects;
|
||||
|
||||
MTSClient* mtsClient{ nullptr };
|
||||
};
|
||||
|
||||
static constexpr float INVERSE_SIN_SQUARED{ 1.f / (juce::MathConstants<float>::pi * juce::MathConstants<float>::pi) };
|
||||
31
Source/Sampler/CustomSynthesizer.h
Normal file
31
Source/Sampler/CustomSynthesizer.h
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
CustomSynthesizer.h
|
||||
Created: 9 Jun 2024 10:37:38am
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
/** JUCE's voice and sound paradigm is not so helpful for us, so we use a blank sound class and pass in our parameters directly to the voices. */
|
||||
class BlankSynthesizerSound final : public juce::SynthesiserSound
|
||||
{
|
||||
public:
|
||||
bool appliesToNote(int) override { return true; }
|
||||
bool appliesToChannel(int) override { return true; }
|
||||
};
|
||||
|
||||
/** We add some custom methods because our Synthesizer does not own its voices */
|
||||
class CustomSynthesizer final : public juce::Synthesiser
|
||||
{
|
||||
public:
|
||||
juce::SynthesiserVoice* removeVoiceWithoutDeleting(const int index)
|
||||
{
|
||||
const juce::ScopedLock sl(lock);
|
||||
return voices.removeAndReturn(index);
|
||||
}
|
||||
};
|
||||
98
Source/Sampler/Effects/BandEQ.h
Normal file
98
Source/Sampler/Effects/BandEQ.h
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
BandEQ.h
|
||||
Created: 2 Jan 2024 5:02:27pm
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
#include "Effect.h"
|
||||
|
||||
/** This is a sample 3-band EQ effect, inspired by the Kiloheart's free 3-Band EQ plugin. */
|
||||
class BandEQ final : public Effect
|
||||
{
|
||||
public:
|
||||
void initialize(int numChannels, int fxSampleRate) override
|
||||
{
|
||||
sampleRate = fxSampleRate;
|
||||
|
||||
juce::dsp::ProcessSpec spec{};
|
||||
spec.numChannels = numChannels;
|
||||
spec.sampleRate = sampleRate;
|
||||
filterChain.reset();
|
||||
filterChain.prepare(spec);
|
||||
}
|
||||
|
||||
void updateParams(float lowFreq, float highFreq, float lowGain, float midGain, float highGain)
|
||||
{
|
||||
// This possibly has an issue where the frequencies are outside of range on plugin initialization
|
||||
auto coeffLow = juce::dsp::IIR::Coefficients<float>::makeLowShelf(sampleRate, lowFreq, Q, juce::Decibels::decibelsToGain(lowGain));
|
||||
auto coeffMid1 = juce::dsp::IIR::Coefficients<float>::makeHighShelf(sampleRate, lowFreq, Q, juce::Decibels::decibelsToGain(midGain));
|
||||
auto coeffMid2 = juce::dsp::IIR::Coefficients<float>::makeHighShelf(sampleRate, highFreq, Q, juce::Decibels::decibelsToGain(-midGain));
|
||||
auto coeffHigh = juce::dsp::IIR::Coefficients<float>::makeHighShelf(sampleRate, highFreq, Q, juce::Decibels::decibelsToGain(highGain));
|
||||
*filterChain.get<0>().state = *coeffLow;
|
||||
*filterChain.get<1>().state = *coeffMid1;
|
||||
*filterChain.get<2>().state = *coeffMid2;
|
||||
*filterChain.get<3>().state = *coeffHigh;
|
||||
}
|
||||
|
||||
void updateParams(const SamplerParameters& samplerSound, bool) override
|
||||
{
|
||||
updateParams(
|
||||
samplerSound.eqLowFreq->get(), samplerSound.eqHighFreq->get(),
|
||||
samplerSound.eqLowGain->get(), samplerSound.eqMidGain->get(), samplerSound.eqHighGain->get()
|
||||
);
|
||||
}
|
||||
|
||||
void process(juce::AudioBuffer<float>& buffer, int numSamples, int startSample = 0) override
|
||||
{
|
||||
juce::dsp::AudioBlock block{ buffer.getArrayOfWritePointers(), size_t(buffer.getNumChannels()), size_t(startSample), size_t(numSamples) };
|
||||
juce::dsp::ProcessContextReplacing context{ block };
|
||||
filterChain.process(context);
|
||||
}
|
||||
|
||||
juce::Array<double> getMagnitudeForFrequencyArray(juce::Array<double> frequencies)
|
||||
{
|
||||
std::array filters{
|
||||
&filterChain.get<0>(),
|
||||
&filterChain.get<1>(),
|
||||
&filterChain.get<2>(),
|
||||
&filterChain.get<3>()
|
||||
};
|
||||
|
||||
juce::AudioBuffer<double> magnitudes{1, frequencies.size()};
|
||||
magnitudes.clear();
|
||||
bool empty{ true };
|
||||
for (const auto& filter : filters)
|
||||
{
|
||||
juce::AudioBuffer<double> temp{ 1, frequencies.size()};
|
||||
filter->state->getMagnitudeForFrequencyArray(frequencies.getRawDataPointer(), temp.getWritePointer(0), frequencies.size(), 48000);
|
||||
if (empty)
|
||||
{
|
||||
magnitudes.addFrom(0, 0, temp.getReadPointer(0), frequencies.size());
|
||||
empty = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < temp.getNumSamples(); i++)
|
||||
magnitudes.setSample(0, i, magnitudes.getSample(0, i) * temp.getSample(0, i));
|
||||
}
|
||||
}
|
||||
|
||||
return juce::Array<double>{magnitudes.getReadPointer(0), frequencies.size()};
|
||||
}
|
||||
|
||||
private:
|
||||
using Filter = juce::dsp::ProcessorDuplicator<juce::dsp::IIR::Filter<float>, juce::dsp::IIR::Coefficients<float>>;
|
||||
using FilterChain = juce::dsp::ProcessorChain<Filter, Filter, Filter, Filter>;
|
||||
|
||||
static constexpr float Q{ 0.6f }; // Magic number I saw online for the response curve
|
||||
|
||||
int sampleRate{ 0 };
|
||||
FilterChain filterChain;
|
||||
};
|
||||
57
Source/Sampler/Effects/Chorus.h
Normal file
57
Source/Sampler/Effects/Chorus.h
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
Chorus.h
|
||||
Created: 4 Jan 2024 7:22:36pm
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
#include "Effect.h"
|
||||
|
||||
/** This is a simple wrapper around the JUCE Chorus class */
|
||||
class Chorus final : public Effect
|
||||
{
|
||||
public:
|
||||
explicit Chorus(int expectedBlockSize=MAX_BLOCK_SIZE) : expectedBlockSize(expectedBlockSize) {}
|
||||
|
||||
void initialize(int numChannels, int fxSampleRate) override
|
||||
{
|
||||
juce::dsp::ProcessSpec processSpec{ double(fxSampleRate), juce::uint32(expectedBlockSize), juce::uint32(numChannels) };
|
||||
|
||||
chorus.reset();
|
||||
chorus.prepare(processSpec);
|
||||
}
|
||||
|
||||
void updateParams(const SamplerParameters& sampleSound, bool realtime) override
|
||||
{
|
||||
chorus.setRate(sampleSound.chorusRate->get());
|
||||
chorus.setDepth(sampleSound.chorusDepth->get());
|
||||
chorus.setFeedback(sampleSound.chorusFeedback->get());
|
||||
if (!realtime) // Center delay cannot be modulated
|
||||
chorus.setCentreDelay(juce::jmin<float>(sampleSound.chorusCenterDelay->get(), 99.9f));
|
||||
chorus.setMix(sampleSound.chorusMix->get());
|
||||
}
|
||||
|
||||
void process(juce::AudioBuffer<float>& buffer, int numSamples, int startSample = 0) override
|
||||
{
|
||||
while (numSamples > 0)
|
||||
{
|
||||
juce::dsp::AudioBlock<float> block{ buffer.getArrayOfWritePointers(), size_t(buffer.getNumChannels()), size_t(startSample), size_t(juce::jmin(MAX_BLOCK_SIZE, numSamples)) };
|
||||
juce::dsp::ProcessContextReplacing<float> context{ block };
|
||||
chorus.process(context);
|
||||
startSample += MAX_BLOCK_SIZE;
|
||||
numSamples -= MAX_BLOCK_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr int MAX_BLOCK_SIZE{ 1024 };
|
||||
|
||||
juce::dsp::Chorus<float> chorus{};
|
||||
int expectedBlockSize;
|
||||
};
|
||||
73
Source/Sampler/Effects/Distortion.h
Normal file
73
Source/Sampler/Effects/Distortion.h
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
Distortion.h
|
||||
Created: 30 Dec 2023 8:39:04pm
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
#include "Effect.h"
|
||||
#include <gin_distortion.h>
|
||||
|
||||
/** This is a simple Distortion class that wraps around gin::AirWindowsDistortion */
|
||||
class Distortion final : public Effect
|
||||
{
|
||||
public:
|
||||
void initialize(int numChannels, int fxSampleRate) override
|
||||
{
|
||||
int numEffects = numChannels / 2 + numChannels % 2;
|
||||
channelDistortions.resize(numEffects);
|
||||
for (int ch = 0; ch < numEffects; ch++)
|
||||
{
|
||||
channelDistortions[ch] = std::make_unique<gin::AirWindowsDistortion>();
|
||||
channelDistortions[ch]->setSampleRate(fxSampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
void updateParams(float density, float highpass, float mix)
|
||||
{
|
||||
float mappedDensity = density >= 0.f ? juce::jmap<float>(density, 0.2f, 1.f) : juce::jmap<float>(density, -0.5f, 0.f, 0.f, 0.2f);
|
||||
|
||||
// We try to keep the gain of the distortion constant:
|
||||
// This is a sigmoid function found manually from graphing the output of the distortion. When the mapped density < 0.2f,
|
||||
// a different function needs to be used, since the distortion actually behaves differently according to that threshold.
|
||||
// Note that the gain parameter only applies if it's less than 1.f, so we need to increase the gain ourselves after processing.
|
||||
float gainChange = mappedDensity >= 0.2f ? (0.2f * (1.f + expf(-7.f * (mappedDensity - 0.5f)))) : 1.f;
|
||||
postGain = mappedDensity < 0.2f ? 1.f / (4.f * mappedDensity + 0.2f) : 1.f;
|
||||
|
||||
for (const auto& channelDistortion : channelDistortions)
|
||||
channelDistortion->setParams(mappedDensity, highpass, gainChange, mix);
|
||||
}
|
||||
|
||||
void updateParams(const SamplerParameters& sampleSound, bool) override
|
||||
{
|
||||
updateParams(
|
||||
sampleSound.distortionDensity->get(),
|
||||
sampleSound.distortionHighpass->get(),
|
||||
sampleSound.distortionMix->get()
|
||||
);
|
||||
}
|
||||
|
||||
void process(juce::AudioBuffer<float>& buffer, int numSamples, int startSample=0) override
|
||||
{
|
||||
bool lastIsMono = buffer.getNumChannels() % 2 == 1;
|
||||
for (int ch = 0; ch < buffer.getNumChannels(); ch += 2)
|
||||
{
|
||||
// Note that I modified the gin header to make this more straightforward
|
||||
if (lastIsMono && ch == buffer.getNumChannels() - 1)
|
||||
channelDistortions[ch / 2]->process(buffer.getWritePointer(ch, startSample), buffer.getWritePointer(ch, startSample), numSamples);
|
||||
else
|
||||
channelDistortions[ch / 2]->process(buffer.getWritePointer(ch, startSample), buffer.getWritePointer(ch + 1, startSample), numSamples);
|
||||
}
|
||||
buffer.applyGain(startSample, numSamples, postGain);
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::unique_ptr<gin::AirWindowsDistortion>> channelDistortions;
|
||||
float postGain{ 0. };
|
||||
};
|
||||
23
Source/Sampler/Effects/Effect.h
Normal file
23
Source/Sampler/Effects/Effect.h
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
Effect.h
|
||||
Created: 30 Dec 2023 8:35:28pm
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
#include "../SamplerParameters.h"
|
||||
|
||||
class Effect
|
||||
{
|
||||
public:
|
||||
virtual ~Effect() = default;
|
||||
virtual void initialize(int numChannels, int fxSampleRate) = 0;
|
||||
virtual void updateParams(const SamplerParameters& sampleSound, bool modulating = false) = 0;
|
||||
virtual void process(juce::AudioBuffer<float>& buffer, int numSamples, int startSample=0) = 0;
|
||||
};
|
||||
85
Source/Sampler/Effects/Reverb.h
Normal file
85
Source/Sampler/Effects/Reverb.h
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
Reverb.h
|
||||
Created: 30 Dec 2023 8:38:55pm
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
#include "Effect.h"
|
||||
#include <gin_simpleverb.h>
|
||||
|
||||
/** This is a simple Effect class that wraps around Gin's SimpleVerb implementation */
|
||||
class Reverb final : public Effect
|
||||
{
|
||||
public:
|
||||
Reverb() = default;
|
||||
|
||||
void initialize(int numChannels, int fxSampleRate) override
|
||||
{
|
||||
int numEffects = numChannels / 2 + numChannels % 2;
|
||||
channelGinReverbs.resize(numEffects);
|
||||
|
||||
for (int ch = 0; ch < numEffects; ch++)
|
||||
{
|
||||
channelGinReverbs[ch] = std::make_unique<gin::SimpleVerb>();
|
||||
channelGinReverbs[ch]->setSampleRate(float(fxSampleRate));
|
||||
channelGinReverbs[ch]->setParameters(0.f, 0.f, 1.f, 0.f, 1.f, 0.f, 0.f);
|
||||
}
|
||||
}
|
||||
|
||||
// The intended ranges of these values are in PluginParameters.h
|
||||
void updateParams(float size, float damping, float predelay, float lows, float highs, float mix) const
|
||||
{
|
||||
for (const auto& channelGinReverb : channelGinReverbs)
|
||||
{
|
||||
channelGinReverb->setParameters(
|
||||
juce::jmap<float>(size, PluginParameters::REVERB_SIZE_RANGE.getStart(), PluginParameters::REVERB_SIZE_RANGE.getEnd(), 0.f, 1.f),
|
||||
juce::jmap<float>(damping, PluginParameters::REVERB_DAMPING_RANGE.getStart(), PluginParameters::REVERB_DAMPING_RANGE.getEnd(), 0.f, 1.f),
|
||||
float(sqrtf(predelay / 250.f)), // conversions to counteract the faders in this algorithm
|
||||
juce::jmap<float>(highs, PluginParameters::REVERB_HIGHS_RANGE.getStart(), PluginParameters::REVERB_HIGHS_RANGE.getEnd(), 0.3f, 1.f),
|
||||
1.f - juce::jmap<float>(lows, PluginParameters::REVERB_LOWS_RANGE.getStart(), PluginParameters::REVERB_LOWS_RANGE.getEnd(), 0.3f, 1.f),
|
||||
mix,
|
||||
(1.f - mix) / 2.f
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void updateParams(const SamplerParameters& sampleSound, bool) override
|
||||
{
|
||||
updateParams(
|
||||
sampleSound.reverbSize->get(),
|
||||
sampleSound.reverbDamping->get(),
|
||||
sampleSound.reverbPredelay->get(),
|
||||
sampleSound.reverbLows->get(),
|
||||
sampleSound.reverbHighs->get(),
|
||||
sampleSound.reverbMix->get()
|
||||
);
|
||||
}
|
||||
|
||||
void process(juce::AudioBuffer<float>& buffer, int numSamples, int startSample = 0) override
|
||||
{
|
||||
bool lastIsMono = buffer.getNumChannels() % 2 == 1;
|
||||
for (int ch = 0; ch < buffer.getNumChannels(); ch += 2)
|
||||
{
|
||||
// I modified the header of the process method to work easier with this code, you'll need to do the same to get it to compile
|
||||
// void SimpleVerb::process (const float* in1, const float* in2, float* out1, float* out2, int numSamples)
|
||||
if (lastIsMono && ch == buffer.getNumChannels() - 1)
|
||||
channelGinReverbs[ch / 2]->process(
|
||||
buffer.getReadPointer(ch, startSample), buffer.getReadPointer(ch, startSample),
|
||||
buffer.getWritePointer(ch, startSample), buffer.getWritePointer(ch, startSample), numSamples);
|
||||
else
|
||||
channelGinReverbs[ch / 2]->process(
|
||||
buffer.getReadPointer(ch, startSample), buffer.getReadPointer(ch + 1, startSample),
|
||||
buffer.getWritePointer(ch, startSample), buffer.getWritePointer(ch + 1, startSample), numSamples);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::unique_ptr<gin::SimpleVerb>> channelGinReverbs;
|
||||
};
|
||||
94
Source/Sampler/SamplerParameters.cpp
Normal file
94
Source/Sampler/SamplerParameters.cpp
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
CustomSamplerSound.cpp
|
||||
Created: 5 Sep 2023 3:35:11pm
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#include "SamplerParameters.h"
|
||||
|
||||
SamplerParameters::SamplerParameters(const juce::AudioProcessorValueTreeState& apvts, PluginParameters::State& pluginState, const juce::AudioBuffer<float>& sample, int sampleRate) :
|
||||
sample(sample), sampleRate(sampleRate),
|
||||
gain(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::SAMPLE_GAIN))),
|
||||
speedFactor(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::SPEED_FACTOR))),
|
||||
octaveSpeedFactor(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::OCTAVE_SPEED_FACTOR))),
|
||||
attack(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::ATTACK))),
|
||||
release(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::RELEASE))),
|
||||
attackShape(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::ATTACK_SHAPE))),
|
||||
releaseShape(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::RELEASE_SHAPE))),
|
||||
a4_freq(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::A4_HZ))),
|
||||
pitchWheelRange(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::PITCH_WHEEL_RANGE))),
|
||||
wideTuningControl(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::WIDE_TUNING))),
|
||||
semitoneTuning(dynamic_cast<juce::AudioParameterInt*>(apvts.getParameter(PluginParameters::SEMITONE_TUNING))),
|
||||
centTuning(dynamic_cast<juce::AudioParameterInt*>(apvts.getParameter(PluginParameters::CENT_TUNING))),
|
||||
waveformSemitoneTuning(dynamic_cast<juce::AudioParameterInt*>(apvts.getParameter(PluginParameters::WAVEFORM_SEMITONE_TUNING))),
|
||||
waveformCentTuning(dynamic_cast<juce::AudioParameterInt*>(apvts.getParameter(PluginParameters::WAVEFORM_CENT_TUNING))),
|
||||
crossfadeSamples(dynamic_cast<juce::AudioParameterInt*>(apvts.getParameter(PluginParameters::CROSSFADE_SAMPLES))),
|
||||
monoOutput(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::MONO_OUTPUT))),
|
||||
disableVelocity(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::DISABLE_VELOCITY))),
|
||||
skipAntialiasing(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::SKIP_ANTIALIASING))),
|
||||
|
||||
applyFXPre(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::PRE_FX))),
|
||||
playUntilEnd(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::PLAY_UNTIL_END))),
|
||||
disableWavetableMode(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))),
|
||||
|
||||
sampleStart(pluginState.sampleStart), sampleEnd(pluginState.sampleEnd),
|
||||
loopStart(pluginState.loopStart), loopEnd(pluginState.loopEnd),
|
||||
midiStart(dynamic_cast<juce::AudioParameterInt*>(apvts.getParameter(PluginParameters::MIDI_START))),
|
||||
midiEnd(dynamic_cast<juce::AudioParameterInt*>(apvts.getParameter(PluginParameters::MIDI_END))),
|
||||
midiRoot(dynamic_cast<juce::AudioParameterInt*>(apvts.getParameter(PluginParameters::MIDI_ROOT))),
|
||||
followMidiPitch(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::FOLLOW_MIDI_PITCH))),
|
||||
|
||||
reverbEnabled(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::REVERB_ENABLED))),
|
||||
distortionEnabled(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::DISTORTION_ENABLED))),
|
||||
eqEnabled(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::EQ_ENABLED))),
|
||||
chorusEnabled(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::CHORUS_ENABLED))),
|
||||
|
||||
reverbMix(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::REVERB_MIX))),
|
||||
reverbSize(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::REVERB_SIZE))),
|
||||
reverbDamping(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::REVERB_DAMPING))),
|
||||
reverbLows(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::REVERB_LOWS))),
|
||||
reverbHighs(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::REVERB_HIGHS))),
|
||||
reverbPredelay(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::REVERB_PREDELAY))),
|
||||
|
||||
distortionMix(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::DISTORTION_MIX))),
|
||||
distortionDensity(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::DISTORTION_DENSITY))),
|
||||
distortionHighpass(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::DISTORTION_HIGHPASS))),
|
||||
|
||||
eqLowGain(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::EQ_LOW_GAIN))),
|
||||
eqMidGain(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::EQ_MID_GAIN))),
|
||||
eqHighGain(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::EQ_HIGH_GAIN))),
|
||||
eqLowFreq(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::EQ_LOW_FREQ))),
|
||||
eqHighFreq(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::EQ_HIGH_FREQ))),
|
||||
|
||||
chorusMix(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::CHORUS_MIX))),
|
||||
chorusRate(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::CHORUS_RATE))),
|
||||
chorusDepth(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::CHORUS_DEPTH))),
|
||||
chorusFeedback(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::CHORUS_FEEDBACK))),
|
||||
chorusCenterDelay(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::CHORUS_CENTER_DELAY))),
|
||||
|
||||
playbackMode(dynamic_cast<juce::AudioParameterChoice*>(apvts.getParameter(PluginParameters::PLAYBACK_MODE))),
|
||||
fxOrder(dynamic_cast<juce::AudioParameterInt*>(apvts.getParameter(PluginParameters::FX_PERM)))
|
||||
{
|
||||
}
|
||||
|
||||
void SamplerParameters::sampleChanged(const int newSampleRate)
|
||||
{
|
||||
sampleRate = newSampleRate;
|
||||
}
|
||||
|
||||
PluginParameters::PLAYBACK_MODES SamplerParameters::getPlaybackMode() const
|
||||
{
|
||||
return PluginParameters::getPlaybackMode(playbackMode->getIndex());
|
||||
}
|
||||
|
||||
std::array<PluginParameters::FxTypes, 4> SamplerParameters::getFxOrder() const
|
||||
{
|
||||
return PluginParameters::paramToPerm(fxOrder->get());
|
||||
}
|
||||
53
Source/Sampler/SamplerParameters.h
Normal file
53
Source/Sampler/SamplerParameters.h
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
CustomSamplerSound.h
|
||||
Created: 5 Sep 2023 3:35:11pm
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
#include "../PluginParameters.h"
|
||||
|
||||
/** A class defining all parameters for a note played by CustomSamplerVoice.cpp */
|
||||
class SamplerParameters final
|
||||
{
|
||||
public:
|
||||
SamplerParameters(const juce::AudioProcessorValueTreeState& apvts, PluginParameters::State& pluginState, const juce::AudioBuffer<float>& sample, int sampleRate);
|
||||
|
||||
void sampleChanged(int newSampleRate);
|
||||
|
||||
/** Fetch the playback mode, as the proper enum type */
|
||||
PluginParameters::PLAYBACK_MODES getPlaybackMode() const;
|
||||
|
||||
/** Fetch the sound's FX chain permutation */
|
||||
std::array<PluginParameters::FxTypes, 4> getFxOrder() const;
|
||||
|
||||
/** The sound to play */
|
||||
const juce::AudioBuffer<float>& sample;
|
||||
int sampleRate;
|
||||
|
||||
/** Playback details */
|
||||
juce::AudioParameterFloat* gain, * speedFactor, * octaveSpeedFactor, * attack, * release, * attackShape, * releaseShape, * a4_freq, * pitchWheelRange, * wideTuningControl;
|
||||
juce::AudioParameterInt* semitoneTuning, * centTuning, * waveformSemitoneTuning, * waveformCentTuning, * crossfadeSamples;
|
||||
juce::AudioParameterBool* monoOutput, * disableVelocity, * skipAntialiasing, * applyFXPre, * playUntilEnd, * disableWavetableMode, * isLooping, * loopingHasStart, * loopingHasEnd;
|
||||
ListenableAtomic<int>& sampleStart, & sampleEnd, & loopStart, & loopEnd;
|
||||
|
||||
juce::AudioParameterInt* midiStart, * midiEnd, * midiRoot;
|
||||
juce::AudioParameterBool* followMidiPitch;
|
||||
|
||||
/** FX parameters */
|
||||
juce::AudioParameterBool* reverbEnabled, * distortionEnabled, * eqEnabled, * chorusEnabled;
|
||||
juce::AudioParameterFloat* reverbMix, * reverbSize, * reverbDamping, * reverbLows, * reverbHighs, * reverbPredelay;
|
||||
juce::AudioParameterFloat* distortionMix, * distortionDensity, * distortionHighpass;
|
||||
juce::AudioParameterFloat* eqLowGain, * eqMidGain, * eqHighGain, * eqLowFreq, * eqHighFreq;
|
||||
juce::AudioParameterFloat* chorusMix, * chorusRate, * chorusDepth, * chorusFeedback, * chorusCenterDelay;
|
||||
|
||||
private:
|
||||
juce::AudioParameterChoice* playbackMode;
|
||||
juce::AudioParameterInt* fxOrder;
|
||||
};
|
||||
161
Source/Sampler/Stretcher.h
Normal file
161
Source/Sampler/Stretcher.h
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
Stretcher.h
|
||||
Created: 27 May 2024 4:09:49pm
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <Bungee.h>
|
||||
|
||||
// Bungee sets a hard limit on the pitch ratio to simplify memory management. We can increase this limit before building
|
||||
// and use a resampling hack when necessary (the hack is not great because it requires reallocation of the stretcher).
|
||||
// This must be set to the value in Timing.cpp (internal to Bungee)
|
||||
static constexpr int bungeeMaxPitchOctaves = BUNGEE_MAX_OCTAVES;
|
||||
static constexpr float bungeeMinimumRatio = 1.f / (1 << bungeeMaxPitchOctaves);
|
||||
|
||||
class BungeeStretcher
|
||||
{
|
||||
public:
|
||||
explicit BungeeStretcher(const juce::AudioBuffer<float>& sampleBuffer, int sampleRate) : buffer(&sampleBuffer),
|
||||
bufferSampleRate(sampleRate)
|
||||
{
|
||||
}
|
||||
|
||||
/** Allocates a new stretcher and the input buffer. */
|
||||
void preallocateStretcher(int appSampleRate)
|
||||
{
|
||||
if (appSampleRate == 0 || appSampleRate == applicationSampleRate)
|
||||
return;
|
||||
|
||||
bungee = std::make_unique<Bungee::Stretcher<Bungee::Basic>>(Bungee::SampleRates{ bufferSampleRate, appSampleRate }, buffer->getNumChannels());
|
||||
inputData.setSize(1, buffer->getNumChannels() * bungee->maxInputFrameCount(), false, false, true); // Note, maxInputFrameCount has a reported overflow issue
|
||||
previousInputRate = bufferSampleRate;
|
||||
applicationSampleRate = appSampleRate;
|
||||
}
|
||||
|
||||
void initialize(long double sampleStart, float initialRatio = 1, float initialSpeed = 1)
|
||||
{
|
||||
setPitchAndSpeed(initialRatio, initialSpeed);
|
||||
|
||||
// We only reallocate when necessary (if resamplingHack requires it, as it's not good for real-time performance)
|
||||
int inputRate = int(bufferSampleRate / resamplingHack);
|
||||
if (inputRate != previousInputRate)
|
||||
{
|
||||
bungee = std::make_unique<Bungee::Stretcher<Bungee::Basic>>(Bungee::SampleRates{ inputRate, applicationSampleRate }, buffer->getNumChannels());
|
||||
inputData.setSize(1, buffer->getNumChannels() * bungee->maxInputFrameCount(), false, false, true); // maxInputFrameCount has a reported overflow issue
|
||||
previousInputRate = inputRate;
|
||||
}
|
||||
|
||||
output = Bungee::OutputChunk{};
|
||||
outputIndex = 0;
|
||||
|
||||
preroll(double(sampleStart));
|
||||
}
|
||||
|
||||
/** Pre-rolls the stretcher to a new position. Use this before you plan to move the position. */
|
||||
void preroll(double newPosition)
|
||||
{
|
||||
request = Bungee::Request{ newPosition, speedFactor * resamplingHack, pitchRatio, true };
|
||||
bungee->preroll(request);
|
||||
|
||||
while (!output.data || std::isnan(output.request[Bungee::OutputChunk::begin]->position) ||
|
||||
newPosition > output.request[Bungee::OutputChunk::end]->position || newPosition < output.request[Bungee::OutputChunk::begin]->position)
|
||||
{
|
||||
auto input = bungee->specifyGrain(request);
|
||||
|
||||
if (buffer->getNumChannels() * (input.end - input.begin) >= inputData.getNumSamples()) // Can happen with extreme ratios
|
||||
inputData.setSize(1, buffer->getNumChannels() * (input.end - input.begin));
|
||||
|
||||
int begin = juce::jlimit<int>(int(std::ceil(newPosition)), buffer->getNumSamples(), input.begin);
|
||||
int end = juce::jlimit<int>(int(std::ceil(newPosition)), buffer->getNumSamples(), input.end);
|
||||
|
||||
inputData.clear();
|
||||
if (begin < end)
|
||||
{
|
||||
for (int ch = 0; ch < buffer->getNumChannels(); ch++)
|
||||
inputData.copyFrom(0, ch * (input.end - input.begin) + begin - input.begin, *buffer, ch, begin, end - begin);
|
||||
}
|
||||
|
||||
bungee->analyseGrain(inputData.getReadPointer(0), (input.end - input.begin));
|
||||
bungee->synthesiseGrain(output);
|
||||
bungee->next(request);
|
||||
|
||||
outputIndex = int(std::round((newPosition - output.request[Bungee::OutputChunk::begin]->position) / positionSpeed));
|
||||
}
|
||||
}
|
||||
|
||||
/** Fetches the next sample. Call this for each channel before advancing. */
|
||||
float nextSample(int channel, bool advance = true)
|
||||
{
|
||||
if (outputIndex >= output.frameCount)
|
||||
{
|
||||
request.pitch = pitchRatio;
|
||||
request.speed = speedFactor * resamplingHack;
|
||||
auto [begin, end] = bungee->specifyGrain(request);
|
||||
|
||||
begin = juce::jlimit<int>(0, buffer->getNumSamples(), begin);
|
||||
end = juce::jlimit<int>(0, buffer->getNumSamples(), end);
|
||||
|
||||
inputData.clear(); // Preferring simplicity over maximum efficiency
|
||||
for (int ch = 0; ch < buffer->getNumChannels(); ch++)
|
||||
inputData.copyFrom(0, ch * (end - begin), *buffer, ch, begin, end - begin);
|
||||
|
||||
bungee->analyseGrain(inputData.getReadPointer(0), end - begin);
|
||||
bungee->synthesiseGrain(output);
|
||||
bungee->next(request);
|
||||
|
||||
outputIndex = 0;
|
||||
}
|
||||
|
||||
float result = output.data[channel * output.channelStride + outputIndex];
|
||||
if (advance)
|
||||
outputIndex++;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void setPitchAndSpeed(float newPitchRatio, float newSpeedFactor)
|
||||
{
|
||||
pitchRatio = newPitchRatio;
|
||||
speedFactor = newSpeedFactor;
|
||||
|
||||
if (newPitchRatio < bungeeMinimumRatio)
|
||||
{
|
||||
pitchRatio = bungeeMinimumRatio;
|
||||
resamplingHack = bungeeMinimumRatio / newPitchRatio;
|
||||
}
|
||||
else
|
||||
{
|
||||
resamplingHack = 1.f;
|
||||
}
|
||||
|
||||
positionSpeed = speedFactor * bufferSampleRate / applicationSampleRate;
|
||||
}
|
||||
|
||||
/** Returns the speed at which the stretcher advances through the buffer, relative to the buffer's sample rate. */
|
||||
float getPositionSpeed() const { return positionSpeed; }
|
||||
|
||||
private:
|
||||
const juce::AudioBuffer<float>* buffer{ nullptr };
|
||||
int bufferSampleRate{ 0 };
|
||||
int applicationSampleRate{ 0 };
|
||||
|
||||
float pitchRatio{ 1. };
|
||||
float speedFactor{ 1. };
|
||||
float positionSpeed{ 0. }; // speedFactor * bufferSampleRate / applicationSampleRate
|
||||
|
||||
// We use a little hack to ignore Bungee's maxPitchOctaves limit
|
||||
float resamplingHack{ 1.f };
|
||||
int previousInputRate{ 0 };
|
||||
|
||||
std::unique_ptr<Bungee::Stretcher<Bungee::Basic>> bungee;
|
||||
Bungee::Request request{};
|
||||
Bungee::OutputChunk output{};
|
||||
|
||||
juce::AudioBuffer<float> inputData;
|
||||
int outputIndex{ 0 };
|
||||
};
|
||||
131
Source/Utilities/BufferUtils.h
Normal file
131
Source/Utilities/BufferUtils.h
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
BufferUtils.h
|
||||
Created: 19 May 2024 7:46:31pm
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
|
||||
/** Given a source and destination buffer with different number of channels, mix channels appropriately to
|
||||
get a reasonable output. If mixMono is true, all channels are mixed together to mono.
|
||||
*/
|
||||
inline void mixToBuffer(const juce::AudioBuffer<float>& src, juce::AudioBuffer<float>& dest, int startSample, int numSamples, bool mixMono)
|
||||
{
|
||||
if (mixMono)
|
||||
{
|
||||
// Mix all source channels to mono
|
||||
for (int ch = 0; ch < src.getNumChannels(); ch++)
|
||||
{
|
||||
for (int outputCh = 0; outputCh < dest.getNumChannels(); outputCh++)
|
||||
{
|
||||
dest.addFrom(outputCh, startSample, src.getReadPointer(ch), numSamples, 1.f / float(src.getNumChannels()));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (dest.getNumChannels() < src.getNumChannels())
|
||||
{
|
||||
// Down mix from more source channels to fewer destination channels
|
||||
float ratio = float(src.getNumChannels()) / dest.getNumChannels();
|
||||
for (int outputCh = 0; outputCh < dest.getNumChannels(); outputCh++)
|
||||
{
|
||||
int startCh = int(outputCh * ratio);
|
||||
int endCh = int((outputCh + 1) * ratio);
|
||||
|
||||
for (int ch = startCh; ch < endCh; ch++)
|
||||
{
|
||||
dest.addFrom(outputCh, startSample, src.getReadPointer(ch), numSamples, 1.f / (endCh - startCh));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (dest.getNumChannels() >= src.getNumChannels())
|
||||
{
|
||||
// Up mix from fewer source channels to more destination channels
|
||||
float ratio = float(dest.getNumChannels()) / src.getNumChannels();
|
||||
for (int inputCh = 0; inputCh < src.getNumChannels(); inputCh++)
|
||||
{
|
||||
int startCh = int(inputCh * ratio);
|
||||
int endCh = int((inputCh + 1) * ratio);
|
||||
|
||||
for (int outputCh = startCh; outputCh < endCh; outputCh++)
|
||||
{
|
||||
dest.addFrom(outputCh, startSample, src.getReadPointer(inputCh), numSamples);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Silences the buffer if bad or loud values are detected in the output buffer.
|
||||
Use this during debugging to avoid blowing out your eardrums on headphones.
|
||||
If the output value is out of the range [-1, +1] it will be hard clipped.
|
||||
|
||||
Credit to https://gist.github.com/hollance/b7219e49c6fc16fa2af05c4a72c186ef for this quick solution.
|
||||
*/
|
||||
inline void protectYourEars(float* buffer, int sampleCount)
|
||||
{
|
||||
if (buffer == nullptr) { return; }
|
||||
bool firstWarning = true;
|
||||
for (int i = 0; i < sampleCount; ++i)
|
||||
{
|
||||
float x = buffer[i];
|
||||
bool silence = false;
|
||||
if (std::isnan(x))
|
||||
{
|
||||
DBG("!!! WARNING: nan detected in audio buffer, silencing !!!");
|
||||
silence = true;
|
||||
}
|
||||
else if (std::isinf(x))
|
||||
{
|
||||
DBG("!!! WARNING: inf detected in audio buffer, silencing !!!");
|
||||
silence = true;
|
||||
}
|
||||
else if (x < -2.0f || x > 2.0f) // screaming feedback
|
||||
{
|
||||
DBG("!!! WARNING: sample out of range, silencing !!!");
|
||||
silence = true;
|
||||
}
|
||||
else if (x < -1.0f)
|
||||
{
|
||||
if (firstWarning)
|
||||
{
|
||||
DBG("!!! WARNING: sample out of range, clamping !!!");
|
||||
firstWarning = false;
|
||||
}
|
||||
buffer[i] = -1.0f;
|
||||
}
|
||||
else if (x > 1.0f)
|
||||
{
|
||||
if (firstWarning)
|
||||
{
|
||||
DBG("!!! WARNING: sample out of range, clamping !!!");
|
||||
firstWarning = false;
|
||||
}
|
||||
buffer[i] = 1.0f;
|
||||
}
|
||||
if (silence)
|
||||
{
|
||||
memset(buffer, 0, sampleCount * sizeof(float));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Uses MD-5 hashing to generate an identifier for the AudioBuffer */
|
||||
inline juce::String getSampleHash(const juce::AudioBuffer<float>& buffer)
|
||||
{
|
||||
juce::MemoryBlock memoryBlock;
|
||||
|
||||
for (int channel = 0; channel < buffer.getNumChannels(); ++channel)
|
||||
{
|
||||
memoryBlock.append(buffer.getReadPointer(channel), buffer.getNumSamples() * sizeof(float));
|
||||
}
|
||||
|
||||
juce::MD5 md5{ memoryBlock };
|
||||
return md5.toHexString();
|
||||
}
|
||||
243
Source/Utilities/ComponentUtils.h
Normal file
243
Source/Utilities/ComponentUtils.h
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
CustomComponent.h
|
||||
Created: 26 Sep 2023 11:49:53pm
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
#include "../CustomLookAndFeel.h"
|
||||
|
||||
// I think I can justify using this one globally. Note that using an entire namespace globally is generally frowned upon.
|
||||
using APVTS = juce::AudioProcessorValueTreeState;
|
||||
|
||||
/** This class is for components which hold a custom help text display.
|
||||
The idea is for the Editor to react to mouse move and drag events on all child components. In addition, children can send
|
||||
help text changed updates to the Editor when necessary.
|
||||
*/
|
||||
class CustomHelpTextDisplay
|
||||
{
|
||||
public:
|
||||
virtual ~CustomHelpTextDisplay() = default;
|
||||
virtual void helpTextChanged(const juce::String& newText) = 0;
|
||||
};
|
||||
|
||||
/** JUCE's default help text functionality is not adequate for the help text label I want to display.
|
||||
This class helps add custom help text functionality to a component.
|
||||
*/
|
||||
class CustomHelpTextProvider
|
||||
{
|
||||
public:
|
||||
explicit CustomHelpTextProvider(juce::Component* associatedComponent, CustomHelpTextDisplay* helpTextDisplay = nullptr) :
|
||||
component(associatedComponent),
|
||||
textDisplay(helpTextDisplay) {}
|
||||
|
||||
virtual ~CustomHelpTextProvider() = default;
|
||||
|
||||
virtual juce::String getCustomHelpText()
|
||||
{
|
||||
return component->getHelpText();
|
||||
}
|
||||
|
||||
virtual void setCustomHelpText(const juce::String& helpText)
|
||||
{
|
||||
bool changed = helpText != component->getHelpText();
|
||||
component->setHelpText(helpText);
|
||||
if (changed)
|
||||
sendHelpTextUpdate();
|
||||
}
|
||||
|
||||
void sendHelpTextUpdate(bool checkMouseOver = true)
|
||||
{
|
||||
if ((component->isMouseOverOrDragging() || !checkMouseOver) && textDisplay)
|
||||
textDisplay->helpTextChanged(getCustomHelpText());
|
||||
}
|
||||
|
||||
private:
|
||||
juce::Component* component{ nullptr };
|
||||
CustomHelpTextDisplay* textDisplay{ nullptr };
|
||||
};
|
||||
|
||||
/** This is a way to store the theme in the top level component of our plugin */
|
||||
class ThemeProvider
|
||||
{
|
||||
public:
|
||||
virtual ~ThemeProvider() = default;
|
||||
virtual Colors getTheme() const = 0;
|
||||
virtual void setTheme(Colors newTheme) = 0;
|
||||
};
|
||||
|
||||
/** It's nice to have my own base class when I need to add broader functionality. */
|
||||
class CustomComponent : public virtual juce::Component, public CustomHelpTextProvider
|
||||
{
|
||||
public:
|
||||
explicit CustomComponent(CustomHelpTextDisplay* helpTextDisplay = nullptr) : CustomHelpTextProvider(this, helpTextDisplay), lnf(dynamic_cast<CustomLookAndFeel&>(getLookAndFeel()))
|
||||
{
|
||||
}
|
||||
|
||||
/** Easy way to get a disabled version of a color */
|
||||
juce::Colour disabled(juce::Colour color, bool disabledCondition) const
|
||||
{
|
||||
return disabledCondition ? color.withMultipliedAlpha(0.5f) : color;
|
||||
}
|
||||
|
||||
/** Easy way to get a disabled version of a color */
|
||||
juce::Colour disabled(juce::Colour color) const
|
||||
{
|
||||
return disabled(color, !isEnabled());
|
||||
}
|
||||
|
||||
/** In situations where a callback can be called on the wrong thread, component methods cannot be safely used. This method wraps repaint in
|
||||
an async call with a SafePointer to make sure nothing bad happens.
|
||||
*/
|
||||
void safeRepaint()
|
||||
{
|
||||
juce::MessageManager::callAsync([p = juce::Component::SafePointer(this)] {
|
||||
if (p.getComponent())
|
||||
p->repaint();
|
||||
});
|
||||
}
|
||||
|
||||
Colors getTheme()
|
||||
{
|
||||
if (themeProvider)
|
||||
return themeProvider->getTheme();
|
||||
|
||||
juce::Component* c{ this };
|
||||
while (c != nullptr)
|
||||
{
|
||||
if (auto* tp = dynamic_cast<ThemeProvider*>(c))
|
||||
{
|
||||
themeProvider = tp;
|
||||
break;
|
||||
}
|
||||
|
||||
c = c->getParentComponent();
|
||||
}
|
||||
|
||||
return themeProvider ? themeProvider->getTheme() : defaultTheme;
|
||||
}
|
||||
|
||||
CustomLookAndFeel& lnf;
|
||||
|
||||
private:
|
||||
ThemeProvider* themeProvider{ nullptr };
|
||||
};
|
||||
|
||||
/** Simple mixin class to set a mouse cursor on a component when enabled only. */
|
||||
class EnabledMouseCursor
|
||||
{
|
||||
public:
|
||||
void setEnabledMouseCursor(const juce::MouseCursor& cursor, const bool use = true)
|
||||
{
|
||||
enabledMouseCursor = cursor;
|
||||
useEnabledCursor = use;
|
||||
}
|
||||
|
||||
void enablementChanged(juce::Component& component) const
|
||||
{
|
||||
if (useEnabledCursor)
|
||||
{
|
||||
if (component.isEnabled())
|
||||
component.setMouseCursor(enabledMouseCursor);
|
||||
else
|
||||
component.setMouseCursor(juce::MouseCursor::NormalCursor);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
juce::MouseCursor enabledMouseCursor{ juce::MouseCursor::NoCursor };
|
||||
bool useEnabledCursor{ false };
|
||||
};
|
||||
|
||||
/** This is a rotary slider that includes custom help text functionality. */
|
||||
class CustomRotary final : public juce::Slider, public CustomHelpTextProvider
|
||||
{
|
||||
public:
|
||||
explicit CustomRotary(SliderStyle style = LinearHorizontal, TextEntryBoxPosition textBoxPosition = NoTextBox) : Slider(style, textBoxPosition), CustomHelpTextProvider(this) {}
|
||||
|
||||
juce::String getCustomHelpText() override
|
||||
{
|
||||
auto numString = getTextFromValue(getValue());
|
||||
if (getProperties().contains(ComponentProps::ROTARY_GREATER_UNIT) && getValue() >= 1000.)
|
||||
numString += " " + getProperties()[ComponentProps::ROTARY_GREATER_UNIT].toString();
|
||||
else if (getProperties().contains(ComponentProps::ROTARY_UNIT))
|
||||
numString += " " + getProperties()[ComponentProps::ROTARY_UNIT].toString();
|
||||
|
||||
if (getProperties().contains(ComponentProps::ROTARY_PARAMETER_NAME))
|
||||
numString = getProperties()[ComponentProps::ROTARY_PARAMETER_NAME].toString() + ": " + numString;
|
||||
|
||||
return numString;
|
||||
}
|
||||
};
|
||||
|
||||
/** Helper class to expose the parameter name to the attached rotary */
|
||||
class CustomRotaryAttachment final : public APVTS::SliderAttachment
|
||||
{
|
||||
public:
|
||||
CustomRotaryAttachment(APVTS& apvts, const juce::String& parameterID, juce::Slider& rotary) : SliderAttachment(apvts, parameterID, rotary)
|
||||
{
|
||||
rotary.getProperties().set(ComponentProps::ROTARY_PARAMETER_NAME, parameterID);
|
||||
|
||||
// While we use textFromValueFunction in the parameter to expose that to the host, we don't want this
|
||||
// to show in the rotaries textbox.
|
||||
float parameterInterval = apvts.getParameter(parameterID)->getNormalisableRange().interval;
|
||||
rotary.textFromValueFunction = [parameterInterval](double v)
|
||||
{
|
||||
int numDecimalPlaces = juce::String{ int(1.f / parameterInterval) }.length() - 1;
|
||||
return juce::String(v, numDecimalPlaces);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
class CustomLabel final : public juce::Label, public EnabledMouseCursor
|
||||
{
|
||||
void enablementChanged() override
|
||||
{
|
||||
EnabledMouseCursor::enablementChanged(*this);
|
||||
}
|
||||
};
|
||||
|
||||
/** This includes some utility methods for dealing with selection on a component.
|
||||
T is intended to be an enum of different parts of the component where the first enum value is NONE.
|
||||
See FilterResponse for a clear example of how these are used.
|
||||
*/
|
||||
template <typename T>
|
||||
struct CompPart
|
||||
{
|
||||
T part;
|
||||
juce::Rectangle<float> area;
|
||||
int priority;
|
||||
|
||||
CompPart(T part, juce::Rectangle<float> area, int priority) : part(part), area(area), priority(priority) {}
|
||||
|
||||
float distanceTo(int x, int y) const
|
||||
{
|
||||
float dx = juce::jmax<float>(area.getX() - x, 0, x - area.getRight());
|
||||
float dy = juce::jmax<float>(area.getY() - y, 0, y - area.getBottom());
|
||||
return std::sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
static T getClosestInRange(juce::Array<CompPart<T>> targets, int x, int y, int snapAmount=0)
|
||||
{
|
||||
T closest = static_cast<T>(0);
|
||||
auto priority = -1;
|
||||
auto closestDistance = INFINITY;
|
||||
for (const auto& p : targets)
|
||||
{
|
||||
auto distance = p.distanceTo(x, y);
|
||||
if (((distance < closestDistance && p.priority == priority) || p.priority > priority) && distance <= snapAmount)
|
||||
{
|
||||
closest = p.part;
|
||||
priority = p.priority;
|
||||
closestDistance = distance;
|
||||
}
|
||||
}
|
||||
return closest;
|
||||
}
|
||||
};
|
||||
220
Source/Utilities/DeviceRecorder.h
Normal file
220
Source/Utilities/DeviceRecorder.h
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
DeviceRecorder.h
|
||||
Created: 11 May 2024 12:09:24pm
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
|
||||
#include <readerwriterqueue.h>
|
||||
|
||||
/** A struct to represent a change in the recording buffer. This is used to update the UI with the latest recording data. */
|
||||
struct RecordingBufferChange
|
||||
{
|
||||
enum RecordingBufferChangeType
|
||||
{
|
||||
CLEAR,
|
||||
ADD
|
||||
};
|
||||
|
||||
explicit RecordingBufferChange(RecordingBufferChangeType type, const juce::AudioBuffer<float>& buffer = {}) :
|
||||
type(type),
|
||||
addedBuffer(buffer)
|
||||
{
|
||||
}
|
||||
|
||||
RecordingBufferChangeType type;
|
||||
juce::AudioBuffer<float> addedBuffer;
|
||||
};
|
||||
|
||||
/** A listener interface for the DeviceRecorder class. */
|
||||
class DeviceRecorderListener
|
||||
{
|
||||
public:
|
||||
DeviceRecorderListener() = default;
|
||||
virtual ~DeviceRecorderListener() = default;
|
||||
|
||||
/** A callback once a recording has started. */
|
||||
virtual void recordingStarted() = 0;
|
||||
|
||||
/** A callback once a recording has finished.
|
||||
This will be called if the user or device manager stops the recording.
|
||||
The recording buffer will be passed to the listener, along with the sample rate of the recording.
|
||||
This is called on the message thread.
|
||||
*/
|
||||
virtual void recordingFinished(juce::AudioBuffer<float> recording, int recordingSampleRate) = 0;
|
||||
};
|
||||
|
||||
using RecordingQueue = moodycamel::ReaderWriterQueue<RecordingBufferChange, 16384>;
|
||||
|
||||
/** This class provides a convenient encapsulation for device recording logic.
|
||||
Due to the reliance on callbacks from the AudioDeviceManager, a user of the class cannot directly start or stop
|
||||
the recording process.
|
||||
*/
|
||||
class DeviceRecorder final : public juce::AudioIODeviceCallback
|
||||
{
|
||||
public:
|
||||
explicit DeviceRecorder(juce::AudioDeviceManager& deviceManager) : deviceManager(deviceManager)
|
||||
{
|
||||
deviceManager.addAudioCallback(this);
|
||||
}
|
||||
|
||||
~DeviceRecorder() override
|
||||
{
|
||||
deviceManager.removeAudioCallback(this);
|
||||
}
|
||||
|
||||
void addListener(DeviceRecorderListener* listener)
|
||||
{
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
void removeListener(DeviceRecorderListener* listener)
|
||||
{
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
/** Start recording audio from the active device, set shouldRecordToQueue to false if you don't need the queue for UI updates */
|
||||
void startRecording(bool shouldRecordToQueue = true)
|
||||
{
|
||||
this->recordToQueue = shouldRecordToQueue;
|
||||
shouldRecord = true;
|
||||
}
|
||||
|
||||
void stopRecording()
|
||||
{
|
||||
shouldRecord = false;
|
||||
}
|
||||
|
||||
/** Check if the device is currently recording */
|
||||
bool isRecordingDevice() const
|
||||
{
|
||||
return isRecording;
|
||||
}
|
||||
|
||||
/** This indicates whether the device is trying to record or not.
|
||||
Essentially, this can be controlled synchronously while isRecording is controlled asynchronously.
|
||||
*/
|
||||
bool shouldRecordDevice() const
|
||||
{
|
||||
return shouldRecord;
|
||||
}
|
||||
|
||||
/** Retrieve a queue of recording updates, which can be used to update the UI */
|
||||
RecordingQueue& getRecordingBufferQueue()
|
||||
{
|
||||
return recordingBufferQueue;
|
||||
}
|
||||
|
||||
private:
|
||||
void audioDeviceIOCallbackWithContext(const float* const* inputChannelData, int numInputChannels, float* const* /* outputChannelData */, int /* numOutputChannels */, int numSamples, const juce::AudioIODeviceCallbackContext&) override
|
||||
{
|
||||
if (shouldRecord)
|
||||
{
|
||||
if (!isRecording)
|
||||
{
|
||||
isRecording = true;
|
||||
recordingBufferList.clear();
|
||||
if (recordToQueue)
|
||||
recordingBufferQueue.emplace(RecordingBufferChange::CLEAR);
|
||||
recordingSize = 0;
|
||||
listeners.call(&DeviceRecorderListener::recordingStarted);
|
||||
}
|
||||
if (isRecording && numInputChannels && numSamples)
|
||||
{
|
||||
accumulatingRecordingBuffer.setSize(
|
||||
accumulatingRecordingSize == 0 ? numInputChannels : juce::jmin<int>(numInputChannels, accumulatingRecordingBuffer.getNumChannels()),
|
||||
juce::jmax<int>(accumulatingRecordingBuffer.getNumSamples(), accumulatingRecordingSize + numSamples), true);
|
||||
for (int i = 0; i < accumulatingRecordingBuffer.getNumChannels(); i++)
|
||||
accumulatingRecordingBuffer.copyFrom(i, accumulatingRecordingSize, inputChannelData[i], numSamples);
|
||||
accumulatingRecordingSize += numSamples;
|
||||
|
||||
const int accumulatingMax = recordingSampleRate / PluginParameters::FRAME_RATE;
|
||||
if (accumulatingRecordingSize > accumulatingMax)
|
||||
{
|
||||
flushAccumulatedBuffer();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (isRecording)
|
||||
{
|
||||
recordingFinished();
|
||||
}
|
||||
}
|
||||
|
||||
void audioDeviceAboutToStart(juce::AudioIODevice* device) override
|
||||
{
|
||||
recordingSampleRate = int(device->getCurrentSampleRate());
|
||||
}
|
||||
|
||||
void audioDeviceStopped() override
|
||||
{
|
||||
if (isRecording)
|
||||
{
|
||||
recordingFinished();
|
||||
}
|
||||
}
|
||||
|
||||
void recordingFinished()
|
||||
{
|
||||
if (recordingBufferList.empty())
|
||||
return;
|
||||
|
||||
if (accumulatingRecordingSize > 0)
|
||||
flushAccumulatedBuffer();
|
||||
|
||||
int numChannels = recordingBufferList[0]->getNumChannels();
|
||||
int size = 0;
|
||||
juce::AudioBuffer<float> recordingBuffer{ numChannels, recordingSize };
|
||||
for (const auto& i : recordingBufferList)
|
||||
{
|
||||
numChannels = juce::jmin<int>(numChannels, i->getNumChannels());
|
||||
recordingBuffer.setSize(numChannels, recordingBuffer.getNumSamples()); // This will only potentially decrease the channel count
|
||||
for (int ch = 0; ch < numChannels; ch++)
|
||||
recordingBuffer.copyFrom(ch, size, *i, ch, 0, i->getNumSamples());
|
||||
size += i->getNumSamples();
|
||||
}
|
||||
|
||||
isRecording = false;
|
||||
listeners.call(&DeviceRecorderListener::recordingFinished, std::move(recordingBuffer), recordingSampleRate);
|
||||
}
|
||||
|
||||
/** Flush the accumulating recording buffer to the recording buffer list.
|
||||
We accumulate samples before pushing to the list so that the UI thread doesn't get
|
||||
outpaced.
|
||||
*/
|
||||
void flushAccumulatedBuffer()
|
||||
{
|
||||
int numChannels = accumulatingRecordingBuffer.getNumChannels();
|
||||
auto recordingBuffer = std::make_unique<juce::AudioBuffer<float>>(numChannels, accumulatingRecordingSize);
|
||||
for (int i = 0; i < numChannels; i++)
|
||||
recordingBuffer->copyFrom(i, 0, accumulatingRecordingBuffer, i, 0, accumulatingRecordingSize);
|
||||
if (recordToQueue)
|
||||
recordingBufferQueue.emplace(RecordingBufferChange::ADD, *recordingBuffer);
|
||||
recordingBufferList.emplace_back(std::move(recordingBuffer));
|
||||
recordingSize += accumulatingRecordingSize;
|
||||
accumulatingRecordingSize = 0;
|
||||
}
|
||||
|
||||
juce::LightweightListenerList<DeviceRecorderListener> listeners;
|
||||
juce::AudioDeviceManager& deviceManager;
|
||||
|
||||
bool shouldRecord{ false };
|
||||
bool isRecording{ false };
|
||||
int recordingSampleRate{ 0 };
|
||||
int recordingSize{ 0 };
|
||||
|
||||
juce::AudioBuffer<float> accumulatingRecordingBuffer; // Necessary since the GUI thread cannot keep up unless the callbacks are accumulated
|
||||
int accumulatingRecordingSize{ 0 };
|
||||
std::vector<std::unique_ptr<juce::AudioBuffer<float>>> recordingBufferList;
|
||||
|
||||
bool recordToQueue{ true };
|
||||
#pragma warning(disable: 4324) // structure was padded due to __declspec(align())
|
||||
RecordingQueue recordingBufferQueue{ 10 };
|
||||
};
|
||||
232
Source/Utilities/ListenableValue.h
Normal file
232
Source/Utilities/ListenableValue.h
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
ListenableAtomic.h
|
||||
Created: 3 Jun 2024 11:50:52pm
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <vector>
|
||||
|
||||
#include "../PluginParameters.h"
|
||||
|
||||
template <typename T>
|
||||
class ListenableValue;
|
||||
|
||||
/** A listener for a ListenableAtomic or a ListenableMutex */
|
||||
template <typename T>
|
||||
class ValueListener
|
||||
{
|
||||
public:
|
||||
virtual ~ValueListener() = default;
|
||||
virtual void valueChanged(ListenableValue<T>& source, T newValue) = 0;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class ListenableValue
|
||||
{
|
||||
public:
|
||||
virtual ~ListenableValue() = default;
|
||||
|
||||
virtual T load() const = 0;
|
||||
virtual ListenableValue& store(T newValue) = 0;
|
||||
|
||||
virtual void addListener(ValueListener<T>* listener) = 0;
|
||||
virtual void removeListener(ValueListener<T>* listener) = 0;
|
||||
|
||||
virtual ListenableValue& operator=(T newValue) = 0;
|
||||
};
|
||||
|
||||
/** A thread-safe atomic value that can be listened to. */
|
||||
template <typename T>
|
||||
class ListenableAtomic final : public ListenableValue<T>
|
||||
{
|
||||
public:
|
||||
ListenableAtomic() : value() {}
|
||||
|
||||
ListenableAtomic(T initial) : value(initial) {}
|
||||
|
||||
ListenableAtomic& store(T newValue) override
|
||||
{
|
||||
value = newValue;
|
||||
for (auto listener : listeners)
|
||||
{
|
||||
listener->valueChanged(*this, newValue);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
T load() const override
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
void addListener(ValueListener<T>* listener) override
|
||||
{
|
||||
listeners.push_back(listener);
|
||||
}
|
||||
|
||||
void removeListener(ValueListener<T>* listener) override
|
||||
{
|
||||
listeners.erase(std::remove(listeners.begin(), listeners.end(), listener), listeners.end());
|
||||
}
|
||||
|
||||
ListenableAtomic& operator=(T newValue) override
|
||||
{
|
||||
return store(newValue);
|
||||
}
|
||||
|
||||
operator T()
|
||||
{
|
||||
return load();
|
||||
}
|
||||
|
||||
bool operator==(const ListenableAtomic& other) const
|
||||
{
|
||||
return &other == this;
|
||||
}
|
||||
|
||||
bool operator==(T newValue) const
|
||||
{
|
||||
return newValue == this->value;
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic<T> value;
|
||||
std::vector<ValueListener<T>*> listeners;
|
||||
};
|
||||
|
||||
/** A thread-safe value that uses locks, for use when atomic operations are not possible. */
|
||||
template <typename T>
|
||||
class ListenableMutex final : public ListenableValue<T>
|
||||
{
|
||||
public:
|
||||
ListenableMutex() : value() {}
|
||||
|
||||
ListenableMutex(T initial) : value(initial) {}
|
||||
|
||||
ListenableMutex& store(T newValue) override
|
||||
{
|
||||
{ // Lock scope
|
||||
juce::ScopedLock lock(mutex);
|
||||
value = newValue;
|
||||
}
|
||||
for (auto listener : listeners)
|
||||
{
|
||||
listener->valueChanged(*this, newValue);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
T load() const override
|
||||
{
|
||||
juce::ScopedLock lock(mutex);
|
||||
return value;
|
||||
}
|
||||
|
||||
void addListener(ValueListener<T>* listener) override
|
||||
{
|
||||
listeners.push_back(listener);
|
||||
}
|
||||
|
||||
void removeListener(ValueListener<T>* listener) override
|
||||
{
|
||||
listeners.erase(std::remove(listeners.begin(), listeners.end(), listener), listeners.end());
|
||||
}
|
||||
|
||||
ListenableMutex& operator=(T newValue) override
|
||||
{
|
||||
return store(newValue);
|
||||
}
|
||||
|
||||
operator T()
|
||||
{
|
||||
return load();
|
||||
}
|
||||
|
||||
bool operator==(const ListenableMutex& other) const
|
||||
{
|
||||
return &other == this;
|
||||
}
|
||||
|
||||
bool operator==(T newValue) const
|
||||
{
|
||||
return newValue == this->value;
|
||||
}
|
||||
|
||||
private:
|
||||
juce::CriticalSection mutex;
|
||||
T value;
|
||||
std::vector<ValueListener<T>*> listeners;
|
||||
};
|
||||
|
||||
/** A dummy parameter that can be used to notify the host of state updates that it otherwise has no knowledge of.
|
||||
This allows for undo/redos of otherwise hidden parameters, tested on Reaper.
|
||||
*/
|
||||
class UIDummyParam final
|
||||
{
|
||||
public:
|
||||
explicit UIDummyParam(const juce::AudioProcessorValueTreeState& apvts, const juce::String& dummyParamID) :
|
||||
dummyParam(apvts.getParameter(dummyParamID))
|
||||
{
|
||||
}
|
||||
|
||||
void sendUIUpdate() const
|
||||
{
|
||||
if (pluginHostType.isLogic() || pluginHostType.isGarageBand() || pluginHostType.isMainStage())
|
||||
return; // Logic implements undo/redo differently
|
||||
|
||||
dummyParam->beginChangeGesture();
|
||||
dummyParam->setValueNotifyingHost(1.f - dummyParam->getValue());
|
||||
dummyParam->endChangeGesture();
|
||||
}
|
||||
|
||||
private:
|
||||
juce::RangedAudioParameter* dummyParam{ nullptr };
|
||||
juce::PluginHostType pluginHostType;
|
||||
};
|
||||
|
||||
/** An attachment connecting juce::Button to a ListenableValue<bool>. */
|
||||
class ToggleButtonAttachment final : public ValueListener<bool>, public juce::Button::Listener
|
||||
{
|
||||
public:
|
||||
ToggleButtonAttachment(juce::Button& toggleButton, ListenableValue<bool>& listenableValue, UIDummyParam* dummyParam = nullptr) :
|
||||
button(toggleButton), value(listenableValue), hostDummy(dummyParam)
|
||||
{
|
||||
button.setToggleState(value.load(), juce::dontSendNotification);
|
||||
button.addListener(this);
|
||||
value.addListener(this);
|
||||
}
|
||||
|
||||
~ToggleButtonAttachment() override
|
||||
{
|
||||
button.removeListener(this);
|
||||
value.removeListener(this);
|
||||
}
|
||||
|
||||
private:
|
||||
void buttonClicked(juce::Button*) override
|
||||
{
|
||||
if (button.getToggleState() != value.load())
|
||||
{
|
||||
value = button.getToggleState();
|
||||
if (hostDummy != nullptr)
|
||||
hostDummy->sendUIUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
void valueChanged(ListenableValue<bool>& /* source */, bool newValue) override
|
||||
{
|
||||
button.setToggleState(newValue, juce::sendNotificationSync);
|
||||
}
|
||||
|
||||
juce::Button& button;
|
||||
ListenableValue<bool>& value;
|
||||
|
||||
UIDummyParam* hostDummy{ nullptr };
|
||||
};
|
||||
80
Source/Utilities/PitchDetector.h
Normal file
80
Source/Utilities/PitchDetector.h
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
PitchDetector.h
|
||||
Created: 27 Dec 2023 2:51:29pm
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
#include <leaf.h>
|
||||
#include <leaf-analysis.h>
|
||||
|
||||
/* Uses the LEAF library to detect pitch in an audio buffer */
|
||||
class PitchDetector final : public juce::Thread
|
||||
{
|
||||
public:
|
||||
PitchDetector() : Thread("Pitch_Detector")
|
||||
{
|
||||
}
|
||||
|
||||
~PitchDetector() override
|
||||
{
|
||||
stopThread(1000);
|
||||
}
|
||||
|
||||
void setData(juce::AudioBuffer<float>& buffer, int startSample, int endSample, double audioSampleRate)
|
||||
{
|
||||
audioBuffer = &buffer;
|
||||
sampleStart = startSample;
|
||||
sampleEnd = endSample;
|
||||
sampleRate = audioSampleRate;
|
||||
}
|
||||
|
||||
// Inherited via Thread
|
||||
void run() override
|
||||
{
|
||||
if (!audioBuffer)
|
||||
return;
|
||||
|
||||
detectPitch();
|
||||
signalThreadShouldExit();
|
||||
}
|
||||
|
||||
void detectPitch()
|
||||
{
|
||||
int numSamples = sampleEnd - sampleStart + 1;
|
||||
|
||||
LEAF leaf;
|
||||
tPitchDetector pitchDetector;
|
||||
|
||||
constexpr size_t memorySize = 5000;
|
||||
char memory[memorySize];
|
||||
LEAF_init(&leaf, float(sampleRate), memory, memorySize, []() -> float { return juce::Random().nextFloat(); });
|
||||
tPitchDetector_init(&pitchDetector, 50, 20000, &leaf);
|
||||
for (int i = 0; i < numSamples; i++)
|
||||
{
|
||||
bool ready = audioBuffer->getNumChannels() == 2 ?
|
||||
tPitchDetector_tick(&pitchDetector, (audioBuffer->getSample(0, sampleStart + i) + audioBuffer->getSample(1, sampleStart + i)) / 2.f) :
|
||||
tPitchDetector_tick(&pitchDetector, audioBuffer->getSample(0, sampleStart + i));
|
||||
if (ready)
|
||||
break;
|
||||
}
|
||||
pitch = double(tPitchDetector_predictFrequency(&pitchDetector));
|
||||
tPitchDetector_free(&pitchDetector);
|
||||
}
|
||||
|
||||
double getPitch() const
|
||||
{
|
||||
return pitch;
|
||||
}
|
||||
|
||||
private:
|
||||
juce::AudioBuffer<float>* audioBuffer{ nullptr };
|
||||
int sampleStart{ 0 }, sampleEnd{ 0 };
|
||||
double sampleRate{ 0. };
|
||||
double pitch{ 0 };
|
||||
};
|
||||
131
Source/Utilities/Reaper/ReaperVST3Extensions.cpp
Normal file
131
Source/Utilities/Reaper/ReaperVST3Extensions.cpp
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
ReaperVST3Extensions.cpp
|
||||
Created: 14 May 2025 9:43:44pm
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#include "ReaperVST3Extensions.h"
|
||||
|
||||
namespace reaper
|
||||
{
|
||||
DEF_CLASS_IID(IReaperHostApplication)
|
||||
}
|
||||
|
||||
juce::String ReaperVST3Extensions::getNamedConfigParam(const juce::String& paramName) const
|
||||
{
|
||||
if (GetSetMediaTrackInfo_String == nullptr || GetTrack == nullptr || trackIndex < 0)
|
||||
return {};
|
||||
|
||||
MediaTrack* track = GetTrack(nullptr, trackIndex);
|
||||
|
||||
if (track == nullptr)
|
||||
return {};
|
||||
|
||||
// If our track index has not yet been updated, exit early
|
||||
if (GetTrackGUID(track) != trackGUID)
|
||||
return {};
|
||||
|
||||
constexpr int buffSize = 512;
|
||||
char buffer[buffSize] = {};
|
||||
|
||||
bool success = GetSetMediaTrackInfo_String(track, paramName.getCharPointer(), buffer, false);
|
||||
|
||||
if (!success || buffer[0] == 0)
|
||||
return {};
|
||||
|
||||
return juce::String::fromUTF8(buffer);
|
||||
}
|
||||
|
||||
void ReaperVST3Extensions::setNamedConfigParam(const juce::String& paramName, const juce::String& value) const
|
||||
{
|
||||
if (GetSetMediaTrackInfo_String == nullptr || GetTrack == nullptr || trackIndex < 0)
|
||||
return;
|
||||
|
||||
MediaTrack* track = GetTrack(nullptr, trackIndex);
|
||||
|
||||
if (track == nullptr)
|
||||
return;
|
||||
|
||||
// If our track index has not yet been updated, exit early
|
||||
if (GetTrackGUID(track) != trackGUID)
|
||||
return;
|
||||
|
||||
GetSetMediaTrackInfo_String(track, paramName.getCharPointer(), value.getCharPointer().getAddress(), true);
|
||||
}
|
||||
|
||||
void ReaperVST3Extensions::setIHostApplication(Steinberg::FUnknown* ptr)
|
||||
{
|
||||
if (ptr == nullptr)
|
||||
return;
|
||||
|
||||
void* rawInterface = nullptr;
|
||||
|
||||
if (ptr->queryInterface(reaper::IReaperHostApplication::iid, &rawInterface) == Steinberg::kResultOk)
|
||||
{
|
||||
if (void* fnPtr = static_cast<reaper::IReaperHostApplication*> (rawInterface)->getReaperApi("GetSetMediaTrackInfo_String"))
|
||||
{
|
||||
GetSetMediaTrackInfo_String = reinterpret_cast<bool (*)(MediaTrack*, const char*, char*, bool)>(fnPtr);
|
||||
}
|
||||
|
||||
if (void* fnPtr = static_cast<reaper::IReaperHostApplication*> (rawInterface)->getReaperApi("GetTrack"))
|
||||
{
|
||||
GetTrack = reinterpret_cast<MediaTrack * (*)(ReaProject*, int)>(fnPtr);
|
||||
}
|
||||
|
||||
if (void* fnPtr = static_cast<reaper::IReaperHostApplication*> (rawInterface)->getReaperApi("GetTrackGUID"))
|
||||
{
|
||||
GetTrackGUID = reinterpret_cast<GUID * (*)(MediaTrack*)>(fnPtr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int32_t ReaperVST3Extensions::queryIEditController(const Steinberg::TUID string, void** obj)
|
||||
{
|
||||
#if JAS_VST3_REAPER_INTEGRATION
|
||||
if (obj == nullptr)
|
||||
return -1;
|
||||
|
||||
if (Steinberg::FUnknownPrivate::iidEqual(string, Steinberg::Vst::ChannelContext::IInfoListener::iid))
|
||||
{
|
||||
*obj = static_cast<Steinberg::Vst::ChannelContext::IInfoListener*>(this);
|
||||
return 0;
|
||||
}
|
||||
|
||||
*obj = nullptr;
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
|
||||
Steinberg::tresult ReaperVST3Extensions::queryInterface(const Steinberg::TUID /*_iid*/, void** /*obj*/)
|
||||
{
|
||||
return Steinberg::kNoInterface;
|
||||
}
|
||||
|
||||
Steinberg::uint32 ReaperVST3Extensions::addRef()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
Steinberg::uint32 ReaperVST3Extensions::release()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
Steinberg::tresult ReaperVST3Extensions::setChannelContextInfos(Steinberg::Vst::IAttributeList* list)
|
||||
{
|
||||
if (!list || GetTrackGUID == nullptr)
|
||||
return Steinberg::kResultFalse;
|
||||
|
||||
Steinberg::int64 idx = -1;
|
||||
if (list->getInt(Steinberg::Vst::ChannelContext::kChannelIndexKey, idx) == Steinberg::kResultTrue)
|
||||
{
|
||||
trackIndex = static_cast<int>(idx) - 1;
|
||||
trackGUID = GetTrackGUID(GetTrack(nullptr, trackIndex));
|
||||
}
|
||||
|
||||
return Steinberg::kResultOk;
|
||||
}
|
||||
56
Source/Utilities/Reaper/ReaperVST3Extensions.h
Normal file
56
Source/Utilities/Reaper/ReaperVST3Extensions.h
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
ReaperVST3Extensions.h
|
||||
Created: 14 May 2025 9:38:25pm
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
|
||||
#include <reaper_plugin.h>
|
||||
#include "pluginterfaces/base/funknown.h"
|
||||
#include "pluginterfaces/vst/ivstchannelcontextinfo.h"
|
||||
|
||||
namespace reaper
|
||||
{
|
||||
using namespace Steinberg;
|
||||
using INT_PTR = juce::pointer_sized_int;
|
||||
using uint32 = Steinberg::uint32;
|
||||
|
||||
#include <reaper_vst3_interfaces.h>
|
||||
}
|
||||
|
||||
class ReaperVST3Extensions final : public juce::VST3ClientExtensions, public Steinberg::Vst::ChannelContext::IInfoListener
|
||||
{
|
||||
public:
|
||||
ReaperVST3Extensions() = default;
|
||||
~ReaperVST3Extensions() override = default;
|
||||
|
||||
juce::String getNamedConfigParam(const juce::String& paramName) const;
|
||||
void setNamedConfigParam(const juce::String& paramName, const juce::String& value) const;
|
||||
|
||||
void setIHostApplication(Steinberg::FUnknown* ptr) override;
|
||||
int32_t queryIEditController(const Steinberg::TUID, void** obj) override;
|
||||
|
||||
Steinberg::tresult PLUGIN_API setChannelContextInfos(Steinberg::Vst::IAttributeList* list) override;
|
||||
|
||||
Steinberg::tresult PLUGIN_API queryInterface(const Steinberg::TUID _iid, void** obj) override;
|
||||
Steinberg::uint32 PLUGIN_API addRef() override;
|
||||
Steinberg::uint32 PLUGIN_API release() override;
|
||||
|
||||
private:
|
||||
// https://github.com/justinfrankel/reaper-sdk/blob/main/sdk/reaper_plugin_functions.h
|
||||
bool (*GetSetMediaTrackInfo_String)(MediaTrack* tr, const char* parmname, char* stringNeedBig, bool setNewValue);
|
||||
MediaTrack* (*GetTrack)(ReaProject* proj, int trackidx) { nullptr };
|
||||
GUID* (*GetTrackGUID)(MediaTrack* tr) { nullptr };
|
||||
|
||||
GUID* trackGUID{ nullptr };
|
||||
int trackIndex{ -1 };
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ReaperVST3Extensions)
|
||||
};
|
||||
98
Source/Utilities/SampleLoader.h
Normal file
98
Source/Utilities/SampleLoader.h
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
SampleLoader.h
|
||||
Created: 14 Jun 2024 5:37:01pm
|
||||
Author: binya
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
|
||||
#include "BufferUtils.h"
|
||||
|
||||
/** A utility thread for asynchronously loading samples */
|
||||
class LoaderThread final : public juce::Thread
|
||||
{
|
||||
public:
|
||||
LoaderThread(std::unique_ptr<juce::AudioFormatReader> formatReader, int threadID) : Thread("Loader_Thread_" + juce::String(threadID)),
|
||||
reader{std::move(formatReader)}
|
||||
{
|
||||
}
|
||||
|
||||
~LoaderThread() override
|
||||
{
|
||||
stopThread(-1);
|
||||
}
|
||||
|
||||
void cancel()
|
||||
{
|
||||
canceled = true;
|
||||
}
|
||||
|
||||
juce::AudioBuffer<float>* releaseSample() { return newSample.release(); }
|
||||
juce::AudioFormatReader* releaseReader() { return reader.release(); }
|
||||
const juce::String& getLoadedSampleHash() const { return sampleHash; }
|
||||
|
||||
private:
|
||||
void run() override
|
||||
{
|
||||
newSample = std::make_unique<juce::AudioBuffer<float>>(int(reader->numChannels), int(reader->lengthInSamples));
|
||||
reader->read(&*newSample, 0, int(reader->lengthInSamples), 0, true, true);
|
||||
sampleHash = getSampleHash(*newSample);
|
||||
|
||||
if (canceled)
|
||||
newSample = nullptr;
|
||||
|
||||
signalThreadShouldExit();
|
||||
}
|
||||
|
||||
std::unique_ptr<juce::AudioFormatReader> reader;
|
||||
std::unique_ptr<juce::AudioBuffer<float>> newSample;
|
||||
juce::String sampleHash;
|
||||
bool canceled{ false };
|
||||
};
|
||||
|
||||
/** Asynchronously loads a sample */
|
||||
class SampleLoader final : public juce::Thread::Listener
|
||||
{
|
||||
public:
|
||||
void loadSample(std::unique_ptr<juce::AudioFormatReader> formatReader,
|
||||
const std::function<void(const std::unique_ptr<juce::AudioBuffer<float>>& loadedSample, const juce::String& sampleHash, const std::unique_ptr<juce::AudioFormatReader>& reader)>& onCompletion)
|
||||
{
|
||||
loading = true;
|
||||
completionCallback = onCompletion;
|
||||
for (const auto& thread : threads)
|
||||
{
|
||||
thread->cancel();
|
||||
thread->removeListener(this);
|
||||
}
|
||||
|
||||
auto newThread = std::make_unique<LoaderThread>(std::move(formatReader), ++ids);
|
||||
newThread->addListener(this);
|
||||
newThread->startThread();
|
||||
threads.emplace_back(std::move(newThread));
|
||||
}
|
||||
|
||||
bool isLoading() const { return loading; }
|
||||
|
||||
private:
|
||||
void exitSignalSent() override
|
||||
{
|
||||
juce::MessageManager::callAsync([this]() -> void {
|
||||
auto& lastThread = threads[threads.size() - 1];
|
||||
completionCallback(std::unique_ptr<juce::AudioBuffer<float>>(lastThread->releaseSample()), lastThread->getLoadedSampleHash(), std::unique_ptr<juce::AudioFormatReader>(lastThread->releaseReader()));
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
int ids{ 0 };
|
||||
std::vector<std::unique_ptr<LoaderThread>> threads;
|
||||
|
||||
std::function<void(std::unique_ptr<juce::AudioBuffer<float>>, const juce::String&, std::unique_ptr<juce::AudioFormatReader>)> completionCallback;
|
||||
bool loading{ false };
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue