mirror of
https://codeberg.org/armin/rmx-19.git
synced 2026-10-11 16:51:55 +02:00
Add delay tempo sync, rack faceplate, README, docs
- Add host-tempo-synced delay: FX screen gains a 4th SYNC row for rhythmic divisions (1/16 .. 1/1) that lock delay time to DAW tempo - Embed rack faceplate background (b2.png) via juce binary data - Add offline dev probe (rmx_probe) for GUI-less MIDI testing - Allow overriding JUCE_ROOT when configuring the build - Write comprehensive README describing the instrument, UI, MIDI, DSP, and build/install steps
This commit is contained in:
parent
d63db3af8a
commit
2e870380d4
17 changed files with 1413 additions and 324 deletions
|
|
@ -3,6 +3,16 @@
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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:
|
||||
|
|
@ -14,7 +24,7 @@ public:
|
|||
delL.prepare ({ sampleRate, 1u, (juce::uint32) maxSamples });
|
||||
delR.prepare ({ sampleRate, 1u, (juce::uint32) maxSamples });
|
||||
|
||||
const int maxDelay = (int) (sampleRate * 1.5);
|
||||
const int maxDelay = (int) (sampleRate * prm::delaySyncMaxTime());
|
||||
chorusL.setMaximumDelayInSamples (maxDelay);
|
||||
chorusR.setMaximumDelayInSamples (maxDelay);
|
||||
delL.setMaximumDelayInSamples (maxDelay);
|
||||
|
|
@ -28,7 +38,7 @@ public:
|
|||
chPh = 0.0;
|
||||
}
|
||||
|
||||
void process (const VParams& p, float* L, float* R, int num)
|
||||
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;
|
||||
|
|
@ -37,7 +47,9 @@ public:
|
|||
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
|
||||
const float delSamples = (float) (p.fxTime * sr);
|
||||
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)
|
||||
|
|
@ -104,6 +116,7 @@ void RMX19AudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
|
|||
engine.allNotesOff (true);
|
||||
fx = std::make_unique<FxEngine>();
|
||||
fx->prepare (sampleRate, samplesPerBlock);
|
||||
osBuf.setSize (2, juce::nextPowerOfTwo (samplesPerBlock * 2) + 4, false, true);
|
||||
}
|
||||
|
||||
void RMX19AudioProcessor::releaseResources()
|
||||
|
|
@ -125,10 +138,12 @@ void RMX19AudioProcessor::processMidi (juce::MidiBuffer& midi, const VParams& p)
|
|||
if (msg.isNoteOn())
|
||||
{
|
||||
engine.noteOn (msg.getNoteNumber(), msg.getFloatVelocity());
|
||||
dbgNote ("ON", msg.getNoteNumber(), msg.getFloatVelocity());
|
||||
}
|
||||
else if (msg.isNoteOff())
|
||||
{
|
||||
engine.noteOff (msg.getNoteNumber());
|
||||
dbgNote ("OFF", msg.getNoteNumber(), -1.0f);
|
||||
}
|
||||
else if (msg.isPitchWheel())
|
||||
{
|
||||
|
|
@ -141,7 +156,10 @@ void RMX19AudioProcessor::processMidi (juce::MidiBuffer& midi, const VParams& p)
|
|||
switch (cc)
|
||||
{
|
||||
case 64: engine.sustainPedal (msg.getControllerValue() >= 64); break;
|
||||
case 1: engine.modWheel (v); 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;
|
||||
|
|
@ -169,6 +187,18 @@ void RMX19AudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::
|
|||
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())
|
||||
|
|
@ -189,8 +219,36 @@ void RMX19AudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::
|
|||
|
||||
if (L != nullptr && R != nullptr)
|
||||
{
|
||||
engine.render (p, L, R, numSamples);
|
||||
if (fx) fx->process (p, L, R, numSamples);
|
||||
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);
|
||||
}
|
||||
else if (L != nullptr)
|
||||
|
|
@ -198,14 +256,44 @@ void RMX19AudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::
|
|||
juce::AudioBuffer<float> dummy (1, numSamples);
|
||||
dummy.clear();
|
||||
float* dL = dummy.getWritePointer (0);
|
||||
float* dR = dummy.getWritePointer (0);
|
||||
engine.render (p, dL, dR, numSamples);
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue