diff --git a/src/sfizz/ADSREnvelope.cpp b/src/sfizz/ADSREnvelope.cpp index b0c10d6e..e8fa12b9 100644 --- a/src/sfizz/ADSREnvelope.cpp +++ b/src/sfizz/ADSREnvelope.cpp @@ -44,9 +44,8 @@ void ADSREnvelope::reset(const EGDescription& desc, const Region& region, const this->decayRate = secondsToExpRate(desc.getDecay(state, velocity)); this->releaseRate = secondsToExpRate(desc.getRelease(state, velocity)); this->hold = secondsToSamples(desc.getHold(state, velocity)); - this->peak = 1.0; this->sustain = normalizePercents(desc.getSustain(state, velocity)); - this->start = this->peak * normalizePercents(desc.getStart(state, velocity)); + this->start = normalizePercents(desc.getStart(state, velocity)); releaseDelay = 0; sustainThreshold = this->sustain + config::virtuallyZero; @@ -59,63 +58,13 @@ void ADSREnvelope::reset(const EGDescription& desc, const Region& region, const currentState = State::Delay; } -Float ADSREnvelope::getNextValue() noexcept -{ - if (shouldRelease && releaseDelay-- == 0) - currentState = State::Release; - - switch (currentState) { - case State::Delay: - if (delay-- > 0) - return start; - - currentState = State::Attack; - // fallthrough - case State::Attack: - currentValue += peak * attackStep; - if (currentValue < peak) - return currentValue; - - currentState = State::Hold; - currentValue = peak; - // fallthrough - case State::Hold: - if (hold-- > 0) - return currentValue; - - currentState = State::Decay; - // fallthrough - case State::Decay: - currentValue *= decayRate; - if (currentValue > sustainThreshold ) - return currentValue; - - currentState = State::Sustain; - currentValue = sustain; - // fallthrough - case State::Sustain: - if (freeRunning) - shouldRelease = true; - return currentValue; - case State::Release: - currentValue *= releaseRate; - if (currentValue > config::egReleaseThreshold) - return currentValue; - - currentState = State::Done; - currentValue = 0.0; - // fallthrough - default: - return 0.0; - } -} - void ADSREnvelope::getBlock(absl::Span output) noexcept { State currentState = this->currentState; Float currentValue = this->currentValue; bool shouldRelease = this->shouldRelease; int releaseDelay = this->releaseDelay; + Float transitionDelta = this->transitionDelta; while (!output.empty()) { size_t count = 0; @@ -130,6 +79,8 @@ void ADSREnvelope::getBlock(absl::Span output) noexcept size = std::min(size, releaseDelay); } + Float previousValue; + switch (currentState) { case State::Delay: while (count < size && delay-- > 0) { @@ -140,10 +91,10 @@ void ADSREnvelope::getBlock(absl::Span output) noexcept currentState = State::Attack; break; case State::Attack: - while (count < size && (currentValue += peak * attackStep) < peak) + while (count < size && (currentValue += attackStep) < 1) output[count++] = currentValue; - if (currentValue >= peak) { - currentValue = peak; + if (currentValue >= 1) { + currentValue = 1; currentState = State::Hold; } break; @@ -157,8 +108,9 @@ void ADSREnvelope::getBlock(absl::Span output) noexcept while (count < size && (currentValue *= decayRate) > sustain) output[count++] = currentValue; if (currentValue <= sustainThreshold) { - currentValue = sustain; currentState = State::Sustain; + currentValue = std::max(sustain, currentValue); + transitionDelta = (sustain - currentValue) / (sampleRate * config::egTransitionTime); } break; case State::Sustain: @@ -166,16 +118,27 @@ void ADSREnvelope::getBlock(absl::Span output) noexcept shouldRelease = true; break; } - count = size; - currentValue = sustain; - sfz::fill(output.first(count), currentValue); + while (count < size) { + currentValue = std::max(sustain, currentValue + transitionDelta); + output[count++] = currentValue; + } break; case State::Release: + previousValue = currentValue; while (count < size && (currentValue *= releaseRate) > config::egReleaseThreshold) - output[count++] = currentValue; + output[count++] = previousValue = currentValue; if (currentValue <= config::egReleaseThreshold) { - currentValue = 0; + currentState = State::Fadeout; + currentValue = previousValue; + transitionDelta = -currentValue / (sampleRate * config::egTransitionTime); + } + break; + case State::Fadeout: + while (count < size && (currentValue += transitionDelta) > 0) + output[count++] = currentValue; + if (currentValue <= 0) { currentState = State::Done; + currentValue = 0; } break; default: @@ -195,6 +158,7 @@ void ADSREnvelope::getBlock(absl::Span output) noexcept this->currentValue = currentValue; this->shouldRelease = shouldRelease; this->releaseDelay = releaseDelay; + this->transitionDelta = transitionDelta; ASSERT(!hasNanInf(output)); } diff --git a/src/sfizz/ADSREnvelope.h b/src/sfizz/ADSREnvelope.h index 648267d4..f5f435fb 100644 --- a/src/sfizz/ADSREnvelope.h +++ b/src/sfizz/ADSREnvelope.h @@ -30,12 +30,6 @@ public: * @param velocity */ void reset(const EGDescription& desc, const Region& region, const MidiState& state, int delay, float velocity, float sampleRate) noexcept; - /** - * @brief Get the next value for the envelope - * - * @return Float - */ - Float getNextValue() noexcept; /** * @brief Get a block of values for the envelope. This method tries hard to be efficient * and hopefully it is. @@ -68,7 +62,7 @@ public: * @return true * @return false */ - bool isReleased() const noexcept { return currentState == State::Release || shouldRelease; } + bool isReleased() const noexcept { return currentState >= State::Release || shouldRelease; } /** * @brief Get the remaining delay samples * @@ -89,6 +83,7 @@ private: Decay, Sustain, Release, + Fadeout, Done }; State currentState { State::Done }; @@ -99,12 +94,12 @@ private: Float releaseRate { 0 }; int hold { 0 }; Float start { 0 }; - Float peak { 0 }; Float sustain { 0 }; Float sustainThreshold { config::virtuallyZero }; int releaseDelay { 0 }; bool shouldRelease { false }; bool freeRunning { false }; + Float transitionDelta {}; LEAK_DETECTOR(ADSREnvelope); }; diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 89a44e09..a551b4f6 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -108,6 +108,11 @@ namespace config { finished. */ constexpr float egReleaseThreshold = 1e-4; + /** + Duration of a linear transition user to smooth cases of otherwise + immediate level transitions. (eg. decay->sustain or release->off) + */ + constexpr float egTransitionTime = 50e-3; /** Default metadata for MIDIName documents */ diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index 07d12c03..6fc1e773 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -517,7 +517,7 @@ TEST_CASE("[Files] Off modes") REQUIRE( synth.getNumActiveVoices() == 3 ); REQUIRE( numPlayingVoices(synth) == 1 ); AudioBuffer buffer { 2, 256 }; - for (unsigned i = 0; i < 10; ++i) // Not enough for the "normal" voice to die + for (unsigned i = 0; i < 20; ++i) // Not enough for the "normal" voice to die synth.renderBlock(buffer); REQUIRE( synth.getNumActiveVoices() == 2 ); REQUIRE( fastVoice->isFree() ); diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index cd309176..3bf2cc64 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -31,7 +31,7 @@ TEST_CASE("[Synth] Play and check active voices") synth.noteOn(0, 36, 89); REQUIRE(synth.getNumActiveVoices() == 2); // Render for a while - for (int i = 0; i < 200; ++i) + for (int i = 0; i < 300; ++i) synth.renderBlock(buffer); REQUIRE(synth.getNumActiveVoices() == 0); }