Add delay tempo sync, rack faceplate, README, docs

- Add host-tempo-synced delay: FX screen gains a 4th SYNC row for
  rhythmic divisions (1/16 .. 1/1) that lock delay time to DAW tempo
- Embed rack faceplate background (b2.png) via juce binary data
- Add offline dev probe (rmx_probe) for GUI-less MIDI testing
- Allow overriding JUCE_ROOT when configuring the build
- Write comprehensive README describing the instrument, UI, MIDI, DSP,
  and build/install steps
This commit is contained in:
Armin 2026-09-08 15:10:12 +02:00
commit 2e870380d4
17 changed files with 1413 additions and 324 deletions

View file

@ -64,8 +64,9 @@ SynthEngine::Voice* SynthEngine::acquireVoice()
void SynthEngine::noteOn (int midiNote, float velocity)
{
const float noteFreq = (float) (440.0 * std::pow (2.0, (midiNote - 69.0) / 12.0));
const float noteFreq = (float) (440.0 * std::exp2 ((midiNote - 69.0) / 12.0));
Voice* v = acquireVoice();
const bool fresh = ! v->active;
v->active = true;
v->note = midiNote;
v->vel = 1.0f - std::pow (1.0f - velocity, 1.35f);
@ -77,6 +78,7 @@ void SynthEngine::noteOn (int midiNote, float velocity)
v->lfoIdx = 0;
v->lfoSH = rng.nextFloat() * 2.0f - 1.0f;
v->lp = v->bp = 0.0f;
v->fcSm = 0.0f;
v->outSmooth = 0.0f;
const float p = juce::jlimit (-1.0f, 1.0f, lastPan + (rng.nextFloat() - 0.5f) * 0.1f);
@ -88,7 +90,7 @@ void SynthEngine::noteOn (int midiNote, float velocity)
v->amp.reset(); v->flt.reset();
v->amp.noteOn(); v->flt.noteOn();
lastNoteFreq = noteFreq;
++active;
if (fresh) ++active;
}
void SynthEngine::noteOff (int midiNote)
@ -106,9 +108,6 @@ void SynthEngine::noteOff (int midiNote)
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);
@ -120,8 +119,8 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
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 detuneRatio = std::exp2f (p.detune / 1200.0f);
const float tuneRatio = std::exp2f (p.tune / 12.0f);
const float maxFc = 18000.0f, minFc = 30.0f;
for (int s = 0; s < num; ++s)
@ -160,10 +159,10 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
// 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);
freq *= std::exp2f (lfo * p.lfoDepth * 1.6f / 12.0f);
if (bendSmooth != bendCents)
bendSmooth += (bendCents - bendSmooth) * 0.08f;
freq *= std::pow (2.0f, bendSmooth / 1200.0f);
freq *= std::exp2f (bendSmooth / 1200.0f);
const float d1 = (float) (freq / sr);
const float d2 = d1 * detuneRatio;
@ -180,16 +179,19 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
// 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);
fc *= std::exp2f (p.fltKey * 0.01f * std::log2f (freq / 261.6256f));
fc *= std::exp2f (p.fltEnv * 0.01f * fe * std::log2f (maxFc / fc));
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::exp2f (lfo * p.lfoDepth * 10.0f / 12.0f);
fc *= std::exp2f (mod);
fc = std::min (maxFc, std::max (minFc, fc));
v.fcSm += (fc - v.fcSm) * 0.25f;
fc = v.fcSm;
// 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);
// 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));
const float q1 = 1.0f / (1.0f + p.fltRes * 3.0f);
v.f1c = f1;
const float hp = sig - v.lp - q1 * v.bp;
v.bp += f1 * hp;
@ -211,7 +213,8 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
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;
float o = v.outSmooth * (ae * ampMod) * v.vel;
if (! std::isfinite (o)) o = 0.0f;
mixL += o * v.panL;
mixR += o * v.panR;
@ -223,13 +226,12 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
}
}
peakL = std::max (peakL, std::abs (mixL));
peakR = std::max (peakR, std::abs (mixR));
if (! std::isfinite (mixL)) mixL = 0.0f;
if (! std::isfinite (mixR)) mixR = 0.0f;
mixL = std::tanh (mixL);
mixR = std::tanh (mixR);
left[s] += mixL;
right[s] += mixR;
}
metL = peakL;
metR = peakR;
}