2026-09-07 21:03:31 +02:00
|
|
|
#include "SynthEngine.h"
|
|
|
|
|
|
|
|
|
|
SynthEngine::SynthEngine() { rng.setSeedRandomly(); }
|
|
|
|
|
SynthEngine::~SynthEngine() {}
|
|
|
|
|
|
|
|
|
|
void SynthEngine::setSampleRate (double s)
|
|
|
|
|
{
|
|
|
|
|
sr = s;
|
|
|
|
|
}
|
|
|
|
|
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
float SynthEngine::osc (int wave, double ph, double phRaw)
|
2026-09-07 21:03:31 +02:00
|
|
|
{
|
|
|
|
|
switch (wave)
|
|
|
|
|
{
|
|
|
|
|
case 0: return (float) std::sin (ph * 2.0 * juce::MathConstants<double>::pi);
|
|
|
|
|
case 1: return (float) (ph < 0.5 ? 4.0 * ph - 1.0 : 3.0 - 4.0 * ph);
|
|
|
|
|
case 2: return (float) (1.0 - 2.0 * ph);
|
|
|
|
|
case 3: return (float) (ph < 0.5 ? -1.0 : 1.0);
|
|
|
|
|
case 4: return rng.nextFloat() * 2.0f - 1.0f;
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
|
|
|
|
|
// 5 FM: 2-op sine FM (1:2 ratio), index slowly evolves over the note
|
|
|
|
|
case 5:
|
|
|
|
|
{
|
|
|
|
|
const double idx = 2.2 + 1.6 * (0.5 + 0.5 * std::sin (phRaw * 0.03));
|
|
|
|
|
return (float) std::sin (ph * 2.0 * juce::MathConstants<double>::pi
|
|
|
|
|
+ idx * std::sin (ph * 4.0 * juce::MathConstants<double>::pi));
|
|
|
|
|
}
|
|
|
|
|
// 6 PWM: pulse whose width animates with the running phase
|
|
|
|
|
case 6:
|
|
|
|
|
{
|
|
|
|
|
const double w = 0.05 + 0.60 * (0.5 + 0.5 * std::sin (phRaw * 0.05));
|
|
|
|
|
return (float) (ph < w ? 1.0 : -1.0);
|
|
|
|
|
}
|
|
|
|
|
// 7 SUPRSW: 5 stacked saws with a slowly moving spread (detuned-stack feel)
|
|
|
|
|
case 7:
|
|
|
|
|
{
|
|
|
|
|
const double df = 0.006 * std::sin (phRaw * 0.02);
|
|
|
|
|
double s = 0.0;
|
|
|
|
|
for (int k = -2; k <= 2; ++k)
|
|
|
|
|
{
|
|
|
|
|
double sp = ph + df + k * 0.13;
|
|
|
|
|
sp -= std::floor (sp);
|
|
|
|
|
s += 1.0 - 2.0 * sp;
|
|
|
|
|
}
|
2026-09-14 02:27:45 +02:00
|
|
|
return (float) (s * 0.28);
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
}
|
|
|
|
|
// 8 DBLSAW: saw plus a sub-octave saw (big, weighted low end)
|
|
|
|
|
case 8:
|
|
|
|
|
{
|
|
|
|
|
double sub = ph * 0.5;
|
|
|
|
|
sub -= std::floor (sub);
|
2026-09-14 02:27:45 +02:00
|
|
|
return (float) (0.56 * (1.0 - 2.0 * ph) + 0.44 * (1.0 - 2.0 * sub));
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
}
|
|
|
|
|
// 9 WARP: saw pushed twice through a wavefolder -> bright, buzzy, raw
|
|
|
|
|
case 9:
|
|
|
|
|
{
|
|
|
|
|
double x = 1.0 - 2.0 * ph;
|
|
|
|
|
x = 1.0 - 2.0 * std::fabs (1.0 - std::fabs (x));
|
|
|
|
|
x = 1.0 - 2.0 * std::fabs (1.0 - std::fabs (x));
|
|
|
|
|
return (float) x;
|
|
|
|
|
}
|
|
|
|
|
// 10 HSYNC: saw hard-synced to a 2.5x master -> bright, cutting lead
|
|
|
|
|
case 10:
|
|
|
|
|
{
|
|
|
|
|
double sp = ph * 2.5;
|
|
|
|
|
sp -= std::floor (sp);
|
|
|
|
|
return (float) (1.0 - 2.0 * sp);
|
|
|
|
|
}
|
|
|
|
|
// 11 TINE: electric-tine partial stack (1x,2x,4x,8x) -> bell/marimba/EP
|
|
|
|
|
case 11:
|
|
|
|
|
{
|
|
|
|
|
const double a = ph * 2.0 * juce::MathConstants<double>::pi;
|
2026-09-14 02:27:45 +02:00
|
|
|
return (float) (0.44 * std::sin (a)
|
|
|
|
|
+ 0.28 * std::sin (2.0 * a)
|
|
|
|
|
+ 0.18 * std::sin (4.0 * a)
|
|
|
|
|
+ 0.09 * std::sin (8.0 * a));
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
}
|
|
|
|
|
// 12 SEQ: 8 pseudo-random levels per cycle -> digital, step-sequencer
|
|
|
|
|
case 12:
|
|
|
|
|
{
|
|
|
|
|
static const float L[8] = { 0.90f, -0.45f, 0.30f, -0.85f,
|
|
|
|
|
0.55f, -0.20f, 0.75f, -0.60f };
|
|
|
|
|
const int k = (int) std::floor (ph * 8.0);
|
|
|
|
|
return L[k & 7];
|
|
|
|
|
}
|
|
|
|
|
// 13 CHORD: built-in minor triad (root, m3, 5th) -> one-osc chord
|
|
|
|
|
case 13:
|
|
|
|
|
{
|
|
|
|
|
const double a = ph * 2.0 * juce::MathConstants<double>::pi;
|
2026-09-14 02:27:45 +02:00
|
|
|
return (float) (0.44 * std::sin (a)
|
|
|
|
|
+ 0.30 * std::sin (a * (6.0 / 5.0))
|
|
|
|
|
+ 0.27 * std::sin (a * 1.5));
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
}
|
|
|
|
|
// 14 SUPRPL: 3 stacked phase-offset pulse waves -> fat chorus-box square
|
|
|
|
|
case 14:
|
|
|
|
|
{
|
|
|
|
|
const double o1 = ph < 0.5 ? 1.0 : -1.0;
|
|
|
|
|
double p2 = ph + 0.10; p2 -= std::floor (p2);
|
|
|
|
|
const double o2 = p2 < 0.5 ? 1.0 : -1.0;
|
|
|
|
|
double p3 = ph + 0.25; p3 -= std::floor (p3);
|
|
|
|
|
const double o3 = p3 < 0.5 ? 1.0 : -1.0;
|
|
|
|
|
return (float) ((o1 + o2 + o3) * (1.0 / 3.0));
|
|
|
|
|
}
|
2026-09-07 21:03:31 +02:00
|
|
|
default: return 0.0f;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SynthEngine::allNotesOff (bool immediate)
|
|
|
|
|
{
|
|
|
|
|
for (auto& v : voices)
|
|
|
|
|
{
|
|
|
|
|
if (v.active)
|
|
|
|
|
{
|
|
|
|
|
if (immediate) v.active = false;
|
|
|
|
|
else v.amp.noteOff();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sustainedNotes.clear();
|
|
|
|
|
if (immediate) active = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SynthEngine::sustainPedal (bool down)
|
|
|
|
|
{
|
|
|
|
|
pedal = down;
|
|
|
|
|
if (! down)
|
|
|
|
|
{
|
|
|
|
|
for (auto n : sustainedNotes)
|
|
|
|
|
for (auto& v : voices)
|
|
|
|
|
if (v.active && v.note == n)
|
|
|
|
|
v.amp.noteOff();
|
|
|
|
|
sustainedNotes.clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SynthEngine::pitchBend (float cents) { bendCents = cents; }
|
|
|
|
|
void SynthEngine::modWheel (float v) { mod = juce::jlimit (0.0f, 1.0f, v); }
|
|
|
|
|
|
|
|
|
|
SynthEngine::Voice* SynthEngine::acquireVoice()
|
|
|
|
|
{
|
|
|
|
|
const int lim = juce::jlimit (1, maxVoice, polyLimit);
|
|
|
|
|
for (int i = 0; i < lim; ++i)
|
|
|
|
|
if (! voices[i].active)
|
|
|
|
|
return &voices[i];
|
|
|
|
|
|
|
|
|
|
stealIdx = (stealIdx + 1) % lim;
|
2026-09-08 21:57:34 +02:00
|
|
|
Voice* v = &voices[stealIdx];
|
|
|
|
|
if (v->active)
|
|
|
|
|
{
|
|
|
|
|
v->amp.noteOff();
|
|
|
|
|
v->flt.noteOff();
|
|
|
|
|
}
|
|
|
|
|
return v;
|
2026-09-07 21:03:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SynthEngine::noteOn (int midiNote, float velocity)
|
|
|
|
|
{
|
2026-09-08 15:10:12 +02:00
|
|
|
const float noteFreq = (float) (440.0 * std::exp2 ((midiNote - 69.0) / 12.0));
|
2026-09-07 21:03:31 +02:00
|
|
|
Voice* v = acquireVoice();
|
2026-09-08 15:10:12 +02:00
|
|
|
const bool fresh = ! v->active;
|
2026-09-07 21:03:31 +02:00
|
|
|
v->active = true;
|
|
|
|
|
v->note = midiNote;
|
|
|
|
|
v->vel = 1.0f - std::pow (1.0f - velocity, 1.35f);
|
|
|
|
|
v->curFreq = (lastGlide > 0.001f && lastNoteFreq > 0.0f) ? lastNoteFreq : noteFreq;
|
|
|
|
|
v->targetFreq = noteFreq;
|
|
|
|
|
v->ph1 = rng.nextDouble() * 0.5;
|
|
|
|
|
v->ph2 = rng.nextDouble() * 0.5;
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
v->ph1r = rng.nextDouble() * 8.0;
|
|
|
|
|
v->ph2r = rng.nextDouble() * 8.0;
|
2026-09-07 21:03:31 +02:00
|
|
|
v->lfoPh = rng.nextDouble();
|
|
|
|
|
v->lfoIdx = 0;
|
|
|
|
|
v->lfoSH = rng.nextFloat() * 2.0f - 1.0f;
|
|
|
|
|
v->lp = v->bp = 0.0f;
|
2026-09-08 15:10:12 +02:00
|
|
|
v->fcSm = 0.0f;
|
2026-09-07 21:03:31 +02:00
|
|
|
v->outSmooth = 0.0f;
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
v->drift = 0.0f;
|
|
|
|
|
v->driftT = rng.nextFloat() * 2.0f - 1.0f;
|
|
|
|
|
// Micro Q-style unison: every voice gets its own small detune offset, the
|
|
|
|
|
// spread widening with the DETUNE 1 setting, for fat stacks on chords
|
|
|
|
|
v->dtSp = (rng.nextFloat() * 2.0f - 1.0f) * (1.5f + 0.30f * std::abs (curDetune1));
|
2026-09-07 21:03:31 +02:00
|
|
|
|
|
|
|
|
const float p = juce::jlimit (-1.0f, 1.0f, lastPan + (rng.nextFloat() - 0.5f) * 0.1f);
|
|
|
|
|
const float ang = (p + 1.0f) * juce::MathConstants<float>::pi * 0.25f;
|
|
|
|
|
const float mag = 0.7071f + 0.25f * rng.nextFloat();
|
|
|
|
|
v->panL = std::cos (ang) * mag;
|
|
|
|
|
v->panR = std::sin (ang) * mag;
|
|
|
|
|
|
|
|
|
|
v->amp.reset(); v->flt.reset();
|
|
|
|
|
v->amp.noteOn(); v->flt.noteOn();
|
|
|
|
|
lastNoteFreq = noteFreq;
|
2026-09-08 15:10:12 +02:00
|
|
|
if (fresh) ++active;
|
2026-09-07 21:03:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SynthEngine::noteOff (int midiNote)
|
|
|
|
|
{
|
|
|
|
|
if (pedal)
|
|
|
|
|
{
|
|
|
|
|
if (! sustainedNotes.contains (midiNote))
|
|
|
|
|
sustainedNotes.add (midiNote);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
for (auto& v : voices)
|
|
|
|
|
if (v.active && v.note == midiNote)
|
|
|
|
|
v.amp.noteOff();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SynthEngine::render (const VParams& p, float* left, float* right, int num)
|
|
|
|
|
{
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
curDetune1 = p.detune1;
|
2026-09-07 21:03:31 +02:00
|
|
|
for (auto& v : voices)
|
|
|
|
|
{
|
2026-09-08 16:37:33 +02:00
|
|
|
v.amp.setRates (p.ampA, p.ampD, p.ampS, p.ampR, sr);
|
|
|
|
|
v.flt.setRates (p.fA, p.fD, p.fS, p.fR, sr);
|
2026-09-07 21:03:31 +02:00
|
|
|
}
|
|
|
|
|
lastGlide = p.glide;
|
2026-09-08 16:37:33 +02:00
|
|
|
lastPan = p.pan;
|
2026-09-07 21:03:31 +02:00
|
|
|
polyLimit = juce::jlimit (1, maxVoice, (int) p.poly);
|
|
|
|
|
|
2026-09-08 16:37:33 +02:00
|
|
|
const float glideRate = p.glide <= 0.001f ? 1.0f
|
|
|
|
|
: 1.0f - std::exp (-1.0f / (p.glide * (float) sr));
|
2026-09-10 02:25:20 +02:00
|
|
|
const float detuneRatio2 = std::exp2f (p.detune / 1200.0f);
|
|
|
|
|
const float detuneRatio1 = std::exp2f (p.detune1 / 1200.0f);
|
2026-09-08 15:10:12 +02:00
|
|
|
const float tuneRatio = std::exp2f (p.tune / 12.0f);
|
2026-09-16 14:54:58 +02:00
|
|
|
const float ctuneRatio1 = std::exp2f (p.ctune1 / 12.0f);
|
|
|
|
|
const float ctuneRatio2 = std::exp2f (p.ctune2 / 12.0f);
|
2026-09-08 21:57:34 +02:00
|
|
|
const float maxFc = 0.4f * (float) sr;
|
|
|
|
|
const float minFc = 30.0f;
|
2026-09-07 21:03:31 +02:00
|
|
|
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
// Free-running slow-evolution clock: every ~8 s the running patch rolls a
|
|
|
|
|
// new wander target, and evoCur slews toward it, so harmonic character,
|
|
|
|
|
// tuning instability and cross-mod bloom all slowly drift and come back.
|
|
|
|
|
// Generative evolution clock. evoCur is a true brownian walk that roams the
|
|
|
|
|
// whole [-1,1] range without ever repeating, plus occasional "excursions"
|
|
|
|
|
// that push it to a freshly rolled extreme for 7-18 s. evoSlow is a second,
|
|
|
|
|
// much slower wander that drives the long-arc soundscape drift over 1-3
|
|
|
|
|
// minutes. Together they make a held note slowly change character forever.
|
2026-09-07 21:03:31 +02:00
|
|
|
for (int s = 0; s < num; ++s)
|
|
|
|
|
{
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
if (--evoGoalT <= 0.0)
|
|
|
|
|
{
|
|
|
|
|
evoGoalT = sr * (7.0 + rng.nextDouble() * 11.0);
|
|
|
|
|
evoGoal = (rng.nextFloat() < 0.5f ? -1.0f : 1.0f)
|
|
|
|
|
* (0.65f + 0.35f * rng.nextFloat());
|
|
|
|
|
for (auto& v : voices)
|
|
|
|
|
if (v.active)
|
|
|
|
|
v.driftT = rng.nextFloat() * 2.0f - 1.0f;
|
|
|
|
|
}
|
|
|
|
|
evoCur += (rng.nextFloat() - 0.5f) * 0.0022f
|
|
|
|
|
+ (evoGoal - evoCur) * 0.00035f;
|
|
|
|
|
evoCur = std::max (-1.0f, std::min (1.0f, evoCur));
|
|
|
|
|
|
|
|
|
|
evoSlow += (rng.nextFloat() - 0.5f) * 0.0028f
|
|
|
|
|
+ (0.0f - evoSlow) * 0.0000002f;
|
|
|
|
|
evoSlow = std::max (-1.0f, std::min (1.0f, evoSlow));
|
|
|
|
|
|
2026-09-07 21:03:31 +02:00
|
|
|
float mixL = 0.0f, mixR = 0.0f;
|
2026-09-08 21:57:34 +02:00
|
|
|
int activeCount = 0;
|
|
|
|
|
for (auto& v : voices)
|
|
|
|
|
if (v.active) ++activeCount;
|
|
|
|
|
const float voiceGain = activeCount > 1 ? 1.0f / std::sqrtf ((float) activeCount) : 1.0f;
|
2026-09-07 21:03:31 +02:00
|
|
|
|
|
|
|
|
for (auto& v : voices)
|
|
|
|
|
{
|
|
|
|
|
if (! v.active)
|
|
|
|
|
continue;
|
|
|
|
|
|
2026-09-08 16:37:33 +02:00
|
|
|
if (p.glide > 0.001f)
|
2026-09-07 21:03:31 +02:00
|
|
|
v.curFreq += (v.targetFreq - v.curFreq) * glideRate;
|
|
|
|
|
else
|
|
|
|
|
v.curFreq = v.targetFreq;
|
|
|
|
|
|
|
|
|
|
// LFO
|
2026-09-08 16:37:33 +02:00
|
|
|
v.lfoPh += (double) p.lfoRate / sr;
|
2026-09-07 21:03:31 +02:00
|
|
|
while (v.lfoPh >= 1.0)
|
|
|
|
|
{
|
|
|
|
|
v.lfoPh -= 1.0;
|
|
|
|
|
v.lfoIdx++;
|
|
|
|
|
v.lfoSH = rng.nextFloat() * 2.0f - 1.0f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float lfo = 0.0f;
|
|
|
|
|
switch ((int) p.lfoShape)
|
|
|
|
|
{
|
|
|
|
|
case 0: lfo = (float) std::sin (v.lfoPh * 2.0 * juce::MathConstants<double>::pi); break;
|
|
|
|
|
case 1: lfo = (float) (v.lfoPh < 0.5 ? 4.0 * v.lfoPh - 1.0 : 3.0 - 4.0 * v.lfoPh); break;
|
|
|
|
|
case 2: lfo = v.lfoPh < 0.5 ? -1.0f : 1.0f; break;
|
|
|
|
|
case 3: lfo = v.lfoSH; break;
|
|
|
|
|
default: lfo = 0.0f; break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// frequency, vibrato, bend, tune
|
|
|
|
|
float freq = v.curFreq * tuneRatio;
|
|
|
|
|
if ((int) p.lfoTarget == 1)
|
2026-09-08 16:37:33 +02:00
|
|
|
freq *= std::exp2f (lfo * p.lfoDepth * 1.6f / 12.0f);
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
if (p.evolve > 0.001f)
|
|
|
|
|
{
|
|
|
|
|
// slow random tuning instability, re-rolled with each excursion
|
|
|
|
|
v.drift += (v.driftT - v.drift) * 0.0020f;
|
|
|
|
|
freq *= std::exp2f (v.drift * p.evolve * 14.0f / 1200.0f);
|
|
|
|
|
}
|
2026-09-07 21:03:31 +02:00
|
|
|
if (bendSmooth != bendCents)
|
|
|
|
|
bendSmooth += (bendCents - bendSmooth) * 0.08f;
|
2026-09-08 15:10:12 +02:00
|
|
|
freq *= std::exp2f (bendSmooth / 1200.0f);
|
2026-09-07 21:03:31 +02:00
|
|
|
|
2026-09-16 14:54:58 +02:00
|
|
|
const float d1 = (float) (freq / sr) * detuneRatio1 * ctuneRatio1 * std::exp2f (v.dtSp / 1200.0f);
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
// soundscape detune sway: osc2 slowly zooms its detune in/out
|
|
|
|
|
const float swayT = std::exp2f (evoSlow * p.evolve * 16.0f / 1200.0f);
|
2026-09-16 14:54:58 +02:00
|
|
|
const float d2 = (float) (freq / sr) * detuneRatio2 * ctuneRatio2 * swayT;
|
2026-09-07 21:03:31 +02:00
|
|
|
|
|
|
|
|
v.ph1 += d1; if (v.ph1 >= 1.0) v.ph1 -= 1.0;
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
v.ph1r += d1;
|
2026-09-07 21:03:31 +02:00
|
|
|
|
2026-09-14 02:27:45 +02:00
|
|
|
// cross-mod "bloom": the amount breathes with the evolution walk
|
|
|
|
|
// (scaled by EVOLVE, so at evolve=0 it sits static at its middle value),
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
// sometimes vanishing almost entirely, sometimes saturating
|
|
|
|
|
float xmA = 0.0f;
|
|
|
|
|
if ((int) p.xmod > 0 && p.xmix > 0.001f)
|
2026-09-14 02:27:45 +02:00
|
|
|
xmA = p.xmix * (0.55f + 0.45f * evoCur * p.evolve);
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
|
|
|
|
|
// oscillators, with osc1 phase-modulating osc2 when in FM cross-mod
|
|
|
|
|
const float o1 = osc ((int) p.osc1w, v.ph1, v.ph1r);
|
|
|
|
|
float d2step = d2;
|
|
|
|
|
if ((int) p.xmod == 2 && xmA > 0.0f)
|
|
|
|
|
d2step = d2 * (1.0f + o1 * xmA);
|
|
|
|
|
v.ph2 += d2step; if (v.ph2 >= 1.0) v.ph2 -= 1.0;
|
|
|
|
|
v.ph2r += d2step;
|
|
|
|
|
const float o2 = osc ((int) p.osc2w, v.ph2, v.ph2r);
|
|
|
|
|
// osc-mix crossfade: the o1<->o2 balance slowly sways with the
|
|
|
|
|
// soundscape wander, visibly respraying the whole timbre over ~a minute
|
|
|
|
|
const float mixBase = p.oscMix * 0.01f;
|
|
|
|
|
float mixb = mixBase * (1.0f + evoSlow * p.evolve * 0.8f);
|
|
|
|
|
mixb = std::max (0.0f, std::min (0.98f, mixb));
|
2026-09-07 21:03:31 +02:00
|
|
|
float sig = o1 * (1.0f - mixb) + o2 * mixb;
|
|
|
|
|
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
// ring / AM cross-mod spiced into the mix
|
|
|
|
|
if (xmA > 0.0f)
|
|
|
|
|
{
|
|
|
|
|
const int xm = (int) p.xmod;
|
2026-09-14 02:27:45 +02:00
|
|
|
if (xm == 1) sig += o1 * o2 * 0.35f * xmA;
|
|
|
|
|
else if (xm == 3) sig += o1 * o2 * 0.55f * xmA;
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// spectral warp: drive + bias swing hard with the texture wander,
|
|
|
|
|
// folding harmonics way in and pulling back out over tens of seconds
|
|
|
|
|
if (p.warp > 0.001f)
|
|
|
|
|
{
|
|
|
|
|
const float drive = 1.0f + p.warp * (1.0f + 3.2f * std::max (0.0f, evoCur));
|
|
|
|
|
sig = std::tanh (sig * drive + evoCur * 0.30f * p.warp);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-07 21:03:31 +02:00
|
|
|
const float fe = v.flt.next();
|
|
|
|
|
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
// gentle analog drive ahead of the filter (resonance pushes harder)
|
|
|
|
|
sig = std::tanh (sig * (1.0f + p.fltRes * 0.15f));
|
|
|
|
|
|
2026-09-07 21:03:31 +02:00
|
|
|
// cutoff with keytrack + filter envelope + LFO + mod wheel
|
2026-09-08 16:37:33 +02:00
|
|
|
float fc = p.fltCut;
|
|
|
|
|
fc *= std::exp2f (p.fltKey * 0.01f * std::log2f (freq / 261.6256f));
|
|
|
|
|
fc *= std::exp2f (p.fltEnv * 0.01f * fe * std::log2f (maxFc / fc));
|
2026-09-07 21:03:31 +02:00
|
|
|
if ((int) p.lfoTarget == 0)
|
2026-09-08 16:37:33 +02:00
|
|
|
fc *= std::exp2f (lfo * p.lfoDepth * 10.0f / 12.0f);
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
if (p.evolve > 0.001f)
|
|
|
|
|
fc *= std::exp2f (evoCur * p.evolve * 4.0f / 12.0f);
|
2026-09-08 15:10:12 +02:00
|
|
|
fc *= std::exp2f (mod);
|
2026-09-07 21:03:31 +02:00
|
|
|
fc = std::min (maxFc, std::max (minFc, fc));
|
2026-09-08 15:10:12 +02:00
|
|
|
v.fcSm += (fc - v.fcSm) * 0.25f;
|
|
|
|
|
fc = v.fcSm;
|
2026-09-07 21:03:31 +02:00
|
|
|
|
2026-09-08 15:10:12 +02:00
|
|
|
// SVF (cap f1 to keep the Chamberlin structure stable; past ~1.5 the
|
|
|
|
|
// resonance feedback can self-oscillate and sputter at maxed cutoff+res)
|
|
|
|
|
const float f1 = std::min (1.50f, 2.0f * std::sin (juce::MathConstants<float>::pi * fc / (float) sr));
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
// resonance growl: filter tightens/loosens with the soundscape wander
|
|
|
|
|
const float q1 = 1.0f / (1.0f + p.fltRes * (3.0f + evoCur * p.evolve * 2.5f));
|
2026-09-07 21:03:31 +02:00
|
|
|
v.f1c = f1;
|
|
|
|
|
const float hp = sig - v.lp - q1 * v.bp;
|
|
|
|
|
v.bp += f1 * hp;
|
|
|
|
|
v.lp += f1 * v.bp;
|
|
|
|
|
|
2026-09-14 02:27:45 +02:00
|
|
|
// The Chamberlin band-pass and high-pass taps carry less energy
|
|
|
|
|
// than the low-pass tap for harmonically rich osc waves, so
|
|
|
|
|
// compensate to keep filter types at a comparable loudness level.
|
2026-09-07 21:03:31 +02:00
|
|
|
float filtered = sig;
|
|
|
|
|
switch ((int) p.fltType)
|
|
|
|
|
{
|
|
|
|
|
case 1: filtered = v.lp; break;
|
2026-09-14 02:27:45 +02:00
|
|
|
case 2: filtered = v.bp * 1.5f; break;
|
|
|
|
|
case 3: filtered = hp * 1.4f; break;
|
2026-09-07 21:03:31 +02:00
|
|
|
default: break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// amplitude envelope (+ tremolo if LFO targets amp)
|
|
|
|
|
const float ae = v.amp.next();
|
|
|
|
|
float ampMod = 1.0f;
|
|
|
|
|
if ((int) p.lfoTarget == 2)
|
2026-09-08 16:37:33 +02:00
|
|
|
ampMod = 1.0f - p.lfoDepth * 0.5f * (1.0f + lfo);
|
add generative evolution, cross-mod and reverb FX; expand preset bank
- 4 new params (xmod/xmix/evolve/warp) with free-running brownian evolution
- evolution: tune drift, osc2 detune sway, osc-mix crossfade, filter cutoff
wander, resonance growl, warp drive/bias, cross-mod bloom
- per-voice unison detune spread for fat stacks
- FX engine: 3-tap ensemble chorus, filtered-feedback ping-pong delay,
Schroeder hall reverb; new FX types 4-7 (REV HALL/ENS+REV/DLY+REV/CATHEDRAL)
- 195 presets: 40-field realignment, genre-tuned FX identities, showcase eval
- calibrate banks vs attack-inclusive peak (median 0.263); fix square-LFO
on filter sustain collapse
2026-09-10 16:55:47 +02:00
|
|
|
ampMod *= 1.0f + evoSlow * p.evolve * 0.10f;
|
2026-09-07 21:03:31 +02:00
|
|
|
|
|
|
|
|
v.outSmooth += (filtered - v.outSmooth) * 0.12f;
|
2026-09-08 21:57:34 +02:00
|
|
|
float o = v.outSmooth * (ae * ampMod) * v.vel * voiceGain;
|
2026-09-08 15:10:12 +02:00
|
|
|
if (! std::isfinite (o)) o = 0.0f;
|
2026-09-07 21:03:31 +02:00
|
|
|
|
|
|
|
|
mixL += o * v.panL;
|
|
|
|
|
mixR += o * v.panR;
|
|
|
|
|
|
|
|
|
|
if (v.amp.getState() == Env::Off)
|
|
|
|
|
{
|
|
|
|
|
v.active = false;
|
|
|
|
|
if (active > 0) --active;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-08 15:10:12 +02:00
|
|
|
if (! std::isfinite (mixL)) mixL = 0.0f;
|
|
|
|
|
if (! std::isfinite (mixR)) mixR = 0.0f;
|
2026-09-08 21:57:34 +02:00
|
|
|
|
|
|
|
|
// Soft limiter before tanh for cleaner saturation
|
|
|
|
|
mixL = std::tanh (mixL * 0.8f);
|
|
|
|
|
mixR = std::tanh (mixR * 0.8f);
|
2026-09-07 21:03:31 +02:00
|
|
|
|
|
|
|
|
left[s] += mixL;
|
|
|
|
|
right[s] += mixR;
|
|
|
|
|
}
|
|
|
|
|
}
|