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
+27 -12
View File
@@ -1,6 +1,8 @@
#pragma once
#include <JuceHeader.h>
#include <string_view>
#include <unordered_map>
#include "Params.h"
#include "Wavetable.h"
#include "SynthVoice.h"
@@ -19,7 +21,8 @@ namespace serum
class Engine
{
public:
void prepare (double sampleRate, int blockSize);
Engine();
void prepare (double sampleRate, int blockSize, juce::AudioProcessorValueTreeState& apvts);
void reset();
void processBlock (juce::AudioBuffer<float>& buffer,
@@ -32,22 +35,31 @@ public:
void noteOff (int noteNumber);
void allNotesOff();
// Modulation / DSP accessors (read-only for the GUI).
ModulationMatrix& getMatrix() { return matrix; }
MacroControls& getMacros() { return macros; }
WavetableLibrary& getWavetables() { return wavetables; }
const std::array<LFO, kNumLfos>& getLfos() const { return lfos; }
// Control-state accessors: hold getControlLock() while reading or editing.
const juce::CriticalSection& getControlLock() const { return controlLock; }
ModulationMatrix& getMatrix() { return matrix; }
MacroControls& getMacros() { return macros; }
const std::array<LFO, kNumLfos>& getLfos() const { return controlLfos; }
const WavetableLibrary& getWavetables() const { return wavetables; }
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:
std::array<SynthVoice, kNumVoices> voices;
std::array<LFO, kNumLfos> lfos;
std::array<LFO, kNumLfos> lfos, controlLfos;
WavetableLibrary wavetables;
FXProcessor fx;
ModulationMatrix matrix;
MacroControls macros;
ModulationMatrix matrix, audioMatrix;
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;
int blockSize = 512;
@@ -55,13 +67,16 @@ private:
float pitchBend = 0.0f;
float modWheel = 0.0f;
juce::uint64 noteCounter = 0;
std::atomic<int> activeVoiceCount { 0 };
juce::AudioBuffer<float> mixBuffer;
SynthVoice* findFreeVoice();
SynthVoice* stealVoice();
void readOscParams (juce::AudioProcessorValueTreeState& apvts, const char* prefix, OscParams& out);
void captureControls();
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