Merge pull request #324 from paulfd/release-multiple-voices
Release logic (multiple notes for release=trigger + rt_dead handling)
This commit is contained in:
commit
20bc1e93d0
6 changed files with 408 additions and 46 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 { "" };
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
260
tests/SynthT.cpp
260
tests/SynthT.cpp
|
|
@ -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() );
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue