2026-09-07 21:03:31 +02:00
|
|
|
#include "PluginProcessor.h"
|
|
|
|
|
#include "PluginEditor.h"
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-09-08 15:10:12 +02:00
|
|
|
static inline void dbgNote (const char* ev, int note, float vel)
|
|
|
|
|
{
|
|
|
|
|
juce::File (juce::File::getSpecialLocation (juce::File::userHomeDirectory)
|
|
|
|
|
.getChildFile ("rmx19_notes.log"))
|
|
|
|
|
.appendText (juce::String ("t=") + juce::String ((juce::int64) juce::Time::getMillisecondCounter())
|
|
|
|
|
+ " " + ev + " n=" + juce::String (note)
|
|
|
|
|
+ (vel >= 0.0f ? juce::String (" v=") + juce::String (vel, 2) : juce::String())
|
|
|
|
|
+ "\n");
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-07 21:03:31 +02:00
|
|
|
class RMX19AudioProcessor::FxEngine
|
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
2026-09-10 16:55:47 +02:00
|
|
|
{
|
|
|
|
|
public:
|
2026-09-07 21:03:31 +02:00
|
|
|
void prepare (double sampleRate, int maxSamples)
|
|
|
|
|
{
|
|
|
|
|
sr = sampleRate;
|
|
|
|
|
chorusL.prepare ({ sampleRate, 1u, (juce::uint32) maxSamples });
|
|
|
|
|
chorusR.prepare ({ sampleRate, 1u, (juce::uint32) maxSamples });
|
|
|
|
|
delL.prepare ({ sampleRate, 1u, (juce::uint32) maxSamples });
|
|
|
|
|
delR.prepare ({ sampleRate, 1u, (juce::uint32) maxSamples });
|
|
|
|
|
|
2026-09-08 15:10:12 +02:00
|
|
|
const int maxDelay = (int) (sampleRate * prm::delaySyncMaxTime());
|
2026-09-07 21:03:31 +02:00
|
|
|
chorusL.setMaximumDelayInSamples (maxDelay);
|
|
|
|
|
chorusR.setMaximumDelayInSamples (maxDelay);
|
|
|
|
|
delL.setMaximumDelayInSamples (maxDelay);
|
|
|
|
|
delR.setMaximumDelayInSamples (maxDelay);
|
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
2026-09-10 16:55:47 +02:00
|
|
|
|
|
|
|
|
rebuildCombBuffers (true);
|
|
|
|
|
rebuildCombBuffers (false);
|
2026-09-07 21:03:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void reset()
|
|
|
|
|
{
|
|
|
|
|
chorusL.reset(); chorusR.reset();
|
|
|
|
|
delL.reset(); delR.reset();
|
|
|
|
|
chPh = 0.0;
|
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
2026-09-10 16:55:47 +02:00
|
|
|
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();
|
2026-09-07 21:03:31 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-08 15:10:12 +02:00
|
|
|
void process (const VParams& p, float* L, float* R, int num, int syncIdx, double bpm)
|
2026-09-07 21:03:31 +02:00
|
|
|
{
|
|
|
|
|
const int type = (int) p.fxType;
|
|
|
|
|
if (type == 0) return;
|
|
|
|
|
|
|
|
|
|
const float wet = p.fxAmt;
|
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
2026-09-10 16:55:47 +02:00
|
|
|
const float dryCh = 1.0f - wet * 0.5f;
|
|
|
|
|
const float chBase = 0.009f * (float) sr; // 9 ms ensemble base
|
|
|
|
|
const float chDepth = 0.006f * (float) sr; // +-6 ms wobble
|
2026-09-08 15:10:12 +02:00
|
|
|
float delSamples = (float) (p.fxTime * sr);
|
|
|
|
|
if (syncIdx > 0 && bpm > 1.0)
|
|
|
|
|
delSamples = (float) ((60.0 / bpm) * prm::delaySyncBeats (syncIdx) * sr);
|
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
2026-09-10 16:55:47 +02:00
|
|
|
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);
|
2026-09-07 21:03:31 +02:00
|
|
|
|
|
|
|
|
for (int s = 0; s < num; ++s)
|
|
|
|
|
{
|
|
|
|
|
const float inL = L[s], inR = R[s];
|
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
2026-09-10 16:55:47 +02:00
|
|
|
float outL = inL * dryCh, outR = inR * dryCh;
|
2026-09-07 21:03:31 +02:00
|
|
|
|
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
2026-09-10 16:55:47 +02:00
|
|
|
if (doChorus)
|
2026-09-07 21:03:31 +02:00
|
|
|
{
|
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
2026-09-10 16:55:47 +02:00
|
|
|
// 3-voice ensemble with a touch of internal regeneration
|
|
|
|
|
const double w = chPh * 2.0 * juce::MathConstants<double>::pi;
|
|
|
|
|
const float t1 = (float) std::sin (w);
|
|
|
|
|
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;
|
2026-09-07 21:03:31 +02:00
|
|
|
|
|
|
|
|
chorusL.pushSample (0, inL);
|
|
|
|
|
chorusR.pushSample (0, inR);
|
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
2026-09-10 16:55:47 +02:00
|
|
|
const float mL = 0.5f * (chorusL.popSample (0, chBase + chDepth * t1)
|
|
|
|
|
+ chorusL.popSample (0, chBase + chDepth * t2));
|
|
|
|
|
const float mR = 0.5f * (chorusR.popSample (0, chBase + chDepth * t2)
|
|
|
|
|
+ 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;
|
2026-09-07 21:03:31 +02:00
|
|
|
}
|
|
|
|
|
|
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
2026-09-10 16:55:47 +02:00
|
|
|
if (doDelay)
|
2026-09-07 21:03:31 +02:00
|
|
|
{
|
|
|
|
|
const float rL = delL.popSample (0, delSamples);
|
|
|
|
|
const float rR = delR.popSample (0, delSamples);
|
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
2026-09-10 16:55:47 +02:00
|
|
|
lpL += ff * (rL - lpL); lpR += ff * (rR - lpR);
|
|
|
|
|
delL.pushSample (0, inL + lpR * fb);
|
|
|
|
|
delR.pushSample (0, inR + lpL * fb);
|
2026-09-07 21:03:31 +02:00
|
|
|
outL += rL * wet;
|
|
|
|
|
outR += rR * wet;
|
|
|
|
|
}
|
|
|
|
|
|
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
2026-09-10 16:55:47 +02:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-07 21:03:31 +02:00
|
|
|
L[s] = outL;
|
|
|
|
|
R[s] = outR;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
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
2026-09-10 16:55:47 +02:00
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-07 21:03:31 +02:00
|
|
|
juce::dsp::DelayLine<float> chorusL, chorusR, delL, delR;
|
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
2026-09-10 16:55:47 +02:00
|
|
|
std::array<Comb, 6> combL, combR;
|
|
|
|
|
std::array<Allpass, 2> apL, apR;
|
2026-09-07 21:03:31 +02:00
|
|
|
double sr = 48000.0;
|
|
|
|
|
double chPh = 0.0;
|
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
2026-09-10 16:55:47 +02:00
|
|
|
float lpL = 0.0f, lpR = 0.0f;
|
2026-09-07 21:03:31 +02:00
|
|
|
};
|
|
|
|
|
|
2026-09-08 21:57:34 +02:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Arpeggiator. Holds a set of pressed notes (from the real MIDI input) and
|
|
|
|
|
// emits a rhythmic sequence of generated notes to the synth engine, synced to
|
|
|
|
|
// the host tempo (or a free-running sample clock when not transport-locked).
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
class RMX19AudioProcessor::ArpEngine
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
struct Event { int note; bool on; float vel; };
|
|
|
|
|
|
|
|
|
|
void reset() { stepPosSamples = 0.0; }
|
|
|
|
|
void setSampleRate (double s) { sr = s; }
|
|
|
|
|
|
|
|
|
|
// Advance the arpeggiator clock by numSamples and produce the note events
|
|
|
|
|
// (relative to block start) to feed into the synth engine.
|
|
|
|
|
void step (bool on, double bpm, bool playing,
|
|
|
|
|
int rateIdx, int patternIdx, int octaves, float gatePct,
|
|
|
|
|
int numSamples, std::vector<Event>& out)
|
|
|
|
|
{
|
|
|
|
|
if (! on)
|
|
|
|
|
{
|
|
|
|
|
finishNote (out);
|
|
|
|
|
stepPosSamples = 0.0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (heldNotes.isEmpty())
|
|
|
|
|
{
|
|
|
|
|
finishNote (out);
|
|
|
|
|
stepPosSamples = 0.0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// step length in samples
|
|
|
|
|
const double beats[8] = { 0.25, 1.0/3.0, 0.5, 2.0/3.0, 1.0, 2.0, 3.0, 4.0 };
|
|
|
|
|
const double stepBeats = beats[rateIdx & 7];
|
|
|
|
|
double stepSamples;
|
|
|
|
|
if (playing && bpm > 1.0)
|
|
|
|
|
stepSamples = (60.0 / bpm) * stepBeats * sr;
|
|
|
|
|
else
|
|
|
|
|
stepSamples = (stepBeats * 0.5) * sr; // fallback ~120bpm quarter note
|
|
|
|
|
|
|
|
|
|
if (stepSamples < 1.0) stepSamples = 1.0;
|
|
|
|
|
|
|
|
|
|
// Build note pool (sorted)
|
|
|
|
|
juce::Array<int> pool = heldNotes;
|
|
|
|
|
pool.sort();
|
|
|
|
|
const int poolSize = pool.size();
|
|
|
|
|
const int seqLen = poolSize * juce::jlimit (1, 4, octaves);
|
|
|
|
|
|
|
|
|
|
// absolute sample clock into the running sequence
|
|
|
|
|
int cursor = 0;
|
|
|
|
|
while (cursor < numSamples)
|
|
|
|
|
{
|
|
|
|
|
// the current step index (absolute count of completed steps)
|
|
|
|
|
const long stepIndex = (long) (stepPosSamples / stepSamples);
|
|
|
|
|
const double stepStart = stepIndex * stepSamples;
|
|
|
|
|
const double stepEnd = stepStart + stepSamples;
|
|
|
|
|
|
|
|
|
|
// trigger the note for this step at the start of the step
|
|
|
|
|
const int triggerSample = (int) (stepStart - stepPosSamples);
|
|
|
|
|
if (triggerSample >= 0 && triggerSample < numSamples)
|
|
|
|
|
{
|
|
|
|
|
const int patternIdxMod = patternIdx & 3;
|
|
|
|
|
const int playback = arpIndex (stepIndex, seqLen, patternIdxMod);
|
|
|
|
|
const int noteIdx = playback % poolSize;
|
|
|
|
|
const int oct = playback / poolSize;
|
|
|
|
|
const int note = pool[noteIdx] + oct * 12;
|
|
|
|
|
|
|
|
|
|
finishNote (out); // release previous arp note (if different handled below)
|
|
|
|
|
apNoteOn (note, lastVel > 0 ? lastVel : 0.85f, out);
|
|
|
|
|
arpNote = note;
|
|
|
|
|
arpNoteActive = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// gate-off within this step
|
|
|
|
|
const int gateSamples = (int) (stepSamples * juce::jlimit (0.02f, 1.0f, gatePct / 100.0f));
|
|
|
|
|
const int gateEnd = triggerSample + gateSamples;
|
|
|
|
|
|
|
|
|
|
// advance the clock through this step
|
|
|
|
|
double advance = stepEnd - stepPosSamples;
|
|
|
|
|
if (advance < 1.0)
|
|
|
|
|
{
|
|
|
|
|
stepPosSamples = stepEnd;
|
|
|
|
|
cursor = numSamples;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
const int consumed = juce::jmin (numSamples - cursor, (int) advance);
|
|
|
|
|
stepPosSamples += consumed;
|
|
|
|
|
cursor += consumed;
|
|
|
|
|
|
|
|
|
|
// turn the note off after its gate time within the block
|
|
|
|
|
if (arpNoteActive && triggerSample >= 0 && gateEnd > triggerSample && gateEnd <= cursor)
|
|
|
|
|
{
|
|
|
|
|
apNoteOff (arpNote, out);
|
|
|
|
|
arpNoteActive = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void noteOn (int midiNote, float vel) { heldNotes.addIfNotAlreadyThere (midiNote); lastVel = vel; }
|
|
|
|
|
void noteOff (int midiNote) { heldNotes.removeAllInstancesOf (midiNote); }
|
|
|
|
|
void allNotesOff() { heldNotes.clear(); }
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
juce::Array<int> heldNotes;
|
|
|
|
|
double sr = 48000.0;
|
|
|
|
|
double stepPosSamples = 0.0;
|
|
|
|
|
int arpNote = -1;
|
|
|
|
|
bool arpNoteActive = false;
|
|
|
|
|
float lastVel = 0.85f;
|
|
|
|
|
|
|
|
|
|
static int arpIndex (long i, int len, int pattern)
|
|
|
|
|
{
|
|
|
|
|
switch (pattern)
|
|
|
|
|
{
|
|
|
|
|
case 0: return (int) (i % len); // UP
|
|
|
|
|
case 1: return (int) ((len - 1) - (i % len)); // DOWN
|
|
|
|
|
case 2: // UP/DOWN
|
|
|
|
|
{
|
|
|
|
|
if (len <= 1) return 0;
|
|
|
|
|
const int period = len * 2 - 2;
|
|
|
|
|
const int t = (int) (i % period);
|
|
|
|
|
return t < len ? t : (len - 1) - (t - (len - 1));
|
|
|
|
|
}
|
|
|
|
|
default: return (int) (juce::Random::getSystemRandom().nextInt (len)); // RANDOM
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void apNoteOn (int note, float vel, std::vector<Event>& out) { out.push_back ({ note, true, vel }); }
|
|
|
|
|
void apNoteOff (int note, std::vector<Event>& out) { out.push_back ({ note, false, 0.0f }); }
|
|
|
|
|
|
|
|
|
|
void finishNote (std::vector<Event>& out)
|
|
|
|
|
{
|
|
|
|
|
if (arpNoteActive)
|
|
|
|
|
{
|
|
|
|
|
apNoteOff (arpNote, out);
|
|
|
|
|
arpNoteActive = false;
|
|
|
|
|
arpNote = -1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-07 21:03:31 +02:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
RMX19AudioProcessor::RMX19AudioProcessor()
|
|
|
|
|
: AudioProcessor (BusesProperties().withOutput ("Output", juce::AudioChannelSet::stereo(), true))
|
|
|
|
|
, apvts (*this, nullptr, "Params", createParams())
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RMX19AudioProcessor::~RMX19AudioProcessor() {}
|
|
|
|
|
|
|
|
|
|
juce::AudioProcessorValueTreeState::ParameterLayout RMX19AudioProcessor::createParams()
|
|
|
|
|
{
|
|
|
|
|
return prm::createLayout();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const juce::String RMX19AudioProcessor::getName() const { return "RMX-19"; }
|
|
|
|
|
|
|
|
|
|
void RMX19AudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
|
|
|
|
|
{
|
|
|
|
|
engine.setSampleRate (sampleRate);
|
|
|
|
|
engine.allNotesOff (true);
|
|
|
|
|
fx = std::make_unique<FxEngine>();
|
|
|
|
|
fx->prepare (sampleRate, samplesPerBlock);
|
2026-09-08 21:57:34 +02:00
|
|
|
arp = std::make_unique<ArpEngine>();
|
|
|
|
|
arp->setSampleRate (sampleRate);
|
2026-09-08 15:10:12 +02:00
|
|
|
osBuf.setSize (2, juce::nextPowerOfTwo (samplesPerBlock * 2) + 4, false, true);
|
2026-09-07 21:03:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RMX19AudioProcessor::releaseResources()
|
|
|
|
|
{
|
|
|
|
|
engine.allNotesOff (true);
|
|
|
|
|
if (fx) fx->reset();
|
2026-09-08 21:57:34 +02:00
|
|
|
if (arp) arp->allNotesOff();
|
2026-09-07 21:03:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RMX19AudioProcessor::processMidi (juce::MidiBuffer& midi, const VParams& p)
|
|
|
|
|
{
|
|
|
|
|
(void) p;
|
|
|
|
|
constexpr double bendSemis = 2.0;
|
2026-09-08 21:57:34 +02:00
|
|
|
bool arpOn = false;
|
|
|
|
|
if (auto* par = apvts.getRawParameterValue ("arpon"))
|
|
|
|
|
arpOn = par->load() >= 0.5f;
|
2026-09-07 21:03:31 +02:00
|
|
|
|
|
|
|
|
for (const auto meta : midi)
|
|
|
|
|
{
|
|
|
|
|
const auto msg = meta.getMessage();
|
|
|
|
|
lastMidi = juce::Time::getMillisecondCounter();
|
|
|
|
|
|
|
|
|
|
if (msg.isNoteOn())
|
|
|
|
|
{
|
2026-09-08 21:57:34 +02:00
|
|
|
if (arpOn)
|
|
|
|
|
arp->noteOn (msg.getNoteNumber(), msg.getFloatVelocity());
|
|
|
|
|
else
|
|
|
|
|
engine.noteOn (msg.getNoteNumber(), msg.getFloatVelocity());
|
2026-09-08 15:10:12 +02:00
|
|
|
dbgNote ("ON", msg.getNoteNumber(), msg.getFloatVelocity());
|
2026-09-07 21:03:31 +02:00
|
|
|
}
|
|
|
|
|
else if (msg.isNoteOff())
|
|
|
|
|
{
|
2026-09-08 21:57:34 +02:00
|
|
|
if (arpOn)
|
|
|
|
|
arp->noteOff (msg.getNoteNumber());
|
|
|
|
|
else
|
|
|
|
|
engine.noteOff (msg.getNoteNumber());
|
2026-09-08 15:10:12 +02:00
|
|
|
dbgNote ("OFF", msg.getNoteNumber(), -1.0f);
|
2026-09-07 21:03:31 +02:00
|
|
|
}
|
|
|
|
|
else if (msg.isPitchWheel())
|
|
|
|
|
{
|
|
|
|
|
engine.pitchBend ((float) ((msg.getPitchWheelValue() - 8192) / 8192.0 * bendSemis * 100.0));
|
|
|
|
|
}
|
|
|
|
|
else if (msg.isController())
|
|
|
|
|
{
|
|
|
|
|
const int cc = msg.getControllerNumber();
|
|
|
|
|
const float v = msg.getControllerValue() / 127.0f;
|
|
|
|
|
switch (cc)
|
|
|
|
|
{
|
|
|
|
|
case 64: engine.sustainPedal (msg.getControllerValue() >= 64); break;
|
2026-09-08 15:10:12 +02:00
|
|
|
case 1: if (auto* par = apvts.getParameter ("fltcut")) par->setValueNotifyingHost (v); break;
|
|
|
|
|
case 2: if (auto* par = apvts.getParameter ("fltres")) par->setValueNotifyingHost (v); break;
|
|
|
|
|
case 3: if (auto* par = apvts.getParameter ("lforate")) par->setValueNotifyingHost (v); break;
|
|
|
|
|
case 4: if (auto* par = apvts.getParameter ("fxamt")) par->setValueNotifyingHost (v); break;
|
2026-09-07 21:03:31 +02:00
|
|
|
case 7: if (auto* par = apvts.getParameter ("vol")) par->setValueNotifyingHost (v); break;
|
|
|
|
|
case 10: if (auto* par = apvts.getParameter ("pan")) par->setValueNotifyingHost (v * 2.0f - 1.0f); break;
|
|
|
|
|
case 71: if (auto* par = apvts.getParameter ("fltres")) par->setValueNotifyingHost (v); break;
|
|
|
|
|
case 72: if (auto* par = apvts.getParameter ("ampR")) par->setValueNotifyingHost (v); break;
|
|
|
|
|
case 73: if (auto* par = apvts.getParameter ("ampA")) par->setValueNotifyingHost (v); break;
|
|
|
|
|
case 74: if (auto* par = apvts.getParameter ("fltcut")) par->setValueNotifyingHost (v); break;
|
|
|
|
|
case 76: if (auto* par = apvts.getParameter ("lforate")) par->setValueNotifyingHost (v); break;
|
|
|
|
|
case 77: if (auto* par = apvts.getParameter ("lfodepth"))par->setValueNotifyingHost (v); break;
|
|
|
|
|
case 91: if (auto* par = apvts.getParameter ("fxamt")) par->setValueNotifyingHost (v); break;
|
|
|
|
|
default: break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RMX19AudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midi)
|
|
|
|
|
{
|
|
|
|
|
juce::ScopedNoDenormals noDenormals;
|
|
|
|
|
|
|
|
|
|
const juce::uint32 start = juce::Time::getMillisecondCounter();
|
|
|
|
|
const int numSamples = buffer.getNumSamples();
|
|
|
|
|
const int numChannels = buffer.getNumChannels();
|
|
|
|
|
if (numSamples == 0) return;
|
|
|
|
|
|
|
|
|
|
VParams p;
|
|
|
|
|
p.update (apvts);
|
|
|
|
|
|
2026-09-08 15:10:12 +02:00
|
|
|
// host tempo (fall back to 120 if not playing / unknown)
|
|
|
|
|
double bpm = 120.0;
|
|
|
|
|
if (auto* ph = getPlayHead())
|
|
|
|
|
if (auto pos = ph->getPosition())
|
|
|
|
|
if (pos->getIsPlaying() && pos->getBpm().hasValue())
|
|
|
|
|
bpm = juce::jlimit (20.0, 300.0, *pos->getBpm());
|
|
|
|
|
const int syncIdx = (int) apvts.getRawParameterValue ("delaysync")->load();
|
|
|
|
|
|
|
|
|
|
const bool hq = p.hq >= 0.5f;
|
|
|
|
|
const double engineSr = hq ? getSampleRate() * 2.0 : getSampleRate();
|
|
|
|
|
engine.setSampleRate (engineSr);
|
|
|
|
|
|
2026-09-07 21:03:31 +02:00
|
|
|
// patch follow
|
|
|
|
|
const int sel = (int) apvts.getRawParameterValue ("patchsel")->load();
|
|
|
|
|
if (sel != lastPatchParam && sel >= 0 && sel < bank.size())
|
|
|
|
|
{
|
|
|
|
|
lastPatchParam = sel;
|
|
|
|
|
patchIndex = sel;
|
|
|
|
|
bank.writeTo (sel, apvts);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
processMidi (midi, p);
|
|
|
|
|
|
2026-09-08 21:57:34 +02:00
|
|
|
// advance the arpeggiator and trigger its generated notes
|
|
|
|
|
if (arp)
|
|
|
|
|
{
|
|
|
|
|
const bool arpOn = p.arpOn >= 0.5f;
|
|
|
|
|
double playingBpm = 120.0;
|
|
|
|
|
bool playing = false;
|
|
|
|
|
if (auto* ph = getPlayHead())
|
|
|
|
|
if (auto pos = ph->getPosition())
|
|
|
|
|
if (pos->getIsPlaying())
|
|
|
|
|
{
|
|
|
|
|
playing = true;
|
|
|
|
|
if (pos->getBpm().hasValue())
|
|
|
|
|
playingBpm = juce::jlimit (20.0, 300.0, *pos->getBpm());
|
|
|
|
|
}
|
|
|
|
|
std::vector<ArpEngine::Event> ae;
|
|
|
|
|
arp->step (arpOn, playingBpm, playing,
|
|
|
|
|
(int) p.arpRate, (int) p.arpPattern, (int) p.arpOct,
|
|
|
|
|
p.arpGate, numSamples, ae);
|
|
|
|
|
for (const auto& e : ae)
|
|
|
|
|
{
|
|
|
|
|
if (e.on)
|
|
|
|
|
engine.noteOn (e.note, e.vel);
|
|
|
|
|
else
|
|
|
|
|
engine.noteOff (e.note);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-07 21:03:31 +02:00
|
|
|
const int chL = 0, chR = 1;
|
|
|
|
|
float* L = buffer.getNumChannels() > 0 ? buffer.getWritePointer (juce::jmin (chL, numChannels - 1)) : nullptr;
|
|
|
|
|
float* R = buffer.getNumChannels() > 1 ? buffer.getWritePointer (juce::jmin (chR, numChannels - 1)) : nullptr;
|
|
|
|
|
|
|
|
|
|
for (int c = 0; c < numChannels; ++c)
|
|
|
|
|
buffer.clear (c, 0, numSamples);
|
|
|
|
|
|
|
|
|
|
if (L != nullptr && R != nullptr)
|
|
|
|
|
{
|
2026-09-08 15:10:12 +02:00
|
|
|
if (hq)
|
|
|
|
|
{
|
|
|
|
|
osBuf.clear();
|
|
|
|
|
engine.render (p, osBuf.getWritePointer (0), osBuf.getWritePointer (1), numSamples * 2);
|
|
|
|
|
for (int s = 0; s < numSamples; ++s)
|
|
|
|
|
{
|
|
|
|
|
const int o = s * 2;
|
|
|
|
|
L[s] = 0.5f * (osBuf.getSample (0, o) + osBuf.getSample (0, o + 1));
|
|
|
|
|
R[s] = 0.5f * (osBuf.getSample (1, o) + osBuf.getSample (1, o + 1));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
engine.render (p, L, R, numSamples);
|
|
|
|
|
}
|
|
|
|
|
if (fx) fx->process (p, L, R, numSamples, syncIdx, bpm);
|
|
|
|
|
|
|
|
|
|
// loud-pop protection: remove DC steps and soft-clamp any blowout
|
|
|
|
|
const float dcCoef = 0.9995f;
|
|
|
|
|
for (int s = 0; s < numSamples; ++s)
|
|
|
|
|
{
|
|
|
|
|
const float tL = std::tanh (L[s]);
|
|
|
|
|
const float tR = std::tanh (R[s]);
|
|
|
|
|
const float oL = tL - dcPrevL + dcCoef * dcL;
|
|
|
|
|
const float oR = tR - dcPrevR + dcCoef * dcR;
|
|
|
|
|
dcPrevL = tL; dcPrevR = tR;
|
|
|
|
|
dcL = oL; dcR = oR;
|
|
|
|
|
L[s] = oL;
|
|
|
|
|
R[s] = oR;
|
|
|
|
|
}
|
2026-09-08 16:37:33 +02:00
|
|
|
if (p.vol != 1.0f) buffer.applyGain (p.vol);
|
2026-09-08 21:57:34 +02:00
|
|
|
|
|
|
|
|
// Final output soft-limiter: prevent hard clipping while preserving dynamics
|
|
|
|
|
for (int c = 0; c < numChannels; ++c)
|
|
|
|
|
{
|
|
|
|
|
float* ch = buffer.getWritePointer (c);
|
|
|
|
|
for (int s = 0; s < numSamples; ++s)
|
|
|
|
|
{
|
|
|
|
|
float x = ch[s];
|
|
|
|
|
// Soft knee limiting at ~-1dBFS with smooth tanh curve
|
|
|
|
|
x = std::tanh (x * 0.89f) * 1.12f;
|
|
|
|
|
if (! std::isfinite (x)) x = 0.0f;
|
|
|
|
|
ch[s] = juce::jlimit (-1.0f, 1.0f, x);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-07 21:03:31 +02:00
|
|
|
}
|
|
|
|
|
else if (L != nullptr)
|
|
|
|
|
{
|
|
|
|
|
juce::AudioBuffer<float> dummy (1, numSamples);
|
|
|
|
|
dummy.clear();
|
|
|
|
|
float* dL = dummy.getWritePointer (0);
|
2026-09-08 15:10:12 +02:00
|
|
|
if (hq)
|
|
|
|
|
{
|
|
|
|
|
osBuf.clear();
|
|
|
|
|
engine.render (p, osBuf.getWritePointer (0), osBuf.getWritePointer (1), numSamples * 2);
|
|
|
|
|
for (int s = 0; s < numSamples; ++s)
|
|
|
|
|
{
|
|
|
|
|
const int o = s * 2;
|
|
|
|
|
dL[s] = 0.5f * (osBuf.getSample (0, o) + osBuf.getSample (1, o)
|
|
|
|
|
+ osBuf.getSample (0, o + 1) + osBuf.getSample (1, o + 1)) * 0.5f;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
float* dR = dummy.getWritePointer (0);
|
|
|
|
|
engine.render (p, dL, dR, numSamples);
|
|
|
|
|
}
|
|
|
|
|
if (fx) fx->process (p, dL, dL, numSamples, syncIdx, bpm);
|
|
|
|
|
for (int s = 0; s < numSamples; ++s)
|
|
|
|
|
{
|
|
|
|
|
dL[s] = std::tanh (dL[s] * 0.5f);
|
|
|
|
|
}
|
2026-09-07 21:03:31 +02:00
|
|
|
for (int c = 0; c < numChannels; ++c)
|
|
|
|
|
buffer.copyFrom (c, 0, dummy, 0, 0, numSamples);
|
2026-09-08 16:37:33 +02:00
|
|
|
buffer.applyGain (p.vol);
|
2026-09-08 21:57:34 +02:00
|
|
|
|
|
|
|
|
// Final output soft-limiter for mono path
|
|
|
|
|
for (int c = 0; c < numChannels; ++c)
|
|
|
|
|
{
|
|
|
|
|
float* ch = buffer.getWritePointer (c);
|
|
|
|
|
for (int s = 0; s < numSamples; ++s)
|
|
|
|
|
{
|
|
|
|
|
float x = ch[s];
|
|
|
|
|
x = std::tanh (x * 0.89f) * 1.12f;
|
|
|
|
|
if (! std::isfinite (x)) x = 0.0f;
|
|
|
|
|
ch[s] = juce::jlimit (-1.0f, 1.0f, x);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-07 21:03:31 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-08 21:57:34 +02:00
|
|
|
// output meter: instant attack, time-constant release so the LCD VU is readable
|
|
|
|
|
float peak = 0.0f;
|
|
|
|
|
for (int c = 0; c < numChannels; ++c)
|
|
|
|
|
{
|
|
|
|
|
const float* d = buffer.getReadPointer (c, 0);
|
|
|
|
|
for (int s = 0; s < numSamples; ++s)
|
|
|
|
|
peak = juce::jmax (peak, std::abs (d[s]));
|
|
|
|
|
}
|
|
|
|
|
const float blockSec = (float) numSamples / (float) std::max (1.0, getSampleRate());
|
|
|
|
|
outMeter = juce::jmax (peak, outMeter * std::exp (-blockSec / 0.25f));
|
|
|
|
|
|
2026-09-07 21:03:31 +02:00
|
|
|
cpu = 0.7f * cpu + 0.3f * (float) (juce::Time::getMillisecondCounter() - start);
|
2026-09-08 15:10:12 +02:00
|
|
|
|
|
|
|
|
// real-time health check (offline probe + debug): flag blocks near the limit
|
|
|
|
|
const float blockMs = (numSamples == 0) ? 0.0f
|
|
|
|
|
: (float) numSamples / (float) std::max (1.0, getSampleRate()) * 1000.0f;
|
|
|
|
|
const float usedMs = (float) (juce::Time::getMillisecondCounter() - start);
|
|
|
|
|
if (usedMs > 0.5f * blockMs)
|
|
|
|
|
juce::File (juce::File::getSpecialLocation (juce::File::userHomeDirectory)
|
|
|
|
|
.getChildFile ("rmx19_notes.log"))
|
|
|
|
|
.appendText (juce::String ("OVER t=") + juce::String ((juce::int64) juce::Time::getMillisecondCounter())
|
|
|
|
|
+ " used=" + juce::String (usedMs, 2)
|
|
|
|
|
+ "ms budget=" + juce::String (blockMs, 2) + "ms\n");
|
2026-09-07 21:03:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RMX19AudioProcessor::selectPatch (int i)
|
|
|
|
|
{
|
|
|
|
|
i = juce::jlimit (0, bank.size() - 1, i);
|
|
|
|
|
patchIndex = i;
|
|
|
|
|
lastPatchParam = i;
|
|
|
|
|
if (auto* par = apvts.getParameter ("patchsel"))
|
|
|
|
|
par->setValueNotifyingHost (par->convertTo0to1 ((float) i));
|
|
|
|
|
bank.writeTo (i, apvts);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int RMX19AudioProcessor::getNumPrograms() { return bank.size(); }
|
|
|
|
|
int RMX19AudioProcessor::getCurrentProgram() { return patchIndex; }
|
|
|
|
|
|
|
|
|
|
void RMX19AudioProcessor::setCurrentProgram (int index) { selectPatch (index); }
|
|
|
|
|
|
|
|
|
|
const juce::String RMX19AudioProcessor::getProgramName (int index)
|
|
|
|
|
{
|
|
|
|
|
if (index >= 0 && index < bank.size()) return bank.get (index).name;
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RMX19AudioProcessor::changeProgramName (int, const juce::String&) {}
|
|
|
|
|
|
|
|
|
|
void RMX19AudioProcessor::getStateInformation (juce::MemoryBlock& destData)
|
|
|
|
|
{
|
|
|
|
|
auto state = apvts.copyState();
|
|
|
|
|
std::unique_ptr<juce::XmlElement> xml (state.createXml());
|
|
|
|
|
copyXmlToBinary (*xml, destData);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RMX19AudioProcessor::setStateInformation (const void* data, int sizeInBytes)
|
|
|
|
|
{
|
|
|
|
|
std::unique_ptr<juce::XmlElement> xml (getXmlFromBinary (data, sizeInBytes));
|
|
|
|
|
if (xml == nullptr) return;
|
|
|
|
|
|
|
|
|
|
apvts.replaceState (juce::ValueTree::fromXml (*xml));
|
|
|
|
|
|
|
|
|
|
const int sel = juce::jlimit (0, bank.size() - 1,
|
|
|
|
|
(int) apvts.getRawParameterValue ("patchsel")->load());
|
|
|
|
|
lastPatchParam = sel;
|
|
|
|
|
patchIndex = sel;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
juce::AudioProcessorEditor* RMX19AudioProcessor::createEditor()
|
|
|
|
|
{
|
|
|
|
|
return new RackEditor (*this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool RMX19AudioProcessor::hasEditor() const { return true; }
|
|
|
|
|
|
|
|
|
|
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
|
|
|
|
|
{
|
|
|
|
|
return new RMX19AudioProcessor();
|
|
|
|
|
}
|