Merge pull request #929 from paulfd/off-vs-released
Differentiate between releasing and "offed"
This commit is contained in:
commit
78673d683a
12 changed files with 150 additions and 62 deletions
|
|
@ -44,7 +44,7 @@ Int32Spec oscillatorQuality { 1, {0, 3}, 0 };
|
||||||
UInt32Spec group { 0, {0, uint32_t_max}, 0 };
|
UInt32Spec group { 0, {0, uint32_t_max}, 0 };
|
||||||
FloatSpec offTime { 6e-3f, {0.0f, 100.0f}, kPermissiveBounds };
|
FloatSpec offTime { 6e-3f, {0.0f, 100.0f}, kPermissiveBounds };
|
||||||
UInt32Spec polyphony { config::maxVoices, {0, config::maxVoices}, kEnforceBounds };
|
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 key { 60, {0, 127}, kCanBeNote };
|
||||||
UInt8Spec loKey { 0, {0, 127}, kCanBeNote };
|
UInt8Spec loKey { 0, {0, 127}, kCanBeNote };
|
||||||
UInt8Spec hiKey { 127, {0, 127}, kCanBeNote };
|
UInt8Spec hiKey { 127, {0, 127}, kCanBeNote };
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,6 @@ void sfz::PolyphonyGroup::removeAllVoices() noexcept
|
||||||
unsigned sfz::PolyphonyGroup::numPlayingVoices() const noexcept
|
unsigned sfz::PolyphonyGroup::numPlayingVoices() const noexcept
|
||||||
{
|
{
|
||||||
return absl::c_count_if(voices, [](const Voice* v) {
|
return absl::c_count_if(voices, [](const Voice* v) {
|
||||||
return !v->releasedOrFree();
|
return !v->offedOrFree();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ void sfz::RegionSet::removeVoiceFromHierarchy(const Region* region, const Voice*
|
||||||
unsigned sfz::RegionSet::numPlayingVoices() const noexcept
|
unsigned sfz::RegionSet::numPlayingVoices() const noexcept
|
||||||
{
|
{
|
||||||
return absl::c_count_if(voices, [](const Voice* v) {
|
return absl::c_count_if(voices, [](const Voice* v) {
|
||||||
return !v->releasedOrFree();
|
return !v->offedOrFree();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,7 @@ struct Voice::Impl
|
||||||
|
|
||||||
State state_ { State::idle };
|
State state_ { State::idle };
|
||||||
bool noteIsOff_ { false };
|
bool noteIsOff_ { false };
|
||||||
|
bool offed_ { false };
|
||||||
enum class SustainState { Up, Sustaining };
|
enum class SustainState { Up, Sustaining };
|
||||||
SustainState sustainState_ { SustainState::Up };
|
SustainState sustainState_ { SustainState::Up };
|
||||||
enum class SostenutoState { Up, Sustaining, PreviouslyDown };
|
enum class SostenutoState { Up, Sustaining, PreviouslyDown };
|
||||||
|
|
@ -605,6 +606,7 @@ void Voice::Impl::off(int delay, bool fast) noexcept
|
||||||
// TODO(jpc): Flex AmpEG
|
// TODO(jpc): Flex AmpEG
|
||||||
}
|
}
|
||||||
|
|
||||||
|
offed_ = true;
|
||||||
release(delay);
|
release(delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1637,8 +1639,18 @@ void Voice::Impl::fillWithGenerator(AudioSpan<float> buffer) noexcept
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Voice::released() const noexcept
|
||||||
|
{
|
||||||
|
Impl& impl = *impl_;
|
||||||
|
return impl.released();
|
||||||
|
}
|
||||||
|
|
||||||
bool Voice::Impl::released() const noexcept
|
bool Voice::Impl::released() const noexcept
|
||||||
{
|
{
|
||||||
|
if (!region_ || state_ != State::playing)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
|
||||||
if (!region_->flexAmpEG)
|
if (!region_->flexAmpEG)
|
||||||
return egAmplitude_.isReleased();
|
return egAmplitude_.isReleased();
|
||||||
else
|
else
|
||||||
|
|
@ -1678,6 +1690,7 @@ void Voice::reset() noexcept
|
||||||
impl.floatPositionOffset_ = 0.0f;
|
impl.floatPositionOffset_ = 0.0f;
|
||||||
impl.noteIsOff_ = false;
|
impl.noteIsOff_ = false;
|
||||||
impl.sostenutoState_ = Impl::SostenutoState::Up;
|
impl.sostenutoState_ = Impl::SostenutoState::Up;
|
||||||
|
impl.offed_ = false;
|
||||||
|
|
||||||
impl.resetLoopInformation();
|
impl.resetLoopInformation();
|
||||||
|
|
||||||
|
|
@ -1756,13 +1769,13 @@ float Voice::getAveragePower() const noexcept
|
||||||
return 0.0f;
|
return 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Voice::releasedOrFree() const noexcept
|
bool Voice::offedOrFree() const noexcept
|
||||||
{
|
{
|
||||||
Impl& impl = *impl_;
|
Impl& impl = *impl_;
|
||||||
if (impl.state_ != State::playing)
|
if (impl.state_ != State::playing)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return impl.released();
|
return impl.offed_;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Voice::setMaxFiltersPerVoice(size_t numFilters)
|
void Voice::setMaxFiltersPerVoice(size_t numFilters)
|
||||||
|
|
|
||||||
|
|
@ -206,12 +206,19 @@ public:
|
||||||
*/
|
*/
|
||||||
bool isFree() const noexcept;
|
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 true
|
||||||
* @return false
|
* @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
|
* @brief Get the event that triggered the voice
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,7 @@ void VoiceManager::requireNumVoices(int numVoices, Resources& resources)
|
||||||
|
|
||||||
clear();
|
clear();
|
||||||
list_.reserve(numEffectiveVoices);
|
list_.reserve(numEffectiveVoices);
|
||||||
|
temp_.reserve(numEffectiveVoices);
|
||||||
activeVoices_.reserve(numEffectiveVoices);
|
activeVoices_.reserve(numEffectiveVoices);
|
||||||
|
|
||||||
for (int i = 0; i < numEffectiveVoices; ++i) {
|
for (int i = 0; i < numEffectiveVoices; ++i) {
|
||||||
|
|
@ -185,33 +186,42 @@ void VoiceManager::checkNotePolyphony(const Region* region, int delay, const Tri
|
||||||
return;
|
return;
|
||||||
|
|
||||||
unsigned notePolyphonyCounter { 0 };
|
unsigned notePolyphonyCounter { 0 };
|
||||||
Voice* selfMaskCandidate { nullptr };
|
temp_.clear();
|
||||||
|
|
||||||
for (Voice* voice : activeVoices_) {
|
for (Voice* voice : activeVoices_) {
|
||||||
const TriggerEvent& voiceTriggerEvent = voice->getTriggerEvent();
|
const TriggerEvent& voiceTriggerEvent = voice->getTriggerEvent();
|
||||||
if (!voice->releasedOrFree()
|
if (!voice->offedOrFree()
|
||||||
&& voice->getRegion()->group == region->group
|
&& voice->getRegion()->group == region->group
|
||||||
&& voiceTriggerEvent.number == triggerEvent.number) {
|
&& voiceTriggerEvent.number == triggerEvent.number) {
|
||||||
notePolyphonyCounter += 1;
|
notePolyphonyCounter += 1;
|
||||||
switch (region->selfMask) {
|
if (region->selfMask == SelfMask::dontMask || voiceTriggerEvent.value <= triggerEvent.value)
|
||||||
case SelfMask::mask:
|
temp_.push_back(voice);
|
||||||
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 (notePolyphonyCounter >= *region->notePolyphony) {
|
if (region->selfMask == SelfMask::mask) {
|
||||||
SisterVoiceRing::offAllSisters(selfMaskCandidate, delay);
|
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++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,7 @@ private:
|
||||||
int getNumEffectiveVoices() const noexcept { return config::calculateActualVoices(numRequiredVoices_); }
|
int getNumEffectiveVoices() const noexcept { return config::calculateActualVoices(numRequiredVoices_); }
|
||||||
std::vector<Voice> list_;
|
std::vector<Voice> list_;
|
||||||
std::vector<Voice*> activeVoices_;
|
std::vector<Voice*> activeVoices_;
|
||||||
|
std::vector<Voice*> temp_;
|
||||||
// These are the `group=` groups where you can off voices
|
// These are the `group=` groups where you can off voices
|
||||||
std::vector<PolyphonyGroup> polyphonyGroups_;
|
std::vector<PolyphonyGroup> polyphonyGroups_;
|
||||||
std::unique_ptr<VoiceStealer> stealer_ { absl::make_unique<OldestStealer>() };
|
std::unique_ptr<VoiceStealer> stealer_ { absl::make_unique<OldestStealer>() };
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ Voice* genericPolyphonyCheck(absl::Span<Voice*> candidates, unsigned polyphony,
|
||||||
*/
|
*/
|
||||||
constexpr bool ignoreVoice(const Voice* voice)
|
constexpr bool ignoreVoice(const Voice* voice)
|
||||||
{
|
{
|
||||||
return (voice == nullptr || voice->releasedOrFree());
|
return (voice == nullptr || voice->offedOrFree());
|
||||||
}
|
}
|
||||||
|
|
||||||
Voice* FirstStealer::checkRegionPolyphony(const Region* region, absl::Span<Voice*> candidates)
|
Voice* FirstStealer::checkRegionPolyphony(const Region* region, absl::Span<Voice*> candidates)
|
||||||
|
|
|
||||||
|
|
@ -229,11 +229,11 @@ TEST_CASE("[Polyphony] Self-masking")
|
||||||
REQUIRE( synth.getNumActiveVoices() == 3 ); // One of these is releasing
|
REQUIRE( synth.getNumActiveVoices() == 3 ); // One of these is releasing
|
||||||
REQUIRE( numPlayingVoices(synth) == 2 );
|
REQUIRE( numPlayingVoices(synth) == 2 );
|
||||||
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
|
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)->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)->getTriggerEvent().value == 64_norm);
|
||||||
REQUIRE(!synth.getVoiceView(2)->releasedOrFree());
|
REQUIRE(!synth.getVoiceView(2)->offedOrFree());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("[Polyphony] Not self-masking")
|
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( synth.getNumActiveVoices() == 3 ); // One of these is releasing
|
||||||
REQUIRE( numPlayingVoices(synth) == 2 );
|
REQUIRE( numPlayingVoices(synth) == 2 );
|
||||||
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
|
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)->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)->getTriggerEvent().value == 64_norm);
|
||||||
REQUIRE(!synth.getVoiceView(2)->releasedOrFree());
|
REQUIRE(!synth.getVoiceView(2)->offedOrFree());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("[Polyphony] Self-masking with the exact same velocity")
|
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( synth.getNumActiveVoices() == 3 ); // One of these is releasing
|
||||||
REQUIRE( numPlayingVoices(synth) == 2 );
|
REQUIRE( numPlayingVoices(synth) == 2 );
|
||||||
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 64_norm);
|
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)->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)->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")
|
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( synth.getNumActiveVoices() == 2 ); // Both notes are playing
|
||||||
REQUIRE( numPlayingVoices(synth) == 2 ); // id
|
REQUIRE( numPlayingVoices(synth) == 2 ); // id
|
||||||
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
|
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)->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)")
|
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);
|
synth.renderBlock(buffer);
|
||||||
REQUIRE( numPlayingVoices(synth) == 1 );
|
REQUIRE( numPlayingVoices(synth) == 1 );
|
||||||
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 62_norm);
|
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)->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)->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)->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)")
|
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( synth.getNumActiveVoices() == 2 );
|
||||||
REQUIRE( numPlayingVoices(synth) == 1 );
|
REQUIRE( numPlayingVoices(synth) == 1 );
|
||||||
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
|
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)->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( synth.getNumActiveVoices() == 4); // Both notes are playing
|
||||||
REQUIRE(numPlayingVoices(synth) == 2 );
|
REQUIRE(numPlayingVoices(synth) == 2 );
|
||||||
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 62_norm);
|
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)->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)->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)->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)")
|
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( synth.getNumActiveVoices() == 2 );
|
||||||
REQUIRE(numPlayingVoices(synth) == 2 );
|
REQUIRE(numPlayingVoices(synth) == 2 );
|
||||||
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
|
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)->getTriggerEvent().value == 64_norm);
|
||||||
REQUIRE(!synth.getVoiceView(1)->releasedOrFree());
|
REQUIRE(!synth.getVoiceView(1)->offedOrFree());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("[Polyphony] Note polyphony operates on release voices")
|
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( synth.getNumActiveVoices() == 2 );
|
||||||
REQUIRE(numPlayingVoices(synth) == 1 );
|
REQUIRE(numPlayingVoices(synth) == 1 );
|
||||||
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
|
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)->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)")
|
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( synth.getNumActiveVoices() == 2 );
|
||||||
REQUIRE( numPlayingVoices(synth) == 2 );
|
REQUIRE( numPlayingVoices(synth) == 2 );
|
||||||
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
|
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)->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")
|
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);
|
synth.renderBlock(buffer);
|
||||||
std::vector<std::string> expectedSamples2 { "*saw" };
|
std::vector<std::string> expectedSamples2 { "*saw" };
|
||||||
std::vector<float> expectedVelocities { 63_norm };
|
std::vector<float> expectedVelocities { 63_norm };
|
||||||
REQUIRE( playingSamples(synth) == expectedSamples2 );
|
|
||||||
REQUIRE( playingVelocities(synth) == expectedVelocities );
|
REQUIRE( playingVelocities(synth) == expectedVelocities );
|
||||||
|
REQUIRE( playingSamples(synth) == expectedSamples2 );
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("[Polyphony] Note polyphony operates on release voices and sustain pedal (masking)")
|
TEST_CASE("[Polyphony] Note polyphony operates on release voices and sustain pedal (masking)")
|
||||||
|
|
@ -528,3 +528,45 @@ TEST_CASE("[Polyphony] Bi-directional choking (with note_polyphony)")
|
||||||
synth.renderBlock(buffer);
|
synth.renderBlock(buffer);
|
||||||
REQUIRE( playingSamples(synth) == std::vector<std::string> { "kick.wav" } );
|
REQUIRE( playingSamples(synth) == std::vector<std::string> { "kick.wav" } );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Polyphony] Choke long release tails")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
|
||||||
|
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
|
||||||
|
<region> sample=*saw ampeg_attack=0.1 ampeg_release=10 polyphony=1
|
||||||
|
)");
|
||||||
|
int attackBlocks = static_cast<int>(0.1f / synth.getSamplesPerBlock() * 48000.0f) + 1;
|
||||||
|
synth.noteOn(0, 60, 63 );
|
||||||
|
for (int i = 0; i < attackBlocks; ++i)
|
||||||
|
synth.renderBlock(buffer);
|
||||||
|
synth.noteOff(10, 60, 63 );
|
||||||
|
synth.renderBlock(buffer);
|
||||||
|
REQUIRE( numPlayingVoices(synth) == 0 ); // Released
|
||||||
|
REQUIRE( numActiveVoices(synth) == 1 );
|
||||||
|
synth.noteOn(0, 60, 63 );
|
||||||
|
synth.renderBlock(buffer);
|
||||||
|
REQUIRE( numPlayingVoices(synth) == 1 ); // Not released, attack phase
|
||||||
|
REQUIRE( numActiveVoices(synth) == 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Polyphony] Choke long release tails with note_polyphony")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
|
||||||
|
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
|
||||||
|
<region> sample=*saw ampeg_attack=0.1 ampeg_release=10 note_polyphony=1
|
||||||
|
)");
|
||||||
|
int attackBlocks = static_cast<int>(0.1f / synth.getSamplesPerBlock() * 48000.0f) + 1;
|
||||||
|
synth.noteOn(0, 60, 63 );
|
||||||
|
for (int i = 0; i < attackBlocks; ++i)
|
||||||
|
synth.renderBlock(buffer);
|
||||||
|
synth.noteOff(10, 60, 63 );
|
||||||
|
synth.renderBlock(buffer);
|
||||||
|
REQUIRE( numPlayingVoices(synth) == 0 ); // Released
|
||||||
|
REQUIRE( numActiveVoices(synth) == 1 );
|
||||||
|
synth.noteOn(0, 60, 63 );
|
||||||
|
synth.renderBlock(buffer);
|
||||||
|
REQUIRE( numPlayingVoices(synth) == 1 ); // Not released, attack phase
|
||||||
|
REQUIRE( numActiveVoices(synth) == 1 );
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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); // Decay (0.02)
|
||||||
synth.renderBlock(buffer);
|
synth.renderBlock(buffer);
|
||||||
synth.renderBlock(buffer); // Release (0.1)
|
synth.renderBlock(buffer); // Release (0.1)
|
||||||
REQUIRE(synth.getVoiceView(0)->releasedOrFree());
|
REQUIRE(synth.getVoiceView(0)->offedOrFree());
|
||||||
// Release is 0.1s
|
// Release is 0.1s
|
||||||
for (int i = 0; i < 10; ++i)
|
for (int i = 0; i < 10; ++i)
|
||||||
synth.renderBlock(buffer);
|
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); // Decay (0.02)
|
||||||
synth.renderBlock(buffer);
|
synth.renderBlock(buffer);
|
||||||
synth.renderBlock(buffer); // Release (0.1)
|
synth.renderBlock(buffer); // Release (0.1)
|
||||||
REQUIRE(synth.getVoiceView(0)->releasedOrFree());
|
REQUIRE(synth.getVoiceView(0)->released());
|
||||||
// Release is 0.1s
|
// Release is 0.1s
|
||||||
for (int i = 0; i < 10; ++i)
|
for (int i = 0; i < 10; ++i)
|
||||||
synth.renderBlock(buffer);
|
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); // Decay (0.02)
|
||||||
synth.renderBlock(buffer);
|
synth.renderBlock(buffer);
|
||||||
synth.renderBlock(buffer); // Release (0.1)
|
synth.renderBlock(buffer); // Release (0.1)
|
||||||
REQUIRE(synth.getVoiceView(0)->releasedOrFree());
|
REQUIRE(synth.getVoiceView(0)->released());
|
||||||
// Release is 0.1s
|
// Release is 0.1s
|
||||||
for (int i = 0; i < 10; ++i)
|
for (int i = 0; i < 10; ++i)
|
||||||
synth.renderBlock(buffer);
|
synth.renderBlock(buffer);
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ const std::vector<const sfz::Voice*> getPlayingVoices(const sfz::Synth& synth)
|
||||||
std::vector<const sfz::Voice*> playingVoices;
|
std::vector<const sfz::Voice*> playingVoices;
|
||||||
for (int i = 0; i < synth.getNumVoices(); ++i) {
|
for (int i = 0; i < synth.getNumVoices(); ++i) {
|
||||||
const auto* voice = synth.getVoiceView(i);
|
const auto* voice = synth.getVoiceView(i);
|
||||||
if (!voice->releasedOrFree())
|
if (!voice->released())
|
||||||
playingVoices.push_back(voice);
|
playingVoices.push_back(voice);
|
||||||
}
|
}
|
||||||
return playingVoices;
|
return playingVoices;
|
||||||
|
|
@ -78,7 +78,14 @@ const std::vector<const sfz::Voice*> getPlayingVoices(const sfz::Synth& synth)
|
||||||
unsigned numPlayingVoices(const sfz::Synth& synth)
|
unsigned numPlayingVoices(const sfz::Synth& synth)
|
||||||
{
|
{
|
||||||
return absl::c_count_if(getActiveVoices(synth), [](const sfz::Voice* v) {
|
return absl::c_count_if(getActiveVoices(synth), [](const sfz::Voice* v) {
|
||||||
return !v->releasedOrFree();
|
return !v->released();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned numActiveVoices(const sfz::Synth& synth)
|
||||||
|
{
|
||||||
|
return absl::c_count_if(getActiveVoices(synth), [](const sfz::Voice* v) {
|
||||||
|
return !v->offedOrFree();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,7 +94,7 @@ const std::vector<std::string> playingSamples(const sfz::Synth& synth)
|
||||||
std::vector<std::string> samples;
|
std::vector<std::string> samples;
|
||||||
for (int i = 0; i < synth.getNumVoices(); ++i) {
|
for (int i = 0; i < synth.getNumVoices(); ++i) {
|
||||||
const auto* voice = synth.getVoiceView(i);
|
const auto* voice = synth.getVoiceView(i);
|
||||||
if (!voice->releasedOrFree()) {
|
if (!voice->released()) {
|
||||||
if (auto region = voice->getRegion())
|
if (auto region = voice->getRegion())
|
||||||
samples.push_back(region->sampleId->filename());
|
samples.push_back(region->sampleId->filename());
|
||||||
}
|
}
|
||||||
|
|
@ -100,7 +107,7 @@ const std::vector<float> playingVelocities(const sfz::Synth& synth)
|
||||||
std::vector<float> velocities;
|
std::vector<float> velocities;
|
||||||
for (int i = 0; i < synth.getNumVoices(); ++i) {
|
for (int i = 0; i < synth.getNumVoices(); ++i) {
|
||||||
const auto* voice = synth.getVoiceView(i);
|
const auto* voice = synth.getVoiceView(i);
|
||||||
if (!voice->releasedOrFree())
|
if (!voice->released())
|
||||||
velocities.push_back(voice->getTriggerEvent().value);
|
velocities.push_back(voice->getTriggerEvent().value);
|
||||||
}
|
}
|
||||||
return velocities;
|
return velocities;
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,14 @@ const std::vector<const sfz::Voice*> getPlayingVoices(const sfz::Synth& synth);
|
||||||
*/
|
*/
|
||||||
unsigned numPlayingVoices(const sfz::Synth& synth);
|
unsigned numPlayingVoices(const sfz::Synth& synth);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Count the number of active (not free or offed) voices from the synth
|
||||||
|
*
|
||||||
|
* @param synth
|
||||||
|
* @return unsigned
|
||||||
|
*/
|
||||||
|
unsigned numActiveVoices(const sfz::Synth& synth);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the playing samples
|
* @brief Get the playing samples
|
||||||
*
|
*
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue