Files
NeuralSynth/Source/SynthVoice.cpp
2025-11-08 00:17:43 +00:00

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