Merge pull request #380 from paulfd/end-disabling-regions
End disabling regions
This commit is contained in:
commit
c61ab364af
11 changed files with 186 additions and 60 deletions
|
|
@ -20,13 +20,18 @@ Type ADSREnvelope<Type>::secondsToSamples (Type timeInSeconds) const noexcept
|
||||||
template<class Type>
|
template<class Type>
|
||||||
Type ADSREnvelope<Type>::secondsToLinRate (Type timeInSeconds) const noexcept
|
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);
|
return 1 / (sampleRate * timeInSeconds);
|
||||||
};
|
};
|
||||||
|
|
||||||
template<class Type>
|
template<class Type>
|
||||||
Type ADSREnvelope<Type>::secondsToExpRate (Type timeInSeconds) const noexcept
|
Type ADSREnvelope<Type>::secondsToExpRate (Type timeInSeconds) const noexcept
|
||||||
{
|
{
|
||||||
|
if (timeInSeconds == 0)
|
||||||
|
return 0.0f;
|
||||||
|
|
||||||
timeInSeconds = std::max<Type>(25e-3, timeInSeconds);
|
timeInSeconds = std::max<Type>(25e-3, timeInSeconds);
|
||||||
return std::exp(-9.0 / (timeInSeconds * sampleRate));
|
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->hold = secondsToSamples(desc.getHold(state, velocity));
|
||||||
this->peak = 1.0;
|
this->peak = 1.0;
|
||||||
this->sustain = normalizePercents(desc.getSustain(state, velocity));
|
this->sustain = normalizePercents(desc.getSustain(state, velocity));
|
||||||
this->sustain = max(this->sustain, config::virtuallyZero);
|
|
||||||
this->start = this->peak * normalizePercents(desc.getStart(state, velocity));
|
this->start = this->peak * normalizePercents(desc.getStart(state, velocity));
|
||||||
|
|
||||||
releaseDelay = 0;
|
releaseDelay = 0;
|
||||||
|
sustainThreshold = this->sustain + config::virtuallyZero;
|
||||||
shouldRelease = false;
|
shouldRelease = false;
|
||||||
freeRunning = ((region.trigger == SfzTrigger::release)
|
freeRunning = (
|
||||||
|
(region.trigger == SfzTrigger::release)
|
||||||
|
|| (this->sustain == 0.0f)
|
||||||
|| (region.trigger == SfzTrigger::release_key)
|
|| (region.trigger == SfzTrigger::release_key)
|
||||||
|| (region.loopMode == SfzLoopMode::one_shot && (region.isGenerator() || region.oscillator)));
|
|| (region.loopMode == SfzLoopMode::one_shot && (region.isGenerator() || region.oscillator)));
|
||||||
currentValue = this->start;
|
currentValue = this->start;
|
||||||
|
|
@ -84,7 +91,7 @@ Type ADSREnvelope<Type>::getNextValue() noexcept
|
||||||
// fallthrough
|
// fallthrough
|
||||||
case State::Decay:
|
case State::Decay:
|
||||||
currentValue *= decayRate;
|
currentValue *= decayRate;
|
||||||
if (currentValue > sustain)
|
if (currentValue > sustainThreshold )
|
||||||
return currentValue;
|
return currentValue;
|
||||||
|
|
||||||
currentState = State::Sustain;
|
currentState = State::Sustain;
|
||||||
|
|
@ -154,7 +161,7 @@ void ADSREnvelope<Type>::getBlock(absl::Span<Type> output) noexcept
|
||||||
case State::Decay:
|
case State::Decay:
|
||||||
while (count < size && (currentValue *= decayRate) > sustain)
|
while (count < size && (currentValue *= decayRate) > sustain)
|
||||||
output[count++] = currentValue;
|
output[count++] = currentValue;
|
||||||
if (currentValue <= sustain) {
|
if (currentValue <= sustainThreshold) {
|
||||||
currentValue = sustain;
|
currentValue = sustain;
|
||||||
currentState = State::Sustain;
|
currentState = State::Sustain;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ private:
|
||||||
Type start { 0 };
|
Type start { 0 };
|
||||||
Type peak { 0 };
|
Type peak { 0 };
|
||||||
Type sustain { 0 };
|
Type sustain { 0 };
|
||||||
|
Type sustainThreshold { config::virtuallyZero };
|
||||||
int releaseDelay { 0 };
|
int releaseDelay { 0 };
|
||||||
bool shouldRelease { false };
|
bool shouldRelease { false };
|
||||||
bool freeRunning { false };
|
bool freeRunning { false };
|
||||||
|
|
|
||||||
|
|
@ -1458,7 +1458,10 @@ float sfz::Region::getDelay() const noexcept
|
||||||
|
|
||||||
uint32_t sfz::Region::trueSampleEnd(Oversampling factor) 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
|
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);
|
connections.push_back(c);
|
||||||
return connections.back();
|
return connections.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool sfz::Region::disabled() const noexcept
|
||||||
|
{
|
||||||
|
return (sampleEnd == 0);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -261,6 +261,11 @@ struct Region {
|
||||||
*/
|
*/
|
||||||
float getGainToEffectBus(unsigned number) const noexcept;
|
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;
|
const NumericId<Region> id;
|
||||||
|
|
||||||
// Sound source: sample playback
|
// Sound source: sample playback
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,10 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value,
|
||||||
triggerValue = value;
|
triggerValue = value;
|
||||||
|
|
||||||
this->region = region;
|
this->region = region;
|
||||||
|
|
||||||
|
if (region->disabled())
|
||||||
|
return;
|
||||||
|
|
||||||
switchState(State::playing);
|
switchState(State::playing);
|
||||||
|
|
||||||
ASSERT(delay >= 0);
|
ASSERT(delay >= 0);
|
||||||
|
|
@ -715,9 +719,6 @@ bool sfz::Voice::checkOffGroup(int delay, uint32_t group) noexcept
|
||||||
if (region == nullptr)
|
if (region == nullptr)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (delay <= this->triggerDelay)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (triggerType == TriggerType::NoteOn && region->offBy == group) {
|
if (triggerType == TriggerType::NoteOn && region->offBy == group) {
|
||||||
off(delay);
|
off(delay);
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
void load_txt(DataPoints& dp, std::istream& in)
|
void load_txt(DataPoints& dp, std::istream& in)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -471,56 +471,6 @@ TEST_CASE("[Files] Note and octave offsets")
|
||||||
REQUIRE( synth.getRegionView(6)->pitchKeycenter == 50 );
|
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")
|
TEST_CASE("[Files] Off modes")
|
||||||
{
|
{
|
||||||
Synth synth;
|
Synth synth;
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,12 @@ TEST_CASE("[Region] Parsing opcodes")
|
||||||
region.parseOpcode({ "end", "184" });
|
region.parseOpcode({ "end", "184" });
|
||||||
REQUIRE(region.sampleEnd == 184);
|
REQUIRE(region.sampleEnd == 184);
|
||||||
region.parseOpcode({ "end", "-1" });
|
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")
|
SECTION("count")
|
||||||
|
|
|
||||||
128
tests/SynthT.cpp
128
tests/SynthT.cpp
|
|
@ -1143,3 +1143,131 @@ TEST_CASE("[Synth] Trigger also on the sustain CC")
|
||||||
synth.cc(0, 64, 127);
|
synth.cc(0, 64, 127);
|
||||||
REQUIRE( synth.getNumActiveVoices(true) == 1 );
|
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" );
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,17 @@ const std::vector<const sfz::Voice*> getActiveVoices(const sfz::Synth& synth)
|
||||||
return activeVoices;
|
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)
|
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) {
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,15 @@ void sortAll(C& container, Args&... others)
|
||||||
*/
|
*/
|
||||||
const std::vector<const sfz::Voice*> getActiveVoices(const sfz::Synth& synth);
|
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
|
* @brief Count the number of playing (unreleased) voices from the synth
|
||||||
*
|
*
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue