Merge pull request #324 from paulfd/release-multiple-voices

Release logic (multiple notes for release=trigger + rt_dead handling)
This commit is contained in:
Paul Ferrand 2020-08-09 22:18:29 +02:00 committed by GitHub
commit 20bc1e93d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 408 additions and 46 deletions

View file

@ -143,6 +143,7 @@ namespace Default
constexpr SfzCrossfadeCurve crossfadeVelCurve { SfzCrossfadeCurve::power };
constexpr SfzCrossfadeCurve crossfadeCCCurve { SfzCrossfadeCurve::power };
constexpr float rtDecay { 0.0f };
constexpr bool rtDead { false };
constexpr Range<float> rtDecayRange { 0.0f, 200.0f };
// Performance parameters: Filters

View file

@ -112,7 +112,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
loopMode = SfzLoopMode::loop_sustain;
break;
default:
DBG("Unkown loop mode:" << std::string(opcode.value));
DBG("Unkown loop mode:" << opcode.value);
}
break;
case hash("loop_end"): // also loopend
@ -162,7 +162,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
offMode = SfzOffMode::normal;
break;
default:
DBG("Unkown off mode:" << std::string(opcode.value));
DBG("Unkown off mode:" << opcode.value);
}
break;
case hash("polyphony"):
@ -182,7 +182,16 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
selfMask = SfzSelfMask::dontMask;
break;
default:
DBG("Unkown self mask value:" << std::string(opcode.value));
DBG("Unkown self mask value:" << opcode.value);
}
break;
case hash("rt_dead"):
if (opcode.value == "on") {
rtDead = true;
} else if (opcode.value == "off") {
rtDead = false;
} else {
DBG("Unkown rt_dead value:" << opcode.value);
}
break;
// Region logic: key mapping
@ -275,7 +284,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
velocityOverride = SfzVelocityOverride::previous;
break;
default:
DBG("Unknown velocity mode: " << std::string(opcode.value));
DBG("Unknown velocity mode: " << opcode.value);
}
break;
@ -338,7 +347,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
trigger = SfzTrigger::release_key;
break;
default:
DBG("Unknown trigger mode: " << std::string(opcode.value));
DBG("Unknown trigger mode: " << opcode.value);
}
break;
case hash("start_locc&"): // also on_locc&
@ -469,7 +478,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
crossfadeKeyCurve = SfzCrossfadeCurve::gain;
break;
default:
DBG("Unknown crossfade power curve: " << std::string(opcode.value));
DBG("Unknown crossfade power curve: " << opcode.value);
}
break;
case hash("xf_velcurve"):
@ -481,7 +490,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
crossfadeVelCurve = SfzCrossfadeCurve::gain;
break;
default:
DBG("Unknown crossfade power curve: " << std::string(opcode.value));
DBG("Unknown crossfade power curve: " << opcode.value);
}
break;
case hash("xfin_locc&"):
@ -517,7 +526,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
crossfadeCCCurve = SfzCrossfadeCurve::gain;
break;
default:
DBG("Unknown crossfade power curve: " << std::string(opcode.value));
DBG("Unknown crossfade power curve: " << opcode.value);
}
break;
case hash("rt_decay"):
@ -637,7 +646,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
filters[filterIndex].type = *ftype;
else {
filters[filterIndex].type = FilterType::kFilterNone;
DBG("Unknown filter type: " << std::string(opcode.value));
DBG("Unknown filter type: " << opcode.value);
}
}
break;
@ -746,7 +755,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
equalizers[eqNumber - 1].type = *ftype;
else {
equalizers[eqNumber - 1].type = EqType::kEqNone;
DBG("Unknown EQ type: " << std::string(opcode.value));
DBG("Unknown EQ type: " << opcode.value);
}
}
break;
@ -1054,24 +1063,37 @@ bool sfz::Region::registerNoteOff(int noteNumber, float velocity, float randValu
keySwitched = true;
}
const bool keyOk = keyRange.containsWithEnd(noteNumber);
if (!isSwitchedOn())
return false;
if (!triggerOnNote)
return false;
// Prerequisites
const bool keyOk = keyRange.containsWithEnd(noteNumber);
const bool velOk = velocityRange.containsWithEnd(velocity);
const bool randOk = randRange.contains(randValue);
bool releaseTrigger = (trigger == SfzTrigger::release_key);
if (!(velOk && keyOk && randOk))
return false;
// Release logic
if (trigger == SfzTrigger::release_key)
return true;
if (trigger == SfzTrigger::release) {
if (midiState.getCCValue(sustainCC) < sustainThreshold)
releaseTrigger = true;
else
noteIsOff = true;
return true;
// If we reach this part, we're storing the notes to delay their release on CC up
// This is handled by the Synth object
delayedReleases.emplace_back(noteNumber, midiState.getNoteVelocity(noteNumber));
}
return keyOk && velOk && randOk && releaseTrigger;
return false;
}
bool sfz::Region::registerCC(int ccNumber, float ccValue) noexcept
@ -1085,11 +1107,6 @@ bool sfz::Region::registerCC(int ccNumber, float ccValue) noexcept
if (!isSwitchedOn())
return false;
if (sustainCC == ccNumber && ccValue < sustainThreshold && noteIsOff) {
noteIsOff = false;
return true;
}
if (!triggerOnCC)
return false;

View file

@ -289,6 +289,7 @@ struct Region {
absl::optional<uint32_t> notePolyphony {}; // note_polyphony
unsigned polyphony { config::maxVoices }; // polyphony
SfzSelfMask selfMask { Default::selfMask };
bool rtDead { Default::rtDead };
// Region logic: key mapping
Range<uint8_t> keyRange { Default::keyRange }; //lokey, hikey and key
@ -382,6 +383,9 @@ struct Region {
// Parent
RegionSet* parent { nullptr };
// Started notes
std::vector<std::pair<int, float>> delayedReleases;
private:
const MidiState& midiState;
bool keySwitched { true };
@ -390,7 +394,6 @@ private:
bool pitchSwitched { true };
bool bpmSwitched { true };
bool aftertouchSwitched { true };
bool noteIsOff { false };
std::bitset<config::numCCs> ccSwitched;
absl::string_view defaultPath { "" };

View file

@ -168,6 +168,9 @@ void sfz::Synth::buildRegion(const std::vector<Opcode>& regionOpcodes)
lastRegion->parent = currentSet;
currentSet->addRegion(lastRegion.get());
// Adapt the size of the delayed releases to avoid allocating later on
lastRegion->delayedReleases.reserve(lastRegion->keyRange.length());
regions.push_back(std::move(lastRegion));
}
@ -849,6 +852,15 @@ 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::noteOffDispatch(int delay, int noteNumber, float velocity) noexcept
{
const auto randValue = randNoteDistribution(Random::randomGenerator);
@ -856,6 +868,20 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex
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)
continue;
@ -1015,18 +1041,44 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept
SisterVoiceRingBuilder ring;
for (auto& region : ccActivationLists[ccNumber]) {
if (region->registerCC(ccNumber, normValue)) {
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();
continue;
}
}
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();
} else if (region->registerCC(ccNumber, normValue)) {
auto voice = findFreeVoice();
if (voice == nullptr)
continue;
if (!region->triggerOnCC) {
// This is a sustain trigger
const auto replacedVelocity = resources.midiState.getNoteVelocity(region->pitchKeycenter);
voice->startVoice(region, delay, region->pitchKeycenter, replacedVelocity, Voice::TriggerType::NoteOff);
} else {
voice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC);
}
voice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC);
ring.addVoiceToRing(voice);
RegionSet::registerVoiceInHierarchy(region, voice);

View file

@ -1635,6 +1635,18 @@ TEST_CASE("[Region] Parsing opcodes")
REQUIRE(region.selfMask == SfzSelfMask::dontMask);
}
SECTION("Release dead")
{
REQUIRE(region.rtDead == false);
region.parseOpcode({ "rt_dead", "on" });
REQUIRE(region.rtDead == true);
region.parseOpcode({ "rt_dead", "off" });
REQUIRE(region.rtDead == false);
region.parseOpcode({ "rt_dead", "on" });
region.parseOpcode({ "rt_dead", "garbage" });
REQUIRE(region.rtDead == true);
}
SECTION("amplitude")
{
REQUIRE(region.amplitude == 1.0_a);
@ -1754,7 +1766,8 @@ TEST_CASE("[Region] Release and release key")
{
MidiState midiState;
Region region { 0, midiState };
region.parseOpcode({ "key", "63" });
region.parseOpcode({ "lokey", "63" });
region.parseOpcode({ "hikey", "65" });
region.parseOpcode({ "sample", "*sine" });
SECTION("Release key without sustain")
{
@ -1770,9 +1783,8 @@ TEST_CASE("[Region] Release and release key")
REQUIRE( !region.registerCC(64, 1.0f) );
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) );
REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) );
midiState.ccEvent(0, 64, 0.0f);
REQUIRE( !region.registerCC(64, 0.0f) );
}
SECTION("Release without sustain")
{
region.parseOpcode({ "trigger", "release" });
@ -1780,20 +1792,53 @@ TEST_CASE("[Region] Release and release key")
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) );
REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) );
}
SECTION("Release with sustain")
{
region.parseOpcode({ "trigger", "release" });
midiState.ccEvent(0, 64, 1.0f);
midiState.noteOnEvent(0, 63, 0.5f);
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) );
REQUIRE( !region.registerNoteOff(63, 0.5f, 0.0f) );
REQUIRE( region.delayedReleases.size() == 1 );
std::vector<std::pair<int, float>> expected = {
{ 63, 0.5f }
};
REQUIRE( region.delayedReleases == expected );
}
SECTION("Release with sustain")
SECTION("Release with sustain and 2 notes")
{
region.parseOpcode({ "trigger", "release" });
midiState.ccEvent(0, 64, 1.0f);
midiState.noteOnEvent(0, 63, 0.5f);
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) );
REQUIRE( !region.registerNoteOff(63, 0.5f, 0.0f) );
midiState.ccEvent(0, 64, 0.0f);
REQUIRE( region.registerCC(64, 0.0f) );
midiState.noteOnEvent(0, 64, 0.6f);
REQUIRE( !region.registerNoteOn(64, 0.6f, 0.0f) );
REQUIRE( !region.registerNoteOff(63, 0.0f, 0.0f) );
REQUIRE( !region.registerNoteOff(64, 0.2f, 0.0f) );
REQUIRE( region.delayedReleases.size() == 2 );
std::vector<std::pair<int, float>> expected = {
{ 63, 0.5f },
{ 64, 0.6f }
};
REQUIRE( region.delayedReleases == expected );
}
SECTION("Release with sustain and 2 notes but 1 outside")
{
region.parseOpcode({ "trigger", "release" });
midiState.ccEvent(0, 64, 1.0f);
midiState.noteOnEvent(0, 63, 0.5f);
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) );
midiState.noteOnEvent(0, 66, 0.6f);
REQUIRE( !region.registerNoteOn(66, 0.6f, 0.0f) );
REQUIRE( !region.registerNoteOff(63, 0.0f, 0.0f) );
REQUIRE( !region.registerNoteOff(66, 0.2f, 0.0f) );
REQUIRE( region.delayedReleases.size() == 1 );
std::vector<std::pair<int, float>> expected = {
{ 63, 0.5f }
};
REQUIRE( region.delayedReleases == expected );
}
}

View file

@ -8,6 +8,7 @@
#include "sfizz/SisterVoiceRing.h"
#include "sfizz/SfzHelpers.h"
#include "sfizz/NumericId.h"
#include <algorithm>
#include "catch2/catch.hpp"
using namespace Catch::literals;
using namespace sfz::literals;
@ -200,6 +201,7 @@ TEST_CASE("[Synth] Trigger=release and an envelope properly kills the voice at t
synth.setNumVoices(1);
synth.loadSfzString(fs::current_path() / "tests/TestFiles/envelope_trigger_release.sfz", R"(
<group> lovel=0 hivel=127
<region> sample=*silence
<region> trigger=release sample=*noise loop_mode=one_shot
ampeg_attack=0.02 ampeg_decay=0.02 ampeg_release=0.1 ampeg_sustain=0
)");
@ -626,14 +628,48 @@ TEST_CASE("[Synth] Release")
{
sfz::Synth synth;
synth.loadSfzString(fs::current_path(), R"(
<region> key=62 sample=*silence
<region> key=62 sample=*sine trigger=release
)");
synth.noteOn(0, 62, 85);
synth.cc(0, 64, 127);
synth.noteOff(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
synth.cc(0, 64, 0);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
synth.cc(0, 64, 0);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
}
TEST_CASE("[Synth] Release (pedal was already down)")
{
sfz::Synth synth;
synth.loadSfzString(fs::current_path(), R"(
<region> key=62 sample=*silence
<region> key=62 sample=*sine trigger=release
)");
synth.cc(0, 64, 127);
synth.noteOn(0, 62, 85);
synth.noteOff(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
synth.cc(0, 64, 0);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
}
TEST_CASE("[Synth] Release samples don't play unless there is another playing region that matches")
{
sfz::Synth synth;
synth.loadSfzString(fs::current_path(), R"(
<region> key=62 sample=*sine trigger=release
)");
synth.noteOn(0, 62, 85);
synth.noteOff(0, 62, 0);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
synth.cc(0, 64, 127);
synth.noteOn(0, 62, 85);
synth.noteOff(0, 62, 0);
synth.cc(0, 64, 0);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
}
TEST_CASE("[Synth] Release key (Different sustain CC)")
@ -654,14 +690,15 @@ TEST_CASE("[Synth] Release (Different sustain CC)")
sfz::Synth synth;
synth.loadSfzString(fs::current_path(), R"(
<global>sustain_cc=54
<region> key=62 sample=*silence
<region> key=62 sample=*sine trigger=release
)");
synth.noteOn(0, 62, 85);
synth.cc(0, 54, 127);
synth.noteOff(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
synth.cc(0, 54, 0);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
synth.cc(0, 54, 0);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
}
TEST_CASE("[Synth] Sustain threshold default")
@ -681,17 +718,224 @@ TEST_CASE("[Synth] Sustain threshold")
sfz::Synth synth;
synth.loadSfzString(fs::current_path(), R"(
<global> sustain_lo=63
<region> key=62 sample=*silence
<region> key=62 sample=*sine trigger=release
)");
synth.noteOn(0, 62, 85);
synth.cc(0, 64, 1);
synth.noteOff(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
synth.noteOn(0, 62, 85);
synth.noteOff(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
synth.noteOn(0, 62, 85);
synth.noteOff(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 4 );
synth.noteOn(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 5 );
synth.cc(0, 64, 64);
synth.noteOff(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
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;
synth.loadSfzString(fs::current_path(), R"(
<region> lokey=62 hikey=64 sample=*sine trigger=release_key
)");
synth.noteOn(0, 62, 85);
synth.noteOn(0, 63, 78);
synth.noteOn(0, 64, 34);
synth.cc(0, 64, 127);
synth.noteOff(0, 64, 0);
synth.noteOff(0, 63, 2);
synth.noteOff(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
std::vector<float> requiredVelocities { 34_norm, 78_norm, 85_norm};
std::vector<float> actualVelocities;
for (auto* v: getActiveVoices(synth)) {
actualVelocities.push_back(v->getTriggerValue());
}
sortAll(requiredVelocities, actualVelocities);
REQUIRE( requiredVelocities == actualVelocities );
}
TEST_CASE("[Synth] Release (Multiple notes, release, cleared the delayed voices after)")
{
sfz::Synth synth;
synth.loadSfzString(fs::current_path(), R"(
<region> lokey=62 hikey=64 sample=*silence
<region> lokey=62 hikey=64 sample=*sine trigger=release
loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1
)");
synth.noteOn(0, 62, 85);
synth.noteOn(0, 63, 78);
synth.noteOn(0, 64, 34);
synth.cc(0, 64, 127);
synth.noteOff(0, 64, 0);
synth.noteOff(0, 63, 2);
synth.noteOff(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
synth.cc(0, 64, 0);
REQUIRE( synth.getNumActiveVoices(true) == 6 );
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());
}
sortAll(requiredVelocities, actualVelocities);
REQUIRE( requiredVelocities == actualVelocities );
REQUIRE( synth.getRegionView(1)->delayedReleases.empty() );
}
TEST_CASE("[Synth] Release (Multiple notes after pedal is down, release, cleared the delayed voices after)")
{
sfz::Synth synth;
synth.loadSfzString(fs::current_path(), R"(
<region> lokey=62 hikey=64 sample=*silence
<region> lokey=62 hikey=64 sample=*sine trigger=release
loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1
)");
synth.cc(0, 64, 127);
synth.noteOn(1, 62, 85);
synth.noteOn(1, 63, 78);
synth.noteOn(1, 64, 34);
synth.noteOff(2, 64, 0);
synth.noteOff(2, 63, 2);
synth.noteOff(2, 62, 3);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
synth.cc(3, 64, 0);
REQUIRE( synth.getNumActiveVoices(true) == 6 );
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());
}
sortAll(requiredVelocities, actualVelocities);
REQUIRE( requiredVelocities == actualVelocities );
REQUIRE( synth.getRegionView(1)->delayedReleases.empty() );
}
TEST_CASE("[Synth] Release (Multiple note ons during pedal down)")
{
sfz::Synth synth;
synth.loadSfzString(fs::current_path(), R"(
<region> lokey=62 hikey=64 sample=*silence
<region> lokey=62 hikey=64 sample=*sine trigger=release
loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1
)");
synth.noteOn(0, 62, 85);
synth.cc(0, 64, 127);
synth.noteOff(0, 62, 0);
synth.noteOn(0, 62, 78);
synth.noteOff(0, 62, 2);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
synth.cc(0, 64, 0);
REQUIRE( synth.getNumActiveVoices(true) == 4 );
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());
}
sortAll(requiredVelocities, actualVelocities);
REQUIRE( requiredVelocities == actualVelocities );
REQUIRE( synth.getRegionView(1)->delayedReleases.empty() );
}
TEST_CASE("[Synth] No release sample after the main sample stopped sounding by default")
{
sfz::Synth synth;
synth.setSamplesPerBlock(4096);
sfz::AudioBuffer<float> buffer { 2, 4096 };
synth.loadSfzString(fs::current_path(), R"(
<region> lokey=62 hikey=64 sample=tests/TestFiles/closedhat.wav loop_mode=one_shot
<region> lokey=62 hikey=64 sample=*sine trigger=release
loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1
)");
synth.noteOn(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
for (unsigned i = 0; i < 100; ++i) {
synth.renderBlock(buffer);
}
REQUIRE( synth.getNumActiveVoices(true) == 0 );
synth.noteOff(0, 62, 0);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
synth.noteOn(0, 62, 85);
synth.cc(0, 64, 127);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
for (unsigned i = 0; i < 100; ++i) {
synth.renderBlock(buffer);
}
REQUIRE( synth.getNumActiveVoices(true) == 0 );
synth.noteOff(0, 62, 0);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
synth.cc(0, 64, 0);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getRegionView(1)->delayedReleases.empty() );
}
TEST_CASE("[Synth] If rt_dead is active the release sample can sound after the attack sample died")
{
sfz::Synth synth;
synth.setSamplesPerBlock(4096);
sfz::AudioBuffer<float> buffer { 2, 4096 };
synth.loadSfzString(fs::current_path(), R"(
<region> lokey=62 hikey=64 sample=tests/TestFiles/closedhat.wav loop_mode=one_shot
<region> lokey=62 hikey=64 sample=*sine trigger=release
loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1
)");
synth.noteOn(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
for (unsigned i = 0; i < 100; ++i) {
synth.renderBlock(buffer);
}
REQUIRE( synth.getNumActiveVoices(true) == 0 );
synth.noteOff(0, 62, 0);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
synth.noteOn(0, 62, 85);
synth.cc(0, 64, 127);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
for (unsigned i = 0; i < 100; ++i) {
synth.renderBlock(buffer);
}
REQUIRE( synth.getNumActiveVoices(true) == 0 );
synth.noteOff(0, 62, 0);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
synth.cc(0, 64, 0);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getRegionView(1)->delayedReleases.empty() );
}