Add Oscillator::setParams() which updates detune, pan, and gain for already-initialised sub-voices without resetting their phases. This preserves phase continuity when unison or spread is LFO-modulated at control rate. noteOn() now just seeds phases and delegates to setParams(). Precompute panL/panR in SubVoice instead of calling cos/sin per sample. Add paramsValid and cachedParams to skip redundant work. Replace SynthVoice's lastUnisonA/B tracking with a single oscillatorsNeedNoteOn flag: noteOn sets it, render clears it after the first block. Add SubLevel and NoiseLevel to the per-voice modulation targets so they can be modulated like other parameters.
234 lines
8.5 KiB
C++
234 lines
8.5 KiB
C++
#include "SynthVoice.h"
|
|
|
|
namespace serum
|
|
{
|
|
|
|
namespace
|
|
{
|
|
inline float clampF (float v, float lo, float hi) noexcept
|
|
{
|
|
return v < lo ? lo : (v > hi ? hi : v);
|
|
}
|
|
}
|
|
|
|
void SynthVoice::prepare (double sampleRate, int maxBlockSize)
|
|
{
|
|
oscA.prepare (sampleRate);
|
|
oscB.prepare (sampleRate);
|
|
sub.prepare (sampleRate);
|
|
noise.prepare (sampleRate);
|
|
filters.prepare (sampleRate, maxBlockSize);
|
|
for (auto& e : env)
|
|
e.prepare (sampleRate);
|
|
scratch.setSize (2, maxBlockSize, false, false, true);
|
|
reset();
|
|
}
|
|
|
|
void SynthVoice::reset()
|
|
{
|
|
oscA.reset(); oscB.reset();
|
|
sub.reset(); noise.reset();
|
|
filters.reset();
|
|
for (auto& e : env)
|
|
e.reset();
|
|
note = -1;
|
|
velocity = 0.0f;
|
|
baseFreq = 0.0;
|
|
active = released = false;
|
|
noteId = 0;
|
|
oscillatorsNeedNoteOn = false;
|
|
noteRandom = 0.5f;
|
|
scratch.clear();
|
|
}
|
|
|
|
void SynthVoice::noteOn (int noteNumber, float velocity01, double freqHz, juce::uint32 noteSeed)
|
|
{
|
|
note = noteNumber;
|
|
velocity = velocity01;
|
|
baseFreq = freqHz;
|
|
active = true;
|
|
released = false;
|
|
seed = noteSeed;
|
|
noteId = noteSeed;
|
|
oscillatorsNeedNoteOn = true; // initialise fresh oscillator phases on first render
|
|
|
|
juce::Random rng (noteSeed);
|
|
noteRandom = rng.nextFloat();
|
|
|
|
sub.noteOn (freqHz, -1);
|
|
noise.noteOn (noteSeed);
|
|
|
|
for (auto& e : env)
|
|
e.noteOn();
|
|
}
|
|
|
|
void SynthVoice::noteOff()
|
|
{
|
|
if (! active || released)
|
|
return;
|
|
released = true;
|
|
for (auto& e : env)
|
|
e.noteOff();
|
|
}
|
|
|
|
void SynthVoice::render (float* outL, float* outR, int numSamples, const RenderContext& ctx) noexcept
|
|
{
|
|
if (! active || numSamples <= 0)
|
|
return;
|
|
|
|
// 1. Refresh envelope parameters (cheap; also lets UI edits affect held notes).
|
|
for (int i = 0; i < kNumEnvelopes; ++i)
|
|
env[(size_t) i].setParams (maps::toSeconds (ctx.envAttack[i]),
|
|
maps::toSeconds (ctx.envDecay[i]),
|
|
ctx.envSustain[i],
|
|
maps::toSeconds (ctx.envRelease[i]),
|
|
ctx.envCurve[i]);
|
|
|
|
// 2. Block-start envelope values (control-rate modulation sources).
|
|
float envStart[kNumEnvelopes];
|
|
for (int i = 0; i < kNumEnvelopes; ++i)
|
|
envStart[i] = env[(size_t) i].getValue();
|
|
|
|
// 3. Per-voice modulation source values.
|
|
float src[kNumModSources];
|
|
src[(int) ModSource::Lfo1] = ctx.lfoValues[0];
|
|
src[(int) ModSource::Lfo2] = ctx.lfoValues[1];
|
|
src[(int) ModSource::Lfo3] = ctx.lfoValues[2];
|
|
src[(int) ModSource::Lfo4] = ctx.lfoValues[3];
|
|
src[(int) ModSource::Env1] = envStart[0];
|
|
src[(int) ModSource::Env2] = envStart[1];
|
|
src[(int) ModSource::Env3] = envStart[2];
|
|
src[(int) ModSource::Env4] = envStart[3];
|
|
src[(int) ModSource::Velocity] = velocity;
|
|
src[(int) ModSource::Note] = clampF ((float) note / 127.0f, 0.0f, 1.0f);
|
|
src[(int) ModSource::ModWheel] = ctx.modWheel;
|
|
src[(int) ModSource::PitchBend]= ctx.pitchBend;
|
|
src[(int) ModSource::Macro1] = ctx.macroValues[0];
|
|
src[(int) ModSource::Macro2] = ctx.macroValues[1];
|
|
src[(int) ModSource::Macro3] = ctx.macroValues[2];
|
|
src[(int) ModSource::Macro4] = ctx.macroValues[3];
|
|
src[(int) ModSource::Random] = noteRandom;
|
|
|
|
// 4. Accumulate modulation offsets (block rate).
|
|
std::array<float, kNumModTargets> mod { { } };
|
|
if (ctx.matrix != nullptr)
|
|
{
|
|
for (const auto& c : ctx.matrix->connections)
|
|
{
|
|
float v = src[(int) c.source];
|
|
if (c.bipolar && ! isBipolarSource (c.source))
|
|
v = v * 2.0f - 1.0f;
|
|
mod[(int) c.target] += v * c.depth;
|
|
}
|
|
}
|
|
if (ctx.macros != nullptr)
|
|
{
|
|
for (int m = 0; m < kNumMacros; ++m)
|
|
for (const auto& a : ctx.macros->assignments[(size_t) m])
|
|
mod[(int) a.target] += ctx.macroValues[m] * a.depth;
|
|
}
|
|
|
|
// 5. Modulated oscillator parameters.
|
|
OscParams a = ctx.oscA;
|
|
OscParams b = ctx.oscB;
|
|
a.level = clampF (a.level + mod[(int) ModTarget::OscALevel], 0.0f, 1.0f);
|
|
a.pan = clampF (a.pan + mod[(int) ModTarget::OscAPan], -1.0f, 1.0f);
|
|
a.wtPos = clampF (a.wtPos + mod[(int) ModTarget::OscAWtPos], 0.0f, 1.0f);
|
|
a.unison = (int) std::llround (clampF ((float) a.unison + mod[(int) ModTarget::OscAUnison] * 16.0f, 1.0f, 16.0f));
|
|
a.detune = clampF (a.detune + mod[(int) ModTarget::OscADetune], 0.0f, 1.0f);
|
|
a.spread = clampF (a.spread + mod[(int) ModTarget::OscASpread], 0.0f, 1.0f);
|
|
a.warpAmt = clampF (a.warpAmt+ mod[(int) ModTarget::OscAWarpAmt], 0.0f, 1.0f);
|
|
|
|
b.level = clampF (b.level + mod[(int) ModTarget::OscBLevel], 0.0f, 1.0f);
|
|
b.pan = clampF (b.pan + mod[(int) ModTarget::OscBPan], -1.0f, 1.0f);
|
|
b.wtPos = clampF (b.wtPos + mod[(int) ModTarget::OscBWtPos], 0.0f, 1.0f);
|
|
b.unison = (int) std::llround (clampF ((float) b.unison + mod[(int) ModTarget::OscBUnison] * 16.0f, 1.0f, 16.0f));
|
|
b.detune = clampF (b.detune + mod[(int) ModTarget::OscBDetune], 0.0f, 1.0f);
|
|
b.spread = clampF (b.spread + mod[(int) ModTarget::OscBSpread], 0.0f, 1.0f);
|
|
b.warpAmt = clampF (b.warpAmt+ mod[(int) ModTarget::OscBWarpAmt], 0.0f, 1.0f);
|
|
|
|
// 6. Frequency (pitch bend + mod matrix pitch + per-osc coarse/fine).
|
|
const double semis = ctx.pitchBend * ctx.pitchBendRange + mod[(int) ModTarget::Pitch] * 24.0;
|
|
const double bent = baseFreq * std::pow (2.0, semis / 12.0);
|
|
const double freqA = bent * std::pow (2.0, (double) a.coarse / 12.0 + (double) a.fine / 1200.0);
|
|
const double freqB = bent * std::pow (2.0, (double) b.coarse / 12.0 + (double) b.fine / 1200.0);
|
|
|
|
// Initialise phases only for a fresh note; update held-note unison parameters
|
|
// without resetting existing phases when modulated at control rate.
|
|
if (oscillatorsNeedNoteOn)
|
|
{
|
|
oscA.noteOn (freqA, a, seed);
|
|
oscB.noteOn (freqB, b, seed + 1);
|
|
oscillatorsNeedNoteOn = false;
|
|
}
|
|
else
|
|
{
|
|
oscA.setParams (a);
|
|
oscB.setParams (b);
|
|
}
|
|
|
|
// 7. Modulated filter parameters.
|
|
FilterBankParams fb = ctx.filters;
|
|
fb.f1Cutoff = clampF (fb.f1Cutoff + mod[(int) ModTarget::Filter1Cutoff], 0.0f, 1.0f);
|
|
fb.f1Res = clampF (fb.f1Res + mod[(int) ModTarget::Filter1Res], 0.0f, 1.0f);
|
|
fb.f1Drive = clampF (fb.f1Drive + mod[(int) ModTarget::Filter1Drive], 0.0f, 1.0f);
|
|
fb.f2Cutoff = clampF (fb.f2Cutoff + mod[(int) ModTarget::Filter2Cutoff], 0.0f, 1.0f);
|
|
fb.f2Res = clampF (fb.f2Res + mod[(int) ModTarget::Filter2Res], 0.0f, 1.0f);
|
|
fb.f2Drive = clampF (fb.f2Drive + mod[(int) ModTarget::Filter2Drive], 0.0f, 1.0f);
|
|
fb.mix = clampF (fb.mix + mod[(int) ModTarget::FilterMix], 0.0f, 1.0f);
|
|
fb.out = clampF (fb.out + mod[(int) ModTarget::FilterOut], 0.0f, 1.5f);
|
|
|
|
// 8. Amp modulation + velocity.
|
|
const float ampMod = 1.0f + mod[(int) ModTarget::Amp];
|
|
const float velGain = velocity * velocity + 0.001f;
|
|
|
|
// 9. Generate oscillators/sub/noise into scratch.
|
|
float* sL = scratch.getWritePointer (0);
|
|
float* sR = scratch.getWritePointer (1);
|
|
const Wavetable& wtA = ctx.wavetables->getTable (a.wave);
|
|
const Wavetable& wtB = ctx.wavetables->getTable (b.wave);
|
|
const double subMult = (ctx.subOct == -2) ? 0.25 : (ctx.subOct == -1) ? 0.5 : 1.0;
|
|
const float subLevel = clampF (ctx.subLevel + mod[(int) ModTarget::SubLevel], 0.0f, 1.0f);
|
|
const float noiseLevel = clampF (ctx.noiseLevel + mod[(int) ModTarget::NoiseLevel], 0.0f, 1.0f);
|
|
|
|
for (int i = 0; i < numSamples; ++i)
|
|
{
|
|
float l = 0.0f, r = 0.0f;
|
|
oscA.processAdd (wtA, a, freqA, l, r);
|
|
oscB.processAdd (wtB, b, freqB, l, r);
|
|
|
|
float mono = 0.0f;
|
|
if (ctx.subOn)
|
|
sub.processAdd (bent * subMult, ctx.subShape, subLevel, mono);
|
|
if (ctx.noiseOn)
|
|
noise.processAdd (ctx.noiseType, noiseLevel, mono);
|
|
l += mono;
|
|
r += mono;
|
|
|
|
sL[i] = l;
|
|
sR[i] = r;
|
|
}
|
|
|
|
// 10. Filter bank (control rate).
|
|
filters.process (sL, sR, numSamples, fb, (float) baseFreq);
|
|
|
|
// 11. Amp envelope (per sample) and advance the remaining envelopes.
|
|
for (int i = 0; i < numSamples; ++i)
|
|
{
|
|
const float amp = env[0].process();
|
|
env[1].process();
|
|
env[2].process();
|
|
env[3].process();
|
|
|
|
const float gain = amp * ampMod * velGain;
|
|
outL[i] += sL[i] * gain;
|
|
outR[i] += sR[i] * gain;
|
|
}
|
|
|
|
// 12. Release completes when the amp envelope has fully decayed.
|
|
if (released && ! env[0].isActive())
|
|
active = false;
|
|
}
|
|
|
|
} // namespace serum
|