Merge pull request #393 from paulfd/polyphony-cleanups

Polyphony cleanups
This commit is contained in:
Paul Ferrand 2020-09-09 10:29:09 +02:00 committed by GitHub
commit afadb171a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 503 additions and 274 deletions

View file

@ -128,14 +128,6 @@ struct SisterVoiceRing {
*/
class SisterVoiceRingBuilder {
public:
~SisterVoiceRingBuilder() noexcept {
if (lastStartedVoice != nullptr) {
ASSERT(firstStartedVoice);
lastStartedVoice->setNextSisterVoice(firstStartedVoice);
firstStartedVoice->setPreviousSisterVoice(lastStartedVoice);
}
}
/**
* @brief Add a voice to the sister ring
*
@ -145,6 +137,9 @@ public:
if (firstStartedVoice == nullptr)
firstStartedVoice = voice;
firstStartedVoice->setPreviousSisterVoice(voice);
voice->setNextSisterVoice(firstStartedVoice);
if (lastStartedVoice != nullptr) {
voice->setPreviousSisterVoice(lastStartedVoice);
lastStartedVoice->setNextSisterVoice(voice);

View file

@ -9,6 +9,7 @@
#include "Debug.h"
#include "Macros.h"
#include "MidiState.h"
#include "TriggerEvent.h"
#include "ModifierHelpers.h"
#include "ScopedFTZ.h"
#include "StringViewHelpers.h"
@ -660,7 +661,23 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept
if (freeVoice != voices.end())
return freeVoice->get();
return {};
// Engine polyphony reached
Voice* stolenVoice = stealer.steal(absl::MakeSpan(voiceViewArray));
if (stolenVoice == nullptr)
return {};
// Never kill age 0 voices
if (stolenVoice->getAge() == 0)
return {};
auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock);
SisterVoiceRing::applyToRing(stolenVoice, [&] (Voice* v) {
renderVoiceToOutputs(*v, *tempSpan);
v->reset();
});
return stolenVoice;
}
int sfz::Synth::getNumActiveVoices(bool recompute) const noexcept
@ -882,163 +899,203 @@ void sfz::Synth::noteOff(int delay, int noteNumber, uint8_t velocity) noexcept
noteOffDispatch(delay, noteNumber, replacedVelocity);
}
bool matchReleaseRegionAndVoice(const sfz::Region& region, const sfz::Voice& voice) {
return (
!voice.isFree()
&& voice.getTriggerType() == sfz::Voice::TriggerType::NoteOn
&& region.keyRange.containsWithEnd(voice.getTriggerNumber())
&& region.velocityRange.containsWithEnd(voice.getTriggerValue())
);
void sfz::Synth::startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept
{
checkNotePolyphony(region, delay, triggerEvent);
checkRegionPolyphony(region, delay);
checkGroupPolyphony(region, delay);
checkSetPolyphony(region, delay);
Voice* selectedVoice = findFreeVoice();
if (selectedVoice == nullptr)
return;
ASSERT(selectedVoice->isFree());
selectedVoice->startVoice(region, delay, triggerEvent);
ring.addVoiceToRing(selectedVoice);
RegionSet::registerVoiceInHierarchy(region, selectedVoice);
polyphonyGroups[region->group].registerVoice(selectedVoice);
}
bool sfz::Synth::playingAttackVoice(const Region* releaseRegion) noexcept
{
const auto compatibleVoice = [releaseRegion](const Voice* v) -> bool {
const sfz::TriggerEvent& event = v->getTriggerEvent();
return (
!v->isFree()
&& event.type == sfz::TriggerEventType::NoteOn
&& releaseRegion->keyRange.containsWithEnd(event.number)
&& releaseRegion->velocityRange.containsWithEnd(event.value)
);
};
if (absl::c_find_if(voiceViewArray, compatibleVoice) == voiceViewArray.end())
return false;
else
return true;
}
void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noexcept
{
const auto randValue = randNoteDistribution(Random::randomGenerator);
SisterVoiceRingBuilder ring;
const TriggerEvent triggerEvent { TriggerEventType::NoteOff, noteNumber, velocity };
for (auto& region : noteActivationLists[noteNumber]) {
if (region->registerNoteOff(noteNumber, velocity, randValue)) {
if (region->triggerOnNote && region->trigger == SfzTrigger::release && !region->rtDead) {
// check that a voice with compatible trigger is playing
// FIXME: we're going twice over the voices, when the synth
// handles the regions completely these dispatch functions
// should be overhauled, also to include voice stealing on
// all events
const auto compatibleVoice = [region](const VoicePtr& v) -> bool {
return matchReleaseRegionAndVoice(*region, *v);
};
if (absl::c_find_if(voices, compatibleVoice) == voices.end())
continue;
}
auto voice = findFreeVoice();
if (voice == nullptr)
if (region->trigger == SfzTrigger::release && !region->rtDead && !playingAttackVoice(region))
continue;
voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOff);
ring.addVoiceToRing(voice);
RegionSet::registerVoiceInHierarchy(region, voice);
polyphonyGroups[region->group].registerVoice(voice);
startVoice(region, delay, triggerEvent, ring);
}
}
}
void sfz::Synth::checkRegionPolyphony(const Region* region, int delay) noexcept
{
tempPolyphonyArray.clear();
for (Voice* voice : voiceViewArray) {
if (voice->getRegion() == region && !voice->releasedOrFree()) {
tempPolyphonyArray.push_back(voice);
}
}
if (tempPolyphonyArray.size() >= region->polyphony) {
const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray));
SisterVoiceRing::offAllSisters(voiceToSteal, delay);
}
}
void sfz::Synth::checkNotePolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept
{
if (!region->notePolyphony)
return;
unsigned notePolyphonyCounter { 0 };
Voice* selfMaskCandidate { nullptr };
for (Voice* voice : voiceViewArray) {
const sfz::TriggerEvent& voiceTriggerEvent = voice->getTriggerEvent();
const bool skipVoice = (triggerEvent.type == TriggerEventType::NoteOn && voice->releasedOrFree()) || voice->isFree();
if (!skipVoice
&& voice->getRegion()->group == region->group
&& voiceTriggerEvent.number == triggerEvent.number
&& voiceTriggerEvent.type == triggerEvent.type) {
notePolyphonyCounter += 1;
switch (region->selfMask) {
case SfzSelfMask::mask:
if (voiceTriggerEvent.value <= triggerEvent.value) {
if (!selfMaskCandidate || selfMaskCandidate->getTriggerEvent().value > voiceTriggerEvent.value) {
selfMaskCandidate = voice;
}
}
break;
case SfzSelfMask::dontMask:
if (!selfMaskCandidate || selfMaskCandidate->getSourcePosition() < voice->getSourcePosition())
selfMaskCandidate = voice;
break;
}
}
}
if (notePolyphonyCounter >= *region->notePolyphony && selfMaskCandidate) {
SisterVoiceRing::offAllSisters(selfMaskCandidate, delay);
}
}
void sfz::Synth::checkGroupPolyphony(const Region* region, int delay) noexcept
{
const auto& activeVoices = polyphonyGroups[region->group].getActiveVoices();
tempPolyphonyArray.clear();
for (Voice* voice : activeVoices) {
if (!voice->releasedOrFree()) {
tempPolyphonyArray.push_back(voice);
}
}
if (tempPolyphonyArray.size() >= polyphonyGroups[region->group].getPolyphonyLimit()) {
const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray));
SisterVoiceRing::offAllSisters(voiceToSteal, delay);
}
}
void sfz::Synth::checkSetPolyphony(const Region* region, int delay) noexcept
{
auto parent = region->parent;
while (parent != nullptr) {
const auto& activeVoices = parent->getActiveVoices();
tempPolyphonyArray.clear();
for (Voice* voice : activeVoices) {
if (!voice->releasedOrFree()) {
tempPolyphonyArray.push_back(voice);
}
}
if (tempPolyphonyArray.size() >= parent->getPolyphonyLimit()) {
const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray));
SisterVoiceRing::offAllSisters(voiceToSteal, delay);
}
parent = parent->getParent();
}
}
void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexcept
{
const auto randValue = randNoteDistribution(Random::randomGenerator);
SisterVoiceRingBuilder ring;
const TriggerEvent triggerEvent { TriggerEventType::NoteOn, noteNumber, velocity };
for (auto& region : noteActivationLists[noteNumber]) {
if (region->registerNoteOn(noteNumber, velocity, randValue)) {
unsigned notePolyphonyCounter { 0 };
Voice* selfMaskCandidate { nullptr };
Voice* selectedVoice { nullptr };
regionPolyphonyArray.clear();
for (auto& voice : voices) {
if (voice->isFree()) {
if (selectedVoice == nullptr)
selectedVoice = voice.get();
continue;
if (voice->checkOffGroup(region, delay, noteNumber)) {
const TriggerEvent& event = voice->getTriggerEvent();
noteOffDispatch(delay, event.number, event.value);
}
if (voice->getRegion() == region && !voice->releasedOrFree()) {
regionPolyphonyArray.push_back(voice.get());
}
if (region->notePolyphony) {
if (!voice->releasedOrFree()
&& voice->getRegion()->group == region->group
&& voice->getTriggerNumber() == noteNumber
&& voice->getTriggerType() == Voice::TriggerType::NoteOn) {
notePolyphonyCounter += 1;
switch (region->selfMask) {
case SfzSelfMask::mask:
if (voice->getTriggerValue() <= velocity) {
if (!selfMaskCandidate || selfMaskCandidate->getTriggerValue() > voice->getTriggerValue())
selfMaskCandidate = voice.get();
}
break;
case SfzSelfMask::dontMask:
if (!selfMaskCandidate || selfMaskCandidate->getSourcePosition() < voice->getSourcePosition())
selfMaskCandidate = voice.get();
break;
}
}
}
if (voice->checkOffGroup(delay, region->group))
noteOffDispatch(delay, voice->getTriggerNumber(), voice->getTriggerValue());
}
// Polyphony reached on note_polyphony
// If there's a self-masking candidate, release it
if (region->notePolyphony
&& notePolyphonyCounter >= *region->notePolyphony
&& selfMaskCandidate != nullptr) {
SisterVoiceRing::offAllSisters(selfMaskCandidate, delay);
}
auto parent = region->parent;
// Polyphony reached on region
if (regionPolyphonyArray.size() >= region->polyphony) {
const auto activeVoices = absl::MakeSpan(regionPolyphonyArray);
SisterVoiceRing::offAllSisters(stealer.steal(activeVoices), delay);
}
// Polyphony reached on polyphony group
if (polyphonyGroups[region->group].numPlayingVoices()
== polyphonyGroups[region->group].getPolyphonyLimit()) {
const auto activeVoices = absl::MakeSpan(polyphonyGroups[region->group].getActiveVoices());
SisterVoiceRing::offAllSisters(stealer.steal(activeVoices), delay);
}
// Polyphony reached some parent group/master/etc
while (parent != nullptr) {
if (parent->numPlayingVoices() >= parent->getPolyphonyLimit()) {
const auto activeVoices = absl::MakeSpan(parent->getActiveVoices());
SisterVoiceRing::offAllSisters(stealer.steal(activeVoices), delay);
}
parent = parent->getParent();
}
// Engine polyphony reached, we're stealing something
if (selectedVoice == nullptr) {
selectedVoice = stealer.steal(absl::MakeSpan(voiceViewArray));
}
// For some reason we did not find a voice to use.
// This is a degraded case but we'll just drop the note on.
if (selectedVoice == nullptr)
continue;
// Kill voice if necessary, pre-rendering it into the output buffers
if (!selectedVoice->isFree()) {
auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock);
SisterVoiceRing::applyToRing(selectedVoice, [&] (Voice* v) {
renderVoiceToOutputs(*v, *tempSpan);
v->reset();
});
}
// Voice should be free now
ASSERT(selectedVoice->isFree());
selectedVoice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn);
ring.addVoiceToRing(selectedVoice);
RegionSet::registerVoiceInHierarchy(region, selectedVoice);
polyphonyGroups[region->group].registerVoice(selectedVoice);
startVoice(region, delay, triggerEvent, ring);
}
}
}
void sfz::Synth::startDelayedReleaseVoices(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept
{
if (!region->rtDead && !playingAttackVoice(region)) {
region->delayedReleases.clear();
return;
}
for (auto& note: region->delayedReleases) {
// FIXME: we really need to have some form of common method to find and start voices...
const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second };
startVoice(region, delay, noteOffEvent, ring);
}
region->delayedReleases.clear();
}
void sfz::Synth::cc(int delay, int ccNumber, uint8_t ccValue) noexcept
{
const auto normalizedCC = normalizeCC(ccValue);
hdcc(delay, ccNumber, normalizedCC);
}
void sfz::Synth::ccDispatch(int delay, int ccNumber, float value) noexcept
{
SisterVoiceRingBuilder ring;
const TriggerEvent triggerEvent { TriggerEventType::CC, ccNumber, value };
for (auto& region : ccActivationLists[ccNumber]) {
if (ccNumber == region->sustainCC)
startDelayedReleaseVoices(region, delay, ring);
if (region->registerCC(ccNumber, value))
startVoice(region, delay, triggerEvent, ring);
}
}
void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept
{
ASSERT(ccNumber < config::numCCs);
@ -1066,53 +1123,7 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept
for (auto& voice : voices)
voice->registerCC(delay, ccNumber, normValue);
SisterVoiceRingBuilder ring;
for (auto& region : ccActivationLists[ccNumber]) {
if (ccNumber == region->sustainCC) {
if (!region->rtDead) {
// check that a voice with compatible trigger is playing
// FIXME: we're going twice over the voices, when the synth
// handles the regions completely these dispatch functions
// should be overhauled, also to include voice stealing on
// all events
const auto compatibleVoice = [region](const VoicePtr& v) -> bool {
return matchReleaseRegionAndVoice(*region, *v);
};
if (absl::c_find_if(voices, compatibleVoice) == voices.end())
region->delayedReleases.clear();
}
for (auto& note: region->delayedReleases) {
// FIXME: we really need to have some form of common method to find and start voices...
auto voice = findFreeVoice();
if (voice == nullptr)
continue;
voice->startVoice(region, delay, note.first, note.second, Voice::TriggerType::NoteOff);
ring.addVoiceToRing(voice);
RegionSet::registerVoiceInHierarchy(region, voice);
polyphonyGroups[region->group].registerVoice(voice);
}
region->delayedReleases.clear();
}
if (region->registerCC(ccNumber, normValue)) {
auto voice = findFreeVoice();
if (voice == nullptr)
continue;
voice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC);
ring.addVoiceToRing(voice);
RegionSet::registerVoiceInHierarchy(region, voice);
polyphonyGroups[region->group].registerVoice(voice);
}
}
ccDispatch(delay, ccNumber, normValue);
}
void sfz::Synth::pitchWheel(int delay, int pitch) noexcept
@ -1436,8 +1447,8 @@ void sfz::Synth::resetVoices(int numVoices)
voiceViewArray.clear();
voiceViewArray.reserve(numVoices);
regionPolyphonyArray.clear();
regionPolyphonyArray.reserve(numVoices);
tempPolyphonyArray.clear();
tempPolyphonyArray.reserve(numVoices);
for (int i = 0; i < numVoices; ++i) {
auto voice = absl::make_unique<Voice>(i, resources);

View file

@ -725,9 +725,33 @@ private:
fs::file_time_type checkModificationTime();
/**
* @brief Check all regions and start voices for note on events
*
* @param delay
* @param noteNumber
* @param velocity
*/
void noteOnDispatch(int delay, int noteNumber, float velocity) noexcept;
/**
* @brief Check all regions and start voices for note off events
*
* @param delay
* @param noteNumber
* @param velocity
*/
void noteOffDispatch(int delay, int noteNumber, float velocity) noexcept;
/**
* @brief Check all regions and start voices for cc events
*
* @param delay
* @param ccNumber
* @param value
*/
void ccDispatch(int delay, int ccNumber, float value) noexcept;
template<class T>
static void updateUsedCCsFromCCMap(std::bitset<sfz::config::numCCs>& usedCCs, const CCMap<T> map)
{
@ -765,18 +789,83 @@ private:
using RegionSetPtr = std::unique_ptr<RegionSet>;
std::vector<RegionPtr> regions;
std::vector<VoicePtr> voices;
// These are more general "groups" than sfz and encapsulates the full hierarchy
RegionSet* currentSet;
OpcodeScope lastHeader { OpcodeScope::kOpcodeScopeGlobal };
std::vector<RegionSetPtr> sets;
// These are the `group=` groups where you can off voices
std::vector<PolyphonyGroup> polyphonyGroups;
// Views to speed up iteration over the regions and voices when events
// occur in the audio callback
VoiceViewVector regionPolyphonyArray;
VoiceViewVector tempPolyphonyArray;
VoiceViewVector voiceViewArray;
VoiceStealing stealer;
VoiceViewVector voiceViewArray;
/**
* @brief Check the region polyphony, releasing voices if necessary
*
* @param region
* @param delay
*/
void checkRegionPolyphony(const Region* region, int delay) noexcept;
/**
* @brief Check the note polyphony, releasing voices if necessary
*
* @param region
* @param delay
* @param triggerEvent
*/
void checkNotePolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept;
/**
* @brief Check the group polyphony, releasing voices if necessary
*
* @param region
* @param delay
*/
void checkGroupPolyphony(const Region* region, int delay) noexcept;
/**
* @brief Check the region set polyphony at all levels, releasing voices if necessary
*
* @param region
* @param delay
*/
void checkSetPolyphony(const Region* region, int delay) noexcept;
/**
* @brief Start a voice for a specific region.
* This will do the needed polyphony checks and voice stealing.
*
* @param region
* @param delay
* @param triggerEvent
* @param ring
*/
void startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept;
/**
* @brief Start all delayed release voices of the region if necessary
*
* @param region
* @param delay
* @param ring
*/
void startDelayedReleaseVoices(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept;
/**
* @brief Check if a playing voice matches the release region
*
* @param releaseRegion
* @return true
* @return false
*/
bool playingAttackVoice(const Region* releaseRegion) noexcept;
std::array<RegionViewVector, 128> noteActivationLists;
std::array<RegionViewVector, config::numCCs> ccActivationLists;

24
src/sfizz/TriggerEvent.h Normal file
View file

@ -0,0 +1,24 @@
// 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
namespace sfz
{
enum class TriggerEventType { NoteOn, NoteOff, CC };
/**
* @brief Encapsulate a midi event with normalized values
*
*/
struct TriggerEvent
{
TriggerEventType type;
int number;
float value;
};
}

View file

@ -36,22 +36,18 @@ sfz::Voice::~Voice()
{
}
void sfz::Voice::startVoice(Region* region, int delay, int number, float value, sfz::Voice::TriggerType triggerType) noexcept
void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event) noexcept
{
ASSERT(value >= 0.0f && value <= 1.0f);
if (triggerType == TriggerType::CC)
number = region->pitchKeycenter;
this->triggerType = triggerType;
triggerNumber = number;
triggerValue = value;
ASSERT(event.value >= 0.0f && event.value <= 1.0f);
this->region = region;
if (region->disabled())
return;
triggerEvent = event;
if (triggerEvent.type == TriggerEventType::CC)
triggerEvent.number = region->pitchKeycenter;
switchState(State::playing);
ASSERT(delay >= 0);
@ -106,18 +102,18 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value,
}
// do Scala retuning and reconvert the frequency into a 12TET key number
const float numberRetuned = resources.tuning.getKeyFractional12TET(number);
const float numberRetuned = resources.tuning.getKeyFractional12TET(triggerEvent.number);
pitchRatio = region->getBasePitchVariation(numberRetuned, value);
pitchRatio = region->getBasePitchVariation(numberRetuned, triggerEvent.value);
// apply stretch tuning if set
if (resources.stretch)
pitchRatio *= resources.stretch->getRatioForFractionalKey(numberRetuned);
baseVolumedB = region->getBaseVolumedB(number);
baseVolumedB = region->getBaseVolumedB(triggerEvent.number);
baseGain = region->getBaseGain();
if (triggerType != TriggerType::CC)
baseGain *= region->getNoteGain(number, value);
if (triggerEvent.type != TriggerEventType::CC)
baseGain *= region->getNoteGain(triggerEvent.number, triggerEvent.value);
gainSmoother.reset();
resetCrossfades();
@ -127,13 +123,13 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value,
const unsigned numChannels = region->isStereo() ? 2 : 1;
for (auto& filter: region->filters) {
auto newFilter = resources.filterPool.getFilter(filter, numChannels, number, value);
auto newFilter = resources.filterPool.getFilter(filter, numChannels, triggerEvent.number, triggerEvent.value);
if (newFilter)
filters.push_back(newFilter);
}
for (auto& eq: region->equalizers) {
auto newEQ = resources.eqPool.getEQ(eq, numChannels, value);
auto newEQ = resources.eqPool.getEQ(eq, numChannels, triggerEvent.value);
if (newEQ)
equalizers.push_back(newEQ);
}
@ -141,11 +137,11 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value,
sourcePosition = region->getOffset();
triggerDelay = delay;
initialDelay = delay + static_cast<int>(region->getDelay() * sampleRate);
baseFrequency = resources.tuning.getFrequencyOfKey(number);
baseFrequency = resources.tuning.getFrequencyOfKey(triggerEvent.number);
bendStepFactor = centsFactor(region->bendStep);
bendSmoother.setSmoothing(region->bendSmooth, sampleRate);
bendSmoother.reset(centsFactor(region->getBendInCents(resources.midiState.getPitchBend())));
egEnvelope.reset(region->amplitudeEG, *region, resources.midiState, delay, value, sampleRate);
egEnvelope.reset(region->amplitudeEG, *region, resources.midiState, delay, triggerEvent.value, sampleRate);
resources.modMatrix.initVoice(id, region->getId(), delay);
}
@ -197,7 +193,7 @@ void sfz::Voice::registerNoteOff(int delay, int noteNumber, float velocity) noex
if (state != State::playing)
return;
if (triggerNumber == noteNumber) {
if (triggerEvent.number == noteNumber && triggerEvent.type == TriggerEventType::NoteOn) {
noteIsOff = true;
if (region->loopMode == SfzLoopMode::one_shot)
@ -712,12 +708,14 @@ void sfz::Voice::fillWithGenerator(AudioSpan<float> buffer) noexcept
#endif
}
bool sfz::Voice::checkOffGroup(int delay, uint32_t group) noexcept
bool sfz::Voice::checkOffGroup(const Region* other, int delay, int noteNumber) noexcept
{
if (region == nullptr)
if (region == nullptr || other == nullptr)
return false;
if (triggerType == TriggerType::NoteOn && region->offBy == group) {
if (triggerEvent.type == TriggerEventType::NoteOn
&& region->offBy == other->group
&& noteNumber != triggerEvent.number) {
off(delay);
return true;
}

View file

@ -5,6 +5,7 @@
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#pragma once
#include "TriggerEvent.h"
#include "Config.h"
#include "ADSREnvelope.h"
#include "HistoricalBuffer.h"
@ -43,12 +44,6 @@ public:
~Voice();
enum class TriggerType {
NoteOn,
NoteOff,
CC
};
/**
* @brief Get the unique identifier of this voice in a synth
*/
@ -113,11 +108,9 @@ public:
*
* @param region
* @param delay
* @param number
* @param value
* @param triggerType
* @param evebt
*/
void startVoice(Region* region, int delay, int number, float value, TriggerType triggerType) noexcept;
void startVoice(Region* region, int delay, const TriggerEvent& event) noexcept;
/**
* @brief Get the sample quality determined by the active region.
@ -170,11 +163,12 @@ public:
* This will trigger the release if true.
*
* @param delay
* @param noteNumber
* @param group
* @return true
* @return false
*/
bool checkOffGroup(int delay, uint32_t group) noexcept;
bool checkOffGroup(const Region* other, int delay, int noteNumber) noexcept;
/**
* @brief Render a block of data for this voice into the span
@ -198,23 +192,11 @@ public:
*/
bool releasedOrFree() const noexcept;
/**
* @brief Get the number that triggered the voice (note number or cc number)
* @brief Get the event that triggered the voice
*
* @return int
*/
int getTriggerNumber() const noexcept { return triggerNumber; }
/**
* @brief Get the value that triggered the voice (note velocity or cc value)
*
* @return float
*/
float getTriggerValue() const noexcept { return triggerValue; }
/**
* @brief Get the type of trigger
*
* @return TriggerType
*/
TriggerType getTriggerType() const noexcept { return triggerType; }
const TriggerEvent& getTriggerEvent() const noexcept { return triggerEvent; }
/**
* @brief Reset the voice to its initial values
@ -432,9 +414,7 @@ private:
State state { State::idle };
bool noteIsOff { false };
TriggerType triggerType;
int triggerNumber;
float triggerValue;
TriggerEvent triggerEvent;
absl::optional<int> triggerDelay;
float speedRatio { 1.0 };
@ -496,13 +476,16 @@ inline bool sisterVoices(const Voice* lhs, const Voice* rhs)
if (lhs->getAge() != rhs->getAge())
return false;
if (lhs->getTriggerNumber() != rhs->getTriggerNumber())
const TriggerEvent& lhsTrigger = lhs->getTriggerEvent();
const TriggerEvent& rhsTrigger = rhs->getTriggerEvent();
if (lhsTrigger.number != rhsTrigger.number)
return false;
if (lhs->getTriggerValue() != rhs->getTriggerValue())
if (lhsTrigger.value != rhsTrigger.value)
return false;
if (lhs->getTriggerType() != rhs->getTriggerType())
if (lhsTrigger.type != rhsTrigger.type)
return false;
return true;
@ -513,14 +496,17 @@ inline bool voiceOrdering(const Voice* lhs, const Voice* rhs)
if (lhs->getAge() != rhs->getAge())
return lhs->getAge() > rhs->getAge();
if (lhs->getTriggerNumber() != rhs->getTriggerNumber())
return lhs->getTriggerNumber() < rhs->getTriggerNumber();
const TriggerEvent& lhsTrigger = lhs->getTriggerEvent();
const TriggerEvent& rhsTrigger = rhs->getTriggerEvent();
if (lhs->getTriggerValue() != rhs->getTriggerValue())
return lhs->getTriggerValue() < rhs->getTriggerValue();
if (lhsTrigger.number != rhsTrigger.number)
return lhsTrigger.number < rhsTrigger.number;
if (lhs->getTriggerType() != rhs->getTriggerType())
return lhs->getTriggerType() > rhs->getTriggerType();
if (lhsTrigger.value != rhsTrigger.value)
return lhsTrigger.value < rhsTrigger.value;
if (lhsTrigger.type != rhsTrigger.type)
return lhsTrigger.type > rhsTrigger.type;
return false;
}

View file

@ -25,7 +25,7 @@ sfz::Voice* sfz::VoiceStealing::steal(absl::Span<sfz::Voice*> voices) noexcept
// their sound, but it's reasonable for sounds with a quick attack and longer
// release.
const auto ageThreshold =
static_cast<int>(voices.front()->getAge() * config::stealingAgeCoeff) + 1;
static_cast<int>(voices.front()->getAge() * config::stealingAgeCoeff);
Voice* returnedVoice = voices.front();
unsigned idx = 0;

View file

@ -227,11 +227,11 @@ TEST_CASE("[Polyphony] Self-masking")
REQUIRE( synth.getNumActiveVoices(true) == 3 ); // One of these is releasing
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 2 );
REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm);
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
REQUIRE(!synth.getVoiceView(0)->releasedOrFree());
REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm);
REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm);
REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // The lowest velocity voice is the masking candidate
REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 64_norm);
REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 64_norm);
REQUIRE(!synth.getVoiceView(2)->releasedOrFree());
}
@ -248,11 +248,11 @@ TEST_CASE("[Polyphony] Not self-masking")
REQUIRE( synth.getNumActiveVoices(true) == 3 ); // One of these is releasing
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 2 );
REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm);
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
REQUIRE( synth.getVoiceView(0)->releasedOrFree());
REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm);
REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm);
REQUIRE(!synth.getVoiceView(1)->releasedOrFree());
REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 64_norm);
REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 64_norm);
REQUIRE(!synth.getVoiceView(2)->releasedOrFree());
}
@ -269,11 +269,11 @@ TEST_CASE("[Polyphony] Self-masking with the exact same velocity")
REQUIRE( synth.getNumActiveVoices(true) == 3 ); // One of these is releasing
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 2 );
REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 64_norm);
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 64_norm);
REQUIRE(!synth.getVoiceView(0)->releasedOrFree());
REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 63_norm);
REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 63_norm);
REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // The first one is the masking candidate since they have the same velocity
REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 63_norm);
REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 63_norm);
REQUIRE(!synth.getVoiceView(2)->releasedOrFree());
}
@ -286,9 +286,10 @@ TEST_CASE("[Polyphony] Self-masking only works from low to high")
synth.noteOn(0, 64, 63 );
synth.noteOn(0, 64, 62 );
REQUIRE( synth.getNumActiveVoices(true) == 2 ); // Both notes are playing
REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm);
REQUIRE( numPlayingVoices(synth) == 2 ); // id
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
REQUIRE(!synth.getVoiceView(0)->releasedOrFree());
REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm);
REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm);
REQUIRE(!synth.getVoiceView(1)->releasedOrFree());
}
@ -305,13 +306,13 @@ TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same po
REQUIRE( synth.getNumActiveVoices(true) == 4);
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 1 );
REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 62_norm);
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 62_norm);
REQUIRE( synth.getVoiceView(0)->releasedOrFree()); // got killed
REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm);
REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm);
REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // got killed
REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 63_norm);
REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 63_norm);
REQUIRE( synth.getVoiceView(2)->releasedOrFree()); // got killed
REQUIRE( synth.getVoiceView(3)->getTriggerValue() == 63_norm);
REQUIRE( synth.getVoiceView(3)->getTriggerEvent().value == 63_norm);
REQUIRE(!synth.getVoiceView(3)->releasedOrFree());
}
@ -333,9 +334,9 @@ TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same po
REQUIRE( synth.getNumActiveVoices(true) == 2 );
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 1 );
REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm);
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
REQUIRE( synth.getVoiceView(0)->releasedOrFree());
REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 64_norm);
REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 64_norm);
REQUIRE(!synth.getVoiceView(1)->releasedOrFree());
}
@ -353,13 +354,13 @@ TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups")
REQUIRE( synth.getNumActiveVoices(true) == 4); // Both notes are playing
synth.renderBlock(buffer);
REQUIRE(numPlayingVoices(synth) == 2 );
REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 62_norm);
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 62_norm);
REQUIRE( synth.getVoiceView(0)->releasedOrFree()); // got killed
REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm);
REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm);
REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // got killed
REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 63_norm);
REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 63_norm);
REQUIRE(!synth.getVoiceView(2)->releasedOrFree());
REQUIRE( synth.getVoiceView(3)->getTriggerValue() == 63_norm);
REQUIRE( synth.getVoiceView(3)->getTriggerEvent().value == 63_norm);
REQUIRE(!synth.getVoiceView(3)->releasedOrFree());
}
@ -381,8 +382,118 @@ TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups (wi
REQUIRE( synth.getNumActiveVoices(true) == 2 );
synth.renderBlock(buffer);
REQUIRE(numPlayingVoices(synth) == 2 );
REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm);
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
REQUIRE(!synth.getVoiceView(0)->releasedOrFree());
REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 64_norm);
REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 64_norm);
REQUIRE(!synth.getVoiceView(1)->releasedOrFree());
}
TEST_CASE("[Polyphony] Note polyphony operates on release voices")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<region> key=48 note_polyphony=1 sample=*saw trigger=release_key ampeg_attack=1 ampeg_decay=1
)");
synth.noteOn(0, 48, 63 );
synth.noteOff(10, 48, 0 );
REQUIRE( synth.getNumActiveVoices(true) == 1);
synth.noteOn(20, 48, 65 );
synth.noteOff(30, 48, 10 );
REQUIRE( synth.getNumActiveVoices(true) == 2 );
synth.renderBlock(buffer);
REQUIRE(numPlayingVoices(synth) == 1 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
REQUIRE( synth.getVoiceView(0)->releasedOrFree());
REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 65_norm);
REQUIRE(!synth.getVoiceView(1)->releasedOrFree());
}
TEST_CASE("[Polyphony] Note polyphony operates on release voices (masking works from low to high but takes into account the replaced velocity)")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<region> key=48 note_polyphony=1 sample=*saw trigger=release_key ampeg_attack=1 ampeg_decay=1
)");
synth.noteOn(0, 48, 63 );
synth.noteOff(10, 48, 0 );
REQUIRE( synth.getNumActiveVoices(true) == 1);
REQUIRE( numPlayingVoices(synth) == 1 );
synth.noteOn(20, 48, 61 );
synth.noteOff(30, 48, 10 );
REQUIRE( synth.getNumActiveVoices(true) == 2 );
REQUIRE( numPlayingVoices(synth) == 2 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
REQUIRE(!synth.getVoiceView(0)->releasedOrFree());
REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 61_norm);
REQUIRE(!synth.getVoiceView(1)->releasedOrFree());
}
TEST_CASE("[Polyphony] Note polyphony operates on release voices and sustain pedal")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<region> key=48 sample=*silence
<region> key=48 note_polyphony=1 sample=*saw trigger=release ampeg_attack=1 ampeg_decay=1
)");
synth.cc(0, 64, 127);
synth.noteOn(0, 48, 61 );
synth.noteOff(1, 48, 0 );
synth.noteOn(2, 48, 62 );
synth.noteOff(3, 48, 0 );
synth.noteOn(4, 48, 63 );
synth.noteOff(5, 48, 0 );
REQUIRE( synth.getNumActiveVoices(true) == 3);
REQUIRE( numPlayingVoices(synth) == 3 );
synth.cc(20, 64, 0);
REQUIRE( synth.getNumActiveVoices(true) == 6 );
REQUIRE( numPlayingVoices(synth) == 1 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 61_norm);
REQUIRE( synth.getVoiceView(0)->releasedOrFree());
REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm);
REQUIRE( synth.getVoiceView(1)->releasedOrFree());
REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 63_norm);
REQUIRE( synth.getVoiceView(2)->releasedOrFree());
REQUIRE( synth.getVoiceView(3)->getTriggerEvent().value == 61_norm);
REQUIRE( synth.getVoiceView(3)->releasedOrFree());
REQUIRE( synth.getVoiceView(4)->getTriggerEvent().value == 62_norm);
REQUIRE( synth.getVoiceView(4)->releasedOrFree());
REQUIRE( synth.getVoiceView(5)->getTriggerEvent().value == 63_norm);
REQUIRE(!synth.getVoiceView(5)->releasedOrFree());
}
TEST_CASE("[Polyphony] Note polyphony operates on release voices and sustain pedal (masking)")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<region> key=48 sample=*silence
<region> key=48 note_polyphony=1 sample=*saw trigger=release ampeg_attack=1 ampeg_decay=1
)");
synth.cc(0, 64, 127);
synth.noteOn(0, 48, 63 );
synth.noteOff(1, 48, 0 );
synth.noteOn(2, 48, 62 );
synth.noteOff(3, 48, 0 );
synth.noteOn(4, 48, 61 );
synth.noteOff(5, 48, 0 );
REQUIRE( synth.getNumActiveVoices(true) == 3);
REQUIRE( numPlayingVoices(synth) == 3 );
synth.cc(20, 64, 0);
REQUIRE( synth.getNumActiveVoices(true) == 6 );
REQUIRE( numPlayingVoices(synth) == 3 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
REQUIRE( synth.getVoiceView(0)->releasedOrFree());
REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm);
REQUIRE( synth.getVoiceView(1)->releasedOrFree());
REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 61_norm);
REQUIRE( synth.getVoiceView(2)->releasedOrFree());
REQUIRE( synth.getVoiceView(3)->getTriggerEvent().value == 63_norm);
REQUIRE(!synth.getVoiceView(3)->releasedOrFree());
REQUIRE( synth.getVoiceView(4)->getTriggerEvent().value == 62_norm);
REQUIRE(!synth.getVoiceView(4)->releasedOrFree());
REQUIRE( synth.getVoiceView(5)->getTriggerEvent().value == 61_norm);
REQUIRE(!synth.getVoiceView(5)->releasedOrFree());
}

View file

@ -660,7 +660,7 @@ TEST_CASE("[Synth] Apply function on sisters")
REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(0)) == 3 );
float start = 1.0f;
sfz::SisterVoiceRing::applyToRing(synth.getVoiceView(0), [&](const sfz::Voice* v) {
start += static_cast<float>(v->getTriggerNumber());
start += static_cast<float>(v->getTriggerEvent().number);
});
REQUIRE( start == 1.0f + 3.0f * 63.0f );
}
@ -828,7 +828,7 @@ TEST_CASE("[Synth] Release (Multiple notes, release_key ignores the pedal)")
std::vector<float> requiredVelocities { 34_norm, 78_norm, 85_norm};
std::vector<float> actualVelocities;
for (auto* v: getActiveVoices(synth)) {
actualVelocities.push_back(v->getTriggerValue());
actualVelocities.push_back(v->getTriggerEvent().value);
}
sortAll(requiredVelocities, actualVelocities);
REQUIRE( requiredVelocities == actualVelocities );
@ -856,7 +856,7 @@ TEST_CASE("[Synth] Release (Multiple notes, release, cleared the delayed voices
std::vector<float> requiredVelocities { 34_norm, 78_norm, 85_norm, 34_norm, 78_norm, 85_norm };
std::vector<float> actualVelocities;
for (auto* v: getActiveVoices(synth)) {
actualVelocities.push_back(v->getTriggerValue());
actualVelocities.push_back(v->getTriggerEvent().value);
}
sortAll(requiredVelocities, actualVelocities);
REQUIRE( requiredVelocities == actualVelocities );
@ -886,7 +886,7 @@ TEST_CASE("[Synth] Release (Multiple notes after pedal is down, release, cleared
std::vector<float> requiredVelocities { 34_norm, 78_norm, 85_norm, 34_norm, 78_norm, 85_norm };
std::vector<float> actualVelocities;
for (auto* v: getActiveVoices(synth)) {
actualVelocities.push_back(v->getTriggerValue());
actualVelocities.push_back(v->getTriggerEvent().value);
}
sortAll(requiredVelocities, actualVelocities);
REQUIRE( requiredVelocities == actualVelocities );
@ -914,7 +914,7 @@ TEST_CASE("[Synth] Release (Multiple note ons during pedal down)")
std::vector<float> requiredVelocities { 78_norm, 85_norm, 78_norm, 85_norm };
std::vector<float> actualVelocities;
for (auto* v: getActiveVoices(synth)) {
actualVelocities.push_back(v->getTriggerValue());
actualVelocities.push_back(v->getTriggerEvent().value);
}
sortAll(requiredVelocities, actualVelocities);
REQUIRE( requiredVelocities == actualVelocities );
@ -1256,8 +1256,24 @@ TEST_CASE("[Synth] Off by same group")
REQUIRE( playingVoices.front()->getRegion()->keyRange.containsWithEnd(60) );
}
TEST_CASE("[Synth] Off by alone and repeated")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, 256 };
TEST_CASE("[Synth] Off by same note")
synth.loadSfzString(fs::current_path(), R"(
<region> group=1 off_by=1 sample=*sine key=60
)");
synth.noteOn(0, 60, 85);
REQUIRE( numPlayingVoices(synth) == 1 );
synth.noteOn(0, 60, 85);
REQUIRE( numPlayingVoices(synth) == 2 );
synth.noteOn(0, 60, 85);
REQUIRE( numPlayingVoices(synth) == 3 );
}
TEST_CASE("[Synth] Off by same note and group")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, 256 };
@ -1267,7 +1283,6 @@ TEST_CASE("[Synth] Off by same note")
<region> group=1 off_by=1 sample=*triangle key=60
)");
synth.noteOn(0, 60, 85);
REQUIRE( numPlayingVoices(synth) == 1 );
auto playingVoices = getPlayingVoices(synth);
REQUIRE( playingVoices.front()->getRegion()->sampleId.filename() == "*triangle" );
REQUIRE( numPlayingVoices(synth) == 2 );
synth.noteOn(0, 60, 85);
}