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:
+106
-34
@@ -176,7 +176,7 @@ juce::AudioProcessorValueTreeState::ParameterLayout SerumAltAudioProcessor::crea
|
||||
// ---------------------------------------------------------------------------
|
||||
void SerumAltAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
|
||||
{
|
||||
engine.prepare (sampleRate, samplesPerBlock);
|
||||
engine.prepare (sampleRate, samplesPerBlock, parameters);
|
||||
}
|
||||
|
||||
void SerumAltAudioProcessor::releaseResources()
|
||||
@@ -205,14 +205,13 @@ int SerumAltAudioProcessor::getNumPrograms()
|
||||
|
||||
int SerumAltAudioProcessor::getCurrentProgram()
|
||||
{
|
||||
return currentProgram;
|
||||
return currentProgram.load();
|
||||
}
|
||||
|
||||
void SerumAltAudioProcessor::setCurrentProgram (int index)
|
||||
{
|
||||
index = juce::jlimit (0, getNumPrograms() - 1, index);
|
||||
loadFactoryPreset (index);
|
||||
currentProgram = index;
|
||||
if (getNumPrograms() > 0)
|
||||
loadFactoryPreset (juce::jlimit (0, getNumPrograms() - 1, index));
|
||||
}
|
||||
|
||||
const juce::String SerumAltAudioProcessor::getProgramName (int index)
|
||||
@@ -238,24 +237,36 @@ void SerumAltAudioProcessor::loadFactoryPreset (int index)
|
||||
if (index < 0 || index >= (int) presets.size())
|
||||
return;
|
||||
|
||||
const juce::ScopedLock lock (engine.getControlLock());
|
||||
const FactoryPreset& preset = presets[(size_t) index];
|
||||
|
||||
// RAVE should start off for a freshly loaded preset.
|
||||
rave.resetSnapshot();
|
||||
if (auto* raveParam = parameters.getParameter (ids::rave))
|
||||
raveParam->setValueNotifyingHost (0.0f);
|
||||
const auto* uiScaleParam = parameters.getParameter (ids::uiScale);
|
||||
for (auto* param : getParameters())
|
||||
if (param != nullptr && param != uiScaleParam)
|
||||
param->setValueNotifyingHost (param->getDefaultValue());
|
||||
|
||||
for (const auto& kv : preset.params)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
{
|
||||
rave.setEnabled (enabled, parameters);
|
||||
const juce::ScopedLock lock (engine.getControlLock());
|
||||
if (auto* raveParam = parameters.getParameter (ids::rave))
|
||||
{
|
||||
raveParam->beginChangeGesture();
|
||||
raveParam->setValueNotifyingHost (enabled ? 1.0f : 0.0f);
|
||||
raveParam->endChangeGesture();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
void SerumAltAudioProcessor::setUiScaleIndex (int index)
|
||||
{
|
||||
const juce::ScopedLock lock (engine.getControlLock());
|
||||
index = juce::jlimit (0, 4, index);
|
||||
if (auto* p = parameters.getParameter (ids::uiScale))
|
||||
p->setValueNotifyingHost ((float) index / 4.0f);
|
||||
@@ -298,7 +320,15 @@ float SerumAltAudioProcessor::getUiScale() const
|
||||
// ---------------------------------------------------------------------------
|
||||
void SerumAltAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
|
||||
{
|
||||
const juce::ScopedLock lock (engine.getControlLock());
|
||||
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.getMacros().toValueTree(), nullptr);
|
||||
saveLfoShapesToState (state);
|
||||
@@ -310,39 +340,47 @@ void SerumAltAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
|
||||
|
||||
void SerumAltAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
|
||||
{
|
||||
if (data == nullptr || sizeInBytes <= 0)
|
||||
return;
|
||||
std::unique_ptr<juce::XmlElement> xml (getXmlFromBinary (data, sizeInBytes));
|
||||
if (xml == nullptr)
|
||||
if (xml == nullptr || ! xml->hasTagName ("SerumAlt"))
|
||||
return;
|
||||
|
||||
juce::ValueTree state = juce::ValueTree::fromXml (*xml);
|
||||
if (! state.isValid())
|
||||
if (! state.hasType ("SerumAlt"))
|
||||
return;
|
||||
|
||||
const juce::ScopedLock lock (engine.getControlLock());
|
||||
// A persisted RAVE toggle is rendered non-destructively, so retain it.
|
||||
parameters.replaceState (state);
|
||||
engine.getMatrix().fromValueTree (state.getChildWithName ("MODMATRIX"));
|
||||
engine.getMacros().fromValueTree (state.getChildWithName ("MACROS"));
|
||||
restoreLfoShapesFromState (state);
|
||||
|
||||
// A persisted RAVE toggle has no live snapshot, so start it off.
|
||||
rave.resetSnapshot();
|
||||
if (auto* raveParam = parameters.getParameter (ids::rave))
|
||||
raveParam->setValueNotifyingHost (0.0f);
|
||||
const double savedProgram = (double) state.getProperty ("currentProgram", 0);
|
||||
currentProgram.store (std::isfinite (savedProgram)
|
||||
? (int) juce::jlimit (0.0, (double) juce::jmax (0, getNumPrograms() - 1), savedProgram) : 0);
|
||||
}
|
||||
|
||||
void SerumAltAudioProcessor::saveLfoShapesToState (juce::ValueTree& state) const
|
||||
{
|
||||
const juce::ScopedLock lock (engine.getControlLock());
|
||||
juce::ValueTree tree ("LFOSHAPES");
|
||||
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");
|
||||
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;
|
||||
for (float vv : data)
|
||||
arr.add (vv);
|
||||
lfo.setProperty ("data", juce::var (arr), nullptr);
|
||||
for (int point = 0; point < juce::jmin (LFO::kShapePoints, (int) data.size()); ++point)
|
||||
{
|
||||
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);
|
||||
}
|
||||
state.appendChild (tree, nullptr);
|
||||
@@ -350,23 +388,57 @@ void SerumAltAudioProcessor::saveLfoShapesToState (juce::ValueTree& state) const
|
||||
|
||||
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");
|
||||
if (! tree.isValid())
|
||||
return;
|
||||
|
||||
std::array<bool, kNumLfos> restored {};
|
||||
for (const auto& lfo : tree)
|
||||
{
|
||||
if (! lfo.hasType ("LFO"))
|
||||
continue;
|
||||
const int index = juce::jlimit (0, kNumLfos - 1, (int) lfo.getProperty ("index", 0));
|
||||
const int steps = (int) lfo.getProperty ("steps", 16);
|
||||
const double savedIndex = (double) lfo.getProperty ("index", -1);
|
||||
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;
|
||||
if (auto* arr = lfo.getProperty ("data").getArray())
|
||||
for (const auto& vv : *arr)
|
||||
data.push_back ((float) vv);
|
||||
data.reserve ((size_t) numPoints);
|
||||
for (int point = 0; point < numPoints; ++point)
|
||||
{
|
||||
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);
|
||||
restored[(size_t) index] = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user