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:
2026-09-09 14:33:30 +02:00
parent cd26beca42
commit 3be1e42242
6 changed files with 408 additions and 336 deletions
+238 -192
View File
@@ -1,22 +1,11 @@
#include "Engine.h" #include "Engine.h"
#include "RAVEButton.h"
namespace serum namespace serum
{ {
namespace 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 inline float limit (float x) noexcept
{ {
const float ax = std::fabs (x); const float ax = std::fabs (x);
@@ -26,47 +15,36 @@ namespace
const float clipped = 0.8f + std::tanh (over) * 0.2f; const float clipped = 0.8f + std::tanh (over) * 0.2f;
return std::copysign (clipped, x); 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; 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) for (auto& voice : voices)
voice.prepare (sampleRate, maxBlockSize); voice.prepare (sampleRate, blockSize);
for (auto& lfo : lfos) for (auto& lfo : lfos)
lfo.prepare (sampleRate); lfo.prepare (sampleRate);
fx.prepare (sampleRate, maxBlockSize); fx.prepare (sampleRate, blockSize);
mixBuffer.setSize (2, maxBlockSize, false, false, true); mixBuffer.setSize (2, blockSize, false, false, true);
reset(); reset();
captureControls();
} }
void Engine::reset() void Engine::reset()
@@ -78,29 +56,57 @@ void Engine::reset()
fx.reset(); fx.reset();
pitchBend = 0.0f; pitchBend = 0.0f;
modWheel = 0.0f; modWheel = 0.0f;
activeVoiceCount.store (0, std::memory_order_relaxed);
mixBuffer.clear(); 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) void Engine::setLfoShapeData (int index, const std::vector<float>& data, int steps)
{ {
const juce::ScopedLock lock (controlLock);
index = juce::jlimit (0, kNumLfos - 1, index); 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; const auto it = parameterValues.find (id);
for (const auto& v : voices) return it != parameterValues.end() ? it->second.value : 0.0f;
if (v.isActive()) }
++count;
return count; int Engine::vic (const char* id, int maxValue) const
{
return juce::jlimit (0, maxValue, (int) std::llround (v (id) * maxValue));
} }
SynthVoice* Engine::findFreeVoice() SynthVoice* Engine::findFreeVoice()
{ {
for (auto& v : voices) for (auto& voice : voices)
if (! v.isActive()) if (! voice.isActive())
return &v; return &voice;
return nullptr; return nullptr;
} }
@@ -110,20 +116,20 @@ SynthVoice* Engine::stealVoice()
SynthVoice* best = nullptr; SynthVoice* best = nullptr;
juce::uint64 bestId = std::numeric_limits<juce::uint64>::max(); juce::uint64 bestId = std::numeric_limits<juce::uint64>::max();
for (auto& v : voices) for (auto& voice : voices)
if (v.isActive() && v.isReleased() && v.getNoteId() < bestId) if (voice.isActive() && voice.isReleased() && voice.getNoteId() < bestId)
{ {
best = &v; best = &voice;
bestId = v.getNoteId(); bestId = voice.getNoteId();
} }
if (best != nullptr) if (best != nullptr)
return best; return best;
for (auto& v : voices) for (auto& voice : voices)
if (v.isActive() && v.getNoteId() < bestId) if (voice.isActive() && voice.getNoteId() < bestId)
{ {
best = &v; best = &voice;
bestId = v.getNoteId(); bestId = voice.getNoteId();
} }
return best != nullptr ? best : &voices[0]; return best != nullptr ? best : &voices[0];
} }
@@ -140,181 +146,221 @@ void Engine::noteOn (int noteNumber, float velocity01)
void Engine::noteOff (int noteNumber) void Engine::noteOff (int noteNumber)
{ {
for (auto& v : voices) for (auto& voice : voices)
if (v.isActive() && v.getNote() == noteNumber && ! v.isReleased()) if (voice.isActive() && voice.getNote() == noteNumber && ! voice.isReleased())
v.noteOff(); voice.noteOff();
} }
void Engine::allNotesOff() void Engine::allNotesOff()
{ {
for (auto& v : voices) for (auto& voice : voices)
if (v.isActive()) if (voice.isActive())
v.noteOff(); 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; o.enabled = v (ids.on) > 0.5f;
auto g = [&] (const char* suffix) { return v (apvts, (p + suffix).toRawUTF8()); }; o.wave = vic (ids.wave, kNumWavetables - 1);
o.wtPos = v (ids.wtPos);
o.enabled = g ("On") > 0.5f; o.warp = vic (ids.warp, (int) WarpMode::Count - 1);
o.wave = juce::jlimit (0, kNumWavetables - 1, (int) std::llround (g ("Wave") * (kNumWavetables - 1))); o.warpAmt = v (ids.warpAmt);
o.wtPos = g ("WtPos"); o.coarse = vic (ids.coarse, 48) - 24;
o.warp = (int) std::llround (g ("Warp") * 7.0f); o.fine = vic (ids.fine, 200) - 100;
o.warpAmt = g ("WarpAmt"); o.level = v (ids.level);
o.coarse = (int) std::llround (g ("Coarse") * 48.0f) - 24; o.pan = v (ids.pan) * 2.0f - 1.0f;
o.fine = (int) std::llround (g ("Fine") * 200.0f) - 100; o.unison = 1 + vic (ids.unison, kMaxUnison - 1);
o.level = g ("Level"); o.detune = v (ids.detune);
o.pan = g ("Pan") * 2.0f - 1.0f; o.spread = v (ids.spread);
o.unison = 1 + (int) std::llround (g ("Unison") * 15.0f); o.phase = v (ids.phase);
o.detune = g ("Detune"); o.randPhase = v (ids.randPhase);
o.spread = g ("Spread");
o.phase = g ("Phase");
o.randPhase = g ("RandPh");
} }
void Engine::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midi, void Engine::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midi,
juce::AudioProcessorValueTreeState& apvts, juce::AudioProcessorValueTreeState&, juce::AudioPlayHead* playhead)
juce::AudioPlayHead* playhead)
{ {
const int n = buffer.getNumSamples(); const int n = buffer.getNumSamples();
const int numCh = buffer.getNumChannels(); const int numCh = buffer.getNumChannels();
captureControls();
// Tempo. // Tempo.
if (playhead != nullptr) if (playhead != nullptr)
if (auto pos = playhead->getPosition()) if (auto pos = playhead->getPosition())
if (auto b = pos->getBpm()) if (auto tempo = pos->getBpm())
bpm = *b; if (std::isfinite (*tempo) && *tempo > 0.0)
bpm = *tempo;
// MIDI. // Advance LFOs and capture their values at control-rate render boundaries.
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];
for (int i = 0; i < kNumLfos; ++i) for (int i = 0; i < kNumLfos; ++i)
{ {
lfos[(size_t) i].setTempo (bpm); auto& lfo = lfos[(size_t) i];
lfos[(size_t) i].setParams (v (apvts, kLfoRate[i]), lfo.setTempo (bpm);
v (apvts, kLfoSync[i]) > 0.5f, lfo.setParams (v (paramIds::lfoRate[i]), v (paramIds::lfoSync[i]) > 0.5f,
v (apvts, kLfoBeat[i]), v (paramIds::lfoBeat[i]), vic (paramIds::lfoShape[i], (int) LfoShape::Count - 1),
vic (apvts, kLfoShape[i], 6), v (paramIds::lfoPhase[i]), v (paramIds::lfoFade[i]), v (paramIds::lfoDelay[i]));
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();
} }
const float macroValues[kNumMacros] = { v (apvts, ids::macro1), v (apvts, ids::macro2),
v (apvts, ids::macro3), v (apvts, ids::macro4) };
// Build the render context. // Build the render context.
RenderContext ctx; RenderContext ctx;
ctx.sampleRate = sr; ctx.sampleRate = sr;
ctx.wavetables = &wavetables; ctx.wavetables = &wavetables;
for (int i = 0; i < kNumLfos; ++i) ctx.lfoValues[i] = lfoValues[i]; ctx.matrix = &audioMatrix;
for (int i = 0; i < kNumMacros; ++i) ctx.macroValues[i] = macroValues[i]; ctx.macros = &audioMacros;
ctx.modWheel = modWheel; for (int i = 0; i < kNumMacros; ++i)
ctx.pitchBend = pitchBend; ctx.macroValues[i] = v (paramIds::macros[i]);
ctx.pitchBendRange = 2.0f;
ctx.matrix = &matrix;
ctx.macros = &macros;
readOscParams (apvts, "oscA", ctx.oscA); readOscParams (paramIds::oscillators[0], ctx.oscA);
readOscParams (apvts, "oscB", ctx.oscB); readOscParams (paramIds::oscillators[1], ctx.oscB);
ctx.subOn = v (apvts, ids::subOn) > 0.5f; ctx.subOn = v (ids::subOn) > 0.5f;
ctx.subShape = vic (apvts, ids::subShape, 1); ctx.subShape = vic (ids::subShape, (int) SubShape::Count - 1);
ctx.subOct = vic (apvts, ids::subOct, 2) - 2; ctx.subOct = vic (ids::subOct, 2) - 2;
ctx.subLevel = v (apvts, ids::subLevel); 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.filters.f1On = v (ids::f1On) > 0.5f;
ctx.noiseType = vic (apvts, ids::noiseType, 1); ctx.filters.f1Type = vic (ids::f1Type, (int) FilterModel::Count - 1);
ctx.noiseLevel = v (apvts, ids::noiseLevel); ctx.filters.f1Cutoff = v (ids::f1Cutoff);
ctx.filters.f1Res = v (ids::f1Res);
ctx.filters.f1On = v (apvts, ids::f1On) > 0.5f; ctx.filters.f1Drive = v (ids::f1Drive);
ctx.filters.f1Type = vic (apvts, ids::f1Type, 6); ctx.filters.f1Key = v (ids::f1Key);
ctx.filters.f1Cutoff = v (apvts, ids::f1Cutoff); ctx.filters.f1Slope = vic (ids::f1Slope, 2);
ctx.filters.f1Res = v (apvts, ids::f1Res); ctx.filters.f2On = v (ids::f2On) > 0.5f;
ctx.filters.f1Drive = v (apvts, ids::f1Drive); ctx.filters.f2Type = vic (ids::f2Type, (int) FilterModel::Count - 1);
ctx.filters.f1Key = v (apvts, ids::f1Key); ctx.filters.f2Cutoff = v (ids::f2Cutoff);
ctx.filters.f1Slope = vic (apvts, ids::f1Slope, 2); ctx.filters.f2Res = v (ids::f2Res);
ctx.filters.f2Drive = v (ids::f2Drive);
ctx.filters.f2On = v (apvts, ids::f2On) > 0.5f; ctx.filters.f2Key = v (ids::f2Key);
ctx.filters.f2Type = vic (apvts, ids::f2Type, 6); ctx.filters.f2Slope = vic (ids::f2Slope, 2);
ctx.filters.f2Cutoff = v (apvts, ids::f2Cutoff); ctx.filters.route = vic (ids::fRoute, (int) FilterRoute::Count - 1);
ctx.filters.f2Res = v (apvts, ids::f2Res); ctx.filters.mix = v (ids::fMix);
ctx.filters.f2Drive = v (apvts, ids::f2Drive); ctx.filters.out = v (ids::fOut) * 1.5f;
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;
for (int i = 0; i < kNumEnvelopes; ++i) for (int i = 0; i < kNumEnvelopes; ++i)
{ {
ctx.envAttack[i] = v (apvts, kEnvAttack[i]); ctx.envAttack[i] = v (paramIds::envAttack[i]);
ctx.envDecay[i] = v (apvts, kEnvDecay[i]); ctx.envDecay[i] = v (paramIds::envDecay[i]);
ctx.envSustain[i] = v (apvts, kEnvSustain[i]); ctx.envSustain[i] = v (paramIds::envSustain[i]);
ctx.envRelease[i] = v (apvts, kEnvRelease[i]); ctx.envRelease[i] = v (paramIds::envRelease[i]);
ctx.envCurve[i] = v (apvts, kEnvCurve[i]); ctx.envCurve[i] = v (paramIds::envCurve[i]);
} }
// FX rack.
std::array<FxSlotParams, kNumFxSlots> slots;
for (int i = 0; i < kNumFxSlots; ++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);
int offset = 0;
auto renderUntil = [&] (int end)
{
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. // Render all active voices into the mix buffer.
for (auto& voice : voices) for (auto& voice : voices)
if (voice.isActive()) if (voice.isActive())
voice.render (mixL, mixR, n, ctx); voice.render (block.getWritePointer (0), block.getWritePointer (1), count, ctx);
// FX rack. auto modulatedSlots = slots;
FxSlotParams slots[kNumFxSlots];
for (int i = 0; i < kNumFxSlots; ++i) for (int i = 0; i < kNumFxSlots; ++i)
{ modulatedSlots[(size_t) i].mix = juce::jlimit (0.0f, 1.0f,
slots[i].type = juce::jlimit (0, (int) FxType::Count - 1, (int) std::llround (v (apvts, kFxType[i]) * ((int) FxType::Count - 1))); slots[(size_t) i].mix + globalMod[(size_t) ModTarget::Fx1Mix + (size_t) i]);
slots[i].mix = v (apvts, kFxMix[i]); fx.process (block, modulatedSlots.data(), kNumFxSlots);
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]);
}
fx.process (mixBuffer, slots, kNumFxSlots);
// Master + soft limiting. // Master + soft limiting.
const float master = v (apvts, ids::master); const float master = juce::jlimit (0.0f, 1.0f, v (ids::master) + globalMod[(size_t) ModTarget::Master]);
for (int ch = 0; ch < numCh; ++ch) for (int ch = 0; ch < numCh; ++ch)
{ {
float* dest = buffer.getWritePointer (ch); float* dest = buffer.getWritePointer (ch, offset);
const float* src = mixBuffer.getReadPointer (ch < 2 ? ch : 0); const float* src = block.getReadPointer (ch < 2 ? ch : 0);
for (int i = 0; i < n; ++i) for (int i = 0; i < count; ++i)
dest[i] = limit (src[i] * master); 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 } // namespace serum
+25 -10
View File
@@ -1,6 +1,8 @@
#pragma once #pragma once
#include <JuceHeader.h> #include <JuceHeader.h>
#include <string_view>
#include <unordered_map>
#include "Params.h" #include "Params.h"
#include "Wavetable.h" #include "Wavetable.h"
#include "SynthVoice.h" #include "SynthVoice.h"
@@ -19,7 +21,8 @@ namespace serum
class Engine class Engine
{ {
public: public:
void prepare (double sampleRate, int blockSize); Engine();
void prepare (double sampleRate, int blockSize, juce::AudioProcessorValueTreeState& apvts);
void reset(); void reset();
void processBlock (juce::AudioBuffer<float>& buffer, void processBlock (juce::AudioBuffer<float>& buffer,
@@ -32,22 +35,31 @@ public:
void noteOff (int noteNumber); void noteOff (int noteNumber);
void allNotesOff(); void allNotesOff();
// Modulation / DSP accessors (read-only for the GUI). // Control-state accessors: hold getControlLock() while reading or editing.
const juce::CriticalSection& getControlLock() const { return controlLock; }
ModulationMatrix& getMatrix() { return matrix; } ModulationMatrix& getMatrix() { return matrix; }
MacroControls& getMacros() { return macros; } MacroControls& getMacros() { return macros; }
WavetableLibrary& getWavetables() { return wavetables; } const std::array<LFO, kNumLfos>& getLfos() const { return controlLfos; }
const std::array<LFO, kNumLfos>& getLfos() const { return lfos; } const WavetableLibrary& getWavetables() const { return wavetables; }
void setLfoShapeData (int index, const std::vector<float>& data, int steps); void setLfoShapeData (int index, const std::vector<float>& data, int steps);
int getActiveVoiceCount() const; int getActiveVoiceCount() const { return activeVoiceCount.load (std::memory_order_relaxed); }
private: private:
std::array<SynthVoice, kNumVoices> voices; std::array<SynthVoice, kNumVoices> voices;
std::array<LFO, kNumLfos> lfos; std::array<LFO, kNumLfos> lfos, controlLfos;
WavetableLibrary wavetables; WavetableLibrary wavetables;
FXProcessor fx; FXProcessor fx;
ModulationMatrix matrix; ModulationMatrix matrix, audioMatrix;
MacroControls macros; MacroControls macros, audioMacros;
juce::CriticalSection controlLock;
struct ParameterValue
{
std::atomic<float>* source;
float value;
};
std::unordered_map<std::string_view, ParameterValue> parameterValues;
double sr = 44100.0; double sr = 44100.0;
int blockSize = 512; int blockSize = 512;
@@ -55,13 +67,16 @@ private:
float pitchBend = 0.0f; float pitchBend = 0.0f;
float modWheel = 0.0f; float modWheel = 0.0f;
juce::uint64 noteCounter = 0; juce::uint64 noteCounter = 0;
std::atomic<int> activeVoiceCount { 0 };
juce::AudioBuffer<float> mixBuffer; juce::AudioBuffer<float> mixBuffer;
SynthVoice* findFreeVoice(); SynthVoice* findFreeVoice();
SynthVoice* stealVoice(); SynthVoice* stealVoice();
void captureControls();
void readOscParams (juce::AudioProcessorValueTreeState& apvts, const char* prefix, OscParams& out); float v (const char* id) const;
int vic (const char* id, int maxValue) const;
void readOscParams (const paramIds::Oscillator& ids, OscParams& out) const;
}; };
} // namespace serum } // namespace serum
+106 -34
View File
@@ -176,7 +176,7 @@ juce::AudioProcessorValueTreeState::ParameterLayout SerumAltAudioProcessor::crea
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void SerumAltAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) void SerumAltAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{ {
engine.prepare (sampleRate, samplesPerBlock); engine.prepare (sampleRate, samplesPerBlock, parameters);
} }
void SerumAltAudioProcessor::releaseResources() void SerumAltAudioProcessor::releaseResources()
@@ -205,14 +205,13 @@ int SerumAltAudioProcessor::getNumPrograms()
int SerumAltAudioProcessor::getCurrentProgram() int SerumAltAudioProcessor::getCurrentProgram()
{ {
return currentProgram; return currentProgram.load();
} }
void SerumAltAudioProcessor::setCurrentProgram (int index) void SerumAltAudioProcessor::setCurrentProgram (int index)
{ {
index = juce::jlimit (0, getNumPrograms() - 1, index); if (getNumPrograms() > 0)
loadFactoryPreset (index); loadFactoryPreset (juce::jlimit (0, getNumPrograms() - 1, index));
currentProgram = index;
} }
const juce::String SerumAltAudioProcessor::getProgramName (int index) const juce::String SerumAltAudioProcessor::getProgramName (int index)
@@ -238,24 +237,36 @@ void SerumAltAudioProcessor::loadFactoryPreset (int index)
if (index < 0 || index >= (int) presets.size()) if (index < 0 || index >= (int) presets.size())
return; return;
const juce::ScopedLock lock (engine.getControlLock());
const FactoryPreset& preset = presets[(size_t) index]; const FactoryPreset& preset = presets[(size_t) index];
const auto* uiScaleParam = parameters.getParameter (ids::uiScale);
// RAVE should start off for a freshly loaded preset. for (auto* param : getParameters())
rave.resetSnapshot(); if (param != nullptr && param != uiScaleParam)
if (auto* raveParam = parameters.getParameter (ids::rave)) param->setValueNotifyingHost (param->getDefaultValue());
raveParam->setValueNotifyingHost (0.0f);
for (const auto& kv : preset.params) for (const auto& kv : preset.params)
if (auto* param = parameters.getParameter (kv.first)) if (auto* param = parameters.getParameter (kv.first))
param->setValueNotifyingHost (kv.second); if (param != uiScaleParam && std::isfinite (kv.second))
param->setValueNotifyingHost (juce::jlimit (0.0f, 1.0f, kv.second));
engine.getMatrix().clear(); // RAVE should start off for a freshly loaded preset.
if (auto* raveParam = parameters.getParameter (ids::rave))
raveParam->setValueNotifyingHost (0.0f);
auto& matrix = engine.getMatrix();
matrix.clear();
for (const auto& mod : preset.mods) for (const auto& mod : preset.mods)
engine.getMatrix().addConnection (mod.source, mod.target, mod.depth, mod.bipolar); if (! matrix.addConnection (mod.source, mod.target, mod.depth, mod.bipolar))
continue;
engine.getMacros().clear(); auto& macros = engine.getMacros();
macros.clear();
for (const auto& ma : preset.macroAssigns) for (const auto& ma : preset.macroAssigns)
engine.getMacros().addAssignment (ma.macro, ma.target, ma.depth); if (! macros.addAssignment (ma.macro, ma.target, ma.depth))
continue;
restoreLfoShapesFromState ({});
currentProgram.store (index);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -263,25 +274,36 @@ void SerumAltAudioProcessor::loadFactoryPreset (int index)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void SerumAltAudioProcessor::setRaveEnabled (bool enabled) void SerumAltAudioProcessor::setRaveEnabled (bool enabled)
{ {
rave.setEnabled (enabled, parameters); const juce::ScopedLock lock (engine.getControlLock());
if (auto* raveParam = parameters.getParameter (ids::rave)) if (auto* raveParam = parameters.getParameter (ids::rave))
{
raveParam->beginChangeGesture();
raveParam->setValueNotifyingHost (enabled ? 1.0f : 0.0f); raveParam->setValueNotifyingHost (enabled ? 1.0f : 0.0f);
raveParam->endChangeGesture();
}
} }
bool SerumAltAudioProcessor::isRaveEnabled() const bool SerumAltAudioProcessor::isRaveEnabled() const
{ {
return rave.isEnabled(); if (auto* raveParam = parameters.getRawParameterValue (ids::rave))
return raveParam->load() > 0.5f;
return false;
} }
int SerumAltAudioProcessor::getUiScaleIndex() const int SerumAltAudioProcessor::getUiScaleIndex() const
{ {
if (auto* p = parameters.getRawParameterValue (ids::uiScale)) if (auto* p = parameters.getRawParameterValue (ids::uiScale))
return juce::jlimit (0, 4, (int) std::llround (p->load() * 4.0f)); {
const float value = p->load();
if (std::isfinite (value))
return (int) std::llround (juce::jlimit (0.0f, 1.0f, value) * 4.0f);
}
return 1; return 1;
} }
void SerumAltAudioProcessor::setUiScaleIndex (int index) void SerumAltAudioProcessor::setUiScaleIndex (int index)
{ {
const juce::ScopedLock lock (engine.getControlLock());
index = juce::jlimit (0, 4, index); index = juce::jlimit (0, 4, index);
if (auto* p = parameters.getParameter (ids::uiScale)) if (auto* p = parameters.getParameter (ids::uiScale))
p->setValueNotifyingHost ((float) index / 4.0f); p->setValueNotifyingHost ((float) index / 4.0f);
@@ -298,7 +320,15 @@ float SerumAltAudioProcessor::getUiScale() const
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void SerumAltAudioProcessor::getStateInformation (juce::MemoryBlock& destData) void SerumAltAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
{ {
const juce::ScopedLock lock (engine.getControlLock());
auto state = parameters.copyState(); auto state = parameters.copyState();
for (int i = state.getNumChildren(); --i >= 0;)
{
const auto child = state.getChild (i);
if (child.hasType ("MODMATRIX") || child.hasType ("MACROS") || child.hasType ("LFOSHAPES"))
state.removeChild (i, nullptr);
}
state.setProperty ("currentProgram", currentProgram.load(), nullptr);
state.appendChild (engine.getMatrix().toValueTree(), nullptr); state.appendChild (engine.getMatrix().toValueTree(), nullptr);
state.appendChild (engine.getMacros().toValueTree(), nullptr); state.appendChild (engine.getMacros().toValueTree(), nullptr);
saveLfoShapesToState (state); saveLfoShapesToState (state);
@@ -310,39 +340,47 @@ void SerumAltAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
void SerumAltAudioProcessor::setStateInformation (const void* data, int sizeInBytes) void SerumAltAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{ {
if (data == nullptr || sizeInBytes <= 0)
return;
std::unique_ptr<juce::XmlElement> xml (getXmlFromBinary (data, sizeInBytes)); std::unique_ptr<juce::XmlElement> xml (getXmlFromBinary (data, sizeInBytes));
if (xml == nullptr) if (xml == nullptr || ! xml->hasTagName ("SerumAlt"))
return; return;
juce::ValueTree state = juce::ValueTree::fromXml (*xml); juce::ValueTree state = juce::ValueTree::fromXml (*xml);
if (! state.isValid()) if (! state.hasType ("SerumAlt"))
return; return;
const juce::ScopedLock lock (engine.getControlLock());
// A persisted RAVE toggle is rendered non-destructively, so retain it.
parameters.replaceState (state); parameters.replaceState (state);
engine.getMatrix().fromValueTree (state.getChildWithName ("MODMATRIX")); engine.getMatrix().fromValueTree (state.getChildWithName ("MODMATRIX"));
engine.getMacros().fromValueTree (state.getChildWithName ("MACROS")); engine.getMacros().fromValueTree (state.getChildWithName ("MACROS"));
restoreLfoShapesFromState (state); restoreLfoShapesFromState (state);
// A persisted RAVE toggle has no live snapshot, so start it off. const double savedProgram = (double) state.getProperty ("currentProgram", 0);
rave.resetSnapshot(); currentProgram.store (std::isfinite (savedProgram)
if (auto* raveParam = parameters.getParameter (ids::rave)) ? (int) juce::jlimit (0.0, (double) juce::jmax (0, getNumPrograms() - 1), savedProgram) : 0);
raveParam->setValueNotifyingHost (0.0f);
} }
void SerumAltAudioProcessor::saveLfoShapesToState (juce::ValueTree& state) const void SerumAltAudioProcessor::saveLfoShapesToState (juce::ValueTree& state) const
{ {
const juce::ScopedLock lock (engine.getControlLock());
juce::ValueTree tree ("LFOSHAPES"); juce::ValueTree tree ("LFOSHAPES");
for (int i = 0; i < kNumLfos; ++i) for (int i = 0; i < kNumLfos; ++i)
{ {
const auto& data = engine.getLfos()[(size_t) i].getShapeData(); const auto& source = engine.getLfos()[(size_t) i];
const auto& data = source.getShapeData();
juce::ValueTree lfo ("LFO"); juce::ValueTree lfo ("LFO");
lfo.setProperty ("index", i, nullptr); lfo.setProperty ("index", i, nullptr);
lfo.setProperty ("steps", engine.getLfos()[(size_t) i].getShapeSteps(), nullptr); lfo.setProperty ("steps", juce::jlimit (2, LFO::kShapePoints, source.getShapeSteps()), nullptr);
juce::Array<juce::var> arr; juce::Array<juce::var> arr;
for (float vv : data) for (int point = 0; point < juce::jmin (LFO::kShapePoints, (int) data.size()); ++point)
arr.add (vv); {
lfo.setProperty ("data", juce::var (arr), nullptr); const float value = data[(size_t) point];
arr.add (std::isfinite (value) ? juce::jlimit (-1.0f, 1.0f, value) : 0.0f);
}
lfo.setProperty ("data", juce::JSON::toString (juce::var (arr), true), nullptr);
tree.appendChild (lfo, nullptr); tree.appendChild (lfo, nullptr);
} }
state.appendChild (tree, nullptr); state.appendChild (tree, nullptr);
@@ -350,23 +388,57 @@ void SerumAltAudioProcessor::saveLfoShapesToState (juce::ValueTree& state) const
void SerumAltAudioProcessor::restoreLfoShapesFromState (const juce::ValueTree& state) void SerumAltAudioProcessor::restoreLfoShapesFromState (const juce::ValueTree& state)
{ {
const juce::ScopedLock lock (engine.getControlLock());
std::vector<float> defaultShape ((size_t) LFO::kShapePoints);
for (int point = 0; point < LFO::kShapePoints; ++point)
defaultShape[(size_t) point] = (point % 2 == 0) ? 1.0f : -1.0f;
for (int index = 0; index < kNumLfos; ++index)
engine.setLfoShapeData (index, defaultShape, 16);
const juce::ValueTree tree = state.getChildWithName ("LFOSHAPES"); const juce::ValueTree tree = state.getChildWithName ("LFOSHAPES");
if (! tree.isValid()) if (! tree.isValid())
return; return;
std::array<bool, kNumLfos> restored {};
for (const auto& lfo : tree) for (const auto& lfo : tree)
{ {
if (! lfo.hasType ("LFO")) if (! lfo.hasType ("LFO"))
continue; continue;
const int index = juce::jlimit (0, kNumLfos - 1, (int) lfo.getProperty ("index", 0)); const double savedIndex = (double) lfo.getProperty ("index", -1);
const int steps = (int) lfo.getProperty ("steps", 16); if (! std::isfinite (savedIndex) || savedIndex < 0.0 || savedIndex >= kNumLfos
|| std::floor (savedIndex) != savedIndex)
continue;
const int index = (int) savedIndex;
if (restored[(size_t) index])
continue;
const double savedSteps = (double) lfo.getProperty ("steps", 16);
const int steps = std::isfinite (savedSteps)
? (int) juce::jlimit (2.0, (double) LFO::kShapePoints, savedSteps) : 16;
juce::var shape = lfo.getProperty ("data");
if (shape.isString())
{
const auto text = shape.toString();
if (text.length() > 8192)
continue;
shape = juce::JSON::parse (text);
}
const auto* arr = shape.getArray();
if (arr == nullptr || arr->isEmpty())
continue;
const int numPoints = juce::jmin (LFO::kShapePoints, arr->size());
std::vector<float> data; std::vector<float> data;
if (auto* arr = lfo.getProperty ("data").getArray()) data.reserve ((size_t) numPoints);
for (const auto& vv : *arr) for (int point = 0; point < numPoints; ++point)
data.push_back ((float) vv); {
const auto& savedValue = arr->getReference (point);
const double value = (savedValue.isDouble() || savedValue.isInt() || savedValue.isInt64())
? (double) savedValue : 0.0;
data.push_back (std::isfinite (value) ? (float) juce::jlimit (-1.0, 1.0, value) : 0.0f);
}
engine.setLfoShapeData (index, data, steps); engine.setLfoShapeData (index, data, steps);
restored[(size_t) index] = true;
} }
} }
+4 -5
View File
@@ -1,16 +1,16 @@
#pragma once #pragma once
#include <JuceHeader.h> #include <JuceHeader.h>
#include <atomic>
#include "Params.h" #include "Params.h"
#include "Engine.h" #include "Engine.h"
#include "RAVEButton.h"
namespace serum namespace serum
{ {
// =========================================================================== // ===========================================================================
// SerumAlt — a wavetable synthesiser (VST3 / AU). Hosts the APVTS, the audio // SerumAlt — a wavetable synthesiser (VST3 / AU). Hosts the APVTS, the audio
// engine, preset management and the RAVE controller. // engine, preset management and the RAVE toggle.
// =========================================================================== // ===========================================================================
class SerumAltAudioProcessor : public juce::AudioProcessor class SerumAltAudioProcessor : public juce::AudioProcessor
{ {
@@ -52,15 +52,14 @@ public:
void loadFactoryPreset (int index); void loadFactoryPreset (int index);
int getNumFactoryPresets() const; int getNumFactoryPresets() const;
// Public DSP state (read/write from the GUI thread). // Public control state (hold the engine control lock for matrix/macros/LFOs).
juce::AudioProcessorValueTreeState parameters; juce::AudioProcessorValueTreeState parameters;
Engine engine; Engine engine;
RaveController rave;
static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout(); static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
private: private:
int currentProgram = 0; std::atomic<int> currentProgram { 0 };
void restoreLfoShapesFromState (const juce::ValueTree& state); void restoreLfoShapesFromState (const juce::ValueTree& state);
void saveLfoShapesToState (juce::ValueTree& state) const; void saveLfoShapesToState (juce::ValueTree& state) const;
+22 -75
View File
@@ -1,90 +1,37 @@
#include "RAVEButton.h" #include "RAVEButton.h"
#include "SynthVoice.h"
#include "FXProcessor.h"
namespace serum namespace serum
{ {
void RaveController::resetSnapshot() void RaveController::apply (RenderContext& context, FxSlotParams* slots, int numSlots) noexcept
{ {
snapshot.clear(); // Boost the static rendering parameters.
enabled = false; context.oscA.unison = juce::jlimit (8, kMaxUnison, context.oscA.unison);
} context.oscB.unison = juce::jlimit (8, kMaxUnison, context.oscB.unison);
context.oscA.spread = 1.0f;
context.oscB.spread = 1.0f;
context.oscA.detune = 1.0f;
context.oscB.detune = 0.7f;
void RaveController::snapshotParam (const juce::String& id, juce::AudioProcessorValueTreeState& apvts) // Apply drive boosts.
{ context.filters.f1Drive = 0.6f;
if (auto* p = apvts.getParameter (id)) context.filters.f2Drive = 0.6f;
snapshot.emplace_back (id, p->getValue());
}
void RaveController::setParam (const juce::String& id, float value, juce::AudioProcessorValueTreeState& apvts) // Boost FX-specific params (Hyper intensity / Reverb mix).
{ if (slots == nullptr)
if (auto* p = apvts.getParameter (id))
p->setValueNotifyingHost (juce::jlimit (0.0f, 1.0f, value));
}
void RaveController::setEnabled (bool shouldEnable, juce::AudioProcessorValueTreeState& apvts)
{
if (shouldEnable == enabled)
return; return;
for (int i = 0; i < juce::jlimit (0, kNumFxSlots, numSlots); ++i)
if (shouldEnable)
{ {
snapshot.clear(); auto& slot = slots[i];
if (slot.type == (int) FxType::Hyper)
// Snapshot the static "boost" parameters. slot.p[0] = 1.0f; // OTT intensity 100%
const char* staticParams[] = else if (slot.type == (int) FxType::Reverb)
{ {
ids::oscAUnison, ids::oscBUnison, const float mix = std::isfinite (slot.mix) ? slot.mix : 0.0f;
ids::oscASpread, ids::oscBSpread, slot.mix = juce::jlimit (0.0f, 1.0f, mix + 0.4f); // reverb send +6dB-ish
ids::oscADetune, ids::oscBDetune,
ids::f1Drive, ids::f2Drive
};
for (auto id : staticParams)
snapshotParam (id, apvts);
// Snapshot + boost FX-specific params (Hyper intensity / Reverb mix).
const char* fxMixIds[kNumFxSlots] = { ids::fx1Mix, ids::fx2Mix, ids::fx3Mix, ids::fx4Mix,
ids::fx5Mix, ids::fx6Mix, ids::fx7Mix, ids::fx8Mix };
const char* fxP1Ids[kNumFxSlots] = { ids::fx1P1, ids::fx2P1, ids::fx3P1, ids::fx4P1,
ids::fx5P1, ids::fx6P1, ids::fx7P1, ids::fx8P1 };
const char* fxTypeIds[kNumFxSlots] = { ids::fx1Type, ids::fx2Type, ids::fx3Type, ids::fx4Type,
ids::fx5Type, ids::fx6Type, ids::fx7Type, ids::fx8Type };
for (int i = 0; i < kNumFxSlots; ++i)
{
const auto* typeParam = apvts.getParameter (fxTypeIds[i]);
const int type = typeParam ? (int) (typeParam->getValue() * (int) FxType::Count) : 0;
if (type == (int) FxType::Hyper)
{
snapshotParam (fxP1Ids[i], apvts);
setParam (fxP1Ids[i], 1.0f, apvts); // OTT intensity 100%
} }
else if (type == (int) FxType::Reverb)
{
snapshotParam (fxMixIds[i], apvts);
const float current = apvts.getParameter (fxMixIds[i])->getValue();
setParam (fxMixIds[i], current + 0.4f, apvts); // reverb send +6dB-ish
}
}
// Apply boosts.
setParam (ids::oscAUnison, 8.0f / 16.0f, apvts);
setParam (ids::oscBUnison, 8.0f / 16.0f, apvts);
setParam (ids::oscASpread, 1.0f, apvts);
setParam (ids::oscBSpread, 1.0f, apvts);
setParam (ids::oscADetune, 1.0f, apvts);
setParam (ids::oscBDetune, 0.7f, apvts);
setParam (ids::f1Drive, 0.6f, apvts);
setParam (ids::f2Drive, 0.6f, apvts);
enabled = true;
}
else
{
for (const auto& entry : snapshot)
setParam (entry.first, entry.second, apvts);
snapshot.clear();
enabled = false;
} }
} }
+7 -14
View File
@@ -1,29 +1,22 @@
#pragma once #pragma once
#include <JuceHeader.h> #include <JuceHeader.h>
#include "Params.h"
namespace serum namespace serum
{ {
struct RenderContext;
struct FxSlotParams;
// =========================================================================== // ===========================================================================
// RAVE — one-shot "make it huge" control. Toggling on snapshots the current // RAVE — non-destructive "make it huge" control. While enabled, rendering
// values of the affected parameters and pushes unison, width, drive, OTT and // boosts unison, width, drive, OTT and reverb in the current block's snapshots;
// reverb to their boosted settings; toggling off restores the snapshot. // the underlying parameters remain unchanged when toggling on or off.
// =========================================================================== // ===========================================================================
class RaveController class RaveController
{ {
public: public:
void setEnabled (bool enabled, juce::AudioProcessorValueTreeState& apvts); static void apply (RenderContext& context, FxSlotParams* slots, int numSlots) noexcept;
bool isEnabled() const noexcept { return enabled; }
void resetSnapshot();
private:
bool enabled = false;
std::vector<std::pair<juce::String, float>> snapshot;
void snapshotParam (const juce::String& id, juce::AudioProcessorValueTreeState& apvts);
void setParam (const juce::String& id, float value, juce::AudioProcessorValueTreeState& apvts);
}; };
} // namespace serum } // namespace serum