refactor(engine): add thread-safe control capture and sub-block MIDI
Introduce a CriticalSection controlLock separating control-thread state (matrix, macros, controlLfos) from audio-thread snapshots (audioMatrix, audioMacros, lfos). captureControls() copies under lock at the start of each processBlock; the audio thread never touches control state directly. Cache parameter values in an unordered_map keyed by string_view to avoid per-sample APVTS lookups. Prebuild wavetables in the constructor instead of lazy allocation. Resize mixBuffer only in prepare(), not per block. Render MIDI events at their sample positions using sub-block rendering with renderUntil(), so notes start at the correct sub-sample offset. Make RaveController non-destructive: replace APVTS mutation with a static apply() that boosts the RenderContext and FX slots for the current block only. Remove the snapshot/restore machinery. Make currentProgram atomic. Validate state I/O: check XML tag, clamp program index, sanitize LFO shape data, and use beginChangeGesture/endChangeGesture for RAVE toggle. Reset parameters to defaults before loading preset values.
This commit is contained in:
+242
-196
@@ -1,22 +1,11 @@
|
||||
#include "Engine.h"
|
||||
#include "RAVEButton.h"
|
||||
|
||||
namespace serum
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
inline float v (juce::AudioProcessorValueTreeState& apvts, const char* id)
|
||||
{
|
||||
if (auto* p = apvts.getRawParameterValue (id))
|
||||
return p->load();
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
inline int vic (juce::AudioProcessorValueTreeState& apvts, const char* id, int maxValue)
|
||||
{
|
||||
return juce::jlimit (0, maxValue, (int) std::llround (v (apvts, id) * maxValue));
|
||||
}
|
||||
|
||||
inline float limit (float x) noexcept
|
||||
{
|
||||
const float ax = std::fabs (x);
|
||||
@@ -26,47 +15,36 @@ namespace
|
||||
const float clipped = 0.8f + std::tanh (over) * 0.2f;
|
||||
return std::copysign (clipped, x);
|
||||
}
|
||||
|
||||
const char* kEnvAttack[kNumEnvelopes] = { ids::env1A, ids::env2A, ids::env3A, ids::env4A };
|
||||
const char* kEnvDecay[kNumEnvelopes] = { ids::env1D, ids::env2D, ids::env3D, ids::env4D };
|
||||
const char* kEnvSustain[kNumEnvelopes]= { ids::env1S, ids::env2S, ids::env3S, ids::env4S };
|
||||
const char* kEnvRelease[kNumEnvelopes]= { ids::env1R, ids::env2R, ids::env3R, ids::env4R };
|
||||
const char* kEnvCurve[kNumEnvelopes] = { ids::env1Curve, ids::env2Curve, ids::env3Curve, ids::env4Curve };
|
||||
|
||||
const char* kLfoRate[kNumLfos] = { ids::lfo1Rate, ids::lfo2Rate, ids::lfo3Rate, ids::lfo4Rate };
|
||||
const char* kLfoSync[kNumLfos] = { ids::lfo1Sync, ids::lfo2Sync, ids::lfo3Sync, ids::lfo4Sync };
|
||||
const char* kLfoBeat[kNumLfos] = { ids::lfo1Beat, ids::lfo2Beat, ids::lfo3Beat, ids::lfo4Beat };
|
||||
const char* kLfoShape[kNumLfos] = { ids::lfo1Shape, ids::lfo2Shape, ids::lfo3Shape, ids::lfo4Shape };
|
||||
const char* kLfoPhase[kNumLfos] = { ids::lfo1Phase, ids::lfo2Phase, ids::lfo3Phase, ids::lfo4Phase };
|
||||
const char* kLfoFade[kNumLfos] = { ids::lfo1Fade, ids::lfo2Fade, ids::lfo3Fade, ids::lfo4Fade };
|
||||
const char* kLfoDelay[kNumLfos] = { ids::lfo1Delay, ids::lfo2Delay, ids::lfo3Delay, ids::lfo4Delay };
|
||||
|
||||
const char* kFxType[kNumFxSlots] = { ids::fx1Type, ids::fx2Type, ids::fx3Type, ids::fx4Type,
|
||||
ids::fx5Type, ids::fx6Type, ids::fx7Type, ids::fx8Type };
|
||||
const char* kFxMix[kNumFxSlots] = { ids::fx1Mix, ids::fx2Mix, ids::fx3Mix, ids::fx4Mix,
|
||||
ids::fx5Mix, ids::fx6Mix, ids::fx7Mix, ids::fx8Mix };
|
||||
const char* kFxP1[kNumFxSlots] = { ids::fx1P1, ids::fx2P1, ids::fx3P1, ids::fx4P1,
|
||||
ids::fx5P1, ids::fx6P1, ids::fx7P1, ids::fx8P1 };
|
||||
const char* kFxP2[kNumFxSlots] = { ids::fx1P2, ids::fx2P2, ids::fx3P2, ids::fx4P2,
|
||||
ids::fx5P2, ids::fx6P2, ids::fx7P2, ids::fx8P2 };
|
||||
const char* kFxP3[kNumFxSlots] = { ids::fx1P3, ids::fx2P3, ids::fx3P3, ids::fx4P3,
|
||||
ids::fx5P3, ids::fx6P3, ids::fx7P3, ids::fx8P3 };
|
||||
const char* kFxP4[kNumFxSlots] = { ids::fx1P4, ids::fx2P4, ids::fx3P4, ids::fx4P4,
|
||||
ids::fx5P4, ids::fx6P4, ids::fx7P4, ids::fx8P4 };
|
||||
}
|
||||
|
||||
void Engine::prepare (double sampleRate, int maxBlockSize)
|
||||
Engine::Engine()
|
||||
{
|
||||
wavetables.prebuild();
|
||||
audioMatrix.connections.reserve (ModulationMatrix::kMaxConnections);
|
||||
for (auto& assignments : audioMacros.assignments)
|
||||
assignments.reserve (MacroControls::kMaxAssignments);
|
||||
}
|
||||
|
||||
void Engine::prepare (double sampleRate, int maxBlockSize, juce::AudioProcessorValueTreeState& apvts)
|
||||
{
|
||||
const juce::ScopedLock lock (controlLock);
|
||||
sr = sampleRate;
|
||||
blockSize = maxBlockSize;
|
||||
blockSize = juce::jmax (1, maxBlockSize);
|
||||
parameterValues.clear();
|
||||
for (auto* parameter : apvts.processor.getParameters())
|
||||
if (auto* ranged = dynamic_cast<juce::RangedAudioParameter*> (parameter))
|
||||
if (auto* raw = apvts.getRawParameterValue (ranged->paramID))
|
||||
parameterValues.emplace (std::string_view (ranged->paramID.toRawUTF8()),
|
||||
ParameterValue { raw, raw->load() });
|
||||
|
||||
for (auto& voice : voices)
|
||||
voice.prepare (sampleRate, maxBlockSize);
|
||||
voice.prepare (sampleRate, blockSize);
|
||||
for (auto& lfo : lfos)
|
||||
lfo.prepare (sampleRate);
|
||||
fx.prepare (sampleRate, maxBlockSize);
|
||||
mixBuffer.setSize (2, maxBlockSize, false, false, true);
|
||||
fx.prepare (sampleRate, blockSize);
|
||||
mixBuffer.setSize (2, blockSize, false, false, true);
|
||||
reset();
|
||||
captureControls();
|
||||
}
|
||||
|
||||
void Engine::reset()
|
||||
@@ -78,29 +56,57 @@ void Engine::reset()
|
||||
fx.reset();
|
||||
pitchBend = 0.0f;
|
||||
modWheel = 0.0f;
|
||||
activeVoiceCount.store (0, std::memory_order_relaxed);
|
||||
mixBuffer.clear();
|
||||
}
|
||||
|
||||
void Engine::captureControls()
|
||||
{
|
||||
const juce::ScopedTryLock lock (controlLock);
|
||||
if (! lock.isLocked())
|
||||
return;
|
||||
|
||||
audioMatrix.connections.assign (matrix.connections.begin(),
|
||||
matrix.connections.begin() + juce::jmin (matrix.size(), ModulationMatrix::kMaxConnections));
|
||||
for (int i = 0; i < kNumMacros; ++i)
|
||||
{
|
||||
const auto& source = macros.assignments[(size_t) i];
|
||||
auto& dest = audioMacros.assignments[(size_t) i];
|
||||
dest.assign (source.begin(), source.begin() + juce::jmin ((int) source.size(), MacroControls::kMaxAssignments));
|
||||
}
|
||||
for (int i = 0; i < kNumLfos; ++i)
|
||||
lfos[(size_t) i].setShapeData (controlLfos[(size_t) i].getShapeData(),
|
||||
controlLfos[(size_t) i].getShapeSteps());
|
||||
for (auto& entry : parameterValues)
|
||||
{
|
||||
const float value = entry.second.source->load (std::memory_order_relaxed);
|
||||
entry.second.value = std::isfinite (value) ? juce::jlimit (0.0f, 1.0f, value) : 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void Engine::setLfoShapeData (int index, const std::vector<float>& data, int steps)
|
||||
{
|
||||
const juce::ScopedLock lock (controlLock);
|
||||
index = juce::jlimit (0, kNumLfos - 1, index);
|
||||
lfos[(size_t) index].setShapeData (data, steps);
|
||||
controlLfos[(size_t) index].setShapeData (data, steps);
|
||||
}
|
||||
|
||||
int Engine::getActiveVoiceCount() const
|
||||
float Engine::v (const char* id) const
|
||||
{
|
||||
int count = 0;
|
||||
for (const auto& v : voices)
|
||||
if (v.isActive())
|
||||
++count;
|
||||
return count;
|
||||
const auto it = parameterValues.find (id);
|
||||
return it != parameterValues.end() ? it->second.value : 0.0f;
|
||||
}
|
||||
|
||||
int Engine::vic (const char* id, int maxValue) const
|
||||
{
|
||||
return juce::jlimit (0, maxValue, (int) std::llround (v (id) * maxValue));
|
||||
}
|
||||
|
||||
SynthVoice* Engine::findFreeVoice()
|
||||
{
|
||||
for (auto& v : voices)
|
||||
if (! v.isActive())
|
||||
return &v;
|
||||
for (auto& voice : voices)
|
||||
if (! voice.isActive())
|
||||
return &voice;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -110,20 +116,20 @@ SynthVoice* Engine::stealVoice()
|
||||
SynthVoice* best = nullptr;
|
||||
juce::uint64 bestId = std::numeric_limits<juce::uint64>::max();
|
||||
|
||||
for (auto& v : voices)
|
||||
if (v.isActive() && v.isReleased() && v.getNoteId() < bestId)
|
||||
for (auto& voice : voices)
|
||||
if (voice.isActive() && voice.isReleased() && voice.getNoteId() < bestId)
|
||||
{
|
||||
best = &v;
|
||||
bestId = v.getNoteId();
|
||||
best = &voice;
|
||||
bestId = voice.getNoteId();
|
||||
}
|
||||
if (best != nullptr)
|
||||
return best;
|
||||
|
||||
for (auto& v : voices)
|
||||
if (v.isActive() && v.getNoteId() < bestId)
|
||||
for (auto& voice : voices)
|
||||
if (voice.isActive() && voice.getNoteId() < bestId)
|
||||
{
|
||||
best = &v;
|
||||
bestId = v.getNoteId();
|
||||
best = &voice;
|
||||
bestId = voice.getNoteId();
|
||||
}
|
||||
return best != nullptr ? best : &voices[0];
|
||||
}
|
||||
@@ -140,181 +146,221 @@ void Engine::noteOn (int noteNumber, float velocity01)
|
||||
|
||||
void Engine::noteOff (int noteNumber)
|
||||
{
|
||||
for (auto& v : voices)
|
||||
if (v.isActive() && v.getNote() == noteNumber && ! v.isReleased())
|
||||
v.noteOff();
|
||||
for (auto& voice : voices)
|
||||
if (voice.isActive() && voice.getNote() == noteNumber && ! voice.isReleased())
|
||||
voice.noteOff();
|
||||
}
|
||||
|
||||
void Engine::allNotesOff()
|
||||
{
|
||||
for (auto& v : voices)
|
||||
if (v.isActive())
|
||||
v.noteOff();
|
||||
for (auto& voice : voices)
|
||||
if (voice.isActive())
|
||||
voice.noteOff();
|
||||
}
|
||||
|
||||
void Engine::readOscParams (juce::AudioProcessorValueTreeState& apvts, const char* prefix, OscParams& o)
|
||||
void Engine::readOscParams (const paramIds::Oscillator& ids, OscParams& o) const
|
||||
{
|
||||
const juce::String p = prefix;
|
||||
auto g = [&] (const char* suffix) { return v (apvts, (p + suffix).toRawUTF8()); };
|
||||
|
||||
o.enabled = g ("On") > 0.5f;
|
||||
o.wave = juce::jlimit (0, kNumWavetables - 1, (int) std::llround (g ("Wave") * (kNumWavetables - 1)));
|
||||
o.wtPos = g ("WtPos");
|
||||
o.warp = (int) std::llround (g ("Warp") * 7.0f);
|
||||
o.warpAmt = g ("WarpAmt");
|
||||
o.coarse = (int) std::llround (g ("Coarse") * 48.0f) - 24;
|
||||
o.fine = (int) std::llround (g ("Fine") * 200.0f) - 100;
|
||||
o.level = g ("Level");
|
||||
o.pan = g ("Pan") * 2.0f - 1.0f;
|
||||
o.unison = 1 + (int) std::llround (g ("Unison") * 15.0f);
|
||||
o.detune = g ("Detune");
|
||||
o.spread = g ("Spread");
|
||||
o.phase = g ("Phase");
|
||||
o.randPhase = g ("RandPh");
|
||||
o.enabled = v (ids.on) > 0.5f;
|
||||
o.wave = vic (ids.wave, kNumWavetables - 1);
|
||||
o.wtPos = v (ids.wtPos);
|
||||
o.warp = vic (ids.warp, (int) WarpMode::Count - 1);
|
||||
o.warpAmt = v (ids.warpAmt);
|
||||
o.coarse = vic (ids.coarse, 48) - 24;
|
||||
o.fine = vic (ids.fine, 200) - 100;
|
||||
o.level = v (ids.level);
|
||||
o.pan = v (ids.pan) * 2.0f - 1.0f;
|
||||
o.unison = 1 + vic (ids.unison, kMaxUnison - 1);
|
||||
o.detune = v (ids.detune);
|
||||
o.spread = v (ids.spread);
|
||||
o.phase = v (ids.phase);
|
||||
o.randPhase = v (ids.randPhase);
|
||||
}
|
||||
|
||||
void Engine::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midi,
|
||||
juce::AudioProcessorValueTreeState& apvts,
|
||||
juce::AudioPlayHead* playhead)
|
||||
juce::AudioProcessorValueTreeState&, juce::AudioPlayHead* playhead)
|
||||
{
|
||||
const int n = buffer.getNumSamples();
|
||||
const int numCh = buffer.getNumChannels();
|
||||
captureControls();
|
||||
|
||||
// Tempo.
|
||||
if (playhead != nullptr)
|
||||
if (auto pos = playhead->getPosition())
|
||||
if (auto b = pos->getBpm())
|
||||
bpm = *b;
|
||||
if (auto tempo = pos->getBpm())
|
||||
if (std::isfinite (*tempo) && *tempo > 0.0)
|
||||
bpm = *tempo;
|
||||
|
||||
// MIDI.
|
||||
for (const auto meta : midi)
|
||||
{
|
||||
const auto m = meta.getMessage();
|
||||
if (m.isNoteOn() && m.getVelocity() > 0)
|
||||
noteOn (m.getNoteNumber(), m.getFloatVelocity());
|
||||
else if (m.isNoteOff() || (m.isNoteOn() && m.getVelocity() == 0))
|
||||
noteOff (m.getNoteNumber());
|
||||
else if (m.isPitchWheel())
|
||||
pitchBend = (m.getPitchWheelValue() - 8192) / 8192.0f;
|
||||
else if (m.isController())
|
||||
{
|
||||
if (m.getControllerNumber() == 1)
|
||||
modWheel = m.getControllerValue() / 127.0f;
|
||||
else if (m.getControllerNumber() == 120 || m.getControllerNumber() == 123)
|
||||
allNotesOff();
|
||||
}
|
||||
else if (m.isAllNotesOff() || m.isAllSoundOff())
|
||||
allNotesOff();
|
||||
}
|
||||
|
||||
// Prepare the voice mix buffer.
|
||||
mixBuffer.setSize (2, n, false, false, true);
|
||||
mixBuffer.clear();
|
||||
float* mixL = mixBuffer.getWritePointer (0);
|
||||
float* mixR = mixBuffer.getWritePointer (1);
|
||||
|
||||
// Advance LFOs and capture their values (control rate).
|
||||
float lfoValues[kNumLfos];
|
||||
// Advance LFOs and capture their values at control-rate render boundaries.
|
||||
for (int i = 0; i < kNumLfos; ++i)
|
||||
{
|
||||
lfos[(size_t) i].setTempo (bpm);
|
||||
lfos[(size_t) i].setParams (v (apvts, kLfoRate[i]),
|
||||
v (apvts, kLfoSync[i]) > 0.5f,
|
||||
v (apvts, kLfoBeat[i]),
|
||||
vic (apvts, kLfoShape[i], 6),
|
||||
v (apvts, kLfoPhase[i]),
|
||||
v (apvts, kLfoFade[i]),
|
||||
v (apvts, kLfoDelay[i]));
|
||||
for (int s = 0; s < n; ++s)
|
||||
lfos[(size_t) i].process();
|
||||
lfoValues[i] = lfos[(size_t) i].getValue();
|
||||
auto& lfo = lfos[(size_t) i];
|
||||
lfo.setTempo (bpm);
|
||||
lfo.setParams (v (paramIds::lfoRate[i]), v (paramIds::lfoSync[i]) > 0.5f,
|
||||
v (paramIds::lfoBeat[i]), vic (paramIds::lfoShape[i], (int) LfoShape::Count - 1),
|
||||
v (paramIds::lfoPhase[i]), v (paramIds::lfoFade[i]), v (paramIds::lfoDelay[i]));
|
||||
}
|
||||
|
||||
const float macroValues[kNumMacros] = { v (apvts, ids::macro1), v (apvts, ids::macro2),
|
||||
v (apvts, ids::macro3), v (apvts, ids::macro4) };
|
||||
|
||||
// Build the render context.
|
||||
RenderContext ctx;
|
||||
ctx.sampleRate = sr;
|
||||
ctx.wavetables = &wavetables;
|
||||
for (int i = 0; i < kNumLfos; ++i) ctx.lfoValues[i] = lfoValues[i];
|
||||
for (int i = 0; i < kNumMacros; ++i) ctx.macroValues[i] = macroValues[i];
|
||||
ctx.modWheel = modWheel;
|
||||
ctx.pitchBend = pitchBend;
|
||||
ctx.pitchBendRange = 2.0f;
|
||||
ctx.matrix = &matrix;
|
||||
ctx.macros = ¯os;
|
||||
ctx.matrix = &audioMatrix;
|
||||
ctx.macros = &audioMacros;
|
||||
for (int i = 0; i < kNumMacros; ++i)
|
||||
ctx.macroValues[i] = v (paramIds::macros[i]);
|
||||
|
||||
readOscParams (apvts, "oscA", ctx.oscA);
|
||||
readOscParams (apvts, "oscB", ctx.oscB);
|
||||
readOscParams (paramIds::oscillators[0], ctx.oscA);
|
||||
readOscParams (paramIds::oscillators[1], ctx.oscB);
|
||||
|
||||
ctx.subOn = v (apvts, ids::subOn) > 0.5f;
|
||||
ctx.subShape = vic (apvts, ids::subShape, 1);
|
||||
ctx.subOct = vic (apvts, ids::subOct, 2) - 2;
|
||||
ctx.subLevel = v (apvts, ids::subLevel);
|
||||
ctx.subOn = v (ids::subOn) > 0.5f;
|
||||
ctx.subShape = vic (ids::subShape, (int) SubShape::Count - 1);
|
||||
ctx.subOct = vic (ids::subOct, 2) - 2;
|
||||
ctx.subLevel = v (ids::subLevel);
|
||||
ctx.noiseOn = v (ids::noiseOn) > 0.5f;
|
||||
ctx.noiseType = vic (ids::noiseType, (int) NoiseType::Count - 1);
|
||||
ctx.noiseLevel = v (ids::noiseLevel);
|
||||
|
||||
ctx.noiseOn = v (apvts, ids::noiseOn) > 0.5f;
|
||||
ctx.noiseType = vic (apvts, ids::noiseType, 1);
|
||||
ctx.noiseLevel = v (apvts, ids::noiseLevel);
|
||||
|
||||
ctx.filters.f1On = v (apvts, ids::f1On) > 0.5f;
|
||||
ctx.filters.f1Type = vic (apvts, ids::f1Type, 6);
|
||||
ctx.filters.f1Cutoff = v (apvts, ids::f1Cutoff);
|
||||
ctx.filters.f1Res = v (apvts, ids::f1Res);
|
||||
ctx.filters.f1Drive = v (apvts, ids::f1Drive);
|
||||
ctx.filters.f1Key = v (apvts, ids::f1Key);
|
||||
ctx.filters.f1Slope = vic (apvts, ids::f1Slope, 2);
|
||||
|
||||
ctx.filters.f2On = v (apvts, ids::f2On) > 0.5f;
|
||||
ctx.filters.f2Type = vic (apvts, ids::f2Type, 6);
|
||||
ctx.filters.f2Cutoff = v (apvts, ids::f2Cutoff);
|
||||
ctx.filters.f2Res = v (apvts, ids::f2Res);
|
||||
ctx.filters.f2Drive = v (apvts, ids::f2Drive);
|
||||
ctx.filters.f2Key = v (apvts, ids::f2Key);
|
||||
ctx.filters.f2Slope = vic (apvts, ids::f2Slope, 2);
|
||||
|
||||
ctx.filters.route = vic (apvts, ids::fRoute, 2);
|
||||
ctx.filters.mix = v (apvts, ids::fMix);
|
||||
ctx.filters.out = v (apvts, ids::fOut) * 1.5f;
|
||||
ctx.filters.f1On = v (ids::f1On) > 0.5f;
|
||||
ctx.filters.f1Type = vic (ids::f1Type, (int) FilterModel::Count - 1);
|
||||
ctx.filters.f1Cutoff = v (ids::f1Cutoff);
|
||||
ctx.filters.f1Res = v (ids::f1Res);
|
||||
ctx.filters.f1Drive = v (ids::f1Drive);
|
||||
ctx.filters.f1Key = v (ids::f1Key);
|
||||
ctx.filters.f1Slope = vic (ids::f1Slope, 2);
|
||||
ctx.filters.f2On = v (ids::f2On) > 0.5f;
|
||||
ctx.filters.f2Type = vic (ids::f2Type, (int) FilterModel::Count - 1);
|
||||
ctx.filters.f2Cutoff = v (ids::f2Cutoff);
|
||||
ctx.filters.f2Res = v (ids::f2Res);
|
||||
ctx.filters.f2Drive = v (ids::f2Drive);
|
||||
ctx.filters.f2Key = v (ids::f2Key);
|
||||
ctx.filters.f2Slope = vic (ids::f2Slope, 2);
|
||||
ctx.filters.route = vic (ids::fRoute, (int) FilterRoute::Count - 1);
|
||||
ctx.filters.mix = v (ids::fMix);
|
||||
ctx.filters.out = v (ids::fOut) * 1.5f;
|
||||
|
||||
for (int i = 0; i < kNumEnvelopes; ++i)
|
||||
{
|
||||
ctx.envAttack[i] = v (apvts, kEnvAttack[i]);
|
||||
ctx.envDecay[i] = v (apvts, kEnvDecay[i]);
|
||||
ctx.envSustain[i] = v (apvts, kEnvSustain[i]);
|
||||
ctx.envRelease[i] = v (apvts, kEnvRelease[i]);
|
||||
ctx.envCurve[i] = v (apvts, kEnvCurve[i]);
|
||||
ctx.envAttack[i] = v (paramIds::envAttack[i]);
|
||||
ctx.envDecay[i] = v (paramIds::envDecay[i]);
|
||||
ctx.envSustain[i] = v (paramIds::envSustain[i]);
|
||||
ctx.envRelease[i] = v (paramIds::envRelease[i]);
|
||||
ctx.envCurve[i] = v (paramIds::envCurve[i]);
|
||||
}
|
||||
|
||||
// Render all active voices into the mix buffer.
|
||||
for (auto& voice : voices)
|
||||
if (voice.isActive())
|
||||
voice.render (mixL, mixR, n, ctx);
|
||||
|
||||
// FX rack.
|
||||
FxSlotParams slots[kNumFxSlots];
|
||||
std::array<FxSlotParams, kNumFxSlots> slots;
|
||||
for (int i = 0; i < kNumFxSlots; ++i)
|
||||
{
|
||||
slots[i].type = juce::jlimit (0, (int) FxType::Count - 1, (int) std::llround (v (apvts, kFxType[i]) * ((int) FxType::Count - 1)));
|
||||
slots[i].mix = v (apvts, kFxMix[i]);
|
||||
slots[i].p[0] = v (apvts, kFxP1[i]);
|
||||
slots[i].p[1] = v (apvts, kFxP2[i]);
|
||||
slots[i].p[2] = v (apvts, kFxP3[i]);
|
||||
slots[i].p[3] = v (apvts, kFxP4[i]);
|
||||
auto& slot = slots[(size_t) i];
|
||||
slot.type = vic (paramIds::fxType[i], (int) FxType::Count - 1);
|
||||
slot.mix = v (paramIds::fxMix[i]);
|
||||
slot.p[0] = v (paramIds::fxP1[i]);
|
||||
slot.p[1] = v (paramIds::fxP2[i]);
|
||||
slot.p[2] = v (paramIds::fxP3[i]);
|
||||
slot.p[3] = v (paramIds::fxP4[i]);
|
||||
}
|
||||
if (v (ids::rave) > 0.5f)
|
||||
RaveController::apply (ctx, slots.data(), kNumFxSlots);
|
||||
|
||||
fx.process (mixBuffer, slots, kNumFxSlots);
|
||||
|
||||
// Master + soft limiting.
|
||||
const float master = v (apvts, ids::master);
|
||||
|
||||
for (int ch = 0; ch < numCh; ++ch)
|
||||
int offset = 0;
|
||||
auto renderUntil = [&] (int end)
|
||||
{
|
||||
float* dest = buffer.getWritePointer (ch);
|
||||
const float* src = mixBuffer.getReadPointer (ch < 2 ? ch : 0);
|
||||
for (int i = 0; i < n; ++i)
|
||||
dest[i] = limit (src[i] * master);
|
||||
while (offset < end)
|
||||
{
|
||||
const int count = juce::jmin (end - offset, juce::jmin (blockSize, 64));
|
||||
ctx.modWheel = modWheel;
|
||||
ctx.pitchBend = pitchBend;
|
||||
for (int i = 0; i < kNumLfos; ++i)
|
||||
ctx.lfoValues[i] = lfos[(size_t) i].getValue();
|
||||
|
||||
const SynthVoice* newest = nullptr;
|
||||
for (const auto& voice : voices)
|
||||
if (voice.isActive() && (newest == nullptr || voice.getNoteId() > newest->getNoteId()))
|
||||
newest = &voice;
|
||||
auto sourceValue = [&] (ModSource source)
|
||||
{
|
||||
const int index = (int) source;
|
||||
if (source >= ModSource::Lfo1 && source <= ModSource::Lfo4)
|
||||
return ctx.lfoValues[index - (int) ModSource::Lfo1];
|
||||
if (source >= ModSource::Macro1 && source <= ModSource::Macro4)
|
||||
return ctx.macroValues[index - (int) ModSource::Macro1];
|
||||
if (source == ModSource::ModWheel) return modWheel;
|
||||
if (source == ModSource::PitchBend) return pitchBend;
|
||||
return newest != nullptr ? newest->getModulationValue (source) : 0.0f;
|
||||
};
|
||||
|
||||
std::array<float, kNumModTargets> globalMod {};
|
||||
for (const auto& connection : audioMatrix.connections)
|
||||
if (! isPerVoiceTarget (connection.target))
|
||||
{
|
||||
float value = sourceValue (connection.source);
|
||||
if (connection.bipolar && ! isBipolarSource (connection.source))
|
||||
value = value * 2.0f - 1.0f;
|
||||
globalMod[(size_t) connection.target] += value * connection.depth;
|
||||
}
|
||||
for (int i = 0; i < kNumMacros; ++i)
|
||||
for (const auto& assignment : audioMacros.assignments[(size_t) i])
|
||||
if (! isPerVoiceTarget (assignment.target))
|
||||
globalMod[(size_t) assignment.target] += ctx.macroValues[i] * assignment.depth;
|
||||
|
||||
// Prepare the voice mix buffer.
|
||||
juce::AudioBuffer<float> block (mixBuffer.getArrayOfWritePointers(), 2, count);
|
||||
block.clear();
|
||||
|
||||
// Render all active voices into the mix buffer.
|
||||
for (auto& voice : voices)
|
||||
if (voice.isActive())
|
||||
voice.render (block.getWritePointer (0), block.getWritePointer (1), count, ctx);
|
||||
|
||||
auto modulatedSlots = slots;
|
||||
for (int i = 0; i < kNumFxSlots; ++i)
|
||||
modulatedSlots[(size_t) i].mix = juce::jlimit (0.0f, 1.0f,
|
||||
slots[(size_t) i].mix + globalMod[(size_t) ModTarget::Fx1Mix + (size_t) i]);
|
||||
fx.process (block, modulatedSlots.data(), kNumFxSlots);
|
||||
|
||||
// Master + soft limiting.
|
||||
const float master = juce::jlimit (0.0f, 1.0f, v (ids::master) + globalMod[(size_t) ModTarget::Master]);
|
||||
for (int ch = 0; ch < numCh; ++ch)
|
||||
{
|
||||
float* dest = buffer.getWritePointer (ch, offset);
|
||||
const float* src = block.getReadPointer (ch < 2 ? ch : 0);
|
||||
for (int i = 0; i < count; ++i)
|
||||
dest[i] = limit (src[i] * master);
|
||||
}
|
||||
for (auto& lfo : lfos)
|
||||
lfo.advance (count);
|
||||
offset += count;
|
||||
}
|
||||
};
|
||||
|
||||
// MIDI.
|
||||
for (const auto meta : midi)
|
||||
{
|
||||
renderUntil (juce::jlimit (offset, n, meta.samplePosition));
|
||||
const auto message = meta.getMessage();
|
||||
if (message.isNoteOn())
|
||||
noteOn (message.getNoteNumber(), message.getFloatVelocity());
|
||||
else if (message.isNoteOff())
|
||||
noteOff (message.getNoteNumber());
|
||||
else if (message.isPitchWheel())
|
||||
pitchBend = (message.getPitchWheelValue() - 8192) / 8192.0f;
|
||||
else if (message.isAllSoundOff())
|
||||
{
|
||||
for (auto& voice : voices)
|
||||
voice.reset();
|
||||
fx.reset();
|
||||
}
|
||||
else if (message.isAllNotesOff())
|
||||
allNotesOff();
|
||||
else if (message.isController() && message.getControllerNumber() == 1)
|
||||
modWheel = message.getControllerValue() / 127.0f;
|
||||
}
|
||||
renderUntil (n);
|
||||
|
||||
int active = 0;
|
||||
for (const auto& voice : voices)
|
||||
active += voice.isActive() ? 1 : 0;
|
||||
activeVoiceCount.store (active, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
} // namespace serum
|
||||
|
||||
Reference in New Issue
Block a user