Merge pull request #352 from paulfd/note-polyphony-again

Polyphony again
This commit is contained in:
Paul Ferrand 2020-08-10 18:26:52 +02:00 committed by GitHub
commit 6b13050ded
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 383 additions and 111 deletions

View file

@ -11,22 +11,30 @@
namespace sfz {
template<class Type>
Type ADSREnvelope<Type>::secondsToSamples (Type timeInSeconds) const noexcept
{
return static_cast<int>(timeInSeconds * sampleRate);
};
template<class Type>
Type ADSREnvelope<Type>::secondsToLinRate (Type timeInSeconds) const noexcept
{
timeInSeconds = std::max<Type>(timeInSeconds, config::virtuallyZero);
return 1 / (sampleRate * timeInSeconds);
};
template<class Type>
Type ADSREnvelope<Type>::secondsToExpRate (Type timeInSeconds) const noexcept
{
timeInSeconds = std::max<Type>(25e-3, timeInSeconds);
return std::exp(-8.0 / (timeInSeconds * sampleRate));
};
template <class Type>
void ADSREnvelope<Type>::reset(const EGDescription& desc, const Region& region, const MidiState& state, int delay, float velocity, float sampleRate) noexcept
{
auto secondsToSamples = [sampleRate](Type timeInSeconds) {
return static_cast<int>(timeInSeconds * sampleRate);
};
auto secondsToLinRate = [sampleRate](Type timeInSeconds) {
timeInSeconds = std::max<Type>(timeInSeconds, config::virtuallyZero);
return 1 / (sampleRate * timeInSeconds);
};
auto secondsToExpRate = [sampleRate](Type timeInSeconds) {
timeInSeconds = std::max<Type>(25e-3, timeInSeconds);
return std::exp(-8.0 / (timeInSeconds * sampleRate));
};
this->sampleRate = sampleRate;
this->delay = delay + secondsToSamples(desc.getDelay(state, velocity));
this->attackStep = secondsToLinRate(desc.getAttack(state, velocity));
@ -208,13 +216,16 @@ int ADSREnvelope<Type>::getRemainingDelay() const noexcept
}
template <class Type>
void ADSREnvelope<Type>::startRelease(int releaseDelay, bool fastRelease) noexcept
void ADSREnvelope<Type>::startRelease(int releaseDelay) noexcept
{
shouldRelease = true;
this->releaseDelay = releaseDelay;
}
if (fastRelease)
this->releaseRate = 0;
template <class Type>
void ADSREnvelope<Type>::setReleaseTime(Type timeInSeconds) noexcept
{
releaseRate = secondsToExpRate(timeInSeconds);
}
}

View file

@ -44,15 +44,18 @@ public:
* @param output
*/
void getBlock(absl::Span<Type> output) noexcept;
/**
* @brief Set the release time for the envelope
*
* @param timeInSeconds
*/
void setReleaseTime(Type timeInSeconds) noexcept;
/**
* @brief Start the envelope release after a delay.
*
* @param releaseDelay the delay before releasing in samples
* @param fastRelease whether the release should be fast (i.e. 0 or so) or
* follow the release duration that was set when
* initializing the envelope
*/
void startRelease(int releaseDelay, bool fastRelease = false) noexcept;
void startRelease(int releaseDelay) noexcept;
/**
* @brief Is the envelope smoothing?
*
@ -75,6 +78,11 @@ public:
int getRemainingDelay() const noexcept;
private:
float sampleRate { config::defaultSampleRate };
Type secondsToSamples (Type timeInSeconds) const noexcept;
Type secondsToLinRate (Type timeInSeconds) const noexcept;
Type secondsToExpRate (Type timeInSeconds) const noexcept;
enum class State {
Delay,
Attack,

View file

@ -31,7 +31,7 @@
enum class SfzTrigger { attack, release, release_key, first, legato };
enum class SfzLoopMode { no_loop, one_shot, loop_continuous, loop_sustain };
enum class SfzOffMode { fast, normal };
enum class SfzOffMode { fast, normal, time };
enum class SfzVelocityOverride { current, previous };
enum class SfzCrossfadeCurve { gain, power };
enum class SfzSelfMask { mask, dontMask };
@ -75,6 +75,7 @@ namespace Default
constexpr uint32_t group { 0 };
constexpr Range<uint32_t> groupRange { 0, std::numeric_limits<uint32_t>::max() };
constexpr SfzOffMode offMode { SfzOffMode::fast };
constexpr float offTime { 6e-3f };
constexpr Range<uint32_t> polyphonyRange { 0, config::maxVoices };
constexpr SfzSelfMask selfMask { SfzSelfMask::mask };

View file

@ -16,3 +16,10 @@ void sfz::PolyphonyGroup::removeVoice(const Voice* voice) noexcept
{
swapAndPopFirst(voices, [voice](const Voice* v) { return v == voice; });
}
unsigned sfz::PolyphonyGroup::numPlayingVoices() const noexcept
{
return absl::c_count_if(voices, [](const Voice* v) {
return !v->releasedOrFree();
});
}

View file

@ -40,6 +40,10 @@ public:
* @return unsigned
*/
unsigned getPolyphonyLimit() const noexcept { return polyphonyLimit; }
/**
* @brief Returns the number of playing (unreleased) voices
*/
unsigned numPlayingVoices() const noexcept;
/**
* @brief Get the active voices
*

View file

@ -161,10 +161,17 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
case hash("normal"):
offMode = SfzOffMode::normal;
break;
case hash("time"):
offMode = SfzOffMode::time;
break;
default:
DBG("Unkown off mode:" << opcode.value);
}
break;
case hash("off_time"):
offMode = SfzOffMode::time;
setValueFromOpcode(opcode, offTime, Default::egTimeRange);
break;
case hash("polyphony"):
if (auto value = readOpcode(opcode.value, Default::polyphonyRange))
polyphony = *value;

View file

@ -286,6 +286,7 @@ struct Region {
uint32_t group { Default::group }; // group
absl::optional<uint32_t> offBy {}; // off_by
SfzOffMode offMode { Default::offMode }; // off_mode
float offTime { Default::offTime }; // off_mode
absl::optional<uint32_t> notePolyphony {}; // note_polyphony
unsigned polyphony { config::maxVoices }; // polyphony
SfzSelfMask selfMask { Default::selfMask };

View file

@ -46,3 +46,10 @@ void sfz::RegionSet::removeVoiceFromHierarchy(const Region* region, const Voice*
parent = parent->getParent();
}
}
unsigned sfz::RegionSet::numPlayingVoices() const noexcept
{
return absl::c_count_if(voices, [](const Voice* v) {
return !v->releasedOrFree();
});
}

View file

@ -79,6 +79,10 @@ public:
* @param parent
*/
void setParent(RegionSet* parent) noexcept { this->parent = parent; }
/**
* @brief Returns the number of playing (unreleased) voices
*/
unsigned numPlayingVoices() const noexcept;
/**
* @brief Get the active voices
*

View file

@ -56,6 +56,22 @@ struct SisterVoiceRing {
return count;
}
/**
* @brief Off all sisters in a ring
*
* @param voice
* @param delay
*/
template<class T,
absl::enable_if_t<std::is_same<Voice, absl::remove_const_t<T>>::value, int> = 0>
static void offAllSisters(T* voice, int delay) {
if (voice != nullptr) {
SisterVoiceRing::applyToRing(voice, [&] (Voice* v) {
v->off(delay);
});
}
}
/**
* @brief Check if a sister voice ring is well formed
*

View file

@ -919,12 +919,13 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc
continue;
}
if (voice->getRegion() == region) {
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;
@ -948,35 +949,33 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc
}
// Polyphony reached on note_polyphony
if (region->notePolyphony && notePolyphonyCounter >= *region->notePolyphony) {
if (selfMaskCandidate != nullptr)
selfMaskCandidate->release(delay);
else // We're the lowest velocity guy here
continue;
// 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) {
selectedVoice = stealer.steal(absl::MakeSpan(regionPolyphonyArray));
goto render;
const auto activeVoices = absl::MakeSpan(regionPolyphonyArray);
SisterVoiceRing::offAllSisters(stealer.steal(activeVoices), delay);
}
// Polyphony reached on polyphony group
if (polyphonyGroups[region->group].getActiveVoices().size()
if (polyphonyGroups[region->group].numPlayingVoices()
== polyphonyGroups[region->group].getPolyphonyLimit()) {
const auto activeVoices = absl::MakeSpan(polyphonyGroups[region->group].getActiveVoices());
selectedVoice = stealer.steal(activeVoices);
goto render;
SisterVoiceRing::offAllSisters(stealer.steal(activeVoices), delay);
}
// Polyphony reached some parent group/master/etc
while (parent != nullptr) {
if (parent->getActiveVoices().size() >= parent->getPolyphonyLimit()) {
if (parent->numPlayingVoices() >= parent->getPolyphonyLimit()) {
const auto activeVoices = absl::MakeSpan(parent->getActiveVoices());
selectedVoice = stealer.steal(activeVoices);
goto render;
SisterVoiceRing::offAllSisters(stealer.steal(activeVoices), delay);
}
parent = parent->getParent();
}
@ -986,7 +985,6 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc
selectedVoice = stealer.steal(absl::MakeSpan(voiceViewArray));
}
render:
// 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)

View file

@ -155,7 +155,7 @@ bool sfz::Voice::isFree() const noexcept
return (state == State::idle);
}
void sfz::Voice::release(int delay, bool fastRelease) noexcept
void sfz::Voice::release(int delay) noexcept
{
if (state != State::playing)
return;
@ -163,10 +163,21 @@ void sfz::Voice::release(int delay, bool fastRelease) noexcept
if (egEnvelope.getRemainingDelay() > delay) {
switchState(State::cleanMeUp);
} else {
egEnvelope.startRelease(delay, fastRelease);
egEnvelope.startRelease(delay);
}
}
void sfz::Voice::off(int delay) noexcept
{
if (region->offMode == SfzOffMode::fast) {
egEnvelope.setReleaseTime( Default::offTime );
} else if (region->offMode == SfzOffMode::time) {
egEnvelope.setReleaseTime(region->offTime);
}
release(delay);
}
void sfz::Voice::registerNoteOff(int delay, int noteNumber, float velocity) noexcept
{
ASSERT(velocity >= 0.0 && velocity <= 1.0);
@ -561,7 +572,8 @@ void sfz::Voice::fillWithData(AudioSpan<float> buffer) noexcept
<< " for sample " << region->sampleId);
}
#endif
egEnvelope.startRelease(i, true);
egEnvelope.setReleaseTime(0.0f);
egEnvelope.startRelease(i);
fill<int>(indices->subspan(i), sampleEnd);
fill<float>(coeffs->subspan(i), 1.0f);
break;
@ -695,7 +707,7 @@ bool sfz::Voice::checkOffGroup(int delay, uint32_t group) noexcept
return false;
if (triggerType == TriggerType::NoteOn && region->offBy == group) {
release(delay, region->offMode == SfzOffMode::fast);
off(delay);
return true;
}

View file

@ -288,7 +288,15 @@ public:
* @param delay
* @param fastRelease whether to do a normal release or cut the voice abruptly
*/
void release(int delay, bool fastRelease = false) noexcept;
void release(int delay) noexcept;
/**
* @brief Off the voice (steal). This will respect the off mode of the region
* and set the envelopes if necessary.
*
* @param delay
*/
void off(int delay) noexcept;
/**
* @brief gets the age of the Voice

View file

@ -7,6 +7,9 @@ sfz::VoiceStealing::VoiceStealing()
sfz::Voice* sfz::VoiceStealing::steal(absl::Span<sfz::Voice*> voices) noexcept
{
if (voices.empty())
return {};
// Start of the voice stealing algorithm
absl::c_stable_sort(voices, voiceOrdering);
@ -49,9 +52,5 @@ sfz::Voice* sfz::VoiceStealing::steal(absl::Span<sfz::Voice*> voices) noexcept
while (idx < voices.size() && sisterVoices(ref, voices[idx]));
}
// Guard for future changes: voices with age 0 just started; don't kill those.
if (returnedVoice->getAge() == 0)
return {};
return returnedVoice;
}

View file

@ -5,8 +5,8 @@ project(sfizz)
set(SFIZZ_TEST_SOURCES
RegionT.cpp
RegionTHelpers.h
RegionTHelpers.cpp
TestHelpers.h
TestHelpers.cpp
ParsingT.cpp
HelpersT.cpp
HelpersT.cpp

View file

@ -4,7 +4,7 @@
// 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 "RegionTHelpers.h"
#include "TestHelpers.h"
#include "sfizz/Synth.h"
#include "sfizz/SfzHelpers.h"
#include "sfizz/modulations/ModId.h"
@ -517,7 +517,8 @@ TEST_CASE("[Files] Off by with the same notes at the same time")
synth.renderBlock(buffer);
synth.noteOn(0, 65, 63);
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
REQUIRE( synth.getNumActiveVoices(true) == 6 );
REQUIRE( numPlayingVoices(synth) == 2 );
}
TEST_CASE("[Files] Off modes")
@ -538,8 +539,10 @@ TEST_CASE("[Files] Off modes")
synth.getVoiceView(0) ;
synth.noteOn(100, 63, 63);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
REQUIRE( numPlayingVoices(synth) == 1 );
AudioBuffer<float> buffer { 2, 256 };
synth.renderBlock(buffer);
for (unsigned i = 0; i < 10; ++i) // Not enough for the "normal" voice to die
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
REQUIRE( fastVoice->isFree() );
REQUIRE( !normalVoice->isFree() );

View file

@ -6,6 +6,8 @@
#include "sfizz/Synth.h"
#include "sfizz/SfzHelpers.h"
#include "TestHelpers.h"
#include <absl/algorithm/container.h>
#include "catch2/catch.hpp"
using namespace Catch::literals;
@ -13,7 +15,6 @@ using namespace sfz::literals;
constexpr int blockSize { 256 };
TEST_CASE("[Polyphony] Polyphony in hierarchy")
{
sfz::Synth synth;
@ -71,6 +72,7 @@ TEST_CASE("[Polyphony] Polyphony groups")
TEST_CASE("[Polyphony] group polyphony limits")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<group> group=1 polyphony=2
<region> sample=*sine key=65
@ -78,12 +80,15 @@ TEST_CASE("[Polyphony] group polyphony limits")
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
REQUIRE(synth.getNumActiveVoices(true) == 2); // group polyphony should block the last note
REQUIRE( synth.getNumActiveVoices(true) == 3 );
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing
}
TEST_CASE("[Polyphony] Hierarchy polyphony limits")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<group> polyphony=2
<region> sample=*sine key=65
@ -91,12 +96,15 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits")
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
REQUIRE(synth.getNumActiveVoices(true) == 2);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing
}
TEST_CASE("[Polyphony] Hierarchy polyphony limits (group)")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<group> polyphony=2
<region> sample=*sine key=65
@ -104,12 +112,15 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (group)")
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
REQUIRE(synth.getNumActiveVoices(true) == 2);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing
}
TEST_CASE("[Polyphony] Hierarchy polyphony limits (master)")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<master> polyphony=2
<group> polyphony=5
@ -118,12 +129,15 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (master)")
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
REQUIRE(synth.getNumActiveVoices(true) == 2);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing
}
TEST_CASE("[Polyphony] Hierarchy polyphony limits (limit in another master)")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<master> polyphony=2
<region> sample=*saw key=65
@ -137,12 +151,15 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (limit in another master)")
synth.noteOn(0, 66, 64);
synth.noteOn(0, 66, 64);
synth.noteOn(0, 66, 64);
REQUIRE(synth.getNumActiveVoices(true) == 5);
REQUIRE( synth.getNumActiveVoices(true) == 6);
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 5); // One is releasing
}
TEST_CASE("[Polyphony] Hierarchy polyphony limits (global)")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<global> polyphony=2
<group> polyphony=5
@ -151,14 +168,16 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (global)")
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
REQUIRE(synth.getNumActiveVoices(true) == 2);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing
}
TEST_CASE("[Polyphony] Polyphony in master")
{
sfz::Synth synth;
synth.setSamplesPerBlock(blockSize);
sfz::AudioBuffer<float> buffer { 2, blockSize };
synth.setSamplesPerBlock(blockSize);
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<master> polyphony=2
<group> group=2
@ -171,74 +190,199 @@ TEST_CASE("[Polyphony] Polyphony in master")
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
REQUIRE(synth.getNumActiveVoices(true) == 2); // group polyphony should block the last note
REQUIRE( synth.getNumActiveVoices(true) == 3 );
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing
synth.allSoundOff();
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices(true) == 0);
REQUIRE( synth.getNumActiveVoices(true) == 0);
synth.noteOn(0, 63, 64);
synth.noteOn(0, 63, 64);
synth.noteOn(0, 63, 64);
REQUIRE(synth.getNumActiveVoices(true) == 2); // group polyphony should block the last note
REQUIRE( synth.getNumActiveVoices(true) == 3 );
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing
synth.allSoundOff();
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices(true) == 0);
REQUIRE( synth.getNumActiveVoices(true) == 0);
synth.noteOn(0, 61, 64);
synth.noteOn(0, 61, 64);
synth.noteOn(0, 61, 64);
REQUIRE(synth.getNumActiveVoices(true) == 3);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 3 );
}
TEST_CASE("[Polyphony] Self-masking")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<region> sample=*sine key=64 note_polyphony=2
)");
synth.noteOn(0, 64, 63);
synth.noteOn(0, 64, 62);
synth.noteOn(0, 64, 63 );
synth.noteOn(0, 64, 62 );
synth.noteOn(0, 64, 64);
REQUIRE(synth.getNumActiveVoices(true) == 3); // One of these is releasing
REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm);
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)->releasedOrFree());
REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 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(1)->getTriggerValue() == 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)->releasedOrFree());
}
TEST_CASE("[Polyphony] Not self-masking")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<region> sample=*sine key=66 note_polyphony=2 note_selfmask=off
)");
synth.noteOn(0, 66, 63);
synth.noteOn(0, 66, 62);
synth.noteOn(0, 66, 63 );
synth.noteOn(0, 66, 62 );
synth.noteOn(0, 66, 64);
REQUIRE(synth.getNumActiveVoices(true) == 3); // One of these is releasing
REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm);
REQUIRE(synth.getVoiceView(0)->releasedOrFree()); // The first encountered voice is the masking candidate
REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm);
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)->releasedOrFree());
REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm);
REQUIRE(!synth.getVoiceView(1)->releasedOrFree());
REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm);
REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 64_norm);
REQUIRE(!synth.getVoiceView(2)->releasedOrFree());
}
TEST_CASE("[Polyphony] Self-masking with the exact same velocity")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
synth.loadSfzString(fs::current_path(), R"(
<region> sample=*sine key=64 note_polyphony=2
)");
synth.noteOn(0, 64, 64);
synth.noteOn(0, 64, 63);
synth.noteOn(0, 64, 63);
REQUIRE(synth.getNumActiveVoices(true) == 3); // One of these is releasing
REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 64_norm);
synth.noteOn(0, 64, 63 );
synth.noteOn(0, 64, 63 );
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)->releasedOrFree());
REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 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(1)->getTriggerValue() == 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)->releasedOrFree());
}
TEST_CASE("[Polyphony] Self-masking only works from low to high")
{
sfz::Synth synth;
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<region> sample=*sine key=64 note_polyphony=1
)");
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(!synth.getVoiceView(0)->releasedOrFree());
REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm);
REQUIRE(!synth.getVoiceView(1)->releasedOrFree());
}
TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same polyphony group (default)")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<region> sample=*saw key=64 note_polyphony=1
<region> sample=*sine key=64 note_polyphony=1
)");
synth.noteOn(0, 64, 62 );
synth.noteOn(0, 64, 63 );
REQUIRE( synth.getNumActiveVoices(true) == 4);
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 1 );
REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 62_norm);
REQUIRE( synth.getVoiceView(0)->releasedOrFree()); // got killed
REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm);
REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // got killed
REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 63_norm);
REQUIRE( synth.getVoiceView(2)->releasedOrFree()); // got killed
REQUIRE( synth.getVoiceView(3)->getTriggerValue() == 63_norm);
REQUIRE(!synth.getVoiceView(3)->releasedOrFree());
}
TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same polyphony group (default, with keyswitches)")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<global> sw_lokey=36 sw_hikey=37 sw_default=36
<region> sw_last=36 key=48 note_polyphony=1 sample=*saw
<region> sw_last=37 key=48 transpose=12 note_polyphony=1 sample=*tri
)");
synth.noteOn(0, 48, 63 );
REQUIRE( synth.getNumActiveVoices(true) == 1);
synth.cc(0, 64, 127);
synth.noteOn(0, 37, 127);
synth.noteOff(0, 37, 0);
synth.noteOn(0, 48, 64);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 1 );
REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm);
REQUIRE( synth.getVoiceView(0)->releasedOrFree());
REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 64_norm);
REQUIRE(!synth.getVoiceView(1)->releasedOrFree());
}
TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<region> group=1 sample=*saw key=64 note_polyphony=1
<region> group=2 sample=*sine key=64 note_polyphony=1
)");
synth.noteOn(0, 64, 62 );
synth.noteOn(0, 64, 63 );
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)->releasedOrFree()); // got killed
REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm);
REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // got killed
REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 63_norm);
REQUIRE(!synth.getVoiceView(2)->releasedOrFree());
REQUIRE( synth.getVoiceView(3)->getTriggerValue() == 63_norm);
REQUIRE(!synth.getVoiceView(3)->releasedOrFree());
}
TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups (with keyswitches)")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, blockSize };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<global> sw_lokey=36 sw_hikey=37 sw_default=36
<region> group=1 sw_last=36 key=48 note_polyphony=1 sample=*saw
<region> group=2 sw_last=37 key=48 transpose=12 note_polyphony=1 sample=*tri
)");
synth.noteOn(0, 48, 63 );
REQUIRE( synth.getNumActiveVoices(true) == 1);
synth.cc(0, 64, 127);
synth.noteOn(0, 37, 127);
synth.noteOff(0, 37, 0);
synth.noteOn(0, 48, 64);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
synth.renderBlock(buffer);
REQUIRE(numPlayingVoices(synth) == 2 );
REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm);
REQUIRE(!synth.getVoiceView(0)->releasedOrFree());
REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 64_norm);
REQUIRE(!synth.getVoiceView(1)->releasedOrFree());
}

View file

@ -4,7 +4,7 @@
// 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 "RegionTHelpers.h"
#include "TestHelpers.h"
#include "sfizz/MidiState.h"
#include "sfizz/Region.h"
#include "sfizz/SfzHelpers.h"
@ -195,6 +195,22 @@ TEST_CASE("[Region] Parsing opcodes")
REQUIRE(region.offMode == SfzOffMode::fast);
region.parseOpcode({ "off_mode", "normal" });
REQUIRE(region.offMode == SfzOffMode::normal);
region.parseOpcode({ "off_mode", "time" });
REQUIRE(region.offMode == SfzOffMode::time);
}
SECTION("off_time")
{
REQUIRE(region.offTime == 0.006f);
REQUIRE(region.offMode == SfzOffMode::fast);
region.parseOpcode({ "off_time", "0.1" });
REQUIRE(region.offTime == 0.1f);
REQUIRE(region.offMode == SfzOffMode::time);
region.parseOpcode({ "off_time", "0" });
REQUIRE(region.offTime == 0.0f);
region.parseOpcode({ "off_time", "0.1" });
region.parseOpcode({ "off_time", "-1" });
REQUIRE(region.offTime == 0.0f);
}
SECTION("lokey, hikey, and key")

View file

@ -8,6 +8,7 @@
#include "sfizz/SisterVoiceRing.h"
#include "sfizz/SfzHelpers.h"
#include "sfizz/NumericId.h"
#include "TestHelpers.h"
#include <algorithm>
#include "catch2/catch.hpp"
using namespace Catch::literals;
@ -607,7 +608,8 @@ TEST_CASE("[Synth] Sisters and off-by")
REQUIRE( synth.getNumActiveVoices(true) == 2 );
synth.noteOn(0, 63, 85);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
synth.renderBlock(buffer);
for (unsigned i = 0; i < 100; ++i)
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(0)) == 1 );
}
@ -735,30 +737,6 @@ TEST_CASE("[Synth] Sustain threshold")
REQUIRE( synth.getNumActiveVoices(true) == 5 );
}
template<class C>
void sortAll(C& container)
{
std::sort(container.begin(), container.end());
}
template<class C, class... Args>
void sortAll(C& container, Args&... others)
{
std::sort(container.begin(), container.end());
sortAll(others...);
}
const std::vector<const sfz::Voice*> getActiveVoices(const sfz::Synth& synth)
{
std::vector<const sfz::Voice*> activeVoices;
for (int i = 0; i < synth.getNumVoices(); ++i) {
const auto* voice = synth.getVoiceView(i);
if (!voice->isFree())
activeVoices.push_back(voice);
}
return activeVoices;
}
TEST_CASE("[Synth] Release (Multiple notes, release_key ignores the pedal)")
{
sfz::Synth synth;

View file

@ -4,7 +4,7 @@
// 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 "RegionTHelpers.h"
#include "TestHelpers.h"
#include "sfizz/modulations/ModId.h"
size_t RegionCCView::size() const
@ -39,3 +39,21 @@ bool RegionCCView::match(const sfz::Region::Connection& conn) const
{
return conn.source.id() == sfz::ModId::Controller && conn.target == target_;
}
const std::vector<const sfz::Voice*> getActiveVoices(const sfz::Synth& synth)
{
std::vector<const sfz::Voice*> activeVoices;
for (int i = 0; i < synth.getNumVoices(); ++i) {
const auto* voice = synth.getVoiceView(i);
if (!voice->isFree())
activeVoices.push_back(voice);
}
return activeVoices;
}
unsigned numPlayingVoices(const sfz::Synth& synth)
{
return absl::c_count_if(getActiveVoices(synth), [](const sfz::Voice* v) {
return !v->releasedOrFree();
});
}

View file

@ -5,6 +5,7 @@
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#pragma once
#include "sfizz/Synth.h"
#include "sfizz/Region.h"
#include "sfizz/modulations/ModKey.h"
@ -26,3 +27,32 @@ private:
const sfz::Region& region_;
sfz::ModKey target_;
};
template<class C>
void sortAll(C& container)
{
std::sort(container.begin(), container.end());
}
template<class C, class... Args>
void sortAll(C& container, Args&... others)
{
std::sort(container.begin(), container.end());
sortAll(others...);
}
/**
* @brief Get active voices from the synth
*
* @param synth
* @return const std::vector<const sfz::Voice*>
*/
const std::vector<const sfz::Voice*> getActiveVoices(const sfz::Synth& synth);
/**
* @brief Count the number of playing (unreleased) voices from the synth
*
* @param synth
* @return unsigned
*/
unsigned numPlayingVoices(const sfz::Synth& synth);