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
This commit is contained in:
Armin 2026-09-10 16:55:47 +02:00
commit 35d6cd014a
8 changed files with 2016 additions and 601 deletions

View file

@ -644,7 +644,8 @@ void LcdPanel::drawParamScreen (juce::Graphics& g)
pxTextM (g, 3, 1, paramScreenTitle (paramScreenIdx), 1.0f); pxTextM (g, 3, 1, paramScreenTitle (paramScreenIdx), 1.0f);
drawFilled (g, 3, 6, gridW - 6, 1, 0.35f); drawFilled (g, 3, 6, gridW - 6, 1, 0.35f);
static const prm::Id voice[] = { prm::Id::osc1Wave, prm::Id::osc2Wave, prm::Id::detune1, prm::Id::detune, prm::Id::oscMix }; static const prm::Id voice[] = { prm::Id::osc1Wave, prm::Id::osc2Wave, prm::Id::detune1, prm::Id::detune, prm::Id::oscMix,
prm::Id::xmod, prm::Id::xmix, prm::Id::evolve, prm::Id::warp };
static const prm::Id filter[] = { prm::Id::fltType, prm::Id::fltCutoff, prm::Id::fltRes, prm::Id::fltEnv, prm::Id::fltKey }; static const prm::Id filter[] = { prm::Id::fltType, prm::Id::fltCutoff, prm::Id::fltRes, prm::Id::fltEnv, prm::Id::fltKey };
static const prm::Id envMt[] = { prm::Id::ampA, prm::Id::ampD, prm::Id::ampS, prm::Id::ampR, prm::Id::fenvA, prm::Id::fenvD, prm::Id::fenvS, prm::Id::fenvR }; static const prm::Id envMt[] = { prm::Id::ampA, prm::Id::ampD, prm::Id::ampS, prm::Id::ampR, prm::Id::fenvA, prm::Id::fenvD, prm::Id::fenvS, prm::Id::fenvR };

View file

@ -5,8 +5,8 @@ namespace prm {
static const Desc table[num] = { static const Desc table[num] = {
// id name short min max def disc fine coarse // id name short min max def disc fine coarse
{ "osc1wave", "OSC1 WAVE", "O1WV", 0, 4, 2, true, 1, 1 }, { "osc1wave", "OSC1 WAVE", "O1WV", 0, 14, 2, true, 1, 1 },
{ "osc2wave", "OSC2 WAVE", "O2WV", 0, 4, 3, true, 1, 1 }, { "osc2wave", "OSC2 WAVE", "O2WV", 0, 14, 3, true, 1, 1 },
{ "detune", "DETUNE 2", "DT2", -50, 50, 8, false, 1, 6 }, { "detune", "DETUNE 2", "DT2", -50, 50, 8, false, 1, 6 },
{ "oscmix", "OSC BAL", "MIX", 0, 100, 52, false, 1, 8 }, { "oscmix", "OSC BAL", "MIX", 0, 100, 52, false, 1, 8 },
{ "flttype", "FILTER TYPE", "FTYP", 0, 3, 1, true, 1, 1 }, { "flttype", "FILTER TYPE", "FTYP", 0, 3, 1, true, 1, 1 },
@ -26,7 +26,7 @@ static const Desc table[num] = {
{ "lfoshape", "LFO SHAPE", "LFS", 0, 3, 0, true, 1, 1 }, { "lfoshape", "LFO SHAPE", "LFS", 0, 3, 0, true, 1, 1 },
{ "lfodepth", "LFO DEPTH", "LFD", 0, 1, 0.22f, false, 0.01f, 0.08f }, { "lfodepth", "LFO DEPTH", "LFD", 0, 1, 0.22f, false, 0.01f, 0.08f },
{ "lfotarget", "LFO TARGET", "LFT", 0, 2, 0, true, 1, 1 }, { "lfotarget", "LFO TARGET", "LFT", 0, 2, 0, true, 1, 1 },
{ "fxtype", "FX TYPE", "FXT", 0, 3, 1, true, 1, 1 }, { "fxtype", "FX TYPE", "FXT", 0, 7, 1, true, 1, 1 },
{ "fxamt", "FX DEPTH", "FXD", 0, 1, 0.30f, false, 0.01f, 0.08f }, { "fxamt", "FX DEPTH", "FXD", 0, 1, 0.30f, false, 0.01f, 0.08f },
{ "fxtime", "FX TIME", "FXT", 0.05f, 1.2f, 0.28f, false, 0.01f, 0.12f }, { "fxtime", "FX TIME", "FXT", 0.05f, 1.2f, 0.28f, false, 0.01f, 0.12f },
{ "vol", "MASTER VOL", "VOL", 0, 1, 0.85f, false, 0.01f, 0.08f }, { "vol", "MASTER VOL", "VOL", 0, 1, 0.85f, false, 0.01f, 0.08f },
@ -41,6 +41,10 @@ static const Desc table[num] = {
{ "arpoct", "ARP OCTAVES", "AOC", 1, 4, 1, true, 1, 1 }, { "arpoct", "ARP OCTAVES", "AOC", 1, 4, 1, true, 1, 1 },
{ "arpgate", "ARP GATE", "AGT", 10, 100, 70, false, 1, 10 }, { "arpgate", "ARP GATE", "AGT", 10, 100, 70, false, 1, 10 },
{ "detune1", "DETUNE 1", "DT1", -50, 50, 0, false, 1, 6 }, { "detune1", "DETUNE 1", "DT1", -50, 50, 0, false, 1, 6 },
{ "xmod", "CROSS MOD", "XML", 0, 3, 0, true, 1, 1 },
{ "xmix", "XMIX AMT", "XMX", 0, 1, 0, false, 0.01f, 0.08f },
{ "evolve", "EVOLVE", "EVO", 0, 1, 0, false, 0.01f, 0.08f },
{ "warp", "WARP", "WRP", 0, 1, 0, false, 0.01f, 0.08f },
}; };
const Desc& desc (Id id) { return table[static_cast<int>(id)]; } const Desc& desc (Id id) { return table[static_cast<int>(id)]; }
@ -74,7 +78,11 @@ juce::AudioProcessorValueTreeState::ParameterLayout createLayout()
const char* waveName (int w) const char* waveName (int w)
{ {
switch (w) { case 0: return "SINE"; case 1: return "TRIANG"; case 2: return "SAWTOOTH"; switch (w) { case 0: return "SINE"; case 1: return "TRIANG"; case 2: return "SAWTOOTH";
case 3: return "PULSE"; case 4: return "NOISE"; default: return "-----"; } case 3: return "PULSE"; case 4: return "NOISE"; case 5: return "FM ";
case 6: return "PWM "; case 7: return "SUPRSW "; case 8: return "DBLSAW ";
case 9: return "WARP "; case 10: return "HSYNC "; case 11: return "TINE ";
case 12: return "SEQ "; case 13: return "CHORD "; case 14: return "SUPRPL ";
default: return "-----"; }
} }
const char* fltTypeName (int t) const char* fltTypeName (int t)
@ -97,8 +105,15 @@ const char* lfoTargetName (int t)
const char* fxTypeName (int t) const char* fxTypeName (int t)
{ {
switch (t) { case 0: return "OFF"; case 1: return "CHORUS"; case 2: return "DELAY "; switch (t) { case 0: return "OFF"; case 1: return "CHORUS"; case 2: return "DELAY ";
case 3: return "SPACE"; default: return "OFF"; } case 3: return "SPACE"; case 4: return "REV HALL"; case 5: return "ENS+REV";
case 6: return "DLY+REV"; case 7: return "CATHEDRAL"; default: return "OFF"; }
}
const char* xmodTypeName (int t)
{
switch (t) { case 0: return "OFF "; case 1: return "RING "; case 2: return "FM ";
case 3: return "AM "; default: return "OFF "; }
} }
const char* arpRateName (int r) const char* arpRateName (int r)
@ -204,6 +219,10 @@ juce::String formatParam (Id id, float v)
return juce::String (t > 0 ? "+" : "") + juce::String (t) + "ST"; return juce::String (t > 0 ? "+" : "") + juce::String (t) + "ST";
} }
case Id::hq: return v >= 0.5f ? "HQ" : "ECO"; case Id::hq: return v >= 0.5f ? "HQ" : "ECO";
case Id::xmod: return xmodTypeName ((int) v);
case Id::xmix:
case Id::evolve:
case Id::warp: return fmtPercent (v);
case Id::arpOn: return v >= 0.5f ? "ON " : "OFF"; case Id::arpOn: return v >= 0.5f ? "ON " : "OFF";
case Id::arpRate: return arpRateName ((int) v); case Id::arpRate: return arpRateName ((int) v);
case Id::arpPattern: return arpPatternName ((int) v); case Id::arpPattern: return arpPatternName ((int) v);

View file

@ -13,10 +13,11 @@ enum class Id : int {
vol, pan, poly, glide, tune, vol, pan, poly, glide, tune,
hq, hq,
arpOn, arpRate, arpPattern, arpOct, arpGate, arpOn, arpRate, arpPattern, arpOct, arpGate,
detune1 detune1,
xmod, xmix, evolve, warp
}; };
constexpr int num = 36; constexpr int num = 40;
struct Desc { struct Desc {
const char* id; const char* id;
@ -40,6 +41,7 @@ const char* fltTypeName (int t);
const char* lfoShapeName (int s); const char* lfoShapeName (int s);
const char* lfoTargetName (int t); const char* lfoTargetName (int t);
const char* fxTypeName (int t); const char* fxTypeName (int t);
const char* xmodTypeName (int t);
const char* arpRateName (int r); const char* arpRateName (int r);
const char* arpPatternName (int p); const char* arpPatternName (int p);

View file

@ -14,7 +14,8 @@ static inline void dbgNote (const char* ev, int note, float vel)
} }
class RMX19AudioProcessor::FxEngine class RMX19AudioProcessor::FxEngine
{public: {
public:
void prepare (double sampleRate, int maxSamples) void prepare (double sampleRate, int maxSamples)
{ {
sr = sampleRate; sr = sampleRate;
@ -28,6 +29,9 @@ class RMX19AudioProcessor::FxEngine
chorusR.setMaximumDelayInSamples (maxDelay); chorusR.setMaximumDelayInSamples (maxDelay);
delL.setMaximumDelayInSamples (maxDelay); delL.setMaximumDelayInSamples (maxDelay);
delR.setMaximumDelayInSamples (maxDelay); delR.setMaximumDelayInSamples (maxDelay);
rebuildCombBuffers (true);
rebuildCombBuffers (false);
} }
void reset() void reset()
@ -35,6 +39,11 @@ class RMX19AudioProcessor::FxEngine
chorusL.reset(); chorusR.reset(); chorusL.reset(); chorusR.reset();
delL.reset(); delR.reset(); delL.reset(); delR.reset();
chPh = 0.0; chPh = 0.0;
lpL = lpR = 0.0f;
for (auto& c : combL) c.reset();
for (auto& c : combR) c.reset();
for (auto& a : apL) a.reset();
for (auto& a : apR) a.reset();
} }
void process (const VParams& p, float* L, float* R, int num, int syncIdx, double bpm) void process (const VParams& p, float* L, float* R, int num, int syncIdx, double bpm)
@ -43,53 +52,137 @@ class RMX19AudioProcessor::FxEngine
if (type == 0) return; if (type == 0) return;
const float wet = p.fxAmt; const float wet = p.fxAmt;
const float dry = 1.0f - wet * 0.5f; const float dryCh = 1.0f - wet * 0.5f;
const float chBase = 0.008f * (float) sr; // 8 ms chorus base const float chBase = 0.009f * (float) sr; // 9 ms ensemble base
const float chDepth = 0.0035f * (float) sr; // +-3.5 ms wobble const float chDepth = 0.006f * (float) sr; // +-6 ms wobble
float delSamples = (float) (p.fxTime * sr); float delSamples = (float) (p.fxTime * sr);
if (syncIdx > 0 && bpm > 1.0) if (syncIdx > 0 && bpm > 1.0)
delSamples = (float) ((60.0 / bpm) * prm::delaySyncBeats (syncIdx) * sr); delSamples = (float) ((60.0 / bpm) * prm::delaySyncBeats (syncIdx) * sr);
const float feedback = 0.40f; const float fb = 0.20f + 0.35f * wet;
const float ff = 0.18f; // darkening of repeats
const bool doChorus = (type == 1 || type == 3 || type == 5 || type == 7);
const bool doDelay = (type == 2 || type == 3 || type == 6 || type == 7);
const bool doVerb = (type >= 4);
for (int s = 0; s < num; ++s) for (int s = 0; s < num; ++s)
{ {
const float inL = L[s], inR = R[s]; const float inL = L[s], inR = R[s];
float outL = inL * dry, outR = inR * dry; float outL = inL * dryCh, outR = inR * dryCh;
if (type == 1 || type == 3) if (doChorus)
{ {
const double rate = type == 3 ? 0.55 : 0.38; // 3-voice ensemble with a touch of internal regeneration
chPh += rate / sr; const double w = chPh * 2.0 * juce::MathConstants<double>::pi;
if (chPh >= 1.0) chPh -= 1.0; const float t1 = (float) std::sin (w);
const float m = 0.5f + 0.5f * (float) std::sin (chPh * 2.0 * juce::MathConstants<double>::pi); const float t2 = (float) std::sin (w + 2.09439f);
const float t3 = (float) std::sin (w + 4.18879f);
chPh += 0.34 / sr; if (chPh >= 1.0) chPh -= 1.0;
chorusL.pushSample (0, inL); chorusL.pushSample (0, inL);
chorusR.pushSample (0, inR); chorusR.pushSample (0, inR);
const float mL = chorusL.popSample (0, chBase + chDepth * m); const float mL = 0.5f * (chorusL.popSample (0, chBase + chDepth * t1)
const float mR = chorusR.popSample (0, chBase + chDepth * (1.0f - m)); + chorusL.popSample (0, chBase + chDepth * t2));
outL += mL * wet * 0.7f; const float mR = 0.5f * (chorusR.popSample (0, chBase + chDepth * t2)
outR += mR * wet * 0.7f; + chorusR.popSample (0, chBase + chDepth * t3));
chorusL.pushSample (0, mL * 0.30f);
chorusR.pushSample (0, mR * 0.30f);
outL += mL * wet * 0.8f;
outR += mR * wet * 0.8f;
} }
if (type == 2 || type == 3) if (doDelay)
{ {
const float rL = delL.popSample (0, delSamples); const float rL = delL.popSample (0, delSamples);
const float rR = delR.popSample (0, delSamples); const float rR = delR.popSample (0, delSamples);
delL.pushSample (0, inL + rR * feedback); lpL += ff * (rL - lpL); lpR += ff * (rR - lpR);
delR.pushSample (0, inR + rL * feedback); delL.pushSample (0, inL + lpR * fb);
delR.pushSample (0, inR + lpL * fb);
outL += rL * wet; outL += rL * wet;
outR += rR * wet; outR += rR * wet;
} }
if (doVerb)
{
float revL = 0.0f, revR = 0.0f;
for (int i = 0; i < 6; ++i)
{
float o = combL[i].process (inL * 0.25f, 0.84f, 0.30f);
revL += o * 0.22f;
o = combR[i].process (inR * 0.25f, 0.84f, 0.30f);
revR += o * 0.22f;
}
apL[0].process (revL, 0.70f); apL[1].process (revL, 0.57f);
apR[0].process (revR, 0.70f); apR[1].process (revR, 0.57f);
const float revWet = (type == 7 ? wet : wet * 0.8f);
outL += revL * revWet;
outR += revR * revWet;
}
L[s] = outL; L[s] = outL;
R[s] = outR; R[s] = outR;
} }
} }
private: private:
struct Comb
{
void prepare (double s, double seconds)
{
size = (int) (s * seconds);
buf.assign (size, 0.0f);
idx = 0; out = 0.0f; lpIn = 0.0f; lpOut = 0.0f;
}
void reset() { std::fill (buf.begin(), buf.end(), 0.0f); idx = 0; out = 0.0f; lpIn = lpOut = 0.0f; }
float process (float in, float fb, float damp)
{
out = buf[idx];
lpOut = out * (1.0f - damp) + lpIn * damp;
lpIn = out;
buf[idx] = in + lpOut * fb;
if (++idx >= size) idx = 0;
return out;
}
std::vector<float> buf;
int size = 0, idx = 0;
float out = 0.0f, lpIn = 0.0f, lpOut = 0.0f;
};
struct Allpass
{
void prepare (double s, double seconds)
{
size = (int) (s * seconds);
buf.assign (size, 0.0f);
idx = 0;
}
void reset() { std::fill (buf.begin(), buf.end(), 0.0f); idx = 0; }
void process (float& x, float fb)
{
const float w = buf[idx];
buf[idx] = x + w * fb;
x = w - fb * buf[idx];
if (++idx >= size) idx = 0;
}
std::vector<float> buf;
int size = 0, idx = 0;
};
void rebuildCombBuffers (bool left)
{
const double combs[6] = { 0.0297, 0.0371, 0.0411, 0.0437, 0.0311, 0.0290 };
const double off = left ? 0.0 : 0.008;
for (int i = 0; i < 6; ++i)
(left ? combL[i] : combR[i]).prepare (sr, combs[i] + off);
(left ? apL[0] : apR[0]).prepare (sr, 0.0050 + (left ? 0.0 : 0.0011));
(left ? apL[1] : apR[1]).prepare (sr, 0.0161 + (left ? 0.0 : 0.0023));
}
juce::dsp::DelayLine<float> chorusL, chorusR, delL, delR; juce::dsp::DelayLine<float> chorusL, chorusR, delL, delR;
std::array<Comb, 6> combL, combR;
std::array<Allpass, 2> apL, apR;
double sr = 48000.0; double sr = 48000.0;
double chPh = 0.0; double chPh = 0.0;
float lpL = 0.0f, lpR = 0.0f;
}; };
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View file

@ -22,7 +22,7 @@ public:
const juce::String getName() const override; const juce::String getName() const override;
bool acceptsMidi() const override { return true; } bool acceptsMidi() const override { return true; }
bool producesMidi() const override { return false; } bool producesMidi() const override { return false; }
double getTailLengthSeconds() const override { return prm::delaySyncMaxTime() + 1.0; } double getTailLengthSeconds() const override { return prm::delaySyncMaxTime() + 2.0; }
int getNumPrograms() override; int getNumPrograms() override;
int getCurrentProgram() override; int getCurrentProgram() override;

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,7 @@ void SynthEngine::setSampleRate (double s)
sr = s; sr = s;
} }
float SynthEngine::osc (int wave, double ph) float SynthEngine::osc (int wave, double ph, double phRaw)
{ {
switch (wave) switch (wave)
{ {
@ -17,6 +17,90 @@ float SynthEngine::osc (int wave, double ph)
case 2: return (float) (1.0 - 2.0 * ph); case 2: return (float) (1.0 - 2.0 * ph);
case 3: return (float) (ph < 0.5 ? -1.0 : 1.0); case 3: return (float) (ph < 0.5 ? -1.0 : 1.0);
case 4: return rng.nextFloat() * 2.0f - 1.0f; case 4: return rng.nextFloat() * 2.0f - 1.0f;
// 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;
}
return (float) (s * 0.40);
}
// 8 DBLSAW: saw plus a sub-octave saw (big, weighted low end)
case 8:
{
double sub = ph * 0.5;
sub -= std::floor (sub);
return (float) (0.62 * (1.0 - 2.0 * ph) + 0.48 * (1.0 - 2.0 * sub));
}
// 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;
return (float) (0.85 * std::sin (a)
+ 0.55 * std::sin (2.0 * a)
+ 0.35 * std::sin (4.0 * a)
+ 0.18 * std::sin (8.0 * a));
}
// 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;
return (float) (0.62 * std::sin (a)
+ 0.42 * std::sin (a * (6.0 / 5.0))
+ 0.38 * std::sin (a * 1.5));
}
// 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));
}
default: return 0.0f; default: return 0.0f;
} }
} }
@ -80,12 +164,19 @@ void SynthEngine::noteOn (int midiNote, float velocity)
v->targetFreq = noteFreq; v->targetFreq = noteFreq;
v->ph1 = rng.nextDouble() * 0.5; v->ph1 = rng.nextDouble() * 0.5;
v->ph2 = rng.nextDouble() * 0.5; v->ph2 = rng.nextDouble() * 0.5;
v->ph1r = rng.nextDouble() * 8.0;
v->ph2r = rng.nextDouble() * 8.0;
v->lfoPh = rng.nextDouble(); v->lfoPh = rng.nextDouble();
v->lfoIdx = 0; v->lfoIdx = 0;
v->lfoSH = rng.nextFloat() * 2.0f - 1.0f; v->lfoSH = rng.nextFloat() * 2.0f - 1.0f;
v->lp = v->bp = 0.0f; v->lp = v->bp = 0.0f;
v->fcSm = 0.0f; v->fcSm = 0.0f;
v->outSmooth = 0.0f; v->outSmooth = 0.0f;
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));
const float p = juce::jlimit (-1.0f, 1.0f, lastPan + (rng.nextFloat() - 0.5f) * 0.1f); 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 ang = (p + 1.0f) * juce::MathConstants<float>::pi * 0.25f;
@ -114,6 +205,7 @@ void SynthEngine::noteOff (int midiNote)
void SynthEngine::render (const VParams& p, float* left, float* right, int num) void SynthEngine::render (const VParams& p, float* left, float* right, int num)
{ {
curDetune1 = p.detune1;
for (auto& v : voices) for (auto& v : voices)
{ {
v.amp.setRates (p.ampA, p.ampD, p.ampS, p.ampR, sr); v.amp.setRates (p.ampA, p.ampD, p.ampS, p.ampR, sr);
@ -131,8 +223,33 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
const float maxFc = 0.4f * (float) sr; const float maxFc = 0.4f * (float) sr;
const float minFc = 30.0f; const float minFc = 30.0f;
// 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.
for (int s = 0; s < num; ++s) for (int s = 0; s < num; ++s)
{ {
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));
float mixL = 0.0f, mixR = 0.0f; float mixL = 0.0f, mixR = 0.0f;
int activeCount = 0; int activeCount = 0;
for (auto& v : voices) for (auto& v : voices)
@ -172,29 +289,74 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
float freq = v.curFreq * tuneRatio; float freq = v.curFreq * tuneRatio;
if ((int) p.lfoTarget == 1) if ((int) p.lfoTarget == 1)
freq *= std::exp2f (lfo * p.lfoDepth * 1.6f / 12.0f); freq *= std::exp2f (lfo * p.lfoDepth * 1.6f / 12.0f);
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);
}
if (bendSmooth != bendCents) if (bendSmooth != bendCents)
bendSmooth += (bendCents - bendSmooth) * 0.08f; bendSmooth += (bendCents - bendSmooth) * 0.08f;
freq *= std::exp2f (bendSmooth / 1200.0f); freq *= std::exp2f (bendSmooth / 1200.0f);
const float d1 = (float) (freq / sr) * detuneRatio1; const float d1 = (float) (freq / sr) * detuneRatio1 * std::exp2f (v.dtSp / 1200.0f);
const float d2 = (float) (freq / sr) * detuneRatio2; // soundscape detune sway: osc2 slowly zooms its detune in/out
const float swayT = std::exp2f (evoSlow * p.evolve * 16.0f / 1200.0f);
const float d2 = (float) (freq / sr) * detuneRatio2 * swayT;
v.ph1 += d1; if (v.ph1 >= 1.0) v.ph1 -= 1.0; v.ph1 += d1; if (v.ph1 >= 1.0) v.ph1 -= 1.0;
v.ph2 += d2; if (v.ph2 >= 1.0) v.ph2 -= 1.0; v.ph1r += d1;
const float o1 = osc ((int) p.osc1w, v.ph1); // cross-mod "bloom": the amount itself breathes with the evolution walk,
const float o2 = osc ((int) p.osc2w, v.ph2); // sometimes vanishing almost entirely, sometimes saturating
const float mixb = p.oscMix * 0.01f; float xmA = 0.0f;
if ((int) p.xmod > 0 && p.xmix > 0.001f)
xmA = p.xmix * (0.10f + 0.90f * (0.5f + 0.5f * evoCur));
// 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));
float sig = o1 * (1.0f - mixb) + o2 * mixb; float sig = o1 * (1.0f - mixb) + o2 * mixb;
// ring / AM cross-mod spiced into the mix
if (xmA > 0.0f)
{
const int xm = (int) p.xmod;
if (xm == 1) sig += o1 * o2 * 0.6f * xmA;
else if (xm == 3) sig += o1 * o2 * 0.9f * xmA;
}
// 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);
}
const float fe = v.flt.next(); const float fe = v.flt.next();
// gentle analog drive ahead of the filter (resonance pushes harder)
sig = std::tanh (sig * (1.0f + p.fltRes * 0.15f));
// cutoff with keytrack + filter envelope + LFO + mod wheel // cutoff with keytrack + filter envelope + LFO + mod wheel
float fc = p.fltCut; float fc = p.fltCut;
fc *= std::exp2f (p.fltKey * 0.01f * std::log2f (freq / 261.6256f)); fc *= std::exp2f (p.fltKey * 0.01f * std::log2f (freq / 261.6256f));
fc *= std::exp2f (p.fltEnv * 0.01f * fe * std::log2f (maxFc / fc)); fc *= std::exp2f (p.fltEnv * 0.01f * fe * std::log2f (maxFc / fc));
if ((int) p.lfoTarget == 0) if ((int) p.lfoTarget == 0)
fc *= std::exp2f (lfo * p.lfoDepth * 10.0f / 12.0f); fc *= std::exp2f (lfo * p.lfoDepth * 10.0f / 12.0f);
if (p.evolve > 0.001f)
fc *= std::exp2f (evoCur * p.evolve * 4.0f / 12.0f);
fc *= std::exp2f (mod); fc *= std::exp2f (mod);
fc = std::min (maxFc, std::max (minFc, fc)); fc = std::min (maxFc, std::max (minFc, fc));
v.fcSm += (fc - v.fcSm) * 0.25f; v.fcSm += (fc - v.fcSm) * 0.25f;
@ -203,18 +365,23 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
// SVF (cap f1 to keep the Chamberlin structure stable; past ~1.5 the // SVF (cap f1 to keep the Chamberlin structure stable; past ~1.5 the
// resonance feedback can self-oscillate and sputter at maxed cutoff+res) // 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 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); // 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));
v.f1c = f1; v.f1c = f1;
const float hp = sig - v.lp - q1 * v.bp; const float hp = sig - v.lp - q1 * v.bp;
v.bp += f1 * hp; v.bp += f1 * hp;
v.lp += f1 * v.bp; v.lp += f1 * v.bp;
// The Chamberlin band-pass and high-pass taps carry much less energy
// than the low-pass tap for the harmonically rich osc waves (2-4x
// quieter in practice), so compensate to keep filter types at a
// comparable loudness level.
float filtered = sig; float filtered = sig;
switch ((int) p.fltType) switch ((int) p.fltType)
{ {
case 1: filtered = v.lp; break; case 1: filtered = v.lp; break;
case 2: filtered = v.bp; break; case 2: filtered = v.bp * 2.9f; break;
case 3: filtered = hp; break; case 3: filtered = hp * 2.7f; break;
default: break; default: break;
} }
@ -223,6 +390,7 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
float ampMod = 1.0f; float ampMod = 1.0f;
if ((int) p.lfoTarget == 2) if ((int) p.lfoTarget == 2)
ampMod = 1.0f - p.lfoDepth * 0.5f * (1.0f + lfo); ampMod = 1.0f - p.lfoDepth * 0.5f * (1.0f + lfo);
ampMod *= 1.0f + evoSlow * p.evolve * 0.10f;
v.outSmooth += (filtered - v.outSmooth) * 0.12f; v.outSmooth += (filtered - v.outSmooth) * 0.12f;
float o = v.outSmooth * (ae * ampMod) * v.vel * voiceGain; float o = v.outSmooth * (ae * ampMod) * v.vel * voiceGain;

View file

@ -14,6 +14,7 @@ struct VParams
float vol = 0.85f, pan = 0.0f, poly = 10.f, glide = 0.f, tune = 0.f; float vol = 0.85f, pan = 0.0f, poly = 10.f, glide = 0.f, tune = 0.f;
float hq = 0.0f; float hq = 0.0f;
float arpOn = 0, arpRate = 2, arpPattern = 0, arpOct = 1, arpGate = 70; float arpOn = 0, arpRate = 2, arpPattern = 0, arpOct = 1, arpGate = 70;
float xmod = 0, xmix = 0, evolve = 0, warp = 0;
void update (const juce::AudioProcessorValueTreeState& apvts) void update (const juce::AudioProcessorValueTreeState& apvts)
{ {
@ -38,6 +39,10 @@ struct VParams
arpPattern = get ("arppattern"); arpPattern = get ("arppattern");
arpOct = get ("arpoct"); arpOct = get ("arpoct");
arpGate = get ("arpgate"); arpGate = get ("arpgate");
xmod = get ("xmod");
xmix = get ("xmix");
evolve = get ("evolve");
warp = get ("warp");
} }
}; };
@ -131,12 +136,15 @@ private:
float panL = 0.71f, panR = 0.71f; float panL = 0.71f, panR = 0.71f;
float curFreq = 0.0f, targetFreq = 0.0f; float curFreq = 0.0f, targetFreq = 0.0f;
double ph1 = 0.0, ph2 = 0.0, lfoPh = 0.0; double ph1 = 0.0, ph2 = 0.0, lfoPh = 0.0;
double ph1r = 0.0, ph2r = 0.0; // unwrapped phase for animated waves (FM/PWM/SUPRSW)
float lfoSH = 0.0f; float lfoSH = 0.0f;
int lfoIdx = 0; int lfoIdx = 0;
Env amp, flt; Env amp, flt;
float lp = 0.0f, bp = 0.0f, f1c = 0.0f; float lp = 0.0f, bp = 0.0f, f1c = 0.0f;
float fcSm = 0.0f; float fcSm = 0.0f;
float outSmooth = 0.0f; float outSmooth = 0.0f;
float drift = 0.0f, driftT = 0.0f; // slow random-unstable tuning wander
float dtSp = 0.0f; // unison detune spread (per voice, cents)
}; };
static constexpr int maxVoice = 16; static constexpr int maxVoice = 16;
@ -150,7 +158,12 @@ private:
int stealIdx = 0; int stealIdx = 0;
juce::Random rng; juce::Random rng;
juce::Array<int> sustainedNotes; juce::Array<int> sustainedNotes;
double evoGoalT = 0.0; // samples until the next evolution excursion
float evoCur = 0.0f; // brownian wander in [-1, 1] (texture-scale)
float evoGoal = 0.0f; // excursion target, re-picked every 7-18 s
float evoSlow = 0.0f; // much slower wander (soundscape-scale, 1-3 min)
float curDetune1 = 0.0f; // cached for noteOn (unison spread sizing)
float osc (int wave, double ph); float osc (int wave, double ph, double phRaw);
Voice* acquireVoice(); Voice* acquireVoice();
}; };