Merge pull request #803 from jpcima/sorted-events

Ordered event processing for VST
This commit is contained in:
JP Cimalando 2021-04-09 17:38:26 +02:00 committed by GitHub
commit 4ecfac1dce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 408 additions and 148 deletions

View file

@ -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)

View file

@ -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 <algorithm>
#include <cstdio>
#include <cassert>
void OrderedEventProcessor::initializeEventProcessor(const Vst::ProcessSetup& setup, int32 paramCount, int32 subdivSize)
{
paramCount_ = paramCount;
subdivSize_ = subdivSize;
queues_.reset(new Cell<QueueStatus>[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<QueueStatus>* head = nullptr;
Cell<QueueStatus>* 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<QueueStatus>* 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<QueueStatus>* head = queueList_;
Cell<QueueStatus>* queues = queues_.get();
const int32 paramCount = paramCount_;
ParameterAndValue* pointsBySample = pointsBySample_.get();
uint32* numPointsBySample = numPointsBySample_.get();
std::fill_n(numPointsBySample, lastOffset - firstOffset + 1, 0);
Cell<QueueStatus>* cur = head;
Cell<QueueStatus>* 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<QueueStatus>* queues = queues_.get();
for (Cell<QueueStatus>* 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;
}

View file

@ -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 <memory>
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 <class T> struct Cell {
Cell<T>* 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<Cell<QueueStatus>[]> queues_;
Cell<QueueStatus>* queueList_ = nullptr;
struct ParameterAndValue {
Vst::ParamID id {};
Vst::ParamValue value {};
};
std::unique_ptr<ParameterAndValue[]> pointsBySample_;
std::unique_ptr<uint32[]> numPointsBySample_;
int32 eventIndex_ = 0;
int32 eventCount_ = 0;
bool haveCurrentEvent_ = false;
Vst::Event currentEvent_ {};
};

View file

@ -25,6 +25,7 @@ enum {
kPidCC0,
kPidCCLast = kPidCC0 + sfz::config::numCCs - 1,
/* Reserved */
kNumParameters,
};
struct SfizzRange {

View file

@ -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<SpinMutex> 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<SpinMutex> 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<int32>(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<int32>(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<int32>(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<int32>(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<int32>(range.denormalize(value));
_state.numVoices = data;
if (writeWorkerMessage(kMsgIdSetNumVoices, &data, sizeof(data)))
_semaToWorker.post();
}
break;
case kPidOversampling:
{
int32 data = static_cast<int32>(range.denormalize(value));
_state.oversamplingLog2 = data;
if (writeWorkerMessage(kMsgIdSetOversampling, &data, sizeof(data)))
_semaToWorker.post();
}
break;
case kPidPreloadSize:
{
int32 data = static_cast<int32>(range.denormalize(value));
_state.preloadSize = data;
if (writeWorkerMessage(kMsgIdSetPreloadSize, &data, sizeof(data)))
_semaToWorker.post();
}
break;
case kPidScalaRootKey:
_state.scalaRootKey = static_cast<int32>(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<int32>(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<int>(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;
}
}
}

View file

@ -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<uint8_t[]> _oscTemp;