From 92f741687ed55ff725916725cf8e062ea0f176ea Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Fri, 25 Jun 2021 17:59:48 +0200 Subject: [PATCH] Differentiate between releasing and "offed" The goal is to be able to off voices in their release stage, possibly shortening the release tail --- src/sfizz/Defaults.cpp | 2 +- src/sfizz/PolyphonyGroup.cpp | 2 +- src/sfizz/RegionSet.cpp | 2 +- src/sfizz/Voice.cpp | 17 +++++++++-- src/sfizz/Voice.h | 11 +++++-- src/sfizz/VoiceManager.cpp | 46 +++++++++++++++++------------ src/sfizz/VoiceManager.h | 1 + src/sfizz/VoiceStealing.cpp | 2 +- tests/PolyphonyT.cpp | 56 ++++++++++++++++++------------------ tests/SynthT.cpp | 8 +++--- tests/TestHelpers.cpp | 8 +++--- 11 files changed, 93 insertions(+), 62 deletions(-) diff --git a/src/sfizz/Defaults.cpp b/src/sfizz/Defaults.cpp index 0cb3803c..5019f1fe 100644 --- a/src/sfizz/Defaults.cpp +++ b/src/sfizz/Defaults.cpp @@ -44,7 +44,7 @@ Int32Spec oscillatorQuality { 1, {0, 3}, 0 }; UInt32Spec group { 0, {0, uint32_t_max}, 0 }; FloatSpec offTime { 6e-3f, {0.0f, 100.0f}, kPermissiveBounds }; UInt32Spec polyphony { config::maxVoices, {0, config::maxVoices}, kEnforceBounds }; -UInt32Spec notePolyphony { config::maxVoices, {0, config::maxVoices}, kEnforceBounds }; +UInt32Spec notePolyphony { config::maxVoices, {1, config::maxVoices}, kEnforceBounds }; UInt8Spec key { 60, {0, 127}, kCanBeNote }; UInt8Spec loKey { 0, {0, 127}, kCanBeNote }; UInt8Spec hiKey { 127, {0, 127}, kCanBeNote }; diff --git a/src/sfizz/PolyphonyGroup.cpp b/src/sfizz/PolyphonyGroup.cpp index 4d3ceb74..2e8ba56a 100644 --- a/src/sfizz/PolyphonyGroup.cpp +++ b/src/sfizz/PolyphonyGroup.cpp @@ -30,6 +30,6 @@ void sfz::PolyphonyGroup::removeAllVoices() noexcept unsigned sfz::PolyphonyGroup::numPlayingVoices() const noexcept { return absl::c_count_if(voices, [](const Voice* v) { - return !v->releasedOrFree(); + return !v->offedOrFree(); }); } diff --git a/src/sfizz/RegionSet.cpp b/src/sfizz/RegionSet.cpp index dfd85872..35369fae 100644 --- a/src/sfizz/RegionSet.cpp +++ b/src/sfizz/RegionSet.cpp @@ -57,7 +57,7 @@ void sfz::RegionSet::removeVoiceFromHierarchy(const Region* region, const Voice* unsigned sfz::RegionSet::numPlayingVoices() const noexcept { return absl::c_count_if(voices, [](const Voice* v) { - return !v->releasedOrFree(); + return !v->offedOrFree(); }); } diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 8a2ba35b..467c09b4 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -221,6 +221,7 @@ struct Voice::Impl State state_ { State::idle }; bool noteIsOff_ { false }; + bool offed_ { false }; enum class SustainState { Up, Sustaining }; SustainState sustainState_ { SustainState::Up }; enum class SostenutoState { Up, Sustaining, PreviouslyDown }; @@ -605,6 +606,7 @@ void Voice::Impl::off(int delay, bool fast) noexcept // TODO(jpc): Flex AmpEG } + offed_ = true; release(delay); } @@ -1637,8 +1639,18 @@ void Voice::Impl::fillWithGenerator(AudioSpan buffer) noexcept #endif } +bool Voice::released() const noexcept +{ + Impl& impl = *impl_; + return impl.released(); +} + bool Voice::Impl::released() const noexcept { + if (!region_ || state_ != State::playing) + return true; + + if (!region_->flexAmpEG) return egAmplitude_.isReleased(); else @@ -1678,6 +1690,7 @@ void Voice::reset() noexcept impl.floatPositionOffset_ = 0.0f; impl.noteIsOff_ = false; impl.sostenutoState_ = Impl::SostenutoState::Up; + impl.offed_ = false; impl.resetLoopInformation(); @@ -1756,13 +1769,13 @@ float Voice::getAveragePower() const noexcept return 0.0f; } -bool Voice::releasedOrFree() const noexcept +bool Voice::offedOrFree() const noexcept { Impl& impl = *impl_; if (impl.state_ != State::playing) return true; - return impl.released(); + return impl.offed_; } void Voice::setMaxFiltersPerVoice(size_t numFilters) diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index f8032a7d..5b8d42c6 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -206,12 +206,19 @@ public: */ bool isFree() const noexcept; /** - * @brief Can the voice be reused (i.e. is it releasing or free) + * @brief Is the voice released? * * @return true * @return false */ - bool releasedOrFree() const noexcept; + bool released() const noexcept; + /** + * @brief Can the voice be reused (i.e. is it releasing after being killed or free) + * + * @return true + * @return false + */ + bool offedOrFree() const noexcept; /** * @brief Get the event that triggered the voice * diff --git a/src/sfizz/VoiceManager.cpp b/src/sfizz/VoiceManager.cpp index eb663fd6..862626d7 100644 --- a/src/sfizz/VoiceManager.cpp +++ b/src/sfizz/VoiceManager.cpp @@ -164,6 +164,7 @@ void VoiceManager::requireNumVoices(int numVoices, Resources& resources) clear(); list_.reserve(numEffectiveVoices); + temp_.reserve(numEffectiveVoices); activeVoices_.reserve(numEffectiveVoices); for (int i = 0; i < numEffectiveVoices; ++i) { @@ -185,33 +186,42 @@ void VoiceManager::checkNotePolyphony(const Region* region, int delay, const Tri return; unsigned notePolyphonyCounter { 0 }; - Voice* selfMaskCandidate { nullptr }; + temp_.clear(); for (Voice* voice : activeVoices_) { const TriggerEvent& voiceTriggerEvent = voice->getTriggerEvent(); - if (!voice->releasedOrFree() + if (!voice->offedOrFree() && voice->getRegion()->group == region->group && voiceTriggerEvent.number == triggerEvent.number) { notePolyphonyCounter += 1; - switch (region->selfMask) { - case SelfMask::mask: - if (voiceTriggerEvent.value <= triggerEvent.value) { - if (!selfMaskCandidate - || selfMaskCandidate->getTriggerEvent().value > voiceTriggerEvent.value) { - selfMaskCandidate = voice; - } - } - break; - case SelfMask::dontMask: - if (!selfMaskCandidate || selfMaskCandidate->getAge() < voice->getAge()) - selfMaskCandidate = voice; - break; - } + if (region->selfMask == SelfMask::dontMask || voiceTriggerEvent.value <= triggerEvent.value) + temp_.push_back(voice); } } - if (notePolyphonyCounter >= *region->notePolyphony) { - SisterVoiceRing::offAllSisters(selfMaskCandidate, delay); + if (region->selfMask == SelfMask::mask) { + absl::c_sort(temp_, [](const Voice* lhs, const Voice* rhs) { + const auto lhsTrigger = lhs->getTriggerEvent(); + const auto rhsTrigger = rhs->getTriggerEvent(); + return lhsTrigger.value < rhsTrigger.value; + }); + } else if (region->selfMask == SelfMask::dontMask) { + absl::c_sort(temp_, [](const Voice* lhs, const Voice* rhs) { + return lhs->getAge() > rhs->getAge(); + }); + } else { + ASSERTFALSE; + } + + auto it = temp_.begin(); + unsigned targetPolyphony { *region->notePolyphony - 1 }; + while (notePolyphonyCounter > targetPolyphony && it < temp_.end()) { + Voice* voice = *it; + if (!voice->offedOrFree()) + SisterVoiceRing::offAllSisters(voice, delay); + + notePolyphonyCounter--; + it++; } } diff --git a/src/sfizz/VoiceManager.h b/src/sfizz/VoiceManager.h index e924ae96..b4e22d4d 100644 --- a/src/sfizz/VoiceManager.h +++ b/src/sfizz/VoiceManager.h @@ -136,6 +136,7 @@ private: int getNumEffectiveVoices() const noexcept { return config::calculateActualVoices(numRequiredVoices_); } std::vector list_; std::vector activeVoices_; + std::vector temp_; // These are the `group=` groups where you can off voices std::vector polyphonyGroups_; std::unique_ptr stealer_ { absl::make_unique() }; diff --git a/src/sfizz/VoiceStealing.cpp b/src/sfizz/VoiceStealing.cpp index d99b447d..13c5dd56 100644 --- a/src/sfizz/VoiceStealing.cpp +++ b/src/sfizz/VoiceStealing.cpp @@ -42,7 +42,7 @@ Voice* genericPolyphonyCheck(absl::Span candidates, unsigned polyphony, */ constexpr bool ignoreVoice(const Voice* voice) { - return (voice == nullptr || voice->releasedOrFree()); + return (voice == nullptr || voice->offedOrFree()); } Voice* FirstStealer::checkRegionPolyphony(const Region* region, absl::Span candidates) diff --git a/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp index f2c56bf7..afa38523 100644 --- a/tests/PolyphonyT.cpp +++ b/tests/PolyphonyT.cpp @@ -229,11 +229,11 @@ TEST_CASE("[Polyphony] Self-masking") REQUIRE( synth.getNumActiveVoices() == 3 ); // One of these is releasing REQUIRE( numPlayingVoices(synth) == 2 ); REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); - REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); + REQUIRE(!synth.getVoiceView(0)->offedOrFree()); REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm); - REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // The lowest velocity voice is the masking candidate + REQUIRE( synth.getVoiceView(1)->offedOrFree()); // The lowest velocity voice is the masking candidate REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 64_norm); - REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); + REQUIRE(!synth.getVoiceView(2)->offedOrFree()); } TEST_CASE("[Polyphony] Not self-masking") @@ -250,11 +250,11 @@ TEST_CASE("[Polyphony] Not self-masking") REQUIRE( synth.getNumActiveVoices() == 3 ); // One of these is releasing REQUIRE( numPlayingVoices(synth) == 2 ); REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); - REQUIRE( synth.getVoiceView(0)->releasedOrFree()); + REQUIRE( synth.getVoiceView(0)->offedOrFree()); REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm); - REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); + REQUIRE(!synth.getVoiceView(1)->offedOrFree()); REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 64_norm); - REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); + REQUIRE(!synth.getVoiceView(2)->offedOrFree()); } TEST_CASE("[Polyphony] Self-masking with the exact same velocity") @@ -271,11 +271,11 @@ TEST_CASE("[Polyphony] Self-masking with the exact same velocity") REQUIRE( synth.getNumActiveVoices() == 3 ); // One of these is releasing REQUIRE( numPlayingVoices(synth) == 2 ); REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 64_norm); - REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); + REQUIRE(!synth.getVoiceView(0)->offedOrFree()); REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 63_norm); - REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // The first one is the masking candidate since they have the same velocity + REQUIRE( synth.getVoiceView(1)->offedOrFree()); // The first one is the masking candidate since they have the same velocity REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 63_norm); - REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); + REQUIRE(!synth.getVoiceView(2)->offedOrFree()); } TEST_CASE("[Polyphony] Self-masking only works from low to high") @@ -291,9 +291,9 @@ TEST_CASE("[Polyphony] Self-masking only works from low to high") REQUIRE( synth.getNumActiveVoices() == 2 ); // Both notes are playing REQUIRE( numPlayingVoices(synth) == 2 ); // id REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); - REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); + REQUIRE(!synth.getVoiceView(0)->offedOrFree()); REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm); - REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); + REQUIRE(!synth.getVoiceView(1)->offedOrFree()); } TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same polyphony group (default)") @@ -309,13 +309,13 @@ TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same po synth.renderBlock(buffer); REQUIRE( numPlayingVoices(synth) == 1 ); REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 62_norm); - REQUIRE( synth.getVoiceView(0)->releasedOrFree()); // got killed + REQUIRE( synth.getVoiceView(0)->offedOrFree()); // got killed REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm); - REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // got killed + REQUIRE( synth.getVoiceView(1)->offedOrFree()); // got killed REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 63_norm); - REQUIRE( synth.getVoiceView(2)->releasedOrFree()); // got killed + REQUIRE( synth.getVoiceView(2)->offedOrFree()); // got killed REQUIRE( synth.getVoiceView(3)->getTriggerEvent().value == 63_norm); - REQUIRE(!synth.getVoiceView(3)->releasedOrFree()); + REQUIRE(!synth.getVoiceView(3)->offedOrFree()); } TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same polyphony group (default, with keyswitches)") @@ -338,9 +338,9 @@ TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same po REQUIRE( synth.getNumActiveVoices() == 2 ); REQUIRE( numPlayingVoices(synth) == 1 ); REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); - REQUIRE( synth.getVoiceView(0)->releasedOrFree()); + REQUIRE( synth.getVoiceView(0)->offedOrFree()); REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 64_norm); - REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); + REQUIRE(!synth.getVoiceView(1)->offedOrFree()); } @@ -358,13 +358,13 @@ TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups") REQUIRE( synth.getNumActiveVoices() == 4); // Both notes are playing REQUIRE(numPlayingVoices(synth) == 2 ); REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 62_norm); - REQUIRE( synth.getVoiceView(0)->releasedOrFree()); // got killed + REQUIRE( synth.getVoiceView(0)->offedOrFree()); // got killed REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm); - REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // got killed + REQUIRE( synth.getVoiceView(1)->offedOrFree()); // got killed REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 63_norm); - REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); + REQUIRE(!synth.getVoiceView(2)->offedOrFree()); REQUIRE( synth.getVoiceView(3)->getTriggerEvent().value == 63_norm); - REQUIRE(!synth.getVoiceView(3)->releasedOrFree()); + REQUIRE(!synth.getVoiceView(3)->offedOrFree()); } TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups (with keyswitches)") @@ -387,9 +387,9 @@ TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups (wi REQUIRE( synth.getNumActiveVoices() == 2 ); REQUIRE(numPlayingVoices(synth) == 2 ); REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); - REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); + REQUIRE(!synth.getVoiceView(0)->offedOrFree()); REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 64_norm); - REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); + REQUIRE(!synth.getVoiceView(1)->offedOrFree()); } TEST_CASE("[Polyphony] Note polyphony operates on release voices") @@ -409,9 +409,9 @@ TEST_CASE("[Polyphony] Note polyphony operates on release voices") REQUIRE( synth.getNumActiveVoices() == 2 ); REQUIRE(numPlayingVoices(synth) == 1 ); REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); - REQUIRE( synth.getVoiceView(0)->releasedOrFree()); + REQUIRE( synth.getVoiceView(0)->offedOrFree()); REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 65_norm); - REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); + REQUIRE(!synth.getVoiceView(1)->offedOrFree()); } TEST_CASE("[Polyphony] Note polyphony operates on release voices (masking works from low to high but takes into account the replaced velocity)") @@ -432,9 +432,9 @@ TEST_CASE("[Polyphony] Note polyphony operates on release voices (masking works REQUIRE( synth.getNumActiveVoices() == 2 ); REQUIRE( numPlayingVoices(synth) == 2 ); REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); - REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); + REQUIRE(!synth.getVoiceView(0)->offedOrFree()); REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 61_norm); - REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); + REQUIRE(!synth.getVoiceView(1)->offedOrFree()); } TEST_CASE("[Polyphony] Note polyphony operates on release voices and sustain pedal") @@ -459,8 +459,8 @@ TEST_CASE("[Polyphony] Note polyphony operates on release voices and sustain ped synth.renderBlock(buffer); std::vector expectedSamples2 { "*saw" }; std::vector expectedVelocities { 63_norm }; - REQUIRE( playingSamples(synth) == expectedSamples2 ); REQUIRE( playingVelocities(synth) == expectedVelocities ); + REQUIRE( playingSamples(synth) == expectedSamples2 ); } TEST_CASE("[Polyphony] Note polyphony operates on release voices and sustain pedal (masking)") diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index facec42e..639acefa 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -205,7 +205,7 @@ TEST_CASE("[Synth] Trigger=release and an envelope properly kills the voice at t synth.renderBlock(buffer); // Decay (0.02) synth.renderBlock(buffer); synth.renderBlock(buffer); // Release (0.1) - REQUIRE(synth.getVoiceView(0)->releasedOrFree()); + REQUIRE(synth.getVoiceView(0)->offedOrFree()); // Release is 0.1s for (int i = 0; i < 10; ++i) synth.renderBlock(buffer); @@ -232,7 +232,7 @@ TEST_CASE("[Synth] Trigger=release_key and an envelope properly kills the voice synth.renderBlock(buffer); // Decay (0.02) synth.renderBlock(buffer); synth.renderBlock(buffer); // Release (0.1) - REQUIRE(synth.getVoiceView(0)->releasedOrFree()); + REQUIRE(synth.getVoiceView(0)->released()); // Release is 0.1s for (int i = 0; i < 10; ++i) synth.renderBlock(buffer); @@ -259,7 +259,7 @@ TEST_CASE("[Synth] loopmode=one_shot and an envelope properly kills the voice at synth.renderBlock(buffer); // Decay (0.02) synth.renderBlock(buffer); synth.renderBlock(buffer); // Release (0.1) - REQUIRE(synth.getVoiceView(0)->releasedOrFree()); + REQUIRE(synth.getVoiceView(0)->released()); // Release is 0.1s for (int i = 0; i < 10; ++i) synth.renderBlock(buffer); @@ -1934,7 +1934,7 @@ TEST_CASE("[Synth] Sustain cancels release is off by default") synth.cc(0, 64, 127); synth.renderBlock(buffer); REQUIRE( playingSamples(synth) == std::vector { } ); -} +} TEST_CASE("[Synth] Resets all controllers to default values") { diff --git a/tests/TestHelpers.cpp b/tests/TestHelpers.cpp index 78e6cb85..92b52630 100644 --- a/tests/TestHelpers.cpp +++ b/tests/TestHelpers.cpp @@ -69,7 +69,7 @@ const std::vector getPlayingVoices(const sfz::Synth& synth) std::vector playingVoices; for (int i = 0; i < synth.getNumVoices(); ++i) { const auto* voice = synth.getVoiceView(i); - if (!voice->releasedOrFree()) + if (!voice->released()) playingVoices.push_back(voice); } return playingVoices; @@ -78,7 +78,7 @@ const std::vector getPlayingVoices(const sfz::Synth& synth) unsigned numPlayingVoices(const sfz::Synth& synth) { return absl::c_count_if(getActiveVoices(synth), [](const sfz::Voice* v) { - return !v->releasedOrFree(); + return !v->released(); }); } @@ -87,7 +87,7 @@ const std::vector playingSamples(const sfz::Synth& synth) std::vector samples; for (int i = 0; i < synth.getNumVoices(); ++i) { const auto* voice = synth.getVoiceView(i); - if (!voice->releasedOrFree()) { + if (!voice->released()) { if (auto region = voice->getRegion()) samples.push_back(region->sampleId->filename()); } @@ -100,7 +100,7 @@ const std::vector playingVelocities(const sfz::Synth& synth) std::vector velocities; for (int i = 0; i < synth.getNumVoices(); ++i) { const auto* voice = synth.getVoiceView(i); - if (!voice->releasedOrFree()) + if (!voice->released()) velocities.push_back(voice->getTriggerEvent().value); } return velocities;