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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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