315 lines
12 KiB
C++
315 lines
12 KiB
C++
#include "SynthVoice.h"
|
|
|
|
#include <cmath>
|
|
#include "SynthVoice/ADSR.h"
|
|
#include "SynthVoice/Chorus.h"
|
|
#include "SynthVoice/Distortion.h"
|
|
#include "SynthVoice/EQ.h"
|
|
#include "SynthVoice/Flanger.h"
|
|
#include "SynthVoice/Reverb.h"
|
|
#include "SynthVoice/SimpleDelay.h"
|
|
|
|
//==============================================================================
|
|
|
|
NeuralSynthVoice::NeuralSynthVoice (NeuralSharedParams& sp)
|
|
: shared (sp) {}
|
|
|
|
//==============================================================================
|
|
|
|
void NeuralSynthVoice::prepare (const juce::dsp::ProcessSpec& newSpec)
|
|
{
|
|
spec = newSpec;
|
|
|
|
// --- Oscillator
|
|
osc.prepare (spec.sampleRate);
|
|
osc.setWave (BlepWave::Sine);
|
|
|
|
// --- Wavetable oscillator factory banks ---
|
|
wtOsc.prepare (spec.sampleRate);
|
|
morphLfo.prepare (spec.sampleRate);
|
|
currentWtBankIndex = -1;
|
|
wtOsc2.prepare (spec.sampleRate);
|
|
morphLfo2.prepare (spec.sampleRate);
|
|
currentWtBankIndex2 = -1;
|
|
|
|
const auto& library = WT::FactoryLibrary::get();
|
|
if (! library.empty())
|
|
{
|
|
wtOsc.setBank (library.front().bank);
|
|
currentWtBankIndex = 0;
|
|
wtOsc2.setBank (library.front().bank);
|
|
currentWtBankIndex2 = 0;
|
|
}
|
|
|
|
// --- Scratch buffer (IMPORTANT: allocate real memory)
|
|
tempBuffer.setSize ((int) spec.numChannels, (int) spec.maximumBlockSize,
|
|
false, false, true);
|
|
tempBlock = juce::dsp::AudioBlock<float> (tempBuffer);
|
|
|
|
// --- Prepare chain elements
|
|
chain.prepare (spec);
|
|
chain.get<masterIndex>().setRampDurationSeconds (0.02f);
|
|
chain.get<limiterIndex>().setThreshold (-1.0f);
|
|
chain.get<limiterIndex>().setRelease (0.05f);
|
|
chain.get<limiterIndex>().reset();
|
|
|
|
// Set maximum delay sizes BEFORE runtime changes
|
|
{
|
|
// Flanger: up to 20 ms
|
|
auto& flanger = chain.get<flangerIndex>();
|
|
const size_t maxFlangerDelay = (size_t) juce::jmax<size_t>(
|
|
1, (size_t) std::ceil (0.020 * spec.sampleRate));
|
|
flanger.setMaximumDelayInSamples (maxFlangerDelay);
|
|
flanger.reset();
|
|
}
|
|
{
|
|
// Simple delay: up to 2 s
|
|
auto& delay = chain.get<delayIndex>();
|
|
const size_t maxDelay = (size_t) juce::jmax<size_t>(
|
|
1, (size_t) std::ceil (2.0 * spec.sampleRate));
|
|
delay.setMaximumDelayInSamples (maxDelay);
|
|
delay.reset();
|
|
}
|
|
|
|
// Envelopes
|
|
adsr.setSampleRate (spec.sampleRate);
|
|
filterAdsr.setSampleRate (spec.sampleRate);
|
|
|
|
// Filter
|
|
svf.reset();
|
|
svf.prepare (spec);
|
|
|
|
// Initial filter type
|
|
const int type = (int) std::lround (juce::jlimit (0.0f, 2.0f,
|
|
shared.filterType ? shared.filterType->load() : 0.0f));
|
|
switch (type)
|
|
{
|
|
case 0: svf.setType (juce::dsp::StateVariableTPTFilterType::lowpass); break;
|
|
case 1: svf.setType (juce::dsp::StateVariableTPTFilterType::highpass); break;
|
|
case 2: svf.setType (juce::dsp::StateVariableTPTFilterType::bandpass); break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
//==============================================================================
|
|
|
|
void NeuralSynthVoice::renderNextBlock (juce::AudioBuffer<float>& outputBuffer,
|
|
int startSample, int numSamples)
|
|
{
|
|
if (numSamples <= 0)
|
|
return;
|
|
|
|
if (! adsr.isActive())
|
|
clearCurrentNote();
|
|
|
|
// --- Generate oscillator into temp buffer (BLEP or Wavetable)
|
|
tempBuffer.clear();
|
|
const int numCh = juce::jmin ((int) spec.numChannels, tempBuffer.getNumChannels());
|
|
|
|
const auto& library = WT::FactoryLibrary::get();
|
|
const int librarySize = (int) library.size();
|
|
|
|
if (librarySize > 0 && shared.wtBank)
|
|
{
|
|
const int targetBank = juce::jlimit (0, librarySize - 1,
|
|
(int) std::lround (shared.wtBank->load()));
|
|
if (targetBank != currentWtBankIndex)
|
|
{
|
|
wtOsc.setBank (library[(size_t) targetBank].bank);
|
|
currentWtBankIndex = targetBank;
|
|
}
|
|
}
|
|
|
|
if (librarySize > 0 && shared.wt2Bank)
|
|
{
|
|
const int targetBank2 = juce::jlimit (0, librarySize - 1,
|
|
(int) std::lround (shared.wt2Bank->load()));
|
|
if (targetBank2 != currentWtBankIndex2)
|
|
{
|
|
wtOsc2.setBank (library[(size_t) targetBank2].bank);
|
|
currentWtBankIndex2 = targetBank2;
|
|
}
|
|
}
|
|
|
|
const bool useWTLayerA = (shared.wtOn && shared.wtOn->load() > 0.5f)
|
|
&& wtOsc.getFrameCount() > 0;
|
|
const bool useWTLayerB = (shared.wt2On && shared.wt2On->load() > 0.5f)
|
|
&& wtOsc2.getFrameCount() > 0;
|
|
|
|
const float morphMaxA = wtOsc.getMaxMorph();
|
|
const float morphBaseA = shared.wtMorph
|
|
? juce::jlimit (0.0f, morphMaxA, shared.wtMorph->load())
|
|
: 0.0f;
|
|
const float lfoDepthA = shared.wtLfoDepth ? shared.wtLfoDepth->load() : 0.0f;
|
|
const float lfoRateA = shared.wtLfoRate ? shared.wtLfoRate->load() : 1.0f;
|
|
const int lfoShapeA = shared.wtLfoShape ? (int) std::lround (shared.wtLfoShape->load()) : 0;
|
|
|
|
morphLfo.setRate (lfoRateA);
|
|
morphLfo.setShape (lfoShapeA);
|
|
|
|
const float depthFramesA = juce::jlimit (0.0f, morphMaxA, lfoDepthA);
|
|
|
|
const float morphMaxB = wtOsc2.getMaxMorph();
|
|
const float morphBaseB = shared.wt2Morph
|
|
? juce::jlimit (0.0f, morphMaxB, shared.wt2Morph->load())
|
|
: 0.0f;
|
|
const float lfoDepthB = shared.wt2LfoDepth ? shared.wt2LfoDepth->load() : 0.0f;
|
|
const float lfoRateB = shared.wt2LfoRate ? shared.wt2LfoRate->load() : 0.3f;
|
|
const int lfoShapeB = shared.wt2LfoShape ? (int) std::lround (shared.wt2LfoShape->load()) : 0;
|
|
|
|
morphLfo2.setRate (lfoRateB);
|
|
morphLfo2.setShape (lfoShapeB);
|
|
|
|
const float depthFramesB = juce::jlimit (0.0f, morphMaxB, lfoDepthB);
|
|
|
|
const float levelA = shared.wtLevel ? juce::jlimit (0.0f, 1.0f, shared.wtLevel->load()) : 0.0f;
|
|
const float levelB = shared.wt2Level ? juce::jlimit (0.0f, 1.0f, shared.wt2Level->load()) : 0.0f;
|
|
const float safeLevelSum = juce::jlimit (0.5f, 2.0f, levelA + levelB + 0.0001f);
|
|
const float mixGain = 0.45f / safeLevelSum;
|
|
|
|
for (int i = 0; i < numSamples; ++i)
|
|
{
|
|
float sampleA = useWTLayerA ? 0.0f : osc.process();
|
|
if (useWTLayerA)
|
|
{
|
|
const float lfoValueA = morphLfo.process();
|
|
const float headroomNegA = juce::jmin (depthFramesA, morphBaseA);
|
|
const float headroomPosA = juce::jmin (depthFramesA, morphMaxA - morphBaseA);
|
|
const float offsetA = (lfoValueA >= 0.0f ? lfoValueA * headroomPosA
|
|
: lfoValueA * headroomNegA);
|
|
const float morphValueA = juce::jlimit (0.0f, morphMaxA, morphBaseA + offsetA);
|
|
sampleA = wtOsc.process (morphValueA);
|
|
}
|
|
else
|
|
{
|
|
morphLfo.process(); // advance for consistency
|
|
}
|
|
|
|
float sampleB = 0.0f;
|
|
if (useWTLayerB)
|
|
{
|
|
const float lfoValueB = morphLfo2.process();
|
|
const float headroomNegB = juce::jmin (depthFramesB, morphBaseB);
|
|
const float headroomPosB = juce::jmin (depthFramesB, morphMaxB - morphBaseB);
|
|
const float offsetB = (lfoValueB >= 0.0f ? lfoValueB * headroomPosB
|
|
: lfoValueB * headroomNegB);
|
|
const float morphValueB = juce::jlimit (0.0f, morphMaxB, morphBaseB + offsetB);
|
|
sampleB = wtOsc2.process (morphValueB);
|
|
}
|
|
else
|
|
{
|
|
morphLfo2.process();
|
|
}
|
|
|
|
const float combined = mixGain * ((sampleA * levelA) + (sampleB * levelB));
|
|
|
|
for (int ch = 0; ch < numCh; ++ch)
|
|
tempBuffer.getWritePointer (ch)[i] = combined;
|
|
}
|
|
|
|
auto block = tempBlock.getSubBlock (0, (size_t) numSamples);
|
|
|
|
renderFlanger(numSamples, numCh);
|
|
renderADSR(numSamples, numCh);
|
|
renderChorus(block);
|
|
renderSimpleDelay(block);
|
|
renderDistortion(numSamples, numCh, block);
|
|
renderEQ(block);
|
|
|
|
|
|
// ================================================================
|
|
// Apply AMP ADSR envelope
|
|
// ================================================================
|
|
{
|
|
juce::AudioBuffer<float> buf (tempBuffer.getArrayOfWritePointers(), numCh, numSamples);
|
|
adsr.applyEnvelopeToBuffer (buf, 0, numSamples);
|
|
}
|
|
|
|
// Mix into output
|
|
juce::dsp::AudioBlock<float> (outputBuffer)
|
|
.getSubBlock ((size_t) startSample, (size_t) numSamples)
|
|
.add (block);
|
|
}
|
|
|
|
//==============================================================================
|
|
|
|
void NeuralSynthVoice::noteStarted()
|
|
{
|
|
const float freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz();
|
|
const float initPhase = shared.wtPhase
|
|
? juce::jlimit (0.0f, 1.0f, shared.wtPhase->load())
|
|
: 0.0f;
|
|
|
|
// Oscillator frequency and phase retrigger (BLEP + WT)
|
|
osc.setFrequency (freqHz);
|
|
osc.resetPhase (initPhase);
|
|
wtOsc.setFrequency (freqHz);
|
|
wtOsc.resetPhase (initPhase);
|
|
morphLfo.reset();
|
|
const float initPhaseB = shared.wt2Phase
|
|
? juce::jlimit (0.0f, 1.0f, shared.wt2Phase->load())
|
|
: initPhase;
|
|
wtOsc2.setFrequency (freqHz);
|
|
wtOsc2.resetPhase (initPhaseB);
|
|
morphLfo2.reset();
|
|
|
|
// Chorus snapshot
|
|
if (shared.chorusCentre) chain.get<chorusIndex>().setCentreDelay (shared.chorusCentre->load());
|
|
if (shared.chorusDepth) chain.get<chorusIndex>().setDepth (shared.chorusDepth->load());
|
|
if (shared.chorusFeedback) chain.get<chorusIndex>().setFeedback (shared.chorusFeedback->load());
|
|
if (shared.chorusMix) chain.get<chorusIndex>().setMix (shared.chorusMix->load());
|
|
if (shared.chorusRate) chain.get<chorusIndex>().setRate (shared.chorusRate->load());
|
|
|
|
// Delay time (in samples)
|
|
if (shared.delayTime)
|
|
chain.get<delayIndex>().setDelay (juce::jmax (0.0f, shared.delayTime->load() * (float) spec.sampleRate));
|
|
|
|
// Reverb snapshot
|
|
juce::Reverb::Parameters rp;
|
|
rp.damping = shared.reverbDamping ? shared.reverbDamping->load() : 0.0f;
|
|
rp.dryLevel = shared.reverbDryLevel ? shared.reverbDryLevel->load() : 0.0f;
|
|
rp.freezeMode = shared.reverbFreezeMode ? shared.reverbFreezeMode->load() : 0.0f;
|
|
rp.roomSize = shared.reverbRoomSize ? shared.reverbRoomSize->load() : 0.0f;
|
|
rp.wetLevel = shared.reverbWetLevel ? shared.reverbWetLevel->load() : 0.0f;
|
|
rp.width = shared.reverbWidth ? shared.reverbWidth->load() : 0.0f;
|
|
chain.get<reverbIndex>().setParameters (rp);
|
|
|
|
// Amp ADSR
|
|
juce::ADSR::Parameters ap;
|
|
ap.attack = shared.adsrAttack ? shared.adsrAttack->load() : 0.01f;
|
|
ap.decay = shared.adsrDecay ? shared.adsrDecay->load() : 0.10f;
|
|
ap.sustain = shared.adsrSustain ? shared.adsrSustain->load() : 0.80f;
|
|
ap.release = shared.adsrRelease ? shared.adsrRelease->load() : 0.40f;
|
|
adsr.setParameters (ap);
|
|
adsr.noteOn();
|
|
|
|
// Filter ADSR
|
|
juce::ADSR::Parameters fp;
|
|
fp.attack = shared.fenvAttack ? shared.fenvAttack->load() : 0.01f;
|
|
fp.decay = shared.fenvDecay ? shared.fenvDecay->load() : 0.10f;
|
|
fp.sustain = shared.fenvSustain ? shared.fenvSustain->load() : 0.80f;
|
|
fp.release = shared.fenvRelease ? shared.fenvRelease->load() : 0.40f;
|
|
filterAdsr.setParameters (fp);
|
|
filterAdsr.noteOn();
|
|
}
|
|
|
|
//==============================================================================
|
|
|
|
void NeuralSynthVoice::notePitchbendChanged()
|
|
{
|
|
const float freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz();
|
|
osc.setFrequency (freqHz);
|
|
wtOsc.setFrequency (freqHz);
|
|
}
|
|
|
|
//==============================================================================
|
|
|
|
void NeuralSynthVoice::noteStopped (bool allowTailOff)
|
|
{
|
|
juce::ignoreUnused (allowTailOff);
|
|
adsr.noteOff();
|
|
filterAdsr.noteOff();
|
|
}
|
|
|
|
//==============================================================================
|