From 7a1a2c9381c84718464fb5337363121ef5c3085f Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 10 Aug 2020 15:22:35 +0200 Subject: [PATCH 01/10] Finer tests and behavior for note_polyphony- note_polyphony is shared amoung polyphony groups- Lower velocity samples do not mute higher velocity samples, but they still play! --- src/sfizz/Synth.cpp | 11 ++--- tests/PolyphonyT.cpp | 100 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 105 insertions(+), 6 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index c746f273..a184803b 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -925,6 +925,7 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc if (region->notePolyphony) { if (!voice->releasedOrFree() + && voice->getRegion()->group == region->group && voice->getTriggerNumber() == noteNumber && voice->getTriggerType() == Voice::TriggerType::NoteOn) { notePolyphonyCounter += 1; @@ -948,11 +949,11 @@ 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) { + selfMaskCandidate->release(delay); } auto parent = region->parent; diff --git a/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp index 0fa3aa01..f1e261d1 100644 --- a/tests/PolyphonyT.cpp +++ b/tests/PolyphonyT.cpp @@ -218,7 +218,7 @@ TEST_CASE("[Polyphony] Not self-masking") 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(0)->releasedOrFree()); REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm); @@ -242,3 +242,101 @@ TEST_CASE("[Polyphony] Self-masking with the exact 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; + 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); // Both notes are playing + 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; + 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); + 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; + 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 + 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; + 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); + REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); + REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 64_norm); + REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); +} From d3cc4281d97fa74748d6620f285b1cf89da434bc Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 10 Aug 2020 16:14:03 +0200 Subject: [PATCH 02/10] Parse off time --- src/sfizz/Defaults.h | 3 ++- src/sfizz/Region.cpp | 6 ++++++ src/sfizz/Region.h | 1 + tests/RegionT.cpp | 14 ++++++++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) 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/Region.cpp b/src/sfizz/Region.cpp index 0c1a228f..8452311b 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -161,10 +161,16 @@ 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"): + 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/tests/RegionT.cpp b/tests/RegionT.cpp index 01469495..c6317a0f 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -195,6 +195,20 @@ 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); + region.parseOpcode({ "off_time", "0.1" }); + REQUIRE(region.offTime == 0.1f); + 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") From 5ef6141c4b07bd8a2147aaabbb8202dd23fbc70c Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 10 Aug 2020 16:14:55 +0200 Subject: [PATCH 03/10] Add a way to change the release rate after the envelope started and separate this from the `release()` method in the Voice --- src/sfizz/ADSREnvelope.cpp | 43 ++++++++++++++++++++++++-------------- src/sfizz/ADSREnvelope.h | 16 ++++++++++---- src/sfizz/Voice.cpp | 20 ++++++++++++++---- src/sfizz/Voice.h | 10 ++++++++- 4 files changed, 64 insertions(+), 25 deletions(-) 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/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 From 2cdf4f165fc37192244cd7c741d00310807267f6 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 10 Aug 2020 17:09:58 +0200 Subject: [PATCH 04/10] Add helpers to count playing voices in polyphony groups --- src/sfizz/PolyphonyGroup.cpp | 7 +++++++ src/sfizz/PolyphonyGroup.h | 4 ++++ src/sfizz/RegionSet.cpp | 7 +++++++ src/sfizz/RegionSet.h | 4 ++++ 4 files changed, 22 insertions(+) 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/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 * From f318c528a26844a7f07c4244d18b3836e68abb4c Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 10 Aug 2020 17:10:19 +0200 Subject: [PATCH 05/10] Guard the voice stealing iif voices are empty --- src/sfizz/VoiceStealing.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sfizz/VoiceStealing.cpp b/src/sfizz/VoiceStealing.cpp index 9cb88bb8..fbac880c 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); From 4529d371f026fe59329b146c6c9d687e287aaf7c Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 10 Aug 2020 17:10:40 +0200 Subject: [PATCH 06/10] Add a helper function to off all sister voices at once --- src/sfizz/SisterVoiceRing.h | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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 * From 3fdf0ef7cd8b5427cdad7c759463b6de614e790d Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 10 Aug 2020 17:21:15 +0200 Subject: [PATCH 07/10] Remove the age guard --- src/sfizz/VoiceStealing.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/sfizz/VoiceStealing.cpp b/src/sfizz/VoiceStealing.cpp index fbac880c..49e1b2d0 100644 --- a/src/sfizz/VoiceStealing.cpp +++ b/src/sfizz/VoiceStealing.cpp @@ -52,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; } From 812a86719474e3c95675f5eb6c328629512c921a Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 10 Aug 2020 17:22:05 +0200 Subject: [PATCH 08/10] Off voices rather than killing them --- src/sfizz/Synth.cpp | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index a184803b..7e257ded 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -919,7 +919,7 @@ 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()); } @@ -953,31 +953,29 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc if (region->notePolyphony && notePolyphonyCounter >= *region->notePolyphony && selfMaskCandidate != nullptr) { - selfMaskCandidate->release(delay); + 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(); } @@ -987,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) From 44dbbcaf54e0ad961558696462e4b3a85adb504a Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 10 Aug 2020 17:22:49 +0200 Subject: [PATCH 09/10] Updated tests --- tests/CMakeLists.txt | 4 +- tests/FilesT.cpp | 9 +- tests/PolyphonyT.cpp | 184 +++++++++++------- tests/RegionT.cpp | 2 +- tests/SynthT.cpp | 28 +-- tests/{RegionTHelpers.cpp => TestHelpers.cpp} | 20 +- tests/{RegionTHelpers.h => TestHelpers.h} | 30 +++ 7 files changed, 176 insertions(+), 101 deletions(-) rename tests/{RegionTHelpers.cpp => TestHelpers.cpp} (68%) rename tests/{RegionTHelpers.h => TestHelpers.h} (52%) 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 f1e261d1..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,75 +190,90 @@ 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()); - 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()); } @@ -249,53 +283,59 @@ TEST_CASE("[Polyphony] Self-masking only works from low to high") 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); + 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)->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); // Both notes are playing - 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); + 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.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); - REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); - REQUIRE(synth.getVoiceView(0)->releasedOrFree()); - REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 64_norm); + 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()); } @@ -303,40 +343,46 @@ TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same po 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 - 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); + 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)->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.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); - REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); + 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)->getTriggerValue() == 64_norm); REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); } diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index c6317a0f..189fae32 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" 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); From 2e0278c7cf20e8b1fff822198726c6b056047c92 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 10 Aug 2020 17:33:21 +0200 Subject: [PATCH 10/10] Passing off_time automatically sets the off_mode to time --- src/sfizz/Region.cpp | 1 + tests/RegionT.cpp | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 8452311b..c8aee196 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -169,6 +169,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) } break; case hash("off_time"): + offMode = SfzOffMode::time; setValueFromOpcode(opcode, offTime, Default::egTimeRange); break; case hash("polyphony"): diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 189fae32..481e8628 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -202,8 +202,10 @@ TEST_CASE("[Region] Parsing opcodes") 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" });