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
|
|
|
|
|
{
|
|
|
|
|
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 });
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void reset()
|
|
|
|
|
{
|
|
|
|
|
chorusL.reset(); chorusR.reset();
|
|
|
|
|
delL.reset(); delR.reset();
|
|
|
|
|
chPh = 0.0;
|
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
const float dry = 1.0f - wet * 0.5f;
|
|
|
|
|
const float chBase = 0.008f * (float) sr; // 8 ms chorus base
|
|
|
|
|
const float chDepth = 0.0035f * (float) sr; // +-3.5 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);
|
2026-09-07 21:03:31 +02:00
|
|
|
const float feedback = 0.40f;
|
|
|
|
|
|
|
|
|
|
for (int s = 0; s < num; ++s)
|
|
|
|
|
{
|
|
|
|
|
const float inL = L[s], inR = R[s];
|
|
|
|
|
float outL = inL * dry, outR = inR * dry;
|
|
|
|
|
|
|
|
|
|
if (type == 1 || type == 3)
|
|
|
|
|
{
|
|
|
|
|
const double rate = type == 3 ? 0.55 : 0.38;
|
|
|
|
|
chPh += rate / sr;
|
|
|
|
|
if (chPh >= 1.0) chPh -= 1.0;
|
|
|
|
|
const float m = 0.5f + 0.5f * (float) std::sin (chPh * 2.0 * juce::MathConstants<double>::pi);
|
|
|
|
|
|
|
|
|
|
chorusL.pushSample (0, inL);
|
|
|
|
|
chorusR.pushSample (0, inR);
|
|
|
|
|
const float mL = chorusL.popSample (0, chBase + chDepth * m);
|
|
|
|
|
const float mR = chorusR.popSample (0, chBase + chDepth * (1.0f - m));
|
|
|
|
|
outL += mL * wet * 0.7f;
|
|
|
|
|
outR += mR * wet * 0.7f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (type == 2 || type == 3)
|
|
|
|
|
{
|
|
|
|
|
const float rL = delL.popSample (0, delSamples);
|
|
|
|
|
const float rR = delR.popSample (0, delSamples);
|
|
|
|
|
delL.pushSample (0, inL + rR * feedback);
|
|
|
|
|
delR.pushSample (0, inR + rL * feedback);
|
|
|
|
|
outL += rL * wet;
|
|
|
|
|
outR += rR * wet;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
L[s] = outL;
|
|
|
|
|
R[s] = outR;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
juce::dsp::DelayLine<float> chorusL, chorusR, delL, delR;
|
|
|
|
|
double sr = 48000.0;
|
|
|
|
|
double chPh = 0.0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
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 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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RMX19AudioProcessor::processMidi (juce::MidiBuffer& midi, const VParams& p)
|
|
|
|
|
{
|
|
|
|
|
(void) p;
|
|
|
|
|
constexpr double bendSemis = 2.0;
|
|
|
|
|
|
|
|
|
|
for (const auto meta : midi)
|
|
|
|
|
{
|
|
|
|
|
const auto msg = meta.getMessage();
|
|
|
|
|
lastMidi = juce::Time::getMillisecondCounter();
|
|
|
|
|
|
|
|
|
|
if (msg.isNoteOn())
|
|
|
|
|
{
|
|
|
|
|
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())
|
|
|
|
|
{
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
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-07 21:03:31 +02:00
|
|
|
if (p.vol != 1.0f) buffer.applyGain (p.vol);
|
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
buffer.applyGain (p.vol);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|