mirror of
https://codeberg.org/armin/rmx-19.git
synced 2026-10-11 16:51:55 +02:00
re-structure menu, darken UI
This commit is contained in:
parent
b03dc2e0c4
commit
4b44841049
16 changed files with 1217 additions and 304 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue