Skip to content

Commit

Permalink
Corrected sfztools#63
Browse files Browse the repository at this point in the history
- The envelope does not sustain if the region has `trigger=release`
- Change the envelope setup to pass the region rather than
individual envelope parameters
  • Loading branch information
paulfd committed Feb 17, 2020
1 parent cf94098 commit 3b2bb96
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 70 deletions.
40 changes: 22 additions & 18 deletions src/sfizz/ADSREnvelope.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,25 @@
namespace sfz {

template <class Type>
void ADSREnvelope<Type>::reset(int attack, int release, Type sustain, int delay, int decay, int hold, Type start, Type depth) noexcept
void ADSREnvelope<Type>::reset(const Region& region, const MidiState& state, int delay, uint8_t velocity, float sampleRate) noexcept
{
ASSERT(start <= 1.0f);
ASSERT(sustain <= 1.0f);

sustain = clamp<Type>(sustain, 0.0, 1.0);
start = clamp<Type>(start, 0.0, 1.0);

currentState = State::Done;
this->delay = delay;
this->attack = attack;
this->decay = decay;
this->release = release;
this->hold = hold;
this->start = depth * start;
this->sustain = depth * sustain;
this->peak = depth;
auto secondsToSamples = [sampleRate](auto timeInSeconds) {
return static_cast<int>(timeInSeconds * sampleRate);
};

const auto ccArray = state.getCCArray();
this->delay = delay + secondsToSamples(region.amplitudeEG.getDelay(ccArray, velocity));
this->attack = secondsToSamples(region.amplitudeEG.getAttack(ccArray, velocity));
this->decay = secondsToSamples(region.amplitudeEG.getDecay(ccArray, velocity));
this->release = secondsToSamples(region.amplitudeEG.getRelease(ccArray, velocity));
this->hold = secondsToSamples(region.amplitudeEG.getHold(ccArray, velocity));
this->peak = 1.0;
this->sustain = normalizePercents(region.amplitudeEG.getSustain(ccArray, velocity));
this->start = this->peak * normalizePercents(region.amplitudeEG.getStart(ccArray, velocity));

releaseDelay = 0;
shouldRelease = false;
freeRunning = ((region.trigger == SfzTrigger::release) || (region.trigger == SfzTrigger::release_key));
step = 0.0;
currentValue = this->start;
currentState = State::Delay;
Expand All @@ -53,7 +53,7 @@ Type ADSREnvelope<Type>::getNextValue() noexcept
return start;

currentState = State::Attack;
step = (static_cast<Type>(1.0) - currentValue) / (attack > 0 ? attack : 1);
step = (peak - currentValue) / (attack > 0 ? attack : 1);
[[fallthrough]];
case State::Attack:
if (attack-- > 0) {
Expand All @@ -62,7 +62,7 @@ Type ADSREnvelope<Type>::getNextValue() noexcept
}

currentState = State::Hold;
currentValue = 1.0;
currentValue = peak;
[[fallthrough]];
case State::Hold:
if (hold-- > 0)
Expand All @@ -81,6 +81,8 @@ Type ADSREnvelope<Type>::getNextValue() noexcept
currentValue = sustain;
[[fallthrough]];
case State::Sustain:
if (freeRunning)
shouldRelease = true;
return currentValue;
case State::Release:
if (release-- > 0) {
Expand Down Expand Up @@ -152,6 +154,8 @@ void ADSREnvelope<Type>::getBlock(absl::Span<Type> output) noexcept
currentState = State::Sustain;
[[fallthrough]];
case State::Sustain:
if (freeRunning)
shouldRelease = true;
break;
case State::Release:
length = min(remainingSamples, release);
Expand Down
19 changes: 9 additions & 10 deletions src/sfizz/ADSREnvelope.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

#pragma once
#include "LeakDetector.h"
#include "Region.h"
#include "MidiState.h"
#include <absl/types/span.h>
namespace sfz {
/**
Expand All @@ -19,19 +21,15 @@ class ADSREnvelope {
public:
ADSREnvelope() = default;
/**
* @brief Resets the ADSR envelope. There's alot of parameter but what can you do.
* They all match the SFZ specification.
* @brief Resets the ADSR envelope given a Region, the current midi state, and a delay and
* trigger velocity
*
* @param attack
* @param release
* @param sustain
* @param region
* @param state
* @param delay
* @param decay
* @param hold
* @param start
* @param depth
* @param velocity
*/
void reset(int attack, int release, Type sustain = 1.0, int delay = 0, int decay = 0, int hold = 0, Type start = 0.0, Type depth = 1) noexcept;
void reset(const Region& region, const MidiState& state, int delay, uint8_t velocity, float sampleRate) noexcept;
/**
* @brief Get the next value for the envelope
*
Expand Down Expand Up @@ -91,6 +89,7 @@ class ADSREnvelope {
Type sustain { 0 };
int releaseDelay { 0 };
bool shouldRelease { false };
bool freeRunning { false };
LEAK_DETECTOR(ADSREnvelope);
};

Expand Down
18 changes: 1 addition & 17 deletions src/sfizz/Voice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,23 +109,7 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, uint8_t value
initialDelay = delay + static_cast<uint32_t>(region->getDelay() * sampleRate);
baseFrequency = midiNoteFrequency(number);
bendStepFactor = centsFactor(region->bendStep);
prepareEGEnvelope(initialDelay, value);
}

void sfz::Voice::prepareEGEnvelope(int delay, uint8_t velocity) noexcept
{
auto secondsToSamples = [this](auto timeInSeconds) {
return static_cast<int>(timeInSeconds * sampleRate);
};
const auto& ccArray = resources.midiState.getCCArray();
egEnvelope.reset(
secondsToSamples(region->amplitudeEG.getAttack(ccArray, velocity)),
secondsToSamples(region->amplitudeEG.getRelease(ccArray, velocity)),
normalizePercents(region->amplitudeEG.getSustain(ccArray, velocity)),
delay + secondsToSamples(region->amplitudeEG.getDelay(ccArray, velocity)),
secondsToSamples(region->amplitudeEG.getDecay(ccArray, velocity)),
secondsToSamples(region->amplitudeEG.getHold(ccArray, velocity)),
normalizePercents(region->amplitudeEG.getStart(ccArray, velocity)));
egEnvelope.reset(*region, resources.midiState, delay, value, sampleRate);
}

bool sfz::Voice::isFree() const noexcept
Expand Down
7 changes: 0 additions & 7 deletions src/sfizz/Voice.h
Original file line number Diff line number Diff line change
Expand Up @@ -224,13 +224,6 @@ class Voice {
* @param buffer
*/
void fillWithGenerator(AudioSpan<float> buffer) noexcept;
/**
* @brief Computes the values for the envelope depending on the note or CC number and the velocity/cc value
*
* @param delay
* @param velocity
*/
void prepareEGEnvelope(int delay, uint8_t velocity) noexcept;
/**
* @brief The function processing a mono sample source
*
Expand Down
91 changes: 73 additions & 18 deletions tests/ADSREnvelopeT.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,18 @@ TEST_CASE("[ADSREnvelope] Basic state")
TEST_CASE("[ADSREnvelope] Attack")
{
sfz::ADSREnvelope<float> envelope;
envelope.reset(2, 0);
sfz::MidiState state;
sfz::Region region { state };
region.amplitudeEG.attack = 0.02f;

envelope.reset(region, state, 0, 0, 100.0f);
std::array<float, 5> output;
std::array<float, 5> expected { 0.5f, 1.0f, 1.0f, 1.0f, 1.0f };
for (auto& out : output)
out = envelope.getNextValue();
REQUIRE(approxEqual<float>(output, expected));

envelope.reset(2, 0);
envelope.reset(region, state, 0, 0, 100.0f);
absl::c_fill(output, -1.0f);
envelope.getBlock(absl::MakeSpan(output));
REQUIRE(approxEqual<float>(output, expected));
Expand All @@ -61,14 +65,18 @@ TEST_CASE("[ADSREnvelope] Attack")
TEST_CASE("[ADSREnvelope] Attack again")
{
sfz::ADSREnvelope<float> envelope;
envelope.reset(3, 0);
sfz::MidiState state;
sfz::Region region { state };
region.amplitudeEG.attack = 0.03f;

envelope.reset(region, state, 0, 0, 100.0f);
std::array<float, 5> output;
std::array<float, 5> expected { 0.33333f, 0.66667f, 1.0f, 1.0f, 1.0f };
for (auto& out : output)
out = envelope.getNextValue();
REQUIRE(approxEqual<float>(output, expected));

envelope.reset(3, 0);
envelope.reset(region, state, 0, 0, 100.0f);
absl::c_fill(output, -1.0f);
envelope.getBlock(absl::MakeSpan(output));
REQUIRE(approxEqual<float>(output, expected));
Expand All @@ -77,15 +85,20 @@ TEST_CASE("[ADSREnvelope] Attack again")
TEST_CASE("[ADSREnvelope] Release")
{
sfz::ADSREnvelope<float> envelope;
envelope.reset(2, 4);
sfz::MidiState state;
sfz::Region region { state };
region.amplitudeEG.attack = 0.02f;
region.amplitudeEG.release = 0.04f;

envelope.reset(region, state, 0, 0, 100.0f);
envelope.startRelease(2);
std::array<float, 8> output;
std::array<float, 8> expected { 0.5f, 1.0f, 0.08409f, 0.00707f, 0.000594604f, 0.00005f, 0.0f, 0.0f };
for (auto& out : output)
out = envelope.getNextValue();
REQUIRE(approxEqual<float>(output, expected));

envelope.reset(2, 4);
envelope.reset(region, state, 0, 0, 100.0f);
envelope.startRelease(2);
absl::c_fill(output, -1.0f);
envelope.getBlock(absl::MakeSpan(output));
Expand All @@ -95,15 +108,20 @@ TEST_CASE("[ADSREnvelope] Release")
TEST_CASE("[ADSREnvelope] Delay")
{
sfz::ADSREnvelope<float> envelope;
envelope.reset(2, 4, 1.0f, 2);
sfz::MidiState state;
sfz::Region region { state };
region.amplitudeEG.attack = 0.02f;
region.amplitudeEG.release = 0.04f;
region.amplitudeEG.delay = 0.02f;
std::array<float, 10> output;
envelope.reset(region, state, 0, 0, 100.0f);
envelope.startRelease(4);
std::array<float, 10> expected { 0.0f, 0.0f, 0.5f, 1.0f, 0.08409f, 0.00707f, 0.000594604f, 0.00005f, 0.0f, 0.0f };
for (auto& out : output)
out = envelope.getNextValue();
REQUIRE(approxEqual<float>(output, expected));

envelope.reset(2, 4, 1.0f, 2);
envelope.reset(region, state, 0, 0, 100.0f);
envelope.startRelease(4);
absl::c_fill(output, -1.0f);
envelope.getBlock(absl::MakeSpan(output));
Expand All @@ -113,14 +131,20 @@ TEST_CASE("[ADSREnvelope] Delay")
TEST_CASE("[ADSREnvelope] Lower sustain")
{
sfz::ADSREnvelope<float> envelope;
envelope.reset(2, 4, 0.5f, 2);
sfz::MidiState state;
sfz::Region region { state };
region.amplitudeEG.attack = 0.02f;
region.amplitudeEG.release = 0.04f;
region.amplitudeEG.delay = 0.02f;
region.amplitudeEG.sustain = 50.0f;
std::array<float, 10> output;
envelope.reset(region, state, 0, 0, 100.0f);
std::array<float, 10> expected { 0.0f, 0.0f, 0.5f, 1.0f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f };
for (auto& out : output)
out = envelope.getNextValue();
REQUIRE(approxEqual<float>(output, expected));

envelope.reset(2, 4, 0.5, 2);
envelope.reset(region, state, 0, 0, 100.0f);
absl::c_fill(output, -1.0f);
envelope.getBlock(absl::MakeSpan(output));
REQUIRE(approxEqual<float>(output, expected));
Expand All @@ -129,14 +153,21 @@ TEST_CASE("[ADSREnvelope] Lower sustain")
TEST_CASE("[ADSREnvelope] Decay")
{
sfz::ADSREnvelope<float> envelope;
envelope.reset(2, 4, 0.5f, 2, 2);
sfz::MidiState state;
sfz::Region region { state };
region.amplitudeEG.attack = 0.02f;
region.amplitudeEG.release = 0.04f;
region.amplitudeEG.delay = 0.02f;
region.amplitudeEG.sustain = 50.0f;
region.amplitudeEG.decay = 0.02f;
std::array<float, 10> output;
envelope.reset(region, state, 0, 0, 100.0f);
std::array<float, 10> expected { 0.0f, 0.0f, 0.5f, 1.0f, 0.707107f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5 };
for (auto& out : output)
out = envelope.getNextValue();
REQUIRE(approxEqual<float>(output, expected));

envelope.reset(2, 4, 0.5f, 2, 2);
envelope.reset(region, state, 0, 0, 100.0f);
absl::c_fill(output, -1.0f);
envelope.getBlock(absl::MakeSpan(output));
REQUIRE(approxEqual<float>(output, expected));
Expand All @@ -145,14 +176,22 @@ TEST_CASE("[ADSREnvelope] Decay")
TEST_CASE("[ADSREnvelope] Hold")
{
sfz::ADSREnvelope<float> envelope;
envelope.reset(2, 4, 0.5f, 2, 2, 2);
sfz::MidiState state;
sfz::Region region { state };
region.amplitudeEG.attack = 0.02f;
region.amplitudeEG.release = 0.04f;
region.amplitudeEG.delay = 0.02f;
region.amplitudeEG.sustain = 50.0f;
region.amplitudeEG.decay = 0.02f;
region.amplitudeEG.hold = 0.02f;
std::array<float, 12> output;
envelope.reset(region, state, 0, 0, 100.0f);
std::array<float, 12> expected { 0.0f, 0.0f, 0.5f, 1.0f, 1.0f, 1.0f, 0.707107f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f };
for (auto& out : output)
out = envelope.getNextValue();
REQUIRE(approxEqual<float>(output, expected));

envelope.reset(2, 4, 0.5f, 2, 2, 2);
envelope.reset(region, state, 0, 0, 100.0f);
absl::c_fill(output, -1.0f);
envelope.getBlock(absl::MakeSpan(output));
REQUIRE(approxEqual<float>(output, expected));
Expand All @@ -161,15 +200,23 @@ TEST_CASE("[ADSREnvelope] Hold")
TEST_CASE("[ADSREnvelope] Hold with release")
{
sfz::ADSREnvelope<float> envelope;
envelope.reset(2, 4, 0.5f, 2, 2, 2);
sfz::MidiState state;
sfz::Region region { state };
region.amplitudeEG.attack = 0.02f;
region.amplitudeEG.release = 0.04f;
region.amplitudeEG.delay = 0.02f;
region.amplitudeEG.sustain = 50.0f;
region.amplitudeEG.decay = 0.02f;
region.amplitudeEG.hold = 0.02f;
envelope.reset(region, state, 0, 0, 100.0f);
envelope.startRelease(8);
std::array<float, 14> output;
std::array<float, 14> expected { 0.0f, 0.0f, 0.5f, 1.0f, 1.0f, 1.0f, 0.707107f, 0.5f, 0.05f, 0.005f, 0.0005f, 0.00005f, 0.0f, 0.0f };
for (auto& out : output)
out = envelope.getNextValue();

REQUIRE(approxEqual<float>(output, expected));
envelope.reset(2, 4, 0.5f, 2, 2, 2);
envelope.reset(region, state, 0, 0, 100.0f);
envelope.startRelease(8);
absl::c_fill(output, -1.0f);
envelope.getBlock(absl::MakeSpan(output));
Expand All @@ -179,14 +226,22 @@ TEST_CASE("[ADSREnvelope] Hold with release")
TEST_CASE("[ADSREnvelope] Hold with release 2")
{
sfz::ADSREnvelope<float> envelope;
envelope.reset(2, 4, 0.5f, 2, 2, 2);
sfz::MidiState state;
sfz::Region region { state };
region.amplitudeEG.attack = 0.02f;
region.amplitudeEG.release = 0.04f;
region.amplitudeEG.delay = 0.02f;
region.amplitudeEG.sustain = 50.0f;
region.amplitudeEG.decay = 0.02f;
region.amplitudeEG.hold = 0.02f;
envelope.reset(region, state, 0, 0, 100.0f);
envelope.startRelease(4);
std::array<float, 14> output;
std::array<float, 14> expected { 0.0f, 0.0f, 0.5f, 1.0f, 0.08409f, 0.00707f, 0.000594604f, 0.00005f, 0.0f, 0.0f, 0.0f, 0.0 };
for (auto& out : output)
out = envelope.getNextValue();
REQUIRE(approxEqual<float>(output, expected));
envelope.reset(2, 4, 0.5f, 2, 2, 2);
envelope.reset(region, state, 0, 0, 100.0f);
envelope.startRelease(4);
absl::c_fill(output, -1.0f);
envelope.getBlock(absl::MakeSpan(output));
Expand Down
15 changes: 15 additions & 0 deletions tests/SynthT.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,18 @@ TEST_CASE("[Synth] Releasing after the initial and normal mode does not trigger
synth.renderBlock(buffer);
REQUIRE( !synth.getVoiceView(0)->isFree() );
}

TEST_CASE("[Synth] Trigger=release and an envelope properly kills the voice at the end of the envelope")
{
sfz::Synth synth;
synth.setSamplesPerBlock(1024);
sfz::AudioBuffer<float> buffer(2, 1024);
synth.setNumVoices(1);
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/envelope_trigger_release.sfz");
synth.noteOn(10, 60, 63);
synth.noteOff(10, 60, 63);
REQUIRE( !synth.getVoiceView(0)->isFree() );
for (int i = 0; i < 10; ++i)
synth.renderBlock(buffer);
REQUIRE( synth.getVoiceView(0)->isFree() );
}
Loading

0 comments on commit 3b2bb96

Please sign in to comment.