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
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