re-structure menu, darken UI

This commit is contained in:
Armin 2026-09-08 21:57:34 +02:00
commit 4b44841049
16 changed files with 1217 additions and 304 deletions

View file

@ -14,8 +14,7 @@ static inline void dbgNote (const char* ev, int note, float vel)
}
class RMX19AudioProcessor::FxEngine
{
public:
{public:
void prepare (double sampleRate, int maxSamples)
{
sr = sampleRate;
@ -93,6 +92,149 @@ private:
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<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()
@ -116,6 +258,8 @@ void RMX19AudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
engine.allNotesOff (true);
fx = std::make_unique<FxEngine>();
fx->prepare (sampleRate, samplesPerBlock);
arp = std::make_unique<ArpEngine>();
arp->setSampleRate (sampleRate);
osBuf.setSize (2, juce::nextPowerOfTwo (samplesPerBlock * 2) + 4, false, true);
}
@ -123,12 +267,16 @@ 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)
{
@ -137,12 +285,18 @@ void RMX19AudioProcessor::processMidi (juce::MidiBuffer& midi, const VParams& p)
if (msg.isNoteOn())
{
engine.noteOn (msg.getNoteNumber(), msg.getFloatVelocity());
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())
{
engine.noteOff (msg.getNoteNumber());
if (arpOn)
arp->noteOff (msg.getNoteNumber());
else
engine.noteOff (msg.getNoteNumber());
dbgNote ("OFF", msg.getNoteNumber(), -1.0f);
}
else if (msg.isPitchWheel())
@ -210,6 +364,33 @@ void RMX19AudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::
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<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;
@ -250,6 +431,20 @@ void RMX19AudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::
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)
{
@ -280,8 +475,32 @@ void RMX19AudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::
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