Initial commit: RMX-19 rack sound module (VST3/AU) with dot-matrix LCD UI

This commit is contained in:
Armin 2026-09-07 21:03:31 +02:00
commit c72c0216fd
16 changed files with 2170 additions and 0 deletions

235
Source/SynthEngine.cpp Normal file
View file

@ -0,0 +1,235 @@
#include "SynthEngine.h"
SynthEngine::SynthEngine() { rng.setSeedRandomly(); }
SynthEngine::~SynthEngine() {}
void SynthEngine::setSampleRate (double s)
{
sr = s;
}
float SynthEngine::osc (int wave, double ph)
{
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;
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;
return &voices[stealIdx];
}
void SynthEngine::noteOn (int midiNote, float velocity)
{
const float noteFreq = (float) (440.0 * std::pow (2.0, (midiNote - 69.0) / 12.0));
Voice* v = acquireVoice();
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;
v->lfoPh = rng.nextDouble();
v->lfoIdx = 0;
v->lfoSH = rng.nextFloat() * 2.0f - 1.0f;
v->lp = v->bp = 0.0f;
v->outSmooth = 0.0f;
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;
++active;
}
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)
{
float peakL = metL, peakR = metR;
metL = metR = 0.0f;
for (auto& v : voices)
{
v.amp.setRates (p.ampA, p.ampD, p.ampS, p.ampR, sr);
v.flt.setRates (p.fA, p.fD, p.fS, p.fR, sr);
}
lastGlide = p.glide;
lastPan = p.pan;
polyLimit = juce::jlimit (1, maxVoice, (int) p.poly);
const float glideRate = p.glide <= 0.001f ? 1.0f
: 1.0f - std::exp (-1.0f / (p.glide * (float) sr));
const float detuneRatio = std::pow (2.0f, p.detune / 1200.0f);
const float tuneRatio = std::pow (2.0f, p.tune / 12.0f);
const float maxFc = 18000.0f, minFc = 30.0f;
for (int s = 0; s < num; ++s)
{
float mixL = 0.0f, mixR = 0.0f;
for (auto& v : voices)
{
if (! v.active)
continue;
if (p.glide > 0.001f)
v.curFreq += (v.targetFreq - v.curFreq) * glideRate;
else
v.curFreq = v.targetFreq;
// LFO
v.lfoPh += (double) p.lfoRate / sr;
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)
freq *= std::pow (2.0f, lfo * p.lfoDepth * 1.6f / 12.0f);
if (bendSmooth != bendCents)
bendSmooth += (bendCents - bendSmooth) * 0.08f;
freq *= std::pow (2.0f, bendSmooth / 1200.0f);
const float d1 = (float) (freq / sr);
const float d2 = d1 * detuneRatio;
v.ph1 += d1; if (v.ph1 >= 1.0) v.ph1 -= 1.0;
v.ph2 += d2; if (v.ph2 >= 1.0) v.ph2 -= 1.0;
const float o1 = osc ((int) p.osc1w, v.ph1);
const float o2 = osc ((int) p.osc2w, v.ph2);
const float mixb = p.oscMix * 0.01f;
float sig = o1 * (1.0f - mixb) + o2 * mixb;
const float fe = v.flt.next();
// cutoff with keytrack + filter envelope + LFO + mod wheel
float fc = p.fltCut;
fc *= std::pow (freq / 261.6256f, p.fltKey * 0.01f);
fc *= std::pow (maxFc / fc, p.fltEnv * 0.01f * fe);
if ((int) p.lfoTarget == 0)
fc *= std::pow (2.0f, lfo * p.lfoDepth * 10.0f / 12.0f);
fc *= std::pow (2.0f, mod);
fc = std::min (maxFc, std::max (minFc, fc));
// SVF
const float f1 = 2.0f * std::sin (juce::MathConstants<float>::pi * fc / (float) sr);
const float q1 = 1.0f / (0.5f + p.fltRes * 3.5f);
v.f1c = f1;
const float hp = sig - v.lp - q1 * v.bp;
v.bp += f1 * hp;
v.lp += f1 * v.bp;
float filtered = sig;
switch ((int) p.fltType)
{
case 1: filtered = v.lp; break;
case 2: filtered = v.bp; break;
case 3: filtered = hp; break;
default: break;
}
// amplitude envelope (+ tremolo if LFO targets amp)
const float ae = v.amp.next();
float ampMod = 1.0f;
if ((int) p.lfoTarget == 2)
ampMod = 1.0f - p.lfoDepth * 0.5f * (1.0f + lfo);
v.outSmooth += (filtered - v.outSmooth) * 0.12f;
const float o = v.outSmooth * (ae * ampMod) * v.vel;
mixL += o * v.panL;
mixR += o * v.panR;
if (v.amp.getState() == Env::Off)
{
v.active = false;
if (active > 0) --active;
}
}
peakL = std::max (peakL, std::abs (mixL));
peakR = std::max (peakR, std::abs (mixR));
left[s] += mixL;
right[s] += mixR;
}
metL = peakL;
metR = peakR;
}