diff --git a/plugins/vst/CMakeLists.txt b/plugins/vst/CMakeLists.txt index 147139fe..8eb8e29c 100644 --- a/plugins/vst/CMakeLists.txt +++ b/plugins/vst/CMakeLists.txt @@ -38,6 +38,8 @@ add_library(sfizz-vst3-core STATIC SfizzVstUpdates.h SfizzVstUpdates.cpp SfizzVstIDs.h + OrderedEventProcessor.h + OrderedEventProcessor.cpp IdleUpdateHandler.h X11RunLoop.h X11RunLoop.cpp) diff --git a/plugins/vst/OrderedEventProcessor.cpp b/plugins/vst/OrderedEventProcessor.cpp new file mode 100644 index 00000000..548f079b --- /dev/null +++ b/plugins/vst/OrderedEventProcessor.cpp @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "OrderedEventProcessor.h" +#include +#include +#include + +void OrderedEventProcessor::initializeEventProcessor(const Vst::ProcessSetup& setup, int32 paramCount, int32 subdivSize) +{ + paramCount_ = paramCount; + subdivSize_ = subdivSize; + queues_.reset(new Cell[paramCount]); + pointsBySample_.reset(new ParameterAndValue[paramCount * subdivSize]); + numPointsBySample_.reset(new uint32[subdivSize]); +} + +void OrderedEventProcessor::processUnorderedEvents(int32 numSamples, Vst::IParameterChanges* pcs, Vst::IEventList* evs) +{ + int32 sampleIndex = 0; + int32 subdivNumber = 0; + const int32 subdivSize = subdivSize_; + + startProcessing(evs, pcs); + + while (sampleIndex < numSamples || subdivNumber == 0) { + int32 subdivCurrentSize = std::min(numSamples - sampleIndex, subdivSize); + processSubdiv(evs, sampleIndex, sampleIndex + subdivCurrentSize - 1); + sampleIndex += subdivCurrentSize; + ++subdivNumber; + } + + playRemainder(std::max(int32(0), numSamples - 1), evs); +} + +void OrderedEventProcessor::startProcessing(Vst::IEventList* evs, Vst::IParameterChanges* pcs) +{ + // collect the parameter queues which have values on them + // push them onto a work list + Cell* head = nullptr; + Cell* queues = queues_.get(); + + if (pcs) { + const int32 count = pcs->getParameterCount(); + for (int32 index = count; index-- > 0; ) { + Vst::IParamValueQueue* vq = pcs->getParameterData(index); + if (vq) { + // Note: expectation that hosts does not send more than one + // queue for one parameter + Vst::ParamID id = vq->getParameterId(); + Cell* cell = &queues[id]; + cell->next = head; + cell->value.queue = vq; + cell->value.pointIndex = 0; + cell->value.pointCount = vq->getPointCount(); + head = cell; + } + } + } + + queueList_ = head; + + // position ourselves to the start of the event list + eventIndex_ = 0; + eventCount_ = evs ? evs->getEventCount() : 0; + haveCurrentEvent_ = eventCount_ > 0 && + evs->getEvent(eventIndex_, currentEvent_) == kResultTrue; +} + +void OrderedEventProcessor::processSubdiv(Vst::IEventList* evs, int32 firstOffset, int32 lastOffset) +{ + sortSubdiv(firstOffset, lastOffset); + playSubdiv(evs, firstOffset, lastOffset); +} + +void OrderedEventProcessor::sortSubdiv(int32 firstOffset, int32 lastOffset) +{ + // this collects parameter changes from the subdivision that goes from + // first offset to last offset, included. + + // these parameter changes are on a array of lists. + // this 2D structure is backed by a contiguous array dimensioned for + // the worst case. + + // Example: + // pointsBySample (column-major →) | numPointsBySample[sample] + // ==================================================================== + // sample 0 [P1] [P2] [ ] [ ] | 2 + // sample 1 [P3] [ ] [ ] [ ] | 1 + // sample 2 [ ] [ ] [ ] [ ] | 0 + // sample 3 [P1] [P2] [P3] [P4] | 4 + + Cell* head = queueList_; + Cell* queues = queues_.get(); + const int32 paramCount = paramCount_; + ParameterAndValue* pointsBySample = pointsBySample_.get(); + uint32* numPointsBySample = numPointsBySample_.get(); + + std::fill_n(numPointsBySample, lastOffset - firstOffset + 1, 0); + + Cell* cur = head; + Cell* prev = nullptr; + + while (cur) { + Vst::IParamValueQueue* vq = cur->value.queue; + const Vst::ParamID id = Vst::ParamID(std::distance(queues, cur)); + + int32 pointIndex = cur->value.pointIndex; + int32 pointCount = cur->value.pointCount; + + int32 previousOffset = firstOffset; + while (pointIndex < pointCount) { + int32 sampleOffset; + Vst::ParamValue value; + + if (vq->getPoint(pointIndex, sampleOffset, value) != kResultTrue) { + ++pointIndex; + continue; + } + + if (sampleOffset > lastOffset) + break; + + // ensure that offsets never go back + if (sampleOffset < previousOffset) + sampleOffset = previousOffset; + + int32 listSize = numPointsBySample[sampleOffset - firstOffset]; + ParameterAndValue* listItems = &pointsBySample[paramCount * (sampleOffset - firstOffset)]; + + bool isDuplicatePoint = listSize > 0 && listItems[listSize - 1].id == id; + if (isDuplicatePoint) { + // protect against duplicates, which might overflow the list + listItems[listSize - 1].value = value; + } + else { + assert(listSize < paramCount); + listItems[listSize].id = id; + listItems[listSize].value = value; + numPointsBySample[sampleOffset - firstOffset] = ++listSize; + } + + previousOffset = sampleOffset; + ++pointIndex; + } + + if (pointIndex < pointCount) { + cur->value.pointIndex = pointIndex; + prev = cur; + } + else { + // if no more points, take this queue off the list + if (prev) + prev->next = cur->next; + else { + head = cur->next; + prev = nullptr; + } + } + cur = cur->next; + } + + queueList_ = head; +} + +void OrderedEventProcessor::playSubdiv(Vst::IEventList* evs, int32 firstOffset, int32 lastOffset) +{ + // go over the points in sample order, and play them intermittently with the + // event queue according to the sample offsets. + + const int32 paramCount = paramCount_; + const ParameterAndValue* pointsBySample = pointsBySample_.get(); + const uint32* numPointsBySample = numPointsBySample_.get(); + + for (int32 sampleOffset = firstOffset; sampleOffset <= lastOffset; ++sampleOffset) { + const int32 listSize = numPointsBySample[sampleOffset - firstOffset]; + const ParameterAndValue* listItems = &pointsBySample[paramCount * (sampleOffset - firstOffset)]; + playEventsUpTo(evs, sampleOffset); + for (int32 i = 0; i < listSize; ++i) { + const ParameterAndValue item = listItems[i]; + playOrderedParameter(sampleOffset, item.id, item.value); + } + } +} + +void OrderedEventProcessor::playRemainder(int32 sampleOffset, Vst::IEventList* evs) +{ + // play any remaining events and parameters in arbitrary order, + // disregarding their respective offsets + + int32 eventIndex = eventIndex_; + int32 eventCount = eventCount_; + bool haveCurrentEvent = haveCurrentEvent_; + while (haveCurrentEvent) { + currentEvent_.sampleOffset = std::min(sampleOffset, currentEvent_.sampleOffset); + playOrderedEvent(currentEvent_); + haveCurrentEvent = false; + while (!haveCurrentEvent && ++eventIndex < eventCount) + haveCurrentEvent = evs->getEvent(eventIndex, currentEvent_) == kResultTrue; + } + + Cell* queues = queues_.get(); + for (Cell* cur = queueList_; cur; cur = cur->next) { + for (int32 i = cur->value.pointIndex, n = cur->value.pointCount; i < n; ++i) { + Vst::IParamValueQueue* vq = cur->value.queue; + const Vst::ParamID id = Vst::ParamID(std::distance(queues, cur)); + int32 unusedSampleOffset; + Vst::ParamValue value; + if (vq->getPoint(i, unusedSampleOffset, value) == kResultTrue) + playOrderedParameter(sampleOffset, id, value); + } + } +} + +void OrderedEventProcessor::playEventsUpTo(Vst::IEventList* evs, int32 sampleOffset) +{ + int32 index = eventIndex_; + int32 count = eventCount_; + bool haveCurrentEvent = haveCurrentEvent_; + + while (haveCurrentEvent && currentEvent_.sampleOffset <= sampleOffset) { + playOrderedEvent(currentEvent_); + haveCurrentEvent = false; + while (!haveCurrentEvent && ++index < count) + haveCurrentEvent = evs->getEvent(index, currentEvent_) == kResultTrue; + } + + eventIndex_ = index; + haveCurrentEvent_ = haveCurrentEvent; +} diff --git a/plugins/vst/OrderedEventProcessor.h b/plugins/vst/OrderedEventProcessor.h new file mode 100644 index 00000000..b13b3fa8 --- /dev/null +++ b/plugins/vst/OrderedEventProcessor.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "pluginterfaces/vst/ivstevents.h" +#include "pluginterfaces/vst/ivstparameterchanges.h" +#include "pluginterfaces/vst/ivstaudioprocessor.h" +#include + +using namespace Steinberg; + +class OrderedEventProcessor { +public: + virtual ~OrderedEventProcessor() {} + + void initializeEventProcessor(const Vst::ProcessSetup& setup, int32 paramCount, int32 subdivSize = 128); + void processUnorderedEvents(int32 numSamples, Vst::IParameterChanges* pcs, Vst::IEventList* evs); + +protected: + virtual void playOrderedParameter(int32 sampleOffset, Vst::ParamID id, Vst::ParamValue value) = 0; + virtual void playOrderedEvent(const Vst::Event& event) = 0; + +private: + void startProcessing(Vst::IEventList* evs, Vst::IParameterChanges* pcs); + void processSubdiv(Vst::IEventList* evs, int32 firstOffset, int32 lastOffset); + + void sortSubdiv(int32 firstOffset, int32 lastOffset); + void playSubdiv(Vst::IEventList* evs, int32 firstOffset, int32 lastOffset); + + void playRemainder(int32 sampleOffset, Vst::IEventList* evs); + + void playEventsUpTo(Vst::IEventList* evs, int32 sampleOffset); + +private: + template struct Cell { + Cell* next = nullptr; + T value {}; + }; + + struct QueueStatus { + Vst::IParamValueQueue* queue = nullptr; + int32 pointIndex = 0; + int32 pointCount = 0; + }; + + int32 paramCount_ = 0; + int32 subdivSize_ = 0; + std::unique_ptr[]> queues_; + Cell* queueList_ = nullptr; + + struct ParameterAndValue { + Vst::ParamID id {}; + Vst::ParamValue value {}; + }; + + std::unique_ptr pointsBySample_; + std::unique_ptr numPointsBySample_; + + int32 eventIndex_ = 0; + int32 eventCount_ = 0; + bool haveCurrentEvent_ = false; + Vst::Event currentEvent_ {}; +}; diff --git a/plugins/vst/SfizzVstParameters.h b/plugins/vst/SfizzVstParameters.h index e9367c8d..9574d463 100644 --- a/plugins/vst/SfizzVstParameters.h +++ b/plugins/vst/SfizzVstParameters.h @@ -25,6 +25,7 @@ enum { kPidCC0, kPidCCLast = kPidCC0 + sfz::config::numCCs - 1, /* Reserved */ + kNumParameters, }; struct SfizzRange { diff --git a/plugins/vst/SfizzVstProcessor.cpp b/plugins/vst/SfizzVstProcessor.cpp index 590445e0..14f2f704 100644 --- a/plugins/vst/SfizzVstProcessor.cpp +++ b/plugins/vst/SfizzVstProcessor.cpp @@ -227,6 +227,7 @@ tresult PLUGIN_API SfizzVstProcessor::setActive(TBool state) if (state) { synth->setSampleRate(processSetup.sampleRate); synth->setSamplesPerBlock(processSetup.maxSamplesPerBlock); + initializeEventProcessor(processSetup, kNumParameters); startBackgroundWork(); } else { stopBackgroundWork(); @@ -241,16 +242,22 @@ tresult PLUGIN_API SfizzVstProcessor::process(Vst::ProcessData& data) { sfz::Sfizz& synth = *_synth; + std::unique_lock lock(_processMutex, std::defer_lock); + if (data.processMode == Vst::kOffline) + lock.lock(); + else + lock.try_lock(); + if (data.processContext) updateTimeInfo(*data.processContext); - if (Vst::IParameterChanges* pc = data.inputParameterChanges) - processParameterChanges(*pc); + const uint32 numFrames = data.numSamples; + _canPerformEventsAndParameters = lock.owns_lock(); + processUnorderedEvents(numFrames, data.inputParameterChanges, data.inputEvents); if (data.numOutputs < 1) // flush mode return kResultTrue; - uint32 numFrames = data.numSamples; constexpr uint32 numChannels = 2; float* outputs[numChannels]; @@ -259,8 +266,6 @@ tresult PLUGIN_API SfizzVstProcessor::process(Vst::ProcessData& data) for (unsigned c = 0; c < numChannels; ++c) outputs[c] = data.outputs[0].channelBuffers32[c]; - std::unique_lock lock(_processMutex, std::try_to_lock); - if (!lock.owns_lock()) { for (unsigned c = 0; c < numChannels; ++c) std::memset(outputs[c], 0, numFrames * sizeof(float)); @@ -275,12 +280,6 @@ tresult PLUGIN_API SfizzVstProcessor::process(Vst::ProcessData& data) processMessagesFromUi(); - if (Vst::IParameterChanges* pc = data.inputParameterChanges) - processControllerChanges(*pc); - - if (Vst::IEventList* events = data.inputEvents) - processEvents(*events); - synth.setVolume(_state.volume); synth.setScalaRootKey(_state.scalaRootKey); synth.setTuningFrequency(_state.tuningFrequency); @@ -336,153 +335,105 @@ void SfizzVstProcessor::updateTimeInfo(const Vst::ProcessContext& context) synth.playbackState(0, (context.state & context.kPlaying) != 0); } -void SfizzVstProcessor::processParameterChanges(Vst::IParameterChanges& pc) +void SfizzVstProcessor::playOrderedParameter(int32 sampleOffset, Vst::ParamID id, Vst::ParamValue value) { - uint32 paramCount = pc.getParameterCount(); + if (!_canPerformEventsAndParameters) + return; - for (uint32 paramIndex = 0; paramIndex < paramCount; ++paramIndex) { - Vst::IParamValueQueue* vq = pc.getParameterData(paramIndex); - if (!vq) - continue; + sfz::Sfizz& synth = *_synth; + const SfizzRange range = SfizzRange::getForParameter(id); - const Vst::ParamID id = vq->getParameterId(); - const SfizzRange range = SfizzRange::getForParameter(id); - - uint32 pointCount = vq->getPointCount(); - int32 sampleOffset; - Vst::ParamValue value; - - switch (id) { - case kPidVolume: - if (pointCount > 0 && vq->getPoint(pointCount - 1, sampleOffset, value) == kResultTrue) - _state.volume = range.denormalize(value); - break; - case kPidNumVoices: - if (pointCount > 0 && vq->getPoint(pointCount - 1, sampleOffset, value) == kResultTrue) { - int32 data = static_cast(range.denormalize(value)); - _state.numVoices = data; - if (writeWorkerMessage(kMsgIdSetNumVoices, &data, sizeof(data))) - _semaToWorker.post(); - } - break; - case kPidOversampling: - if (pointCount > 0 && vq->getPoint(pointCount - 1, sampleOffset, value) == kResultTrue) { - int32 data = static_cast(range.denormalize(value)); - _state.oversamplingLog2 = data; - if (writeWorkerMessage(kMsgIdSetOversampling, &data, sizeof(data))) - _semaToWorker.post(); - } - break; - case kPidPreloadSize: - if (pointCount > 0 && vq->getPoint(pointCount - 1, sampleOffset, value) == kResultTrue) { - int32 data = static_cast(range.denormalize(value)); - _state.preloadSize = data; - if (writeWorkerMessage(kMsgIdSetPreloadSize, &data, sizeof(data))) - _semaToWorker.post(); - } - break; - case kPidScalaRootKey: - if (pointCount > 0 && vq->getPoint(pointCount - 1, sampleOffset, value) == kResultTrue) - _state.scalaRootKey = static_cast(range.denormalize(value)); - break; - case kPidTuningFrequency: - if (pointCount > 0 && vq->getPoint(pointCount - 1, sampleOffset, value) == kResultTrue) - _state.tuningFrequency = range.denormalize(value); - break; - case kPidStretchedTuning: - if (pointCount > 0 && vq->getPoint(pointCount - 1, sampleOffset, value) == kResultTrue) - _state.stretchedTuning = range.denormalize(value); - break; + switch (id) { + case kPidVolume: + _state.volume = range.denormalize(value); + break; + case kPidNumVoices: + { + int32 data = static_cast(range.denormalize(value)); + _state.numVoices = data; + if (writeWorkerMessage(kMsgIdSetNumVoices, &data, sizeof(data))) + _semaToWorker.post(); } + break; + case kPidOversampling: + { + int32 data = static_cast(range.denormalize(value)); + _state.oversamplingLog2 = data; + if (writeWorkerMessage(kMsgIdSetOversampling, &data, sizeof(data))) + _semaToWorker.post(); + } + break; + case kPidPreloadSize: + { + int32 data = static_cast(range.denormalize(value)); + _state.preloadSize = data; + if (writeWorkerMessage(kMsgIdSetPreloadSize, &data, sizeof(data))) + _semaToWorker.post(); + } + break; + case kPidScalaRootKey: + _state.scalaRootKey = static_cast(range.denormalize(value)); + break; + case kPidTuningFrequency: + _state.tuningFrequency = range.denormalize(value); + break; + case kPidStretchedTuning: + _state.stretchedTuning = range.denormalize(value); + break; + case kPidAftertouch: + synth.aftertouch(sampleOffset, fastRound(value * 127.0)); + break; + case kPidPitchBend: + synth.pitchWheel(sampleOffset, fastRound(value * 16383) - 8192); + break; + default: + if (id >= kPidCC0 && id <= kPidCCLast) { + int32 ccNumber = static_cast(id - kPidCC0); + synth.hdcc(sampleOffset, ccNumber, value); + _state.controllers[ccNumber] = value; + } + break; } } -void SfizzVstProcessor::processControllerChanges(Vst::IParameterChanges& pc) +void SfizzVstProcessor::playOrderedEvent(const Vst::Event& event) { + if (!_canPerformEventsAndParameters) + return; + sfz::Sfizz& synth = *_synth; - uint32 paramCount = pc.getParameterCount(); + const int32 sampleOffset = event.sampleOffset; - for (uint32 paramIndex = 0; paramIndex < paramCount; ++paramIndex) { - Vst::IParamValueQueue* vq = pc.getParameterData(paramIndex); - if (!vq) - continue; - - Vst::ParamID id = vq->getParameterId(); - uint32 pointCount = vq->getPointCount(); - int32 sampleOffset; - Vst::ParamValue value; - - switch (id) { - default: - if (id >= kPidCC0 && id <= kPidCCLast) { - auto ccNumber = static_cast(id - kPidCC0); - for (uint32 pointIndex = 0; pointIndex < pointCount; ++pointIndex) { - if (vq->getPoint(pointIndex, sampleOffset, value) == kResultTrue) { - synth.hdcc(sampleOffset, ccNumber, value); - _state.controllers[ccNumber] = value; - } - } - } + switch (event.type) { + case Vst::Event::kNoteOnEvent: { + int pitch = event.noteOn.pitch; + if (pitch < 0 || pitch >= 128) break; - - case kPidAftertouch: - for (uint32 pointIndex = 0; pointIndex < pointCount; ++pointIndex) { - if (vq->getPoint(pointIndex, sampleOffset, value) == kResultTrue) - synth.aftertouch(sampleOffset, fastRound(value * 127.0)); - } - break; - - case kPidPitchBend: - for (uint32 pointIndex = 0; pointIndex < pointCount; ++pointIndex) { - if (vq->getPoint(pointIndex, sampleOffset, value) == kResultTrue) - synth.pitchWheel(sampleOffset, fastRound(value * 16383) - 8192); - } - break; - } - } -} - -void SfizzVstProcessor::processEvents(Vst::IEventList& events) -{ - sfz::Sfizz& synth = *_synth; - uint32 numEvents = events.getEventCount(); - - for (uint32 i = 0; i < numEvents; i++) { - Vst::Event e; - if (events.getEvent(i, e) != kResultTrue) - continue; - - switch (e.type) { - case Vst::Event::kNoteOnEvent: { - int pitch = e.noteOn.pitch; - if (pitch < 0 || pitch >= 128) - break; - if (e.noteOn.velocity <= 0.0f) { - synth.noteOff(e.sampleOffset, pitch, 0); - _noteEventsCurrentCycle[pitch] = 0.0f; - } - else { - synth.noteOn(e.sampleOffset, pitch, convertVelocityFromFloat(e.noteOn.velocity)); - _noteEventsCurrentCycle[pitch] = e.noteOn.velocity; - } - break; - } - case Vst::Event::kNoteOffEvent: { - int pitch = e.noteOn.pitch; - if (pitch < 0 || pitch >= 128) - break; - synth.noteOff(e.sampleOffset, pitch, convertVelocityFromFloat(e.noteOff.velocity)); + if (event.noteOn.velocity <= 0.0f) { + synth.noteOff(sampleOffset, pitch, 0); _noteEventsCurrentCycle[pitch] = 0.0f; + } + else { + synth.noteOn(sampleOffset, pitch, convertVelocityFromFloat(event.noteOn.velocity)); + _noteEventsCurrentCycle[pitch] = event.noteOn.velocity; + } + break; + } + case Vst::Event::kNoteOffEvent: { + int pitch = event.noteOn.pitch; + if (pitch < 0 || pitch >= 128) break; - } - case Vst::Event::kPolyPressureEvent: { - int pitch = e.polyPressure.pitch; - if (pitch < 0 || pitch >= 128) - break; - synth.polyAftertouch(e.sampleOffset, pitch, convertVelocityFromFloat(e.polyPressure.pressure)); + synth.noteOff(sampleOffset, pitch, convertVelocityFromFloat(event.noteOff.velocity)); + _noteEventsCurrentCycle[pitch] = 0.0f; + break; + } + case Vst::Event::kPolyPressureEvent: { + int pitch = event.polyPressure.pitch; + if (pitch < 0 || pitch >= 128) break; - } - } + synth.polyAftertouch(sampleOffset, pitch, convertVelocityFromFloat(event.polyPressure.pressure)); + break; + } } } diff --git a/plugins/vst/SfizzVstProcessor.h b/plugins/vst/SfizzVstProcessor.h index ad764ff7..30c5e607 100644 --- a/plugins/vst/SfizzVstProcessor.h +++ b/plugins/vst/SfizzVstProcessor.h @@ -6,6 +6,7 @@ #pragma once #include "SfizzVstState.h" +#include "OrderedEventProcessor.h" #include "sfizz/RTSemaphore.h" #include "ring_buffer/ring_buffer.h" #include "public.sdk/source/vst/vstaudioeffect.h" @@ -18,7 +19,8 @@ using namespace Steinberg; -class SfizzVstProcessor : public Vst::AudioEffect { +class SfizzVstProcessor : public Vst::AudioEffect, + public OrderedEventProcessor { public: SfizzVstProcessor(); ~SfizzVstProcessor(); @@ -35,9 +37,11 @@ public: tresult PLUGIN_API canProcessSampleSize(int32 symbolicSampleSize) override; tresult PLUGIN_API setActive(TBool state) override; tresult PLUGIN_API process(Vst::ProcessData& data) override; - void processParameterChanges(Vst::IParameterChanges& pc); - void processControllerChanges(Vst::IParameterChanges& pc); - void processEvents(Vst::IEventList& events); + + // OrderedEventProcessor + void playOrderedParameter(int32 sampleOffset, Vst::ParamID id, Vst::ParamValue value) override; + void playOrderedEvent(const Vst::Event& event) override; + void processMessagesFromUi(); static int convertVelocityFromFloat(float x); @@ -57,6 +61,9 @@ private: SfizzVstState _state; float _currentStretchedTuning = 0; + // whether allowed to perform events (owns the processing lock) + bool _canPerformEventsAndParameters {}; + // client sfz::ClientPtr _client; std::unique_ptr _oscTemp;