Correct a bug with dynamic updates on offed EGs

When offing a voice, the release time is cut to a minimum. Dynamic
updates would reset this to the original value. The commit adds a way to
disable dynamic EG updates and does so when offing the voice.
The engine will also steal the oldest offed voice as a last resort to
hopefully sound the newly played note every time.
This commit is contained in:
Paul Fd 2022-05-15 01:47:30 +02:00
parent a652e513bc
commit 897ddc1a8f
11 changed files with 133 additions and 35 deletions

View file

@ -34,7 +34,7 @@ Float ADSREnvelope::secondsToExpRate(Float timeInSeconds) const noexcept
if (timeInSeconds <= 0) if (timeInSeconds <= 0)
return Float(0.0); return Float(0.0);
timeInSeconds = std::max(Float(25e-3), timeInSeconds); timeInSeconds = std::max(Float(Default::offTime), timeInSeconds);
return std::exp(Float(-9.0) / (timeInSeconds * sampleRate)); return std::exp(Float(-9.0) / (timeInSeconds * sampleRate));
}; };
@ -42,6 +42,7 @@ void ADSREnvelope::reset(const EGDescription& desc, const Region& region, int de
{ {
this->sampleRate = sampleRate; this->sampleRate = sampleRate;
desc_ = &desc; desc_ = &desc;
dynamic_ = desc.dynamic;
triggerVelocity_ = velocity; triggerVelocity_ = velocity;
currentState = State::Delay; // Has to be before the update currentState = State::Delay; // Has to be before the update
updateValues(delay); updateValues(delay);
@ -58,7 +59,6 @@ void ADSREnvelope::updateValues(int delay) noexcept
{ {
if (currentState == State::Delay) if (currentState == State::Delay)
this->delay = delay + secondsToSamples(desc_->getDelay(midiState_, triggerVelocity_, delay)); this->delay = delay + secondsToSamples(desc_->getDelay(midiState_, triggerVelocity_, delay));
this->attackStep = secondsToLinRate(desc_->getAttack(midiState_, triggerVelocity_, delay)); this->attackStep = secondsToLinRate(desc_->getAttack(midiState_, triggerVelocity_, delay));
this->decayRate = secondsToExpRate(desc_->getDecay(midiState_, triggerVelocity_, delay)); this->decayRate = secondsToExpRate(desc_->getDecay(midiState_, triggerVelocity_, delay));
this->releaseRate = secondsToExpRate(desc_->getRelease(midiState_, triggerVelocity_, delay)); this->releaseRate = secondsToExpRate(desc_->getRelease(midiState_, triggerVelocity_, delay));
@ -70,7 +70,7 @@ void ADSREnvelope::updateValues(int delay) noexcept
void ADSREnvelope::getBlock(absl::Span<Float> output) noexcept void ADSREnvelope::getBlock(absl::Span<Float> output) noexcept
{ {
if (desc_ && desc_->dynamic) { if (dynamic_) {
int processed = 0; int processed = 0;
int remaining = static_cast<int>(output.size()); int remaining = static_cast<int>(output.size());
while(remaining > 0) { while(remaining > 0) {

View file

@ -76,6 +76,12 @@ public:
*/ */
int getRemainingDelay() const noexcept { return delay; } int getRemainingDelay() const noexcept { return delay; }
/**
* @brief Stop dynamic updates of the EG values.
*
*/
void stopDynamicUpdates() { dynamic_ = false; }
private: private:
float sampleRate { config::defaultSampleRate }; float sampleRate { config::defaultSampleRate };
int secondsToSamples(Float timeInSeconds) const noexcept; int secondsToSamples(Float timeInSeconds) const noexcept;
@ -99,6 +105,7 @@ private:
const EGDescription* desc_ { nullptr }; const EGDescription* desc_ { nullptr };
const MidiState& midiState_; const MidiState& midiState_;
float triggerVelocity_ { 0.0f }; float triggerVelocity_ { 0.0f };
bool dynamic_ { false };
int delay { 0 }; int delay { 0 };
Float attackStep { 0 }; Float attackStep { 0 };
Float decayRate { 0 }; Float decayRate { 0 };

View file

@ -153,17 +153,12 @@ namespace config {
*/ */
static constexpr float overflowVoiceMultiplier { 1.5f }; static constexpr float overflowVoiceMultiplier { 1.5f };
static_assert(overflowVoiceMultiplier >= 1.0f, "This needs to add voices"); static_assert(overflowVoiceMultiplier >= 1.0f, "This needs to add voices");
/** /**
* @brief Calculate the effective voice number for the polyphony setting, * @brief Minimum number of overflow voices to add
* accounting for the overflow factor. *
*/ */
inline constexpr int calculateActualVoices(int polyphony) static constexpr int minOverflowVoices { 4 };
{
return
(int(polyphony * config::overflowVoiceMultiplier) < int(config::maxVoices)) ?
int(polyphony * config::overflowVoiceMultiplier) : int(config::maxVoices);
}
/** /**
* @brief The smoothing time constant per "smooth" steps * @brief The smoothing time constant per "smooth" steps
*/ */

View file

@ -1042,6 +1042,12 @@ int Synth::getNumActiveVoices() const noexcept
std::min(impl.numVoices_, activeVoices) : activeVoices; std::min(impl.numVoices_, activeVoices) : activeVoices;
} }
std::vector<const Voice*> Synth::getActiveVoices() const noexcept
{
Impl& impl = *impl_;
return impl.voiceManager_.getActiveVoices();
}
void Synth::setSamplesPerBlock(int samplesPerBlock) noexcept void Synth::setSamplesPerBlock(int samplesPerBlock) noexcept
{ {
Impl& impl = *impl_; Impl& impl = *impl_;
@ -1295,7 +1301,6 @@ void Synth::Impl::startVoice(Layer* layer, int delay, const TriggerEvent& trigge
if (selectedVoice == nullptr) if (selectedVoice == nullptr)
return; return;
ASSERT(selectedVoice->isFree());
if (selectedVoice->startVoice(layer, delay, triggerEvent)) if (selectedVoice->startVoice(layer, delay, triggerEvent))
ring.addVoiceToRing(selectedVoice); ring.addVoiceToRing(selectedVoice);
} }

View file

@ -555,6 +555,14 @@ public:
* @return int * @return int
*/ */
int getNumActiveVoices() const noexcept; int getNumActiveVoices() const noexcept;
/**
* @brief Get the active voices as a view
*
* @return std::vector<const Voice*>
*/
std::vector<const Voice*> getActiveVoices() const noexcept;
/** /**
* @brief Get the total number of voices in the synth (the polyphony) * @brief Get the total number of voices in the synth (the polyphony)
* *

View file

@ -607,6 +607,7 @@ void Voice::Impl::off(int delay, bool fast) noexcept
} else if (region_->offMode == OffMode::time) { } else if (region_->offMode == OffMode::time) {
egAmplitude_.setReleaseTime(region_->offTime); egAmplitude_.setReleaseTime(region_->offTime);
} }
egAmplitude_.stopDynamicUpdates();
} }
else { else {
// TODO(jpc): Flex AmpEG // TODO(jpc): Flex AmpEG

View file

@ -104,6 +104,14 @@ const PolyphonyGroup* VoiceManager::getPolyphonyGroupView(int idx) noexcept
return &polyphonyGroups_[idx]; return &polyphonyGroups_[idx];
} }
std::vector<const Voice*> VoiceManager::getActiveVoices() const noexcept
{
std::vector<const Voice*> returned;
std::copy(activeVoices_.begin(), activeVoices_.end(), std::back_inserter(returned));
return returned;
}
void VoiceManager::clear() void VoiceManager::clear()
{ {
for (auto& pg : polyphonyGroups_) for (auto& pg : polyphonyGroups_)
@ -147,12 +155,21 @@ void VoiceManager::checkPolyphony(const Region* region, int delay, const Trigger
Voice* VoiceManager::findFreeVoice() noexcept Voice* VoiceManager::findFreeVoice() noexcept
{ {
auto freeVoice = absl::c_find_if(list_, [](const Voice& voice) { Voice* freeVoice = nullptr;
return voice.isFree(); for (auto& v: list_) {
}); if (v.isFree()) {
freeVoice = &v;
break;
}
if (freeVoice != list_.end()) if (v.offedOrFree()) {
return &*freeVoice; if (freeVoice == nullptr || v.getAge() > freeVoice->getAge())
freeVoice = &v;
}
};
if (freeVoice != nullptr)
return freeVoice;
DBG("Engine hard polyphony reached"); DBG("Engine hard polyphony reached");
return {}; return {};
@ -161,7 +178,8 @@ Voice* VoiceManager::findFreeVoice() noexcept
void VoiceManager::requireNumVoices(int numVoices, Resources& resources) void VoiceManager::requireNumVoices(int numVoices, Resources& resources)
{ {
numRequiredVoices_ = numVoices; numRequiredVoices_ = numVoices;
const int numEffectiveVoices = getNumEffectiveVoices(); const int numEffectiveVoices = std::min(int(config::maxVoices), numVoices +
std::max(int(numVoices * config::overflowVoiceMultiplier), config::minOverflowVoices));
clear(); clear();
list_.reserve(numEffectiveVoices); list_.reserve(numEffectiveVoices);
@ -249,7 +267,7 @@ void VoiceManager::checkEnginePolyphony(int delay) noexcept
{ {
Voice* candidate = stealer_->checkPolyphony( Voice* candidate = stealer_->checkPolyphony(
absl::MakeSpan(activeVoices_), numRequiredVoices_); absl::MakeSpan(activeVoices_), numRequiredVoices_);
SisterVoiceRing::offAllSisters(candidate, delay); SisterVoiceRing::offAllSisters(candidate, delay, true);
} }
} // namespace sfz } // namespace sfz

View file

@ -80,6 +80,13 @@ struct VoiceManager final : public Voice::StateListener
*/ */
const PolyphonyGroup* getPolyphonyGroupView(int idx) noexcept; const PolyphonyGroup* getPolyphonyGroupView(int idx) noexcept;
/**
* @brief Get the actives voices as a view
*
* @return std::vector<const Voice*>
*/
std::vector<const Voice*> getActiveVoices() const noexcept;
/** /**
* @brief Clear all voices and polyphony groups. * @brief Clear all voices and polyphony groups.
* Also resets the stealing algorithm to default. * Also resets the stealing algorithm to default.
@ -134,7 +141,6 @@ struct VoiceManager final : public Voice::StateListener
private: private:
int numRequiredVoices_ { config::numVoices }; int numRequiredVoices_ { config::numVoices };
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_; std::vector<Voice*> temp_;

View file

@ -989,11 +989,11 @@ TEST_CASE("[Synth] Sustain threshold")
REQUIRE( playingSamples(synth) == std::vector<std::string> { "*sine", "*sine" } ); REQUIRE( playingSamples(synth) == std::vector<std::string> { "*sine", "*sine" } );
synth.noteOn(0, 62, 85); synth.noteOn(0, 62, 85);
synth.renderBlock(buffer); synth.renderBlock(buffer);
REQUIRE( playingSamples(synth) == std::vector<std::string> { "*silence", "*sine", "*sine" } ); REQUIRE( playingSamples(synth) == std::vector<std::string> { "*sine", "*sine", "*silence" } );
synth.cc(0, 64, 64); synth.cc(0, 64, 64);
synth.noteOff(0, 62, 85); synth.noteOff(0, 62, 85);
synth.renderBlock(buffer); synth.renderBlock(buffer);
REQUIRE( playingSamples(synth) == std::vector<std::string> { "*silence", "*sine", "*sine" } ); REQUIRE( playingSamples(synth) == std::vector<std::string> {"*sine", "*sine", "*silence" } );
} }
TEST_CASE("[Synth] Sustain") TEST_CASE("[Synth] Sustain")
@ -2140,3 +2140,23 @@ TEST_CASE("[Synth] Reloading a file ignores the `set_ccN` opcodes")
REQUIRE(messageList == expected); REQUIRE(messageList == expected);
} }
TEST_CASE("[Synth] Reuse offed voices in the last case scenario for new notes")
{
sfz::Synth synth;
synth.setNumVoices(2);
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<region> sample=*sine ampeg_release=10
)");
synth.noteOn(0, 60, 63);
synth.renderBlock(buffer);
REQUIRE( playingNotes(synth) == std::vector<int> { 60 } );
for (int i = 61; i < 80; ++i) {
synth.noteOn(0, i, 63);
synth.renderBlock(buffer);
auto notes = playingNotes(synth);
std::sort(notes.begin(), notes.end());
REQUIRE( notes == std::vector<int> { i - 1, i } );
}
}

View file

@ -92,8 +92,8 @@ unsigned numActiveVoices(const sfz::Synth& synth)
const std::vector<std::string> playingSamples(const sfz::Synth& synth) 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) { auto activeVoices = synth.getActiveVoices();
const auto* voice = synth.getVoiceView(i); for (const auto* voice: activeVoices) {
if (!voice->released()) { 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());
@ -105,19 +105,30 @@ const std::vector<std::string> playingSamples(const sfz::Synth& synth)
const std::vector<float> playingVelocities(const sfz::Synth& synth) 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) { auto activeVoices = synth.getActiveVoices();
const auto* voice = synth.getVoiceView(i); for (const auto* voice: activeVoices) {
if (!voice->released()) if (!voice->released())
velocities.push_back(voice->getTriggerEvent().value); velocities.push_back(voice->getTriggerEvent().value);
} }
return velocities; return velocities;
} }
const std::vector<int> playingNotes(const sfz::Synth& synth)
{
std::vector<int> notes;
auto activeVoices = synth.getActiveVoices();
for (const auto* voice: activeVoices) {
if (!voice->released())
notes.push_back(voice->getTriggerEvent().number);
}
return notes;
}
const std::vector<std::string> activeSamples(const sfz::Synth& synth) const std::vector<std::string> activeSamples(const sfz::Synth& synth)
{ {
std::vector<std::string> samples; std::vector<std::string> samples;
for (int i = 0; i < synth.getNumVoices(); ++i) { auto activeVoices = synth.getActiveVoices();
const auto* voice = synth.getVoiceView(i); for (const auto* voice: activeVoices) {
if (!voice->isFree()) { if (!voice->isFree()) {
const sfz::Region* region = voice->getRegion(); const sfz::Region* region = voice->getRegion();
if (region) if (region)
@ -130,14 +141,25 @@ const std::vector<std::string> activeSamples(const sfz::Synth& synth)
const std::vector<float> activeVelocities(const sfz::Synth& synth) const std::vector<float> activeVelocities(const sfz::Synth& synth)
{ {
std::vector<float> velocities; std::vector<float> velocities;
for (int i = 0; i < synth.getNumVoices(); ++i) { auto activeVoices = synth.getActiveVoices();
const auto* voice = synth.getVoiceView(i); for (const auto* voice: activeVoices) {
if (!voice->isFree()) if (!voice->isFree())
velocities.push_back(voice->getTriggerEvent().value); velocities.push_back(voice->getTriggerEvent().value);
} }
return velocities; return velocities;
} }
const std::vector<int> activeNotes(const sfz::Synth& synth)
{
std::vector<int> notes;
auto activeVoices = synth.getActiveVoices();
for (const auto* voice: activeVoices) {
if (!voice->isFree())
notes.push_back(voice->getTriggerEvent().number);
}
return notes;
}
std::string createDefaultGraph(std::vector<std::string> lines, int numRegions) std::string createDefaultGraph(std::vector<std::string> lines, int numRegions)
{ {
for (int regionIdx = 0; regionIdx < numRegions; ++regionIdx) { for (int regionIdx = 0; regionIdx < numRegions; ++regionIdx) {

View file

@ -90,7 +90,7 @@ unsigned numActiveVoices(const sfz::Synth& synth);
* @brief Get the playing samples * @brief Get the playing samples
* *
* @param synth * @param synth
* @return unsigned * @return std::vector<std::string>
*/ */
const std::vector<std::string> playingSamples(const sfz::Synth& synth); const std::vector<std::string> playingSamples(const sfz::Synth& synth);
@ -98,15 +98,23 @@ const std::vector<std::string> playingSamples(const sfz::Synth& synth);
* @brief Get the playing notes velocities * @brief Get the playing notes velocities
* *
* @param synth * @param synth
* @return unsigned * @return std::vector<float>
*/ */
const std::vector<float> playingVelocities(const sfz::Synth& synth); const std::vector<float> playingVelocities(const sfz::Synth& synth);
/**
* @brief Get the playing notes
*
* @param synth
* @return std::vector<int>
*/
const std::vector<int> playingNotes(const sfz::Synth& synth);
/** /**
* @brief Get the active samples * @brief Get the active samples
* *
* @param synth * @param synth
* @return unsigned * @return std::vector<std::string>
*/ */
const std::vector<std::string> activeSamples(const sfz::Synth& synth); const std::vector<std::string> activeSamples(const sfz::Synth& synth);
@ -114,10 +122,18 @@ const std::vector<std::string> activeSamples(const sfz::Synth& synth);
* @brief Get the active notes velocities * @brief Get the active notes velocities
* *
* @param synth * @param synth
* @return unsigned * @return std::vector<float>
*/ */
const std::vector<float> activeVelocities(const sfz::Synth& synth); const std::vector<float> activeVelocities(const sfz::Synth& synth);
/**
* @brief Get the active notes
*
* @param synth
* @return std::vector<int>
*/
const std::vector<int> activeNotes(const sfz::Synth& synth);
/** /**
* @brief Create the default dot graph representation for standard regions * @brief Create the default dot graph representation for standard regions
* *