diff --git a/src/sfizz/ADSREnvelope.cpp b/src/sfizz/ADSREnvelope.cpp index bb86115b..bfd7f3f8 100644 --- a/src/sfizz/ADSREnvelope.cpp +++ b/src/sfizz/ADSREnvelope.cpp @@ -11,22 +11,30 @@ namespace sfz { +template +Type ADSREnvelope::secondsToSamples (Type timeInSeconds) const noexcept +{ + return static_cast(timeInSeconds * sampleRate); +}; + +template +Type ADSREnvelope::secondsToLinRate (Type timeInSeconds) const noexcept +{ + timeInSeconds = std::max(timeInSeconds, config::virtuallyZero); + return 1 / (sampleRate * timeInSeconds); +}; + +template +Type ADSREnvelope::secondsToExpRate (Type timeInSeconds) const noexcept +{ + timeInSeconds = std::max(25e-3, timeInSeconds); + return std::exp(-8.0 / (timeInSeconds * sampleRate)); +}; + template void ADSREnvelope::reset(const EGDescription& desc, const Region& region, const MidiState& state, int delay, float velocity, float sampleRate) noexcept { - auto secondsToSamples = [sampleRate](Type timeInSeconds) { - return static_cast(timeInSeconds * sampleRate); - }; - - auto secondsToLinRate = [sampleRate](Type timeInSeconds) { - timeInSeconds = std::max(timeInSeconds, config::virtuallyZero); - return 1 / (sampleRate * timeInSeconds); - }; - - auto secondsToExpRate = [sampleRate](Type timeInSeconds) { - timeInSeconds = std::max(25e-3, timeInSeconds); - return std::exp(-8.0 / (timeInSeconds * sampleRate)); - }; + this->sampleRate = sampleRate; this->delay = delay + secondsToSamples(desc.getDelay(state, velocity)); this->attackStep = secondsToLinRate(desc.getAttack(state, velocity)); @@ -208,13 +216,16 @@ int ADSREnvelope::getRemainingDelay() const noexcept } template -void ADSREnvelope::startRelease(int releaseDelay, bool fastRelease) noexcept +void ADSREnvelope::startRelease(int releaseDelay) noexcept { shouldRelease = true; this->releaseDelay = releaseDelay; +} - if (fastRelease) - this->releaseRate = 0; +template +void ADSREnvelope::setReleaseTime(Type timeInSeconds) noexcept +{ + releaseRate = secondsToExpRate(timeInSeconds); } } diff --git a/src/sfizz/ADSREnvelope.h b/src/sfizz/ADSREnvelope.h index 608567c6..892388f7 100644 --- a/src/sfizz/ADSREnvelope.h +++ b/src/sfizz/ADSREnvelope.h @@ -44,15 +44,18 @@ public: * @param output */ void getBlock(absl::Span output) noexcept; + /** + * @brief Set the release time for the envelope + * + * @param timeInSeconds + */ + void setReleaseTime(Type timeInSeconds) noexcept; /** * @brief Start the envelope release after a delay. * * @param releaseDelay the delay before releasing in samples - * @param fastRelease whether the release should be fast (i.e. 0 or so) or - * follow the release duration that was set when - * initializing the envelope */ - void startRelease(int releaseDelay, bool fastRelease = false) noexcept; + void startRelease(int releaseDelay) noexcept; /** * @brief Is the envelope smoothing? * @@ -75,6 +78,11 @@ public: int getRemainingDelay() const noexcept; private: + float sampleRate { config::defaultSampleRate }; + Type secondsToSamples (Type timeInSeconds) const noexcept; + Type secondsToLinRate (Type timeInSeconds) const noexcept; + Type secondsToExpRate (Type timeInSeconds) const noexcept; + enum class State { Delay, Attack, diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index cc0ac0b3..18ec3df8 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -31,7 +31,7 @@ enum class SfzTrigger { attack, release, release_key, first, legato }; enum class SfzLoopMode { no_loop, one_shot, loop_continuous, loop_sustain }; -enum class SfzOffMode { fast, normal }; +enum class SfzOffMode { fast, normal, time }; enum class SfzVelocityOverride { current, previous }; enum class SfzCrossfadeCurve { gain, power }; enum class SfzSelfMask { mask, dontMask }; @@ -75,6 +75,7 @@ namespace Default constexpr uint32_t group { 0 }; constexpr Range groupRange { 0, std::numeric_limits::max() }; constexpr SfzOffMode offMode { SfzOffMode::fast }; + constexpr float offTime { 6e-3f }; constexpr Range polyphonyRange { 0, config::maxVoices }; constexpr SfzSelfMask selfMask { SfzSelfMask::mask }; diff --git a/src/sfizz/PolyphonyGroup.cpp b/src/sfizz/PolyphonyGroup.cpp index 7ac1e3d6..d5b1615c 100644 --- a/src/sfizz/PolyphonyGroup.cpp +++ b/src/sfizz/PolyphonyGroup.cpp @@ -16,3 +16,10 @@ void sfz::PolyphonyGroup::removeVoice(const Voice* voice) noexcept { swapAndPopFirst(voices, [voice](const Voice* v) { return v == voice; }); } + +unsigned sfz::PolyphonyGroup::numPlayingVoices() const noexcept +{ + return absl::c_count_if(voices, [](const Voice* v) { + return !v->releasedOrFree(); + }); +} diff --git a/src/sfizz/PolyphonyGroup.h b/src/sfizz/PolyphonyGroup.h index d3e1413b..7a4281af 100644 --- a/src/sfizz/PolyphonyGroup.h +++ b/src/sfizz/PolyphonyGroup.h @@ -40,6 +40,10 @@ public: * @return unsigned */ unsigned getPolyphonyLimit() const noexcept { return polyphonyLimit; } + /** + * @brief Returns the number of playing (unreleased) voices + */ + unsigned numPlayingVoices() const noexcept; /** * @brief Get the active voices * diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 0c1a228f..c8aee196 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -161,10 +161,17 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case hash("normal"): offMode = SfzOffMode::normal; break; + case hash("time"): + offMode = SfzOffMode::time; + break; default: DBG("Unkown off mode:" << opcode.value); } break; + case hash("off_time"): + offMode = SfzOffMode::time; + setValueFromOpcode(opcode, offTime, Default::egTimeRange); + break; case hash("polyphony"): if (auto value = readOpcode(opcode.value, Default::polyphonyRange)) polyphony = *value; diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 76113910..72c29ad9 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -286,6 +286,7 @@ struct Region { uint32_t group { Default::group }; // group absl::optional offBy {}; // off_by SfzOffMode offMode { Default::offMode }; // off_mode + float offTime { Default::offTime }; // off_mode absl::optional notePolyphony {}; // note_polyphony unsigned polyphony { config::maxVoices }; // polyphony SfzSelfMask selfMask { Default::selfMask }; diff --git a/src/sfizz/RegionSet.cpp b/src/sfizz/RegionSet.cpp index d6f8a585..1387438a 100644 --- a/src/sfizz/RegionSet.cpp +++ b/src/sfizz/RegionSet.cpp @@ -46,3 +46,10 @@ void sfz::RegionSet::removeVoiceFromHierarchy(const Region* region, const Voice* parent = parent->getParent(); } } + +unsigned sfz::RegionSet::numPlayingVoices() const noexcept +{ + return absl::c_count_if(voices, [](const Voice* v) { + return !v->releasedOrFree(); + }); +} diff --git a/src/sfizz/RegionSet.h b/src/sfizz/RegionSet.h index 24120173..bbfadce0 100644 --- a/src/sfizz/RegionSet.h +++ b/src/sfizz/RegionSet.h @@ -79,6 +79,10 @@ public: * @param parent */ void setParent(RegionSet* parent) noexcept { this->parent = parent; } + /** + * @brief Returns the number of playing (unreleased) voices + */ + unsigned numPlayingVoices() const noexcept; /** * @brief Get the active voices * diff --git a/src/sfizz/SisterVoiceRing.h b/src/sfizz/SisterVoiceRing.h index 78935335..93dbc515 100644 --- a/src/sfizz/SisterVoiceRing.h +++ b/src/sfizz/SisterVoiceRing.h @@ -56,6 +56,22 @@ struct SisterVoiceRing { return count; } + /** + * @brief Off all sisters in a ring + * + * @param voice + * @param delay + */ + template>::value, int> = 0> + static void offAllSisters(T* voice, int delay) { + if (voice != nullptr) { + SisterVoiceRing::applyToRing(voice, [&] (Voice* v) { + v->off(delay); + }); + } + } + /** * @brief Check if a sister voice ring is well formed * diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index c746f273..7e257ded 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -919,12 +919,13 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc continue; } - if (voice->getRegion() == region) { + if (voice->getRegion() == region && !voice->releasedOrFree()) { regionPolyphonyArray.push_back(voice.get()); } if (region->notePolyphony) { if (!voice->releasedOrFree() + && voice->getRegion()->group == region->group && voice->getTriggerNumber() == noteNumber && voice->getTriggerType() == Voice::TriggerType::NoteOn) { notePolyphonyCounter += 1; @@ -948,35 +949,33 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc } // Polyphony reached on note_polyphony - if (region->notePolyphony && notePolyphonyCounter >= *region->notePolyphony) { - if (selfMaskCandidate != nullptr) - selfMaskCandidate->release(delay); - else // We're the lowest velocity guy here - continue; + // If there's a self-masking candidate, release it + if (region->notePolyphony + && notePolyphonyCounter >= *region->notePolyphony + && selfMaskCandidate != nullptr) { + SisterVoiceRing::offAllSisters(selfMaskCandidate, delay); } auto parent = region->parent; // Polyphony reached on region if (regionPolyphonyArray.size() >= region->polyphony) { - selectedVoice = stealer.steal(absl::MakeSpan(regionPolyphonyArray)); - goto render; + const auto activeVoices = absl::MakeSpan(regionPolyphonyArray); + SisterVoiceRing::offAllSisters(stealer.steal(activeVoices), delay); } // Polyphony reached on polyphony group - if (polyphonyGroups[region->group].getActiveVoices().size() + if (polyphonyGroups[region->group].numPlayingVoices() == polyphonyGroups[region->group].getPolyphonyLimit()) { const auto activeVoices = absl::MakeSpan(polyphonyGroups[region->group].getActiveVoices()); - selectedVoice = stealer.steal(activeVoices); - goto render; + SisterVoiceRing::offAllSisters(stealer.steal(activeVoices), delay); } // Polyphony reached some parent group/master/etc while (parent != nullptr) { - if (parent->getActiveVoices().size() >= parent->getPolyphonyLimit()) { + if (parent->numPlayingVoices() >= parent->getPolyphonyLimit()) { const auto activeVoices = absl::MakeSpan(parent->getActiveVoices()); - selectedVoice = stealer.steal(activeVoices); - goto render; + SisterVoiceRing::offAllSisters(stealer.steal(activeVoices), delay); } parent = parent->getParent(); } @@ -986,7 +985,6 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc selectedVoice = stealer.steal(absl::MakeSpan(voiceViewArray)); } - render: // For some reason we did not find a voice to use. // This is a degraded case but we'll just drop the note on. if (selectedVoice == nullptr) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index b4f9598a..d9ade7f8 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -155,7 +155,7 @@ bool sfz::Voice::isFree() const noexcept return (state == State::idle); } -void sfz::Voice::release(int delay, bool fastRelease) noexcept +void sfz::Voice::release(int delay) noexcept { if (state != State::playing) return; @@ -163,10 +163,21 @@ void sfz::Voice::release(int delay, bool fastRelease) noexcept if (egEnvelope.getRemainingDelay() > delay) { switchState(State::cleanMeUp); } else { - egEnvelope.startRelease(delay, fastRelease); + egEnvelope.startRelease(delay); } } +void sfz::Voice::off(int delay) noexcept +{ + if (region->offMode == SfzOffMode::fast) { + egEnvelope.setReleaseTime( Default::offTime ); + } else if (region->offMode == SfzOffMode::time) { + egEnvelope.setReleaseTime(region->offTime); + } + + release(delay); +} + void sfz::Voice::registerNoteOff(int delay, int noteNumber, float velocity) noexcept { ASSERT(velocity >= 0.0 && velocity <= 1.0); @@ -561,7 +572,8 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept << " for sample " << region->sampleId); } #endif - egEnvelope.startRelease(i, true); + egEnvelope.setReleaseTime(0.0f); + egEnvelope.startRelease(i); fill(indices->subspan(i), sampleEnd); fill(coeffs->subspan(i), 1.0f); break; @@ -695,7 +707,7 @@ bool sfz::Voice::checkOffGroup(int delay, uint32_t group) noexcept return false; if (triggerType == TriggerType::NoteOn && region->offBy == group) { - release(delay, region->offMode == SfzOffMode::fast); + off(delay); return true; } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 52a7f0c8..ce473c7b 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -288,7 +288,15 @@ public: * @param delay * @param fastRelease whether to do a normal release or cut the voice abruptly */ - void release(int delay, bool fastRelease = false) noexcept; + void release(int delay) noexcept; + + /** + * @brief Off the voice (steal). This will respect the off mode of the region + * and set the envelopes if necessary. + * + * @param delay + */ + void off(int delay) noexcept; /** * @brief gets the age of the Voice diff --git a/src/sfizz/VoiceStealing.cpp b/src/sfizz/VoiceStealing.cpp index 9cb88bb8..49e1b2d0 100644 --- a/src/sfizz/VoiceStealing.cpp +++ b/src/sfizz/VoiceStealing.cpp @@ -7,6 +7,9 @@ sfz::VoiceStealing::VoiceStealing() sfz::Voice* sfz::VoiceStealing::steal(absl::Span voices) noexcept { + if (voices.empty()) + return {}; + // Start of the voice stealing algorithm absl::c_stable_sort(voices, voiceOrdering); @@ -49,9 +52,5 @@ sfz::Voice* sfz::VoiceStealing::steal(absl::Span voices) noexcept while (idx < voices.size() && sisterVoices(ref, voices[idx])); } - // Guard for future changes: voices with age 0 just started; don't kill those. - if (returnedVoice->getAge() == 0) - return {}; - return returnedVoice; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4018edee..dba296bd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,8 +5,8 @@ project(sfizz) set(SFIZZ_TEST_SOURCES RegionT.cpp - RegionTHelpers.h - RegionTHelpers.cpp + TestHelpers.h + TestHelpers.cpp ParsingT.cpp HelpersT.cpp HelpersT.cpp diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index b47181fe..98b16dce 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -4,7 +4,7 @@ // license. You should have receive a LICENSE.md file along with the code. // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz -#include "RegionTHelpers.h" +#include "TestHelpers.h" #include "sfizz/Synth.h" #include "sfizz/SfzHelpers.h" #include "sfizz/modulations/ModId.h" @@ -517,7 +517,8 @@ TEST_CASE("[Files] Off by with the same notes at the same time") synth.renderBlock(buffer); synth.noteOn(0, 65, 63); synth.renderBlock(buffer); - REQUIRE( synth.getNumActiveVoices(true) == 2 ); + REQUIRE( synth.getNumActiveVoices(true) == 6 ); + REQUIRE( numPlayingVoices(synth) == 2 ); } TEST_CASE("[Files] Off modes") @@ -538,8 +539,10 @@ TEST_CASE("[Files] Off modes") synth.getVoiceView(0) ; synth.noteOn(100, 63, 63); REQUIRE( synth.getNumActiveVoices(true) == 3 ); + REQUIRE( numPlayingVoices(synth) == 1 ); AudioBuffer buffer { 2, 256 }; - synth.renderBlock(buffer); + for (unsigned i = 0; i < 10; ++i) // Not enough for the "normal" voice to die + synth.renderBlock(buffer); REQUIRE( synth.getNumActiveVoices(true) == 2 ); REQUIRE( fastVoice->isFree() ); REQUIRE( !normalVoice->isFree() ); diff --git a/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp index 0fa3aa01..b45b2e4c 100644 --- a/tests/PolyphonyT.cpp +++ b/tests/PolyphonyT.cpp @@ -6,6 +6,8 @@ #include "sfizz/Synth.h" #include "sfizz/SfzHelpers.h" +#include "TestHelpers.h" +#include #include "catch2/catch.hpp" using namespace Catch::literals; @@ -13,7 +15,6 @@ using namespace sfz::literals; constexpr int blockSize { 256 }; - TEST_CASE("[Polyphony] Polyphony in hierarchy") { sfz::Synth synth; @@ -71,6 +72,7 @@ TEST_CASE("[Polyphony] Polyphony groups") TEST_CASE("[Polyphony] group polyphony limits") { sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( group=1 polyphony=2 sample=*sine key=65 @@ -78,12 +80,15 @@ TEST_CASE("[Polyphony] group polyphony limits") synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); - REQUIRE(synth.getNumActiveVoices(true) == 2); // group polyphony should block the last note + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing } TEST_CASE("[Polyphony] Hierarchy polyphony limits") { sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 sample=*sine key=65 @@ -91,12 +96,15 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits") synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); - REQUIRE(synth.getNumActiveVoices(true) == 2); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing } TEST_CASE("[Polyphony] Hierarchy polyphony limits (group)") { sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 sample=*sine key=65 @@ -104,12 +112,15 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (group)") synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); - REQUIRE(synth.getNumActiveVoices(true) == 2); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing } TEST_CASE("[Polyphony] Hierarchy polyphony limits (master)") { sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 polyphony=5 @@ -118,12 +129,15 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (master)") synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); - REQUIRE(synth.getNumActiveVoices(true) == 2); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing } TEST_CASE("[Polyphony] Hierarchy polyphony limits (limit in another master)") { sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 sample=*saw key=65 @@ -137,12 +151,15 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (limit in another master)") synth.noteOn(0, 66, 64); synth.noteOn(0, 66, 64); synth.noteOn(0, 66, 64); - REQUIRE(synth.getNumActiveVoices(true) == 5); + REQUIRE( synth.getNumActiveVoices(true) == 6); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 5); // One is releasing } TEST_CASE("[Polyphony] Hierarchy polyphony limits (global)") { sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 polyphony=5 @@ -151,14 +168,16 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (global)") synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); - REQUIRE(synth.getNumActiveVoices(true) == 2); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing } TEST_CASE("[Polyphony] Polyphony in master") { sfz::Synth synth; - synth.setSamplesPerBlock(blockSize); sfz::AudioBuffer buffer { 2, blockSize }; + synth.setSamplesPerBlock(blockSize); synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( polyphony=2 group=2 @@ -171,74 +190,199 @@ TEST_CASE("[Polyphony] Polyphony in master") synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); - REQUIRE(synth.getNumActiveVoices(true) == 2); // group polyphony should block the last note + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing synth.allSoundOff(); synth.renderBlock(buffer); - REQUIRE(synth.getNumActiveVoices(true) == 0); + REQUIRE( synth.getNumActiveVoices(true) == 0); synth.noteOn(0, 63, 64); synth.noteOn(0, 63, 64); synth.noteOn(0, 63, 64); - REQUIRE(synth.getNumActiveVoices(true) == 2); // group polyphony should block the last note + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing synth.allSoundOff(); synth.renderBlock(buffer); - REQUIRE(synth.getNumActiveVoices(true) == 0); + REQUIRE( synth.getNumActiveVoices(true) == 0); synth.noteOn(0, 61, 64); synth.noteOn(0, 61, 64); synth.noteOn(0, 61, 64); - REQUIRE(synth.getNumActiveVoices(true) == 3); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 3 ); } TEST_CASE("[Polyphony] Self-masking") { sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( sample=*sine key=64 note_polyphony=2 )"); - synth.noteOn(0, 64, 63); - synth.noteOn(0, 64, 62); + synth.noteOn(0, 64, 63 ); + synth.noteOn(0, 64, 62 ); synth.noteOn(0, 64, 64); - REQUIRE(synth.getNumActiveVoices(true) == 3); // One of these is releasing - REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); // One of these is releasing + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); + REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm); REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); - REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); - REQUIRE(synth.getVoiceView(1)->releasedOrFree()); // The lowest velocity voice is the masking candidate - REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // The lowest velocity voice is the masking candidate + REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 64_norm); REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); } TEST_CASE("[Polyphony] Not self-masking") { sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( sample=*sine key=66 note_polyphony=2 note_selfmask=off )"); - synth.noteOn(0, 66, 63); - synth.noteOn(0, 66, 62); + synth.noteOn(0, 66, 63 ); + synth.noteOn(0, 66, 62 ); synth.noteOn(0, 66, 64); - REQUIRE(synth.getNumActiveVoices(true) == 3); // One of these is releasing - REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); - REQUIRE(synth.getVoiceView(0)->releasedOrFree()); // The first encountered voice is the masking candidate - REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); // One of these is releasing + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); + REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(0)->releasedOrFree()); + REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm); REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); - REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm); + REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 64_norm); REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); } TEST_CASE("[Polyphony] Self-masking with the exact same velocity") { sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; synth.loadSfzString(fs::current_path(), R"( sample=*sine key=64 note_polyphony=2 )"); synth.noteOn(0, 64, 64); - synth.noteOn(0, 64, 63); - synth.noteOn(0, 64, 63); - REQUIRE(synth.getNumActiveVoices(true) == 3); // One of these is releasing - REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 64_norm); + synth.noteOn(0, 64, 63 ); + synth.noteOn(0, 64, 63 ); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); // One of these is releasing + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); + REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 64_norm); REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); - REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 63_norm); - REQUIRE(synth.getVoiceView(1)->releasedOrFree()); // The first one is the masking candidate since they have the same velocity - REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // The first one is the masking candidate since they have the same velocity + REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 63_norm); REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); } + +TEST_CASE("[Polyphony] Self-masking only works from low to high") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( + sample=*sine key=64 note_polyphony=1 + )"); + synth.noteOn(0, 64, 63 ); + synth.noteOn(0, 64, 62 ); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); // Both notes are playing + REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); + REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); +} + +TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same polyphony group (default)") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( + sample=*saw key=64 note_polyphony=1 + sample=*sine key=64 note_polyphony=1 + )"); + synth.noteOn(0, 64, 62 ); + synth.noteOn(0, 64, 63 ); + REQUIRE( synth.getNumActiveVoices(true) == 4); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 1 ); + REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(0)->releasedOrFree()); // got killed + REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // got killed + REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(2)->releasedOrFree()); // got killed + REQUIRE( synth.getVoiceView(3)->getTriggerValue() == 63_norm); + REQUIRE(!synth.getVoiceView(3)->releasedOrFree()); +} + +TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same polyphony group (default, with keyswitches)") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( + sw_lokey=36 sw_hikey=37 sw_default=36 + sw_last=36 key=48 note_polyphony=1 sample=*saw + sw_last=37 key=48 transpose=12 note_polyphony=1 sample=*tri + )"); + synth.noteOn(0, 48, 63 ); + REQUIRE( synth.getNumActiveVoices(true) == 1); + synth.cc(0, 64, 127); + synth.noteOn(0, 37, 127); + synth.noteOff(0, 37, 0); + synth.noteOn(0, 48, 64); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 1 ); + REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(0)->releasedOrFree()); + REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 64_norm); + REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); +} + + +TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( + group=1 sample=*saw key=64 note_polyphony=1 + group=2 sample=*sine key=64 note_polyphony=1 + )"); + synth.noteOn(0, 64, 62 ); + synth.noteOn(0, 64, 63 ); + REQUIRE( synth.getNumActiveVoices(true) == 4); // Both notes are playing + synth.renderBlock(buffer); + REQUIRE(numPlayingVoices(synth) == 2 ); + REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(0)->releasedOrFree()); // got killed + REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // got killed + REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 63_norm); + REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); + REQUIRE( synth.getVoiceView(3)->getTriggerValue() == 63_norm); + REQUIRE(!synth.getVoiceView(3)->releasedOrFree()); +} + +TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups (with keyswitches)") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( + sw_lokey=36 sw_hikey=37 sw_default=36 + group=1 sw_last=36 key=48 note_polyphony=1 sample=*saw + group=2 sw_last=37 key=48 transpose=12 note_polyphony=1 sample=*tri + )"); + synth.noteOn(0, 48, 63 ); + REQUIRE( synth.getNumActiveVoices(true) == 1); + synth.cc(0, 64, 127); + synth.noteOn(0, 37, 127); + synth.noteOff(0, 37, 0); + synth.noteOn(0, 48, 64); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); + synth.renderBlock(buffer); + REQUIRE(numPlayingVoices(synth) == 2 ); + REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); + REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 64_norm); + REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); +} diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 01469495..481e8628 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -4,7 +4,7 @@ // license. You should have receive a LICENSE.md file along with the code. // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz -#include "RegionTHelpers.h" +#include "TestHelpers.h" #include "sfizz/MidiState.h" #include "sfizz/Region.h" #include "sfizz/SfzHelpers.h" @@ -195,6 +195,22 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.offMode == SfzOffMode::fast); region.parseOpcode({ "off_mode", "normal" }); REQUIRE(region.offMode == SfzOffMode::normal); + region.parseOpcode({ "off_mode", "time" }); + REQUIRE(region.offMode == SfzOffMode::time); + } + + SECTION("off_time") + { + REQUIRE(region.offTime == 0.006f); + REQUIRE(region.offMode == SfzOffMode::fast); + region.parseOpcode({ "off_time", "0.1" }); + REQUIRE(region.offTime == 0.1f); + REQUIRE(region.offMode == SfzOffMode::time); + region.parseOpcode({ "off_time", "0" }); + REQUIRE(region.offTime == 0.0f); + region.parseOpcode({ "off_time", "0.1" }); + region.parseOpcode({ "off_time", "-1" }); + REQUIRE(region.offTime == 0.0f); } SECTION("lokey, hikey, and key") diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index a6ec1aa2..bd949dd3 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -8,6 +8,7 @@ #include "sfizz/SisterVoiceRing.h" #include "sfizz/SfzHelpers.h" #include "sfizz/NumericId.h" +#include "TestHelpers.h" #include #include "catch2/catch.hpp" using namespace Catch::literals; @@ -607,7 +608,8 @@ TEST_CASE("[Synth] Sisters and off-by") REQUIRE( synth.getNumActiveVoices(true) == 2 ); synth.noteOn(0, 63, 85); REQUIRE( synth.getNumActiveVoices(true) == 3 ); - synth.renderBlock(buffer); + for (unsigned i = 0; i < 100; ++i) + synth.renderBlock(buffer); REQUIRE( synth.getNumActiveVoices(true) == 2 ); REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(0)) == 1 ); } @@ -735,30 +737,6 @@ TEST_CASE("[Synth] Sustain threshold") REQUIRE( synth.getNumActiveVoices(true) == 5 ); } -template -void sortAll(C& container) -{ - std::sort(container.begin(), container.end()); -} - -template -void sortAll(C& container, Args&... others) -{ - std::sort(container.begin(), container.end()); - sortAll(others...); -} - -const std::vector getActiveVoices(const sfz::Synth& synth) -{ - std::vector 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; diff --git a/tests/RegionTHelpers.cpp b/tests/TestHelpers.cpp similarity index 68% rename from tests/RegionTHelpers.cpp rename to tests/TestHelpers.cpp index a47b56ed..ca32de0b 100644 --- a/tests/RegionTHelpers.cpp +++ b/tests/TestHelpers.cpp @@ -4,7 +4,7 @@ // license. You should have receive a LICENSE.md file along with the code. // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz -#include "RegionTHelpers.h" +#include "TestHelpers.h" #include "sfizz/modulations/ModId.h" size_t RegionCCView::size() const @@ -39,3 +39,21 @@ bool RegionCCView::match(const sfz::Region::Connection& conn) const { return conn.source.id() == sfz::ModId::Controller && conn.target == target_; } + +const std::vector getActiveVoices(const sfz::Synth& synth) +{ + std::vector activeVoices; + for (int i = 0; i < synth.getNumVoices(); ++i) { + const auto* voice = synth.getVoiceView(i); + if (!voice->isFree()) + activeVoices.push_back(voice); + } + return activeVoices; +} + +unsigned numPlayingVoices(const sfz::Synth& synth) +{ + return absl::c_count_if(getActiveVoices(synth), [](const sfz::Voice* v) { + return !v->releasedOrFree(); + }); +} diff --git a/tests/RegionTHelpers.h b/tests/TestHelpers.h similarity index 52% rename from tests/RegionTHelpers.h rename to tests/TestHelpers.h index e9fcf897..ac8be3dc 100644 --- a/tests/RegionTHelpers.h +++ b/tests/TestHelpers.h @@ -5,6 +5,7 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #pragma once +#include "sfizz/Synth.h" #include "sfizz/Region.h" #include "sfizz/modulations/ModKey.h" @@ -26,3 +27,32 @@ private: const sfz::Region& region_; sfz::ModKey target_; }; + +template +void sortAll(C& container) +{ + std::sort(container.begin(), container.end()); +} + +template +void sortAll(C& container, Args&... others) +{ + std::sort(container.begin(), container.end()); + sortAll(others...); +} + +/** + * @brief Get active voices from the synth + * + * @param synth + * @return const std::vector + */ +const std::vector getActiveVoices(const sfz::Synth& synth); + +/** + * @brief Count the number of playing (unreleased) voices from the synth + * + * @param synth + * @return unsigned + */ +unsigned numPlayingVoices(const sfz::Synth& synth);