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>
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
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 );
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue