rmx-19/Source/PluginProcessor.cpp

643 lines
24 KiB
C++
Raw Normal View History

#include "PluginProcessor.h"
#include "PluginEditor.h"
// ---------------------------------------------------------------------------
class RMX19AudioProcessor::FxEngine
{
public:
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 });
const int maxDelay = (int) (sampleRate * prm::delaySyncMaxTime());
chorusL.setMaximumDelayInSamples (maxDelay);
chorusR.setMaximumDelayInSamples (maxDelay);
delL.setMaximumDelayInSamples (maxDelay);
delR.setMaximumDelayInSamples (maxDelay);
rebuildCombBuffers (true);
rebuildCombBuffers (false);
}
void reset()
{
chorusL.reset(); chorusR.reset();
delL.reset(); delR.reset();
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)
{
const int type = (int) p.fxType;
if (type == 0) return;
const float wet = p.fxAmt;
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
float delSamples = (float) (p.fxTime * sr);
if (syncIdx > 0 && bpm > 1.0)
delSamples = (float) ((60.0 / bpm) * prm::delaySyncBeats (syncIdx) * sr);
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)
{
const float inL = L[s], inR = R[s];
float outL = inL * dryCh, outR = inR * dryCh;
if (doChorus)
{
// 3-voice ensemble chorus
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;
chorusL.pushSample (0, inL);
chorusR.pushSample (0, inR);
// multi-tap: only the first popSample advances the read pointer,
// otherwise one push + two pops drifts the taps through the whole
// buffer and late-stale echoes of old notes come back (octave up).
const float mL = 0.5f * (chorusL.popSample (0, chBase + chDepth * t1, true)
+ chorusL.popSample (0, chBase + chDepth * t2, false));
const float mR = 0.5f * (chorusR.popSample (0, chBase + chDepth * t2, true)
+ chorusR.popSample (0, chBase + chDepth * t3, false));
outL += mL * wet * 0.8f;
outR += mR * wet * 0.8f;
}
if (doDelay)
{
const float rL = delL.popSample (0, delSamples);
const float rR = delR.popSample (0, delSamples);
lpL += ff * (rL - lpL); lpR += ff * (rR - lpR);
delL.pushSample (0, inL + lpR * fb);
delR.pushSample (0, inR + lpL * fb);
outL += rL * 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.78f, 0.55f);
revL += o * 0.22f;
o = combR[i].process (inR * 0.25f, 0.78f, 0.55f);
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 = wet * 0.8f;
outL += revL * revWet;
outR += revR * revWet;
}
L[s] = std::tanh (outL * 0.85f);
R[s] = std::tanh (outR * 0.85f);
}
}
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;
std::array<Comb, 6> combL, combR;
std::array<Allpass, 2> apL, apR;
double sr = 48000.0;
double chPh = 0.0;
float lpL = 0.0f, lpR = 0.0f;
};
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;
}
}
};
// ---------------------------------------------------------------------------
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);
osBuf.setSize (2, juce::nextPowerOfTwo (samplesPerBlock * 2) + 4, false, true);
}
void RMX19AudioProcessor::releaseResources()
{
engine.allNotesOff (true);
if (fx) fx->reset();
2026-09-08 21:57:34 +02:00
if (arp) arp->allNotesOff();
}
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;
// arp is only allocated by prepareToPlay(); treat it as off until then.
2026-09-08 21:57:34 +02:00
if (auto* par = apvts.getRawParameterValue ("arpon"))
arpOn = (arp != nullptr) && par->load() >= 0.5f;
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());
}
else if (msg.isNoteOff())
{
2026-09-08 21:57:34 +02:00
if (arpOn)
arp->noteOff (msg.getNoteNumber());
else
engine.noteOff (msg.getNoteNumber());
}
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;
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;
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);
// 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);
// 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);
}
}
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)
{
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;
}
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);
2026-09-08 21:57:34 +02:00
if (! std::isfinite (x)) x = 0.0f;
ch[s] = juce::jlimit (-1.0f, 1.0f, x);
}
}
}
else if (L != nullptr)
{
juce::AudioBuffer<float> dummy (1, numSamples);
dummy.clear();
float* dL = dummy.getWritePointer (0);
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);
}
for (int c = 0; c < numChannels; ++c)
buffer.copyFrom (c, 0, dummy, 0, 0, numSamples);
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);
2026-09-08 21:57:34 +02:00
if (! std::isfinite (x)) x = 0.0f;
ch[s] = juce::jlimit (-1.0f, 1.0f, x);
}
}
}
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));
cpu = 0.7f * cpu + 0.3f * (float) (juce::Time::getMillisecondCounter() - start);
}
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();
}