Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release logic (multiple notes for release=trigger + rt_dead handling) #324

Merged
merged 8 commits into from
Aug 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/sfizz/Defaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 39 additions & 22 deletions src/sfizz/Region.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,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
Expand Down Expand Up @@ -161,7 +161,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"):
Expand All @@ -181,7 +181,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
Expand Down Expand Up @@ -274,7 +283,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;

Expand Down Expand Up @@ -337,7 +346,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&
Expand Down Expand Up @@ -468,7 +477,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"):
Expand All @@ -480,7 +489,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&"):
Expand Down Expand Up @@ -516,7 +525,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"):
Expand Down Expand Up @@ -636,7 +645,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;
Expand Down Expand Up @@ -745,7 +754,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;
Expand Down Expand Up @@ -1031,24 +1040,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
Expand All @@ -1062,11 +1084,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;

Expand Down
5 changes: 4 additions & 1 deletion src/sfizz/Region.h
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,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
Expand Down Expand Up @@ -376,6 +377,9 @@ struct Region {

// Parent
RegionSet* parent { nullptr };

// Started notes
std::vector<std::pair<int, float>> delayedReleases;
private:
const MidiState& midiState;
bool keySwitched { true };
Expand All @@ -384,7 +388,6 @@ struct Region {
bool pitchSwitched { true };
bool bpmSwitched { true };
bool aftertouchSwitched { true };
bool noteIsOff { false };
std::bitset<config::numCCs> ccSwitched;
absl::string_view defaultPath { "" };

Expand Down
68 changes: 60 additions & 8 deletions src/sfizz/Synth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,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));
}

Expand Down Expand Up @@ -834,13 +837,36 @@ 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);
SisterVoiceRingBuilder ring;

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;
Expand Down Expand Up @@ -998,18 +1024,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);
Expand Down
59 changes: 52 additions & 7 deletions tests/RegionT.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1628,6 +1628,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);
Expand Down Expand Up @@ -1749,7 +1761,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")
{
Expand All @@ -1765,30 +1778,62 @@ 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" });
midiState.ccEvent(0, 64, 0.0f);
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 );
}
}
Loading