diff --git a/README.md b/README.md index 1bbf0d5..800b88f 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,17 @@ state-variable filter, dual envelopes, an LFO, a built-in FX unit (chorus / delay / space), and a factory patch bank - all edited and browsed through the on-screen LCD instead of a conventional panel of knobs. +## Screenshot + ![RMX-19 screenshot](shot.png) +--- + +## Blueprint + +![RMX-19 blueprint](blueprint.png) + + --- ## What it is at a glance @@ -126,7 +135,7 @@ The interface is a small nested menu system: | Screen | Parameters | |--------|------------| -| **VOICE** | OSC1 WAVE, OSC2 WAVE, DETUNE, OSC BAL | +| **VOICE** | OSC1 WAVE, OSC2 WAVE, COARSE TUNE 1, COARSE TUNE 2, DETUNE 1, DETUNE 2, OSC BAL | | **FILTER** | FILTER TYPE, CUTOFF, RESONANCE, FILT ENV, KEY TRACK | | **ENVELOPE** | AMP ATTACK, AMP DECAY, AMP SUSTAIN, AMP RELEASE, FENV ATTACK, FENV DECAY, FENV SUSTAIN, FENV RELEASE | | **LFO** | LFO RATE, LFO SHAPE, LFO DEPTH, LFO TARGET | @@ -174,7 +183,9 @@ the host map onto the factory bank. - **Voice engine** - each voice is a classic two-osc → mix → SVF filter → amp-envelope chain. The oscillator mix is a crossfade between OSC1 and - OSC2, and detune offsets OSC2's frequency (OSC1 × `2^(detune/1200)`). + OSC2, and each oscillator has its own tuning: a coarse tune in semitones + (`2^(ctune/12)`) plus a fine detune in cents (`2^(detuneN/1200)`, DETUNE 1 → + OSC1, DETUNE 2 → OSC2). - **Filter** - a stable Chamberlin state-variable filter; cutoff is modulated by the filter envelope, key tracking, the LFO, and the mod wheel, with a smoothed control signal to suppress zipper noise. diff --git a/Source/LcdPanel.cpp b/Source/LcdPanel.cpp index ecaf59f..b6c5343 100644 --- a/Source/LcdPanel.cpp +++ b/Source/LcdPanel.cpp @@ -644,8 +644,8 @@ void LcdPanel::drawParamScreen (juce::Graphics& g) pxTextM (g, 3, 1, paramScreenTitle (paramScreenIdx), 1.0f); 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, - prm::Id::xmod, prm::Id::xmix, prm::Id::evolve, prm::Id::warp }; + static const prm::Id voice[] = { prm::Id::osc1Wave, prm::Id::osc2Wave, prm::Id::ctune1, prm::Id::ctune2, + 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 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 }; @@ -745,7 +745,7 @@ int LcdPanel::paramCountForScreen (int idx) const { switch (idx) { - case 0: return 5; + case 0: return 7; case 1: return 5; case 2: return 8; case 3: return 4; @@ -844,7 +844,8 @@ void LcdPanel::stepParam (prm::Id id, int dir, bool coarse) static prm::Id paramForCursor (int screenIdx, int row) { - 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::ctune1, prm::Id::ctune2, + 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 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 }; diff --git a/Source/Params.cpp b/Source/Params.cpp index 61ff47a..5fa3b95 100644 --- a/Source/Params.cpp +++ b/Source/Params.cpp @@ -45,6 +45,8 @@ static const Desc table[num] = { { "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 }, + { "ctune1", "COARSE TUNE 1","CT1", -24, 24, 0, true, 1, 1 }, + { "ctune2", "COARSE TUNE 2","CT2", -24, 24, 0, true, 1, 1 }, }; const Desc& desc (Id id) { return table[static_cast(id)]; } @@ -218,6 +220,11 @@ juce::String formatParam (Id id, float v) const int t = (int) v; return juce::String (t > 0 ? "+" : "") + juce::String (t) + "ST"; } + case Id::ctune1: + case Id::ctune2: { + const int t = (int) v; + return juce::String (t > 0 ? "+" : "") + juce::String (t) + "ST"; + } case Id::hq: return v >= 0.5f ? "HQ" : "ECO"; case Id::xmod: return xmodTypeName ((int) v); case Id::xmix: diff --git a/Source/Params.h b/Source/Params.h index fceb0aa..7947a15 100644 --- a/Source/Params.h +++ b/Source/Params.h @@ -14,10 +14,11 @@ enum class Id : int { hq, arpOn, arpRate, arpPattern, arpOct, arpGate, detune1, - xmod, xmix, evolve, warp + xmod, xmix, evolve, warp, + ctune1, ctune2 }; -constexpr int num = 40; +constexpr int num = 42; struct Desc { const char* id; diff --git a/Source/PresetBank.cpp b/Source/PresetBank.cpp index 0e20cb7..f25dddd 100644 --- a/Source/PresetBank.cpp +++ b/Source/PresetBank.cpp @@ -14,6 +14,7 @@ enum { O_SIN = 0, O_TRI, O_SAW, O_PUL, O_NOI, O_FM, O_PWM, O_SUP, O_DSW, O_WRP, // 24 vol 25 pan 26 poly 27 glide 28 tune 29 hq // 30 arpOn 31 arpRate 32 arpPattern 33 arpOct 34 arpGate // 35 detune1 36 xmod 37 xmix 38 evolve 39 warp +// 40 ctune1 41 ctune2 (default 0; not stored per-patch) static const Patch defs[] = { // == POLY PAD ========================================================== diff --git a/Source/SynthEngine.cpp b/Source/SynthEngine.cpp index f808f6e..1ccd621 100644 --- a/Source/SynthEngine.cpp +++ b/Source/SynthEngine.cpp @@ -220,6 +220,8 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num) 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 ctuneRatio1 = std::exp2f (p.ctune1 / 12.0f); + const float ctuneRatio2 = std::exp2f (p.ctune2 / 12.0f); const float maxFc = 0.4f * (float) sr; const float minFc = 30.0f; @@ -299,10 +301,10 @@ void SynthEngine::render (const VParams& p, float* left, float* right, int num) bendSmooth += (bendCents - bendSmooth) * 0.08f; freq *= std::exp2f (bendSmooth / 1200.0f); - const float d1 = (float) (freq / sr) * detuneRatio1 * std::exp2f (v.dtSp / 1200.0f); + const float d1 = (float) (freq / sr) * detuneRatio1 * ctuneRatio1 * std::exp2f (v.dtSp / 1200.0f); // 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; + const float d2 = (float) (freq / sr) * detuneRatio2 * ctuneRatio2 * swayT; v.ph1 += d1; if (v.ph1 >= 1.0) v.ph1 -= 1.0; v.ph1r += d1; diff --git a/Source/SynthEngine.h b/Source/SynthEngine.h index dbd5bf9..106a8b7 100644 --- a/Source/SynthEngine.h +++ b/Source/SynthEngine.h @@ -6,6 +6,7 @@ struct VParams { float osc1w = 2, osc2w = 3, detune = 8, oscMix = 52.f; float detune1 = 0; + float ctune1 = 0, ctune2 = 0; 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 fA = 0.02f, fD = 0.16f, fS = 0.26f, fR = 0.25f; @@ -21,6 +22,7 @@ struct VParams auto get = [&] (const char* id) { return apvts.getRawParameterValue (id) -> load(); }; osc1w = get ("osc1wave"); osc2w = get ("osc2wave"); detune = get ("detune"); detune1 = get ("detune1"); + ctune1 = get ("ctune1"); ctune2 = get ("ctune2"); oscMix = get ("oscmix"); fltType = get ("flttype"); fltCut = get ("fltcut"); fltRes = get ("fltres"); fltEnv = get ("fltenv"); fltKey = get ("fltkey"); diff --git a/blueprint.png b/blueprint.png new file mode 100644 index 0000000..bb6346d Binary files /dev/null and b/blueprint.png differ