Merge pull request #393 from paulfd/polyphony-cleanups
Polyphony cleanups
This commit is contained in:
commit
afadb171a4
9 changed files with 503 additions and 274 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
24
src/sfizz/TriggerEvent.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue