#include "PluginProcessor.h" #include "PluginEditor.h" // --------------------------------------------------------------------------- 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"); } 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); } void reset() { chorusL.reset(); chorusR.reset(); delL.reset(); delR.reset(); chPh = 0.0; } 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 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 float delSamples = (float) (p.fxTime * sr); if (syncIdx > 0 && bpm > 1.0) delSamples = (float) ((60.0 / bpm) * prm::delaySyncBeats (syncIdx) * sr); 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::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 chorusL, chorusR, delL, delR; double sr = 48000.0; double chPh = 0.0; }; // --------------------------------------------------------------------------- // 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& 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 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 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& out) { out.push_back ({ note, true, vel }); } void apNoteOff (int note, std::vector& out) { out.push_back ({ note, false, 0.0f }); } void finishNote (std::vector& 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(); fx->prepare (sampleRate, samplesPerBlock); arp = std::make_unique(); arp->setSampleRate (sampleRate); osBuf.setSize (2, juce::nextPowerOfTwo (samplesPerBlock * 2) + 4, false, true); } void RMX19AudioProcessor::releaseResources() { engine.allNotesOff (true); if (fx) fx->reset(); if (arp) arp->allNotesOff(); } void RMX19AudioProcessor::processMidi (juce::MidiBuffer& midi, const VParams& p) { (void) p; constexpr double bendSemis = 2.0; bool arpOn = false; if (auto* par = apvts.getRawParameterValue ("arpon")) arpOn = par->load() >= 0.5f; for (const auto meta : midi) { const auto msg = meta.getMessage(); lastMidi = juce::Time::getMillisecondCounter(); if (msg.isNoteOn()) { if (arpOn) arp->noteOn (msg.getNoteNumber(), msg.getFloatVelocity()); else engine.noteOn (msg.getNoteNumber(), msg.getFloatVelocity()); dbgNote ("ON", msg.getNoteNumber(), msg.getFloatVelocity()); } else if (msg.isNoteOff()) { if (arpOn) arp->noteOff (msg.getNoteNumber()); else engine.noteOff (msg.getNoteNumber()); dbgNote ("OFF", msg.getNoteNumber(), -1.0f); } 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& 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); // 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 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); // 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); } } } else if (L != nullptr) { juce::AudioBuffer 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); // 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); } } } // 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); // 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"); } 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 xml (state.createXml()); copyXmlToBinary (*xml, destData); } void RMX19AudioProcessor::setStateInformation (const void* data, int sizeInBytes) { std::unique_ptr 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(); }