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,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();
}

View 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;
}
};

View 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 };
};

View 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 };
};

View 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 };
};

View 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;
}

View 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)
};

View 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 };
};