add per-oscillator detune (detune1); fix ASR attack handoff

This commit is contained in:
Armin 2026-09-10 02:25:20 +02:00
commit 2a79067a1c
5 changed files with 21 additions and 11 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::detune, prm::Id::oscMix }; static const prm::Id voice[] = { prm::Id::osc1Wave, prm::Id::osc2Wave, prm::Id::detune1, prm::Id::detune, prm::Id::oscMix };
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 };
static const prm::Id lfo[] = { prm::Id::lfoRate, prm::Id::lfoShape, prm::Id::lfoDepth, prm::Id::lfoTarget }; static const prm::Id lfo[] = { prm::Id::lfoRate, prm::Id::lfoShape, prm::Id::lfoDepth, prm::Id::lfoTarget };
@ -743,7 +744,7 @@ int LcdPanel::paramCountForScreen (int idx) const
{ {
switch (idx) switch (idx)
{ {
case 0: return 4; case 0: return 5;
case 1: return 5; case 1: return 5;
case 2: return 8; case 2: return 8;
case 3: return 4; case 3: return 4;
@ -842,7 +843,7 @@ void LcdPanel::stepParam (prm::Id id, int dir, bool coarse)
static prm::Id paramForCursor (int screenIdx, int row) static prm::Id paramForCursor (int screenIdx, int row)
{ {
static const prm::Id voice[] = { prm::Id::osc1Wave, prm::Id::osc2Wave, 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 };
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 };
static const prm::Id lfo[] = { prm::Id::lfoRate, prm::Id::lfoShape, prm::Id::lfoDepth, prm::Id::lfoTarget }; static const prm::Id lfo[] = { prm::Id::lfoRate, prm::Id::lfoShape, prm::Id::lfoDepth, prm::Id::lfoTarget };

View file

@ -7,7 +7,7 @@ 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, 4, 2, true, 1, 1 },
{ "osc2wave", "OSC2 WAVE", "O2WV", 0, 4, 3, true, 1, 1 }, { "osc2wave", "OSC2 WAVE", "O2WV", 0, 4, 3, true, 1, 1 },
{ "detune", "DETUNE", "DTN", -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 },
{ "fltcut", "CUTOFF", "CUT", 20, 20000, 3400, false, 20, 400 }, { "fltcut", "CUTOFF", "CUT", 20, 20000, 3400, false, 20, 400 },
@ -40,6 +40,7 @@ static const Desc table[num] = {
{ "arppattern", "ARP PATTERN", "APT", 0, 3, 0, true, 1, 1 }, { "arppattern", "ARP PATTERN", "APT", 0, 3, 0, true, 1, 1 },
{ "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 },
}; };
const Desc& desc (Id id) { return table[static_cast<int>(id)]; } const Desc& desc (Id id) { return table[static_cast<int>(id)]; }
@ -171,7 +172,8 @@ juce::String formatParam (Id id, float v)
{ {
case Id::osc1Wave: case Id::osc1Wave:
case Id::osc2Wave: return waveName ((int) v); case Id::osc2Wave: return waveName ((int) v);
case Id::detune: return juce::String ((int) v > 0 ? "+" : "") + juce::String ((int) v) + "c"; case Id::detune:
case Id::detune1: return juce::String ((int) v > 0 ? "+" : "") + juce::String ((int) v) + "c";
case Id::oscMix: return juce::String ((int) (100.0f - v)) + "/" + juce::String ((int) v); case Id::oscMix: return juce::String ((int) (100.0f - v)) + "/" + juce::String ((int) v);
case Id::fltType: return fltTypeName ((int) v); case Id::fltType: return fltTypeName ((int) v);
case Id::fltCutoff: case Id::fltCutoff:

View file

@ -12,10 +12,11 @@ enum class Id : int {
fxType, fxAmt, fxTime, fxType, fxAmt, fxTime,
vol, pan, poly, glide, tune, vol, pan, poly, glide, tune,
hq, hq,
arpOn, arpRate, arpPattern, arpOct, arpGate arpOn, arpRate, arpPattern, arpOct, arpGate,
detune1
}; };
constexpr int num = 35; constexpr int num = 36;
struct Desc { struct Desc {
const char* id; const char* id;

View file

@ -125,7 +125,8 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
const float glideRate = p.glide <= 0.001f ? 1.0f const float glideRate = p.glide <= 0.001f ? 1.0f
: 1.0f - std::exp (-1.0f / (p.glide * (float) sr)); : 1.0f - std::exp (-1.0f / (p.glide * (float) sr));
const float detuneRatio = std::exp2f (p.detune / 1200.0f); const float detuneRatio2 = std::exp2f (p.detune / 1200.0f);
const float detuneRatio1 = std::exp2f (p.detune1 / 1200.0f);
const float tuneRatio = std::exp2f (p.tune / 12.0f); const float tuneRatio = std::exp2f (p.tune / 12.0f);
const float maxFc = 0.4f * (float) sr; const float maxFc = 0.4f * (float) sr;
const float minFc = 30.0f; const float minFc = 30.0f;
@ -175,8 +176,8 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num)
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); const float d1 = (float) (freq / sr) * detuneRatio1;
const float d2 = d1 * detuneRatio; const float d2 = (float) (freq / sr) * detuneRatio2;
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.ph2 += d2; if (v.ph2 >= 1.0) v.ph2 -= 1.0;

View file

@ -5,6 +5,7 @@
struct VParams struct VParams
{ {
float osc1w = 2, osc2w = 3, detune = 8, oscMix = 52.f; float osc1w = 2, osc2w = 3, detune = 8, oscMix = 52.f;
float detune1 = 0;
float fltType = 1, fltCut = 3400.f, fltRes = 0.18f, fltEnv = 52.f, fltKey = 40.f; float fltType = 1, fltCut = 3400.f, fltRes = 0.18f, fltEnv = 52.f, fltKey = 40.f;
float ampA = 0.008f, ampD = 0.26f, ampS = 0.70f, ampR = 0.35f; float ampA = 0.008f, ampD = 0.26f, ampS = 0.70f, ampR = 0.35f;
float fA = 0.02f, fD = 0.16f, fS = 0.26f, fR = 0.25f; float fA = 0.02f, fD = 0.16f, fS = 0.26f, fR = 0.25f;
@ -18,6 +19,7 @@ struct VParams
{ {
auto get = [&] (const char* id) { return apvts.getRawParameterValue (id) -> load(); }; auto get = [&] (const char* id) { return apvts.getRawParameterValue (id) -> load(); };
osc1w = get ("osc1wave"); osc2w = get ("osc2wave"); detune = get ("detune"); osc1w = get ("osc1wave"); osc2w = get ("osc2wave"); detune = get ("detune");
detune1 = get ("detune1");
oscMix = get ("oscmix"); oscMix = get ("oscmix");
fltType = get ("flttype"); fltCut = get ("fltcut"); fltRes = get ("fltres"); fltType = get ("flttype"); fltCut = get ("fltcut"); fltRes = get ("fltres");
fltEnv = get ("fltenv"); fltKey = get ("fltkey"); fltEnv = get ("fltenv"); fltKey = get ("fltkey");
@ -69,8 +71,11 @@ public:
switch (st) switch (st)
{ {
case Atk: case Atk:
// (1-cur)*k converges to 1.0 only asymptotically in float and can
// stall below 1.0 forever, so clamp with a tolerance to guarantee
// the attack always hands off to the decay stage.
cur += (1.0f - cur) * std::min (1.0f, (float) (attTau * srInv) * 8.0f); cur += (1.0f - cur) * std::min (1.0f, (float) (attTau * srInv) * 8.0f);
if (cur >= 1.0f) { cur = 1.0f; st = Dec; } if (cur >= 1.0f - 1.0e-4f) { cur = 1.0f; st = Dec; }
break; break;
case Dec: case Dec:
cur = susL + (cur - susL) * std::exp (-decTau * (float) srInv); cur = susL + (cur - susL) * std::exp (-decTau * (float) srInv);