Files
serumalt/Source/SynthVoice.cpp
T

221 lines
8.3 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;
lastUnisonA = lastUnisonB = 1;
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;
lastUnisonA = lastUnisonB = 0; // force unison reconfigure 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);
// Reconfigure unison only when the integer count changes (avoids phase reset
// on every block when unison is LFO-modulated at control rate).
if (a.unison != lastUnisonA) { oscA.noteOn (freqA, a, seed); lastUnisonA = a.unison; }
if (b.unison != lastUnisonB) { oscB.noteOn (freqB, b, seed + 1); lastUnisonB = b.unison; }
// 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;
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, ctx.subLevel, mono);
if (ctx.noiseOn)
noise.processAdd (ctx.noiseType, ctx.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