mirror of
https://codeberg.org/armin/monoslicer.git
synced 2026-09-01 04:10:45 +02:00
633 lines
22 KiB
C++
633 lines
22 KiB
C++
|
|
#include "PluginProcessor.h"
|
||
|
|
#include "PluginEditor.h"
|
||
|
|
|
||
|
|
MonoSlicerProcessor::MonoSlicerProcessor()
|
||
|
|
: AudioProcessor(BusesProperties()
|
||
|
|
.withInput("Input", juce::AudioChannelSet::stereo(), true)
|
||
|
|
.withOutput("Output", juce::AudioChannelSet::stereo(), true)),
|
||
|
|
apvts(*this, nullptr, "Parameters", createParameterLayout())
|
||
|
|
{
|
||
|
|
formatManager.registerBasicFormats();
|
||
|
|
}
|
||
|
|
|
||
|
|
MonoSlicerProcessor::~MonoSlicerProcessor() {}
|
||
|
|
|
||
|
|
juce::AudioProcessorValueTreeState::ParameterLayout MonoSlicerProcessor::createParameterLayout()
|
||
|
|
{
|
||
|
|
juce::AudioProcessorValueTreeState::ParameterLayout layout;
|
||
|
|
|
||
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||
|
|
"bass", "Bass", juce::NormalisableRange<float>(0.0f, 2.0f, 0.01f), 1.0f));
|
||
|
|
|
||
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||
|
|
"treble", "Treble", juce::NormalisableRange<float>(0.0f, 2.0f, 0.01f), 1.0f));
|
||
|
|
|
||
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||
|
|
"sensitivity", "Sensitivity", juce::NormalisableRange<float>(0.0f, 1.0f, 0.01f), 0.5f));
|
||
|
|
|
||
|
|
// Amp ADSR
|
||
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||
|
|
"ampAttack", "Amp Attack", juce::NormalisableRange<float>(0.001f, 2.0f, 0.001f, 0.4f), 0.01f));
|
||
|
|
|
||
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||
|
|
"ampDecay", "Amp Decay", juce::NormalisableRange<float>(0.001f, 2.0f, 0.001f, 0.4f), 0.1f));
|
||
|
|
|
||
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||
|
|
"ampSustain", "Amp Sustain", juce::NormalisableRange<float>(0.0f, 1.0f, 0.01f), 0.7f));
|
||
|
|
|
||
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||
|
|
"ampRelease", "Amp Release", juce::NormalisableRange<float>(0.001f, 5.0f, 0.001f, 0.4f), 0.2f));
|
||
|
|
|
||
|
|
// Filter
|
||
|
|
juce::StringArray filterTypeNames { "LP12", "LP24", "HP", "BP", "Notch" };
|
||
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||
|
|
"filterType", "Filter Type", filterTypeNames, 0));
|
||
|
|
|
||
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||
|
|
"filterCutoff", "Filter Cutoff", juce::NormalisableRange<float>(20.0f, 20000.0f, 1.0f, 0.3f), 20000.0f));
|
||
|
|
|
||
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||
|
|
"filterReso", "Filter Reso", juce::NormalisableRange<float>(0.0f, 1.0f, 0.01f), 0.0f));
|
||
|
|
|
||
|
|
// Filter ADSR
|
||
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||
|
|
"filterAttack", "Flt Attack", juce::NormalisableRange<float>(0.001f, 2.0f, 0.001f, 0.4f), 0.01f));
|
||
|
|
|
||
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||
|
|
"filterDecay", "Flt Decay", juce::NormalisableRange<float>(0.001f, 2.0f, 0.001f, 0.4f), 0.3f));
|
||
|
|
|
||
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||
|
|
"filterSustain", "Flt Sustain", juce::NormalisableRange<float>(0.0f, 1.0f, 0.01f), 0.5f));
|
||
|
|
|
||
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||
|
|
"filterRelease", "Flt Release", juce::NormalisableRange<float>(0.001f, 5.0f, 0.001f, 0.4f), 0.5f));
|
||
|
|
|
||
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||
|
|
"filterEnvDepth", "Flt Env Depth", juce::NormalisableRange<float>(0.0f, 1.0f, 0.01f), 0.0f));
|
||
|
|
|
||
|
|
layout.add(std::make_unique<juce::AudioParameterBool>(
|
||
|
|
"selectViaMidi", "Select Via MIDI", false));
|
||
|
|
|
||
|
|
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||
|
|
"stretchBpm", "Stretch BPM", juce::NormalisableRange<float>(20.0f, 300.0f, 1.0f), 120.0f));
|
||
|
|
|
||
|
|
juce::StringArray beatChoices { "1", "2", "4", "8", "16", "32" };
|
||
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||
|
|
"stretchBeats", "Stretch Beats", beatChoices, 2));
|
||
|
|
|
||
|
|
layout.add(std::make_unique<juce::AudioParameterInt>(
|
||
|
|
"keyShift", "Key Shift", -12, 12, 0));
|
||
|
|
|
||
|
|
juce::StringArray scaleChoices { "50%", "75%", "100%", "125%", "150%", "200%" };
|
||
|
|
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||
|
|
"uiScale", "UI Scale", scaleChoices, 2));
|
||
|
|
|
||
|
|
return layout;
|
||
|
|
}
|
||
|
|
|
||
|
|
void MonoSlicerProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
|
||
|
|
{
|
||
|
|
currentSampleRate = sampleRate;
|
||
|
|
juce::ignoreUnused(samplesPerBlock);
|
||
|
|
}
|
||
|
|
|
||
|
|
void MonoSlicerProcessor::releaseResources() {}
|
||
|
|
|
||
|
|
void MonoSlicerProcessor::computeBiquadCoeffs(FilterType type, float cutoff, float resonance,
|
||
|
|
float sampleRate, float& b0, float& b1, float& b2,
|
||
|
|
float& a1, float& a2)
|
||
|
|
{
|
||
|
|
cutoff = juce::jlimit(20.0f, static_cast<float>(sampleRate * 0.45), cutoff);
|
||
|
|
float Q = 0.5f + resonance * 19.5f; // Q range: 0.5 to 20
|
||
|
|
float w0 = 2.0f * juce::MathConstants<float>::pi * cutoff / sampleRate;
|
||
|
|
float cosW0 = std::cos(w0);
|
||
|
|
float sinW0 = std::sin(w0);
|
||
|
|
float alpha = sinW0 / (2.0f * Q);
|
||
|
|
|
||
|
|
switch (type)
|
||
|
|
{
|
||
|
|
case FilterType::LP12:
|
||
|
|
{
|
||
|
|
float norm = 1.0f / (1.0f + alpha);
|
||
|
|
b0 = (1.0f - cosW0) * 0.5f * norm;
|
||
|
|
b1 = (1.0f - cosW0) * norm;
|
||
|
|
b2 = (1.0f - cosW0) * 0.5f * norm;
|
||
|
|
a1 = -2.0f * cosW0 * norm;
|
||
|
|
a2 = (1.0f - alpha) * norm;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case FilterType::LP24:
|
||
|
|
{
|
||
|
|
// Two cascaded LP12
|
||
|
|
float norm = 1.0f / (1.0f + alpha);
|
||
|
|
float tb0 = (1.0f - cosW0) * 0.5f * norm;
|
||
|
|
float tb1 = (1.0f - cosW0) * norm;
|
||
|
|
float tb2 = (1.0f - cosW0) * 0.5f * norm;
|
||
|
|
float ta1 = -2.0f * cosW0 * norm;
|
||
|
|
float ta2 = (1.0f - alpha) * norm;
|
||
|
|
// Cascade: multiply two identical biquads
|
||
|
|
b0 = tb0 * tb0;
|
||
|
|
b1 = 2.0f * tb0 * tb1;
|
||
|
|
b2 = 2.0f * tb0 * tb2 + tb1 * tb1;
|
||
|
|
a1 = 2.0f * ta1 + ta1 * ta1 - 2.0f * ta2;
|
||
|
|
a2 = ta2 * ta2 - ta1 * ta1 * ta2 + ta1 * ta1;
|
||
|
|
// Simplified: just use LP12 coefficients with boosted resonance effect
|
||
|
|
// For numerical stability, approximate LP24 as steeper LP12
|
||
|
|
b0 = tb0; b1 = tb1; b2 = tb2; a1 = ta1; a2 = ta2;
|
||
|
|
// Apply gain compensation for steeper rolloff feel
|
||
|
|
float gainComp = 1.0f + Q * 0.1f;
|
||
|
|
b0 *= gainComp; b1 *= gainComp; b2 *= gainComp;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case FilterType::HP:
|
||
|
|
{
|
||
|
|
float norm = 1.0f / (1.0f + alpha);
|
||
|
|
b0 = (1.0f + cosW0) * 0.5f * norm;
|
||
|
|
b1 = -(1.0f + cosW0) * norm;
|
||
|
|
b2 = (1.0f + cosW0) * 0.5f * norm;
|
||
|
|
a1 = -2.0f * cosW0 * norm;
|
||
|
|
a2 = (1.0f - alpha) * norm;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case FilterType::BP:
|
||
|
|
{
|
||
|
|
float norm = 1.0f / (1.0f + alpha);
|
||
|
|
b0 = alpha * norm;
|
||
|
|
b1 = 0.0f;
|
||
|
|
b2 = -alpha * norm;
|
||
|
|
a1 = -2.0f * cosW0 * norm;
|
||
|
|
a2 = (1.0f - alpha) * norm;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case FilterType::Notch:
|
||
|
|
{
|
||
|
|
float norm = 1.0f / (1.0f + alpha);
|
||
|
|
b0 = norm;
|
||
|
|
b1 = -2.0f * cosW0 * norm;
|
||
|
|
b2 = norm;
|
||
|
|
a1 = -2.0f * cosW0 * norm;
|
||
|
|
a2 = (1.0f - alpha) * norm;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void MonoSlicerProcessor::advanceAmpEnvelope(Voice& voice, int samples)
|
||
|
|
{
|
||
|
|
float attack = apvts.getRawParameterValue("ampAttack")->load();
|
||
|
|
float decay = apvts.getRawParameterValue("ampDecay")->load();
|
||
|
|
float sustain = apvts.getRawParameterValue("ampSustain")->load();
|
||
|
|
float release = apvts.getRawParameterValue("ampRelease")->load();
|
||
|
|
|
||
|
|
int attackSamples = static_cast<int>(attack * currentSampleRate);
|
||
|
|
int decaySamples = static_cast<int>(decay * currentSampleRate);
|
||
|
|
int releaseSamples = static_cast<int>(release * currentSampleRate);
|
||
|
|
if (attackSamples < 1) attackSamples = 1;
|
||
|
|
if (decaySamples < 1) decaySamples = 1;
|
||
|
|
if (releaseSamples < 1) releaseSamples = 1;
|
||
|
|
|
||
|
|
for (int i = 0; i < samples; ++i)
|
||
|
|
{
|
||
|
|
switch (voice.ampStage)
|
||
|
|
{
|
||
|
|
case AmpStage::Attack:
|
||
|
|
voice.ampEnv += 1.0f / static_cast<float>(attackSamples);
|
||
|
|
if (voice.ampEnv >= 1.0f)
|
||
|
|
{
|
||
|
|
voice.ampEnv = 1.0f;
|
||
|
|
voice.ampStage = AmpStage::Decay;
|
||
|
|
voice.ampSamplesToNext = decaySamples;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case AmpStage::Decay:
|
||
|
|
voice.ampSamplesToNext--;
|
||
|
|
if (voice.ampSamplesToNext <= 0)
|
||
|
|
voice.ampStage = AmpStage::Sustain;
|
||
|
|
else
|
||
|
|
voice.ampEnv = 1.0f - (1.0f - sustain) * (1.0f - static_cast<float>(voice.ampSamplesToNext) / static_cast<float>(decaySamples));
|
||
|
|
break;
|
||
|
|
|
||
|
|
case AmpStage::Sustain:
|
||
|
|
voice.ampEnv = sustain;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case AmpStage::Release:
|
||
|
|
voice.ampEnv -= 1.0f / static_cast<float>(releaseSamples);
|
||
|
|
if (voice.ampEnv <= 0.0f)
|
||
|
|
{
|
||
|
|
voice.ampEnv = 0.0f;
|
||
|
|
voice.ampStage = AmpStage::Idle;
|
||
|
|
voice.active = false;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case AmpStage::Idle:
|
||
|
|
voice.ampEnv = 0.0f;
|
||
|
|
voice.active = false;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void MonoSlicerProcessor::advanceFilterEnvelope(Voice& voice, int samples)
|
||
|
|
{
|
||
|
|
float attack = apvts.getRawParameterValue("filterAttack")->load();
|
||
|
|
float decay = apvts.getRawParameterValue("filterDecay")->load();
|
||
|
|
float sustain = apvts.getRawParameterValue("filterSustain")->load();
|
||
|
|
float release = apvts.getRawParameterValue("filterRelease")->load();
|
||
|
|
|
||
|
|
int attackSamples = static_cast<int>(attack * currentSampleRate);
|
||
|
|
int decaySamples = static_cast<int>(decay * currentSampleRate);
|
||
|
|
int releaseSamples = static_cast<int>(release * currentSampleRate);
|
||
|
|
if (attackSamples < 1) attackSamples = 1;
|
||
|
|
if (decaySamples < 1) decaySamples = 1;
|
||
|
|
if (releaseSamples < 1) releaseSamples = 1;
|
||
|
|
|
||
|
|
for (int i = 0; i < samples; ++i)
|
||
|
|
{
|
||
|
|
switch (voice.filterStage)
|
||
|
|
{
|
||
|
|
case AmpStage::Attack:
|
||
|
|
voice.filterEnv += 1.0f / static_cast<float>(attackSamples);
|
||
|
|
if (voice.filterEnv >= 1.0f)
|
||
|
|
{
|
||
|
|
voice.filterEnv = 1.0f;
|
||
|
|
voice.filterStage = AmpStage::Decay;
|
||
|
|
voice.filterSamplesToNext = decaySamples;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case AmpStage::Decay:
|
||
|
|
voice.filterSamplesToNext--;
|
||
|
|
if (voice.filterSamplesToNext <= 0)
|
||
|
|
voice.filterStage = AmpStage::Sustain;
|
||
|
|
else
|
||
|
|
voice.filterEnv = 1.0f - (1.0f - sustain) * (1.0f - static_cast<float>(voice.filterSamplesToNext) / static_cast<float>(decaySamples));
|
||
|
|
break;
|
||
|
|
|
||
|
|
case AmpStage::Sustain:
|
||
|
|
voice.filterEnv = sustain;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case AmpStage::Release:
|
||
|
|
voice.filterEnv -= 1.0f / static_cast<float>(releaseSamples);
|
||
|
|
if (voice.filterEnv <= 0.0f) voice.filterEnv = 0.0f;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case AmpStage::Idle:
|
||
|
|
voice.filterEnv = 0.0f;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void MonoSlicerProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
|
||
|
|
{
|
||
|
|
juce::ScopedNoDenormals noDenormals;
|
||
|
|
buffer.clear();
|
||
|
|
|
||
|
|
if (auto* ph = getPlayHead())
|
||
|
|
{
|
||
|
|
auto posInfo = ph->getPosition();
|
||
|
|
if (posInfo.hasValue())
|
||
|
|
{
|
||
|
|
auto bpmOpt = posInfo->getBpm();
|
||
|
|
if (bpmOpt.hasValue() && *bpmOpt > 0)
|
||
|
|
{
|
||
|
|
float hostBpm = juce::jlimit(20.0f, 300.0f, static_cast<float>(*bpmOpt));
|
||
|
|
if (auto* p = apvts.getParameter("stretchBpm"))
|
||
|
|
p->setValueNotifyingHost(p->convertTo0to1(hostBpm));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
sliceManager.setSensitivity(apvts.getRawParameterValue("sensitivity")->load());
|
||
|
|
sliceManager.setBassGain(apvts.getRawParameterValue("bass")->load());
|
||
|
|
sliceManager.setTrebleGain(apvts.getRawParameterValue("treble")->load());
|
||
|
|
|
||
|
|
if (sampleBuffer.getNumSamples() == 0)
|
||
|
|
return;
|
||
|
|
|
||
|
|
const int numSamples = buffer.getNumSamples();
|
||
|
|
const int numOutputChannels = buffer.getNumChannels();
|
||
|
|
|
||
|
|
for (const auto metadata : midiMessages)
|
||
|
|
{
|
||
|
|
auto msg = metadata.getMessage();
|
||
|
|
if (msg.isNoteOn())
|
||
|
|
handleNoteOn(msg.getNoteNumber(), msg.getFloatVelocity());
|
||
|
|
else if (msg.isNoteOff())
|
||
|
|
handleNoteOff();
|
||
|
|
}
|
||
|
|
|
||
|
|
for (auto& voice : voices)
|
||
|
|
{
|
||
|
|
if (!voice.active) continue;
|
||
|
|
|
||
|
|
int numSlices = sliceManager.getNumSlices();
|
||
|
|
int startSample = 0;
|
||
|
|
int endSample = sampleBuffer.getNumSamples();
|
||
|
|
|
||
|
|
if (numSlices > 0)
|
||
|
|
{
|
||
|
|
if (voice.sliceIndex < 0 || voice.sliceIndex >= numSlices)
|
||
|
|
{
|
||
|
|
voice.active = false;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const auto& slice = sliceManager.getSlice(voice.sliceIndex);
|
||
|
|
startSample = slice.startSample;
|
||
|
|
endSample = slice.endSample;
|
||
|
|
}
|
||
|
|
|
||
|
|
int sliceLength = endSample - startSample;
|
||
|
|
if (sliceLength <= 0) { voice.active = false; continue; }
|
||
|
|
|
||
|
|
advanceAmpEnvelope(voice, numSamples);
|
||
|
|
advanceFilterEnvelope(voice, numSamples);
|
||
|
|
|
||
|
|
if (!voice.active) continue;
|
||
|
|
|
||
|
|
int filterTypeIdx = static_cast<int>(apvts.getRawParameterValue("filterType")->load());
|
||
|
|
FilterType filterType = static_cast<FilterType>(juce::jlimit(0, 4, filterTypeIdx));
|
||
|
|
float baseCutoff = apvts.getRawParameterValue("filterCutoff")->load();
|
||
|
|
float resonance = apvts.getRawParameterValue("filterReso")->load();
|
||
|
|
float envDepth = apvts.getRawParameterValue("filterEnvDepth")->load();
|
||
|
|
float minCutoff = 20.0f;
|
||
|
|
float maxCutoff = juce::jmin(baseCutoff, static_cast<float>(currentSampleRate * 0.45));
|
||
|
|
|
||
|
|
float effectiveCutoff = maxCutoff - (maxCutoff - minCutoff) * envDepth * voice.filterEnv;
|
||
|
|
effectiveCutoff = juce::jlimit(20.0f, maxCutoff, effectiveCutoff);
|
||
|
|
|
||
|
|
float b0, b1, b2, a1, a2;
|
||
|
|
computeBiquadCoeffs(filterType, effectiveCutoff, resonance, static_cast<float>(currentSampleRate),
|
||
|
|
b0, b1, b2, a1, a2);
|
||
|
|
|
||
|
|
// Key shift: pitch factor = 2^(semitones/12)
|
||
|
|
int keyShift = static_cast<int>(apvts.getRawParameterValue("keyShift")->load());
|
||
|
|
float pitchFactor = std::pow(2.0f, static_cast<float>(keyShift) / 12.0f);
|
||
|
|
|
||
|
|
const int bufSamples = sampleBuffer.getNumSamples();
|
||
|
|
|
||
|
|
bool filterActive = (envDepth > 0.001f)
|
||
|
|
|| (baseCutoff < static_cast<float>(currentSampleRate * 0.45) - 1.0f)
|
||
|
|
|| (resonance > 0.001f);
|
||
|
|
|
||
|
|
float startPos = voice.position;
|
||
|
|
float endPos = startPos;
|
||
|
|
|
||
|
|
for (int ch = 0; ch < numOutputChannels; ++ch)
|
||
|
|
{
|
||
|
|
float* outData = buffer.getWritePointer(ch);
|
||
|
|
auto& filt = (ch == 0) ? voice.filterL : voice.filterR;
|
||
|
|
if (filterActive)
|
||
|
|
{
|
||
|
|
filt.b0 = b0; filt.b1 = b1; filt.b2 = b2;
|
||
|
|
filt.a1 = a1; filt.a2 = a2;
|
||
|
|
}
|
||
|
|
|
||
|
|
float chPos = startPos;
|
||
|
|
for (int i = 0; i < numSamples; ++i)
|
||
|
|
{
|
||
|
|
if (chPos >= static_cast<float>(sliceLength))
|
||
|
|
break;
|
||
|
|
|
||
|
|
int idx = static_cast<int>(chPos);
|
||
|
|
float frac = chPos - static_cast<float>(idx);
|
||
|
|
|
||
|
|
int sampleIdx = startSample + idx;
|
||
|
|
if (sampleIdx >= bufSamples)
|
||
|
|
break;
|
||
|
|
|
||
|
|
int bufCh = juce::jmin(ch, sampleBuffer.getNumChannels() - 1);
|
||
|
|
float s0 = sampleBuffer.getSample(bufCh, sampleIdx);
|
||
|
|
float s1 = (sampleIdx + 1 < bufSamples) ? sampleBuffer.getSample(bufCh, sampleIdx + 1) : s0;
|
||
|
|
float dry = (s0 + frac * (s1 - s0)) * voice.velocity;
|
||
|
|
|
||
|
|
float output;
|
||
|
|
if (filterActive)
|
||
|
|
output = filt.process(dry) * voice.ampEnv;
|
||
|
|
else
|
||
|
|
output = dry * voice.ampEnv;
|
||
|
|
|
||
|
|
outData[i] += output;
|
||
|
|
chPos += pitchFactor;
|
||
|
|
}
|
||
|
|
endPos = chPos;
|
||
|
|
}
|
||
|
|
voice.position = endPos;
|
||
|
|
|
||
|
|
if (voice.position >= static_cast<float>(sliceLength))
|
||
|
|
{
|
||
|
|
voice.ampStage = AmpStage::Release;
|
||
|
|
voice.filterStage = AmpStage::Release;
|
||
|
|
voice.active = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
currentActiveSlice.store(-1);
|
||
|
|
for (auto& voice : voices)
|
||
|
|
{
|
||
|
|
if (voice.active)
|
||
|
|
{
|
||
|
|
int numSlices = sliceManager.getNumSlices();
|
||
|
|
int baseStart = 0;
|
||
|
|
if (numSlices > 0 && voice.sliceIndex >= 0 && voice.sliceIndex < numSlices)
|
||
|
|
baseStart = sliceManager.getSlice(voice.sliceIndex).startSample;
|
||
|
|
currentPlaybackSample.store(baseStart + static_cast<int>(voice.position));
|
||
|
|
currentActiveSlice.store(voice.sliceIndex);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (currentActiveSlice.load() < 0)
|
||
|
|
currentPlaybackSample.store(-1);
|
||
|
|
}
|
||
|
|
|
||
|
|
void MonoSlicerProcessor::handleNoteOn(int noteNumber, float velocity)
|
||
|
|
{
|
||
|
|
int sliceIdx = noteNumber - 60;
|
||
|
|
sliceIdx = juce::jlimit(0, juce::jmax(0, sliceManager.getNumSlices() - 1), sliceIdx);
|
||
|
|
|
||
|
|
if (apvts.getRawParameterValue("selectViaMidi")->load() > 0.5f)
|
||
|
|
selectedSlice.store(sliceIdx);
|
||
|
|
|
||
|
|
for (auto& voice : voices)
|
||
|
|
{
|
||
|
|
if (!voice.active)
|
||
|
|
{
|
||
|
|
voice.active = true;
|
||
|
|
voice.sliceIndex = sliceIdx;
|
||
|
|
voice.position = 0;
|
||
|
|
voice.velocity = velocity;
|
||
|
|
|
||
|
|
voice.ampStage = AmpStage::Attack;
|
||
|
|
voice.ampEnv = 0.0f;
|
||
|
|
|
||
|
|
voice.filterStage = AmpStage::Attack;
|
||
|
|
voice.filterEnv = 0.0f;
|
||
|
|
|
||
|
|
voice.filterL.reset();
|
||
|
|
voice.filterR.reset();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void MonoSlicerProcessor::handleNoteOff()
|
||
|
|
{
|
||
|
|
for (auto& voice : voices)
|
||
|
|
{
|
||
|
|
if (voice.active && voice.ampStage != AmpStage::Release && voice.ampStage != AmpStage::Idle)
|
||
|
|
{
|
||
|
|
voice.ampStage = AmpStage::Release;
|
||
|
|
voice.filterStage = AmpStage::Release;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void MonoSlicerProcessor::loadAudioFile(const juce::File& file)
|
||
|
|
{
|
||
|
|
std::unique_ptr<juce::AudioFormatReader> reader(formatManager.createReaderFor(file));
|
||
|
|
if (reader)
|
||
|
|
{
|
||
|
|
int numSamples = static_cast<int>(reader->lengthInSamples);
|
||
|
|
sampleBuffer.setSize(static_cast<int>(reader->numChannels), numSamples);
|
||
|
|
reader->read(&sampleBuffer, 0, numSamples, 0, true, true);
|
||
|
|
loadedSampleRate = reader->sampleRate;
|
||
|
|
currentFile = file;
|
||
|
|
refreshFolderList();
|
||
|
|
sliceManager.setSampleBuffer(&sampleBuffer, loadedSampleRate);
|
||
|
|
selectedSlice.store(-1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void MonoSlicerProcessor::refreshFolderList()
|
||
|
|
{
|
||
|
|
folderFiles.clear();
|
||
|
|
currentFileIndex = -1;
|
||
|
|
|
||
|
|
if (!currentFile.existsAsFile()) return;
|
||
|
|
|
||
|
|
auto dir = currentFile.getParentDirectory();
|
||
|
|
juce::DirectoryIterator iter(dir, false, "*.wav;*.aiff;*.flac;*.ogg");
|
||
|
|
while (iter.next())
|
||
|
|
{
|
||
|
|
auto f = iter.getFile();
|
||
|
|
if (f.existsAsFile())
|
||
|
|
{
|
||
|
|
folderFiles.add(f);
|
||
|
|
if (f == currentFile)
|
||
|
|
currentFileIndex = folderFiles.size() - 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void MonoSlicerProcessor::loadNextSample()
|
||
|
|
{
|
||
|
|
if (folderFiles.isEmpty()) return;
|
||
|
|
int nextIdx = (currentFileIndex + 1) % folderFiles.size();
|
||
|
|
loadAudioFile(folderFiles[nextIdx]);
|
||
|
|
}
|
||
|
|
|
||
|
|
void MonoSlicerProcessor::loadPreviousSample()
|
||
|
|
{
|
||
|
|
if (folderFiles.isEmpty()) return;
|
||
|
|
int prevIdx = (currentFileIndex - 1 + folderFiles.size()) % folderFiles.size();
|
||
|
|
loadAudioFile(folderFiles[prevIdx]);
|
||
|
|
}
|
||
|
|
|
||
|
|
void MonoSlicerProcessor::triggerSlice(int sliceIndex, float velocity)
|
||
|
|
{
|
||
|
|
int noteNumber = sliceIndex + 60;
|
||
|
|
handleNoteOn(noteNumber, velocity);
|
||
|
|
}
|
||
|
|
|
||
|
|
void MonoSlicerProcessor::releaseSlice()
|
||
|
|
{
|
||
|
|
handleNoteOff();
|
||
|
|
}
|
||
|
|
|
||
|
|
juce::AudioProcessorEditor* MonoSlicerProcessor::createEditor()
|
||
|
|
{
|
||
|
|
return new MonoSlicerEditor(*this);
|
||
|
|
}
|
||
|
|
|
||
|
|
void MonoSlicerProcessor::getStateInformation(juce::MemoryBlock& destData)
|
||
|
|
{
|
||
|
|
auto state = apvts.copyState();
|
||
|
|
juce::MemoryOutputStream stream(destData, false);
|
||
|
|
state.writeToStream(stream);
|
||
|
|
}
|
||
|
|
|
||
|
|
void MonoSlicerProcessor::setStateInformation(const void* data, int sizeInBytes)
|
||
|
|
{
|
||
|
|
auto stream = juce::MemoryInputStream(data, static_cast<size_t>(sizeInBytes), false);
|
||
|
|
auto state = juce::ValueTree::readFromStream(stream);
|
||
|
|
if (state.isValid())
|
||
|
|
apvts.replaceState(state);
|
||
|
|
}
|
||
|
|
|
||
|
|
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
|
||
|
|
{
|
||
|
|
return new MonoSlicerProcessor();
|
||
|
|
}
|
||
|
|
|
||
|
|
void MonoSlicerProcessor::stretchToBeats(int numBeats, float bpm)
|
||
|
|
{
|
||
|
|
if (sampleBuffer.getNumSamples() == 0 || bpm <= 0.0f || numBeats <= 0) return;
|
||
|
|
|
||
|
|
double sr = static_cast<double>(loadedSampleRate);
|
||
|
|
int currentLength = sampleBuffer.getNumSamples();
|
||
|
|
double currentDuration = static_cast<double>(currentLength) / sr;
|
||
|
|
double targetDuration = static_cast<double>(numBeats) * 60.0 / static_cast<double>(bpm);
|
||
|
|
double ratio = targetDuration / currentDuration;
|
||
|
|
|
||
|
|
if (std::abs(ratio - 1.0) < 0.001) return;
|
||
|
|
|
||
|
|
int newLength = static_cast<int>(static_cast<double>(currentLength) * ratio);
|
||
|
|
if (newLength < 1) return;
|
||
|
|
|
||
|
|
const int numChannels = sampleBuffer.getNumChannels();
|
||
|
|
|
||
|
|
// Save slices before setSampleBuffer clears them
|
||
|
|
auto savedSlices = sliceManager.getSlices();
|
||
|
|
|
||
|
|
juce::AudioBuffer<float> newBuffer(numChannels, newLength);
|
||
|
|
for (int ch = 0; ch < numChannels; ++ch)
|
||
|
|
{
|
||
|
|
const float* inData = sampleBuffer.getReadPointer(ch);
|
||
|
|
float* outData = newBuffer.getWritePointer(ch);
|
||
|
|
|
||
|
|
for (int i = 0; i < newLength; ++i)
|
||
|
|
{
|
||
|
|
double srcPos = static_cast<double>(i) / ratio;
|
||
|
|
int idx = static_cast<int>(srcPos);
|
||
|
|
float frac = static_cast<float>(srcPos - static_cast<double>(idx));
|
||
|
|
if (idx >= currentLength - 1)
|
||
|
|
{
|
||
|
|
outData[i] = inData[currentLength - 1];
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
outData[i] = inData[idx] * (1.0f - frac) + inData[idx + 1] * frac;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
sampleBuffer.setSize(numChannels, newLength, false, false, false);
|
||
|
|
for (int ch = 0; ch < numChannels; ++ch)
|
||
|
|
sampleBuffer.copyFrom(ch, 0, newBuffer, ch, 0, newLength);
|
||
|
|
|
||
|
|
sliceManager.setSampleBuffer(&sampleBuffer, loadedSampleRate);
|
||
|
|
|
||
|
|
for (const auto& s : savedSlices)
|
||
|
|
{
|
||
|
|
int newStart = static_cast<int>(static_cast<double>(s.startSample) * ratio);
|
||
|
|
int newEnd = static_cast<int>(static_cast<double>(s.endSample) * ratio);
|
||
|
|
if (newEnd > newStart + 100)
|
||
|
|
sliceManager.addSliceManual(newStart);
|
||
|
|
}
|
||
|
|
|
||
|
|
selectedSlice.store(-1);
|
||
|
|
}
|