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 crossfadeVelCurve { SfzCrossfadeCurve::power };
|
||||||
constexpr SfzCrossfadeCurve crossfadeCCCurve { SfzCrossfadeCurve::power };
|
constexpr SfzCrossfadeCurve crossfadeCCCurve { SfzCrossfadeCurve::power };
|
||||||
constexpr float rtDecay { 0.0f };
|
constexpr float rtDecay { 0.0f };
|
||||||
|
constexpr bool rtDead { false };
|
||||||
constexpr Range<float> rtDecayRange { 0.0f, 200.0f };
|
constexpr Range<float> rtDecayRange { 0.0f, 200.0f };
|
||||||
|
|
||||||
// Performance parameters: Filters
|
// Performance parameters: Filters
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
|
||||||
loopMode = SfzLoopMode::loop_sustain;
|
loopMode = SfzLoopMode::loop_sustain;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
DBG("Unkown loop mode:" << std::string(opcode.value));
|
DBG("Unkown loop mode:" << opcode.value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case hash("loop_end"): // also loopend
|
case hash("loop_end"): // also loopend
|
||||||
|
|
@ -162,7 +162,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
|
||||||
offMode = SfzOffMode::normal;
|
offMode = SfzOffMode::normal;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
DBG("Unkown off mode:" << std::string(opcode.value));
|
DBG("Unkown off mode:" << opcode.value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case hash("polyphony"):
|
case hash("polyphony"):
|
||||||
|
|
@ -182,7 +182,16 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
|
||||||
selfMask = SfzSelfMask::dontMask;
|
selfMask = SfzSelfMask::dontMask;
|
||||||
break;
|
break;
|
||||||
default:
|
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;
|
break;
|
||||||
// Region logic: key mapping
|
// Region logic: key mapping
|
||||||
|
|
@ -275,7 +284,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
|
||||||
velocityOverride = SfzVelocityOverride::previous;
|
velocityOverride = SfzVelocityOverride::previous;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
DBG("Unknown velocity mode: " << std::string(opcode.value));
|
DBG("Unknown velocity mode: " << opcode.value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -338,7 +347,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
|
||||||
trigger = SfzTrigger::release_key;
|
trigger = SfzTrigger::release_key;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
DBG("Unknown trigger mode: " << std::string(opcode.value));
|
DBG("Unknown trigger mode: " << opcode.value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case hash("start_locc&"): // also on_locc&
|
case hash("start_locc&"): // also on_locc&
|
||||||
|
|
@ -469,7 +478,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
|
||||||
crossfadeKeyCurve = SfzCrossfadeCurve::gain;
|
crossfadeKeyCurve = SfzCrossfadeCurve::gain;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
DBG("Unknown crossfade power curve: " << std::string(opcode.value));
|
DBG("Unknown crossfade power curve: " << opcode.value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case hash("xf_velcurve"):
|
case hash("xf_velcurve"):
|
||||||
|
|
@ -481,7 +490,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
|
||||||
crossfadeVelCurve = SfzCrossfadeCurve::gain;
|
crossfadeVelCurve = SfzCrossfadeCurve::gain;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
DBG("Unknown crossfade power curve: " << std::string(opcode.value));
|
DBG("Unknown crossfade power curve: " << opcode.value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case hash("xfin_locc&"):
|
case hash("xfin_locc&"):
|
||||||
|
|
@ -517,7 +526,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
|
||||||
crossfadeCCCurve = SfzCrossfadeCurve::gain;
|
crossfadeCCCurve = SfzCrossfadeCurve::gain;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
DBG("Unknown crossfade power curve: " << std::string(opcode.value));
|
DBG("Unknown crossfade power curve: " << opcode.value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case hash("rt_decay"):
|
case hash("rt_decay"):
|
||||||
|
|
@ -637,7 +646,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
|
||||||
filters[filterIndex].type = *ftype;
|
filters[filterIndex].type = *ftype;
|
||||||
else {
|
else {
|
||||||
filters[filterIndex].type = FilterType::kFilterNone;
|
filters[filterIndex].type = FilterType::kFilterNone;
|
||||||
DBG("Unknown filter type: " << std::string(opcode.value));
|
DBG("Unknown filter type: " << opcode.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -746,7 +755,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
|
||||||
equalizers[eqNumber - 1].type = *ftype;
|
equalizers[eqNumber - 1].type = *ftype;
|
||||||
else {
|
else {
|
||||||
equalizers[eqNumber - 1].type = EqType::kEqNone;
|
equalizers[eqNumber - 1].type = EqType::kEqNone;
|
||||||
DBG("Unknown EQ type: " << std::string(opcode.value));
|
DBG("Unknown EQ type: " << opcode.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -1054,24 +1063,37 @@ bool sfz::Region::registerNoteOff(int noteNumber, float velocity, float randValu
|
||||||
keySwitched = true;
|
keySwitched = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool keyOk = keyRange.containsWithEnd(noteNumber);
|
|
||||||
|
|
||||||
if (!isSwitchedOn())
|
if (!isSwitchedOn())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (!triggerOnNote)
|
if (!triggerOnNote)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// Prerequisites
|
||||||
|
|
||||||
|
const bool keyOk = keyRange.containsWithEnd(noteNumber);
|
||||||
const bool velOk = velocityRange.containsWithEnd(velocity);
|
const bool velOk = velocityRange.containsWithEnd(velocity);
|
||||||
const bool randOk = randRange.contains(randValue);
|
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 (trigger == SfzTrigger::release) {
|
||||||
if (midiState.getCCValue(sustainCC) < sustainThreshold)
|
if (midiState.getCCValue(sustainCC) < sustainThreshold)
|
||||||
releaseTrigger = true;
|
return true;
|
||||||
else
|
|
||||||
noteIsOff = 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
|
bool sfz::Region::registerCC(int ccNumber, float ccValue) noexcept
|
||||||
|
|
@ -1085,11 +1107,6 @@ bool sfz::Region::registerCC(int ccNumber, float ccValue) noexcept
|
||||||
if (!isSwitchedOn())
|
if (!isSwitchedOn())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (sustainCC == ccNumber && ccValue < sustainThreshold && noteIsOff) {
|
|
||||||
noteIsOff = false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!triggerOnCC)
|
if (!triggerOnCC)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -289,6 +289,7 @@ struct Region {
|
||||||
absl::optional<uint32_t> notePolyphony {}; // note_polyphony
|
absl::optional<uint32_t> notePolyphony {}; // note_polyphony
|
||||||
unsigned polyphony { config::maxVoices }; // polyphony
|
unsigned polyphony { config::maxVoices }; // polyphony
|
||||||
SfzSelfMask selfMask { Default::selfMask };
|
SfzSelfMask selfMask { Default::selfMask };
|
||||||
|
bool rtDead { Default::rtDead };
|
||||||
|
|
||||||
// Region logic: key mapping
|
// Region logic: key mapping
|
||||||
Range<uint8_t> keyRange { Default::keyRange }; //lokey, hikey and key
|
Range<uint8_t> keyRange { Default::keyRange }; //lokey, hikey and key
|
||||||
|
|
@ -382,6 +383,9 @@ struct Region {
|
||||||
|
|
||||||
// Parent
|
// Parent
|
||||||
RegionSet* parent { nullptr };
|
RegionSet* parent { nullptr };
|
||||||
|
|
||||||
|
// Started notes
|
||||||
|
std::vector<std::pair<int, float>> delayedReleases;
|
||||||
private:
|
private:
|
||||||
const MidiState& midiState;
|
const MidiState& midiState;
|
||||||
bool keySwitched { true };
|
bool keySwitched { true };
|
||||||
|
|
@ -390,7 +394,6 @@ private:
|
||||||
bool pitchSwitched { true };
|
bool pitchSwitched { true };
|
||||||
bool bpmSwitched { true };
|
bool bpmSwitched { true };
|
||||||
bool aftertouchSwitched { true };
|
bool aftertouchSwitched { true };
|
||||||
bool noteIsOff { false };
|
|
||||||
std::bitset<config::numCCs> ccSwitched;
|
std::bitset<config::numCCs> ccSwitched;
|
||||||
absl::string_view defaultPath { "" };
|
absl::string_view defaultPath { "" };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,9 @@ void sfz::Synth::buildRegion(const std::vector<Opcode>& regionOpcodes)
|
||||||
lastRegion->parent = currentSet;
|
lastRegion->parent = currentSet;
|
||||||
currentSet->addRegion(lastRegion.get());
|
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));
|
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);
|
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
|
void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noexcept
|
||||||
{
|
{
|
||||||
const auto randValue = randNoteDistribution(Random::randomGenerator);
|
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]) {
|
for (auto& region : noteActivationLists[noteNumber]) {
|
||||||
if (region->registerNoteOff(noteNumber, velocity, randValue)) {
|
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();
|
auto voice = findFreeVoice();
|
||||||
if (voice == nullptr)
|
if (voice == nullptr)
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -1015,18 +1041,44 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept
|
||||||
SisterVoiceRingBuilder ring;
|
SisterVoiceRingBuilder ring;
|
||||||
|
|
||||||
for (auto& region : ccActivationLists[ccNumber]) {
|
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();
|
auto voice = findFreeVoice();
|
||||||
if (voice == nullptr)
|
if (voice == nullptr)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (!region->triggerOnCC) {
|
|
||||||
// This is a sustain trigger
|
voice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
ring.addVoiceToRing(voice);
|
ring.addVoiceToRing(voice);
|
||||||
RegionSet::registerVoiceInHierarchy(region, voice);
|
RegionSet::registerVoiceInHierarchy(region, voice);
|
||||||
|
|
|
||||||
|
|
@ -1635,6 +1635,18 @@ TEST_CASE("[Region] Parsing opcodes")
|
||||||
REQUIRE(region.selfMask == SfzSelfMask::dontMask);
|
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")
|
SECTION("amplitude")
|
||||||
{
|
{
|
||||||
REQUIRE(region.amplitude == 1.0_a);
|
REQUIRE(region.amplitude == 1.0_a);
|
||||||
|
|
@ -1754,7 +1766,8 @@ TEST_CASE("[Region] Release and release key")
|
||||||
{
|
{
|
||||||
MidiState midiState;
|
MidiState midiState;
|
||||||
Region region { 0, midiState };
|
Region region { 0, midiState };
|
||||||
region.parseOpcode({ "key", "63" });
|
region.parseOpcode({ "lokey", "63" });
|
||||||
|
region.parseOpcode({ "hikey", "65" });
|
||||||
region.parseOpcode({ "sample", "*sine" });
|
region.parseOpcode({ "sample", "*sine" });
|
||||||
SECTION("Release key without sustain")
|
SECTION("Release key without sustain")
|
||||||
{
|
{
|
||||||
|
|
@ -1770,9 +1783,8 @@ TEST_CASE("[Region] Release and release key")
|
||||||
REQUIRE( !region.registerCC(64, 1.0f) );
|
REQUIRE( !region.registerCC(64, 1.0f) );
|
||||||
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) );
|
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) );
|
||||||
REQUIRE( region.registerNoteOff(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")
|
SECTION("Release without sustain")
|
||||||
{
|
{
|
||||||
region.parseOpcode({ "trigger", "release" });
|
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.registerNoteOn(63, 0.5f, 0.0f) );
|
||||||
REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) );
|
REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) );
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("Release with sustain")
|
SECTION("Release with sustain")
|
||||||
{
|
{
|
||||||
region.parseOpcode({ "trigger", "release" });
|
region.parseOpcode({ "trigger", "release" });
|
||||||
midiState.ccEvent(0, 64, 1.0f);
|
midiState.ccEvent(0, 64, 1.0f);
|
||||||
|
midiState.noteOnEvent(0, 63, 0.5f);
|
||||||
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) );
|
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) );
|
||||||
REQUIRE( !region.registerNoteOff(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" });
|
region.parseOpcode({ "trigger", "release" });
|
||||||
midiState.ccEvent(0, 64, 1.0f);
|
midiState.ccEvent(0, 64, 1.0f);
|
||||||
|
midiState.noteOnEvent(0, 63, 0.5f);
|
||||||
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) );
|
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) );
|
||||||
REQUIRE( !region.registerNoteOff(63, 0.5f, 0.0f) );
|
midiState.noteOnEvent(0, 64, 0.6f);
|
||||||
midiState.ccEvent(0, 64, 0.0f);
|
REQUIRE( !region.registerNoteOn(64, 0.6f, 0.0f) );
|
||||||
REQUIRE( region.registerCC(64, 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/SisterVoiceRing.h"
|
||||||
#include "sfizz/SfzHelpers.h"
|
#include "sfizz/SfzHelpers.h"
|
||||||
#include "sfizz/NumericId.h"
|
#include "sfizz/NumericId.h"
|
||||||
|
#include <algorithm>
|
||||||
#include "catch2/catch.hpp"
|
#include "catch2/catch.hpp"
|
||||||
using namespace Catch::literals;
|
using namespace Catch::literals;
|
||||||
using namespace sfz::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.setNumVoices(1);
|
||||||
synth.loadSfzString(fs::current_path() / "tests/TestFiles/envelope_trigger_release.sfz", R"(
|
synth.loadSfzString(fs::current_path() / "tests/TestFiles/envelope_trigger_release.sfz", R"(
|
||||||
<group> lovel=0 hivel=127
|
<group> lovel=0 hivel=127
|
||||||
|
<region> sample=*silence
|
||||||
<region> trigger=release sample=*noise loop_mode=one_shot
|
<region> trigger=release sample=*noise loop_mode=one_shot
|
||||||
ampeg_attack=0.02 ampeg_decay=0.02 ampeg_release=0.1 ampeg_sustain=0
|
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;
|
sfz::Synth synth;
|
||||||
synth.loadSfzString(fs::current_path(), R"(
|
synth.loadSfzString(fs::current_path(), R"(
|
||||||
|
<region> key=62 sample=*silence
|
||||||
<region> key=62 sample=*sine trigger=release
|
<region> key=62 sample=*sine trigger=release
|
||||||
)");
|
)");
|
||||||
synth.noteOn(0, 62, 85);
|
synth.noteOn(0, 62, 85);
|
||||||
synth.cc(0, 64, 127);
|
synth.cc(0, 64, 127);
|
||||||
synth.noteOff(0, 62, 85);
|
synth.noteOff(0, 62, 85);
|
||||||
REQUIRE( synth.getNumActiveVoices(true) == 0 );
|
|
||||||
synth.cc(0, 64, 0);
|
|
||||||
REQUIRE( synth.getNumActiveVoices(true) == 1 );
|
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)")
|
TEST_CASE("[Synth] Release key (Different sustain CC)")
|
||||||
|
|
@ -654,14 +690,15 @@ TEST_CASE("[Synth] Release (Different sustain CC)")
|
||||||
sfz::Synth synth;
|
sfz::Synth synth;
|
||||||
synth.loadSfzString(fs::current_path(), R"(
|
synth.loadSfzString(fs::current_path(), R"(
|
||||||
<global>sustain_cc=54
|
<global>sustain_cc=54
|
||||||
|
<region> key=62 sample=*silence
|
||||||
<region> key=62 sample=*sine trigger=release
|
<region> key=62 sample=*sine trigger=release
|
||||||
)");
|
)");
|
||||||
synth.noteOn(0, 62, 85);
|
synth.noteOn(0, 62, 85);
|
||||||
synth.cc(0, 54, 127);
|
synth.cc(0, 54, 127);
|
||||||
synth.noteOff(0, 62, 85);
|
synth.noteOff(0, 62, 85);
|
||||||
REQUIRE( synth.getNumActiveVoices(true) == 0 );
|
|
||||||
synth.cc(0, 54, 0);
|
|
||||||
REQUIRE( synth.getNumActiveVoices(true) == 1 );
|
REQUIRE( synth.getNumActiveVoices(true) == 1 );
|
||||||
|
synth.cc(0, 54, 0);
|
||||||
|
REQUIRE( synth.getNumActiveVoices(true) == 2 );
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("[Synth] Sustain threshold default")
|
TEST_CASE("[Synth] Sustain threshold default")
|
||||||
|
|
@ -681,17 +718,224 @@ TEST_CASE("[Synth] Sustain threshold")
|
||||||
sfz::Synth synth;
|
sfz::Synth synth;
|
||||||
synth.loadSfzString(fs::current_path(), R"(
|
synth.loadSfzString(fs::current_path(), R"(
|
||||||
<global> sustain_lo=63
|
<global> sustain_lo=63
|
||||||
|
<region> key=62 sample=*silence
|
||||||
<region> key=62 sample=*sine trigger=release
|
<region> key=62 sample=*sine trigger=release
|
||||||
)");
|
)");
|
||||||
synth.noteOn(0, 62, 85);
|
synth.noteOn(0, 62, 85);
|
||||||
synth.cc(0, 64, 1);
|
synth.cc(0, 64, 1);
|
||||||
synth.noteOff(0, 62, 85);
|
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 );
|
REQUIRE( synth.getNumActiveVoices(true) == 2 );
|
||||||
synth.noteOn(0, 62, 85);
|
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.cc(0, 64, 64);
|
||||||
synth.noteOff(0, 62, 85);
|
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