Merge pull request #380 from paulfd/end-disabling-regions

End disabling regions
This commit is contained in:
Paul Ferrand 2020-08-21 00:25:30 +02:00 committed by GitHub
commit c61ab364af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 186 additions and 60 deletions

View file

@ -20,13 +20,18 @@ Type ADSREnvelope<Type>::secondsToSamples (Type timeInSeconds) const noexcept
template<class Type>
Type ADSREnvelope<Type>::secondsToLinRate (Type timeInSeconds) const noexcept
{
timeInSeconds = std::max<Type>(timeInSeconds, config::virtuallyZero);
if (timeInSeconds == 0)
return 1.0f;
return 1 / (sampleRate * timeInSeconds);
};
template<class Type>
Type ADSREnvelope<Type>::secondsToExpRate (Type timeInSeconds) const noexcept
{
if (timeInSeconds == 0)
return 0.0f;
timeInSeconds = std::max<Type>(25e-3, timeInSeconds);
return std::exp(-9.0 / (timeInSeconds * sampleRate));
};
@ -43,12 +48,14 @@ void ADSREnvelope<Type>::reset(const EGDescription& desc, const Region& region,
this->hold = secondsToSamples(desc.getHold(state, velocity));
this->peak = 1.0;
this->sustain = normalizePercents(desc.getSustain(state, velocity));
this->sustain = max(this->sustain, config::virtuallyZero);
this->start = this->peak * normalizePercents(desc.getStart(state, velocity));
releaseDelay = 0;
sustainThreshold = this->sustain + config::virtuallyZero;
shouldRelease = false;
freeRunning = ((region.trigger == SfzTrigger::release)
freeRunning = (
(region.trigger == SfzTrigger::release)
|| (this->sustain == 0.0f)
|| (region.trigger == SfzTrigger::release_key)
|| (region.loopMode == SfzLoopMode::one_shot && (region.isGenerator() || region.oscillator)));
currentValue = this->start;
@ -84,7 +91,7 @@ Type ADSREnvelope<Type>::getNextValue() noexcept
// fallthrough
case State::Decay:
currentValue *= decayRate;
if (currentValue > sustain)
if (currentValue > sustainThreshold )
return currentValue;
currentState = State::Sustain;
@ -154,7 +161,7 @@ void ADSREnvelope<Type>::getBlock(absl::Span<Type> output) noexcept
case State::Decay:
while (count < size && (currentValue *= decayRate) > sustain)
output[count++] = currentValue;
if (currentValue <= sustain) {
if (currentValue <= sustainThreshold) {
currentValue = sustain;
currentState = State::Sustain;
}

View file

@ -102,6 +102,7 @@ private:
Type start { 0 };
Type peak { 0 };
Type sustain { 0 };
Type sustainThreshold { config::virtuallyZero };
int releaseDelay { 0 };
bool shouldRelease { false };
bool freeRunning { false };

View file

@ -1458,7 +1458,10 @@ float sfz::Region::getDelay() const noexcept
uint32_t sfz::Region::trueSampleEnd(Oversampling factor) const noexcept
{
return min(sampleEnd, loopRange.getEnd()) * static_cast<uint32_t>(factor);
if (sampleEnd <= 0)
return 0;
return min(static_cast<uint32_t>(sampleEnd), loopRange.getEnd()) * static_cast<uint32_t>(factor);
}
uint32_t sfz::Region::loopStart(Oversampling factor) const noexcept
@ -1615,3 +1618,8 @@ sfz::Region::Connection& sfz::Region::getOrCreateConnection(const ModKey& source
connections.push_back(c);
return connections.back();
}
bool sfz::Region::disabled() const noexcept
{
return (sampleEnd == 0);
}

View file

@ -261,6 +261,11 @@ struct Region {
*/
float getGainToEffectBus(unsigned number) const noexcept;
/**
* @brief Check if a region is disabled, if its sample end is weakly negative for example.
*/
bool disabled() const noexcept;
const NumericId<Region> id;
// Sound source: sample playback

View file

@ -51,6 +51,10 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value,
triggerValue = value;
this->region = region;
if (region->disabled())
return;
switchState(State::playing);
ASSERT(delay >= 0);
@ -715,9 +719,6 @@ bool sfz::Voice::checkOffGroup(int delay, uint32_t group) noexcept
if (region == nullptr)
return false;
if (delay <= this->triggerDelay)
return false;
if (triggerType == TriggerType::NoteOn && region->offBy == group) {
off(delay);
return true;

View file

@ -10,6 +10,7 @@
#include <iostream>
#include <sstream>
#include <cstring>
#include <fstream>
void load_txt(DataPoints& dp, std::istream& in)
{

View file

@ -471,56 +471,6 @@ TEST_CASE("[Files] Note and octave offsets")
REQUIRE( synth.getRegionView(6)->pitchKeycenter == 50 );
}
TEST_CASE("[Files] Off by with different delays")
{
Synth synth;
synth.setSamplesPerBlock(256);
AudioBuffer<float> buffer(2, 256);
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/off_by.sfz");
REQUIRE( synth.getNumRegions() == 4 );
synth.noteOn(0, 63, 63);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
auto group1Voice = synth.getVoiceView(0);
REQUIRE( group1Voice->getRegion()->group == 1ul );
REQUIRE( group1Voice->getRegion()->offBy == 2ul );
synth.noteOn(100, 64, 63);
synth.renderBlock(buffer);
REQUIRE(group1Voice->releasedOrFree());
}
TEST_CASE("[Files] Off by with the same delays")
{
Synth synth;
synth.setSamplesPerBlock(256);
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/off_by.sfz");
REQUIRE( synth.getNumRegions() == 4 );
synth.noteOn(0, 63, 63);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
auto group1Voice = synth.getVoiceView(0);
REQUIRE( group1Voice->getRegion()->group == 1ul );
REQUIRE( group1Voice->getRegion()->offBy == 2ul );
synth.noteOn(0, 64, 63);
REQUIRE(!group1Voice->releasedOrFree());
}
TEST_CASE("[Files] Off by with the same notes at the same time")
{
Synth synth;
synth.setSamplesPerBlock(256);
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/off_by.sfz");
REQUIRE( synth.getNumRegions() == 4 );
synth.noteOn(0, 65, 63);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
synth.noteOn(0, 65, 63);
REQUIRE( synth.getNumActiveVoices(true) == 4 );
AudioBuffer<float> buffer { 2, 256 };
synth.renderBlock(buffer);
synth.noteOn(0, 65, 63);
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 6 );
REQUIRE( numPlayingVoices(synth) == 2 );
}
TEST_CASE("[Files] Off modes")
{
Synth synth;

View file

@ -95,7 +95,12 @@ TEST_CASE("[Region] Parsing opcodes")
region.parseOpcode({ "end", "184" });
REQUIRE(region.sampleEnd == 184);
region.parseOpcode({ "end", "-1" });
REQUIRE(region.sampleEnd == 0);
REQUIRE(region.disabled());
region.parseOpcode({ "end", "2" });
REQUIRE(!region.disabled());
REQUIRE(region.sampleEnd == 2);
region.parseOpcode({ "end", "0" });
REQUIRE(region.disabled());
}
SECTION("count")

View file

@ -1143,3 +1143,131 @@ TEST_CASE("[Synth] Trigger also on the sustain CC")
synth.cc(0, 64, 127);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
}
TEST_CASE("[Synth] end=-1 voices are immediately killed after triggering but they kill other voices")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, 256 };
synth.loadSfzString(fs::current_path(), R"(
<region> key=60 end=-1 sample=*sine
<region> key=61 end=-1 sample=*silence
<region> key=62 sample=*sine off_by=2
<region> key=63 end=-1 sample=*saw group=2
)");
synth.noteOn(0, 60, 85);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
synth.noteOn(0, 61, 85);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
synth.noteOn(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( numPlayingVoices(synth) == 1 );
synth.noteOn(1, 63, 85);
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 0 );
}
TEST_CASE("[Synth] end=0 voices are immediately killed after triggering but they kill other voices")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, 256 };
synth.loadSfzString(fs::current_path(), R"(
<region> key=60 end=0 sample=*sine
<region> key=61 end=0 sample=*silence
<region> key=62 sample=*sine off_by=2
<region> key=63 end=0 sample=*saw group=2
)");
synth.noteOn(0, 60, 85);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
synth.noteOn(0, 61, 85);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
synth.noteOn(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( numPlayingVoices(synth) == 1 );
synth.noteOn(1, 63, 85);
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 0 );
}
TEST_CASE("[Synth] ampeg_sustain = 0 puts the ampeg envelope in free-running mode, which kills the voice almost instantly in most cases")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, 256 };
synth.loadSfzString(fs::current_path(), R"(
<region> key=60 sample=*sine ampeg_sustain=0
<region> key=61 sample=*sine ampeg_sustain=0 ampeg_attack=0.1 ampeg_decay=0.1
)");
synth.noteOn(0, 60, 85);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
synth.noteOn(0, 61, 85);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
// Render a bit; this does not kill the voice
for (unsigned i = 0; i < 5; ++i)
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
// Render about half a second
for (unsigned i = 0; i < 100; ++i)
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
}
TEST_CASE("[Synth] Off by standard")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, 256 };
synth.loadSfzString(fs::current_path(), R"(
<region> group=1 off_by=2 sample=*saw transpose=12 key=60
<region> group=2 off_by=1 sample=*triangle key=62
)");
synth.noteOn(0, 60, 85);
REQUIRE( numPlayingVoices(synth) == 1 );
synth.noteOn(10, 62, 85);
REQUIRE( numPlayingVoices(synth) == 1 );
auto playingVoices = getPlayingVoices(synth);
REQUIRE( playingVoices.front()->getRegion()->keyRange.containsWithEnd(62) );
synth.noteOn(10, 60, 85);
playingVoices = getPlayingVoices(synth);
REQUIRE( playingVoices.front()->getRegion()->keyRange.containsWithEnd(60) );
}
TEST_CASE("[Synth] Off by same group")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, 256 };
synth.loadSfzString(fs::current_path(), R"(
<region> group=1 off_by=1 sample=*saw transpose=12 key=60
<region> group=1 off_by=1 sample=*triangle key=62
)");
synth.noteOn(0, 60, 85);
REQUIRE( numPlayingVoices(synth) == 1 );
synth.noteOn(10, 62, 85);
REQUIRE( numPlayingVoices(synth) == 1 );
auto playingVoices = getPlayingVoices(synth);
REQUIRE( playingVoices.front()->getRegion()->keyRange.containsWithEnd(62) );
synth.noteOn(10, 60, 85);
playingVoices = getPlayingVoices(synth);
REQUIRE( playingVoices.front()->getRegion()->keyRange.containsWithEnd(60) );
}
TEST_CASE("[Synth] Off by same note")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, 256 };
synth.loadSfzString(fs::current_path(), R"(
<region> group=1 off_by=1 sample=*saw transpose=12 key=60
<region> group=1 off_by=1 sample=*triangle key=60
)");
synth.noteOn(0, 60, 85);
REQUIRE( numPlayingVoices(synth) == 1 );
auto playingVoices = getPlayingVoices(synth);
REQUIRE( playingVoices.front()->getRegion()->sampleId.filename() == "*triangle" );
}

View file

@ -51,6 +51,17 @@ const std::vector<const sfz::Voice*> getActiveVoices(const sfz::Synth& synth)
return activeVoices;
}
const std::vector<const sfz::Voice*> getPlayingVoices(const sfz::Synth& synth)
{
std::vector<const sfz::Voice*> playingVoices;
for (int i = 0; i < synth.getNumVoices(); ++i) {
const auto* voice = synth.getVoiceView(i);
if (!voice->releasedOrFree())
playingVoices.push_back(voice);
}
return playingVoices;
}
unsigned numPlayingVoices(const sfz::Synth& synth)
{
return absl::c_count_if(getActiveVoices(synth), [](const sfz::Voice* v) {

View file

@ -49,6 +49,15 @@ void sortAll(C& container, Args&... others)
*/
const std::vector<const sfz::Voice*> getActiveVoices(const sfz::Synth& synth);
/**
* @brief Get playing (unreleased) voices from the synth
*
* @param synth
* @return const std::vector<const sfz::Voice*>
*/
const std::vector<const sfz::Voice*> getPlayingVoices(const sfz::Synth& synth);
/**
* @brief Count the number of playing (unreleased) voices from the synth
*