feat(dsp): add core synthesizer oscillators, wavetable and voice engine
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user