Merge pull request #898 from paulfd/release-sustain

Add the ability to cancel the release on the basic ADSR Envelope
This commit is contained in:
Paul Ferrand 2021-06-23 00:18:29 +02:00 committed by GitHub
commit 1a8185aa27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 270 additions and 81 deletions

View file

@ -122,7 +122,8 @@ void ADSREnvelope::getBlock(absl::Span<Float> output) noexcept
break; break;
} }
while (count < size) { while (count < size) {
currentValue = std::max(sustain, currentValue + transitionDelta); if (currentValue > sustain)
currentValue += transitionDelta;
output[count++] = currentValue; output[count++] = currentValue;
} }
break; break;
@ -173,6 +174,14 @@ void ADSREnvelope::startRelease(int releaseDelay) noexcept
this->releaseDelay = releaseDelay; this->releaseDelay = releaseDelay;
} }
void ADSREnvelope::cancelRelease(int delay) noexcept
{
(void)delay;
currentState = State::Sustain;
shouldRelease = false;
this->releaseDelay = -1;
}
void ADSREnvelope::setReleaseTime(Float timeInSeconds) noexcept void ADSREnvelope::setReleaseTime(Float timeInSeconds) noexcept
{ {
releaseRate = secondsToExpRate(timeInSeconds); releaseRate = secondsToExpRate(timeInSeconds);

View file

@ -49,6 +49,12 @@ public:
* @param releaseDelay the delay before releasing in samples * @param releaseDelay the delay before releasing in samples
*/ */
void startRelease(int releaseDelay) noexcept; void startRelease(int releaseDelay) noexcept;
/**
* @brief Cancel a release and get back into sustain.
*
* @param delay
*/
void cancelRelease(int delay) noexcept;
/** /**
* @brief Is the envelope smoothing? * @brief Is the envelope smoothing?
* *

View file

@ -187,6 +187,7 @@ FloatSpec lofiBitred { 0.0f, {0.0f, 100.0f}, 0 };
FloatSpec lofiDecim { 0.0f, {0.0f, 100.0f}, 0 }; FloatSpec lofiDecim { 0.0f, {0.0f, 100.0f}, 0 };
FloatSpec rectify { 0.0f, {0.0f, 100.0f}, 0 }; FloatSpec rectify { 0.0f, {0.0f, 100.0f}, 0 };
UInt32Spec stringsNumber { maxStrings, {0, maxStrings}, 0 }; UInt32Spec stringsNumber { maxStrings, {0, maxStrings}, 0 };
BoolSpec sustainCancelsRelease { false, {0, 1}, kEnforceBounds };
ESpec<Trigger> trigger { Trigger::attack, {Trigger::attack, Trigger::release_key}, 0}; ESpec<Trigger> trigger { Trigger::attack, {Trigger::attack, Trigger::release_key}, 0};
ESpec<CrossfadeCurve> crossfadeCurve { CrossfadeCurve::power, {CrossfadeCurve::gain, CrossfadeCurve::power}, 0}; ESpec<CrossfadeCurve> crossfadeCurve { CrossfadeCurve::power, {CrossfadeCurve::gain, CrossfadeCurve::power}, 0};

View file

@ -305,6 +305,7 @@ namespace Default
extern const OpcodeSpec<SelfMask> selfMask; extern const OpcodeSpec<SelfMask> selfMask;
extern const OpcodeSpec<FilterType> filter; extern const OpcodeSpec<FilterType> filter;
extern const OpcodeSpec<EqType> eq; extern const OpcodeSpec<EqType> eq;
extern const OpcodeSpec<bool> sustainCancelsRelease;
// Default/max count for objects // Default/max count for objects
constexpr int numEQs { 3 }; constexpr int numEQs { 3 };

View file

@ -49,6 +49,7 @@ struct FlexEnvelope::Impl {
// //
void process(absl::Span<float> out); void process(absl::Span<float> out);
bool advanceToStage(unsigned stageNumber);
bool advanceToNextStage(); bool advanceToNextStage();
}; };
@ -114,6 +115,25 @@ void FlexEnvelope::release(unsigned releaseDelay)
impl.currentFramesUntilRelease_ = releaseDelay; impl.currentFramesUntilRelease_ = releaseDelay;
} }
void FlexEnvelope::cancelRelease(unsigned delay)
{
Impl& impl = *impl_;
const FlexEGDescription& desc = *impl.desc_;
if (impl.currentFramesUntilRelease_) {
// Cancel the future release
impl.currentFramesUntilRelease_ = absl::nullopt;
return;
}
if (!impl.isReleased_)
return;
impl.isReleased_ = false;
impl.advanceToStage(desc.sustain);
impl.stageTargetLevel_ = impl.currentLevel_;
}
unsigned FlexEnvelope::getRemainingDelay() const noexcept unsigned FlexEnvelope::getRemainingDelay() const noexcept
{ {
const Impl& impl = *impl_; const Impl& impl = *impl_;
@ -226,25 +246,29 @@ void FlexEnvelope::Impl::process(absl::Span<float> out)
} }
} }
bool FlexEnvelope::Impl::advanceToNextStage() bool FlexEnvelope::Impl::advanceToStage(unsigned stageNumber)
{ {
const FlexEGDescription& desc = *desc_; const FlexEGDescription& desc = *desc_;
unsigned nextStageNo = currentStageNumber_ + 1; currentStageNumber_ = stageNumber;
currentStageNumber_ = nextStageNo;
if (nextStageNo >= desc.points.size()) if (stageNumber >= desc.points.size())
return false; return false;
const FlexEGPoint& point = desc.points[nextStageNo]; const FlexEGPoint& point = desc.points[stageNumber];
stageSourceLevel_ = currentLevel_; stageSourceLevel_ = currentLevel_;
stageTargetLevel_ = point.level; stageTargetLevel_ = point.level;
stageTime_ = point.time; stageTime_ = point.time;
stageSustained_ = int(nextStageNo) == desc.sustain; stageSustained_ = int(stageNumber) == desc.sustain;
stageCurve_ = &point.curve(); stageCurve_ = &point.curve();
currentTime_ = 0; currentTime_ = 0;
return true; return true;
}; };
bool FlexEnvelope::Impl::advanceToNextStage()
{
return advanceToStage(currentStageNumber_ + 1);
}
} // namespace sfz } // namespace sfz

View file

@ -47,6 +47,11 @@ public:
*/ */
void release(unsigned releaseDelay); void release(unsigned releaseDelay);
/**
Cancel the release
*/
void cancelRelease(unsigned delay);
/** /**
Get the remaining delay samples Get the remaining delay samples
*/ */

View file

@ -437,6 +437,12 @@ void Synth::Impl::handleControlOpcodes(const std::vector<Opcode>& members)
DBG("Unsupported value for hint_stealing: " << member.value); DBG("Unsupported value for hint_stealing: " << member.value);
} }
break; break;
case hash("hint_sustain_cancels_release"):
{
SynthConfig& config = resources_.getSynthConfig();
config.sustainCancelsRelease = member.read(Default::sustainCancelsRelease);
}
break;
default: default:
// Unsupported control opcode // Unsupported control opcode
DBG("Unsupported control opcode: " << member.name); DBG("Unsupported control opcode: " << member.name);

View file

@ -28,5 +28,7 @@ struct SynthConfig
{ {
return freeWheeling ? freeWheelingOscillatorQuality : liveOscillatorQuality; return freeWheeling ? freeWheelingOscillatorQuality : liveOscillatorQuality;
} }
bool sustainCancelsRelease { Default::sustainCancelsRelease };
}; };
} }

View file

@ -676,6 +676,13 @@ void Voice::registerCC(int delay, int ccNumber, float ccValue) noexcept
if (impl.noteIsOff_ && region.loopMode != LoopMode::one_shot if (impl.noteIsOff_ && region.loopMode != LoopMode::one_shot
&& sostenutoPedalReleaseCondition && sustainPedalReleaseCondition) && sostenutoPedalReleaseCondition && sustainPedalReleaseCondition)
release(delay); release(delay);
if (region.checkSustain && (impl.sustainState_ == Impl::SustainState::Sustaining)
&& impl.resources_.getSynthConfig().sustainCancelsRelease
&& impl.released() && (region.trigger != Trigger::release && region.trigger != Trigger::release_key) ) {
ModMatrix& modMatrix = impl.resources_.getModMatrix();
modMatrix.cancelRelease(impl.id_, impl.region_->getId(), delay);
}
} }
void Voice::registerPitchWheel(int delay, float pitch) noexcept void Voice::registerPitchWheel(int delay, float pitch) noexcept

View file

@ -49,6 +49,15 @@ public:
*/ */
virtual void release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) { (void)sourceKey; (void)voiceId; (void)delay; } virtual void release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) { (void)sourceKey; (void)voiceId; (void)delay; }
/**
* @brief Cancel the release and get back into sustain
*
* @param sourceKey identifier of the source to release
* @param voiceId the particular voice to initialize, if per-voice
* @param delay the frame time when it happens
*/
virtual void cancelRelease(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) { (void)sourceKey; (void)voiceId; (void)delay; }
/** /**
* @brief Generate a cycle of the modulator * @brief Generate a cycle of the modulator
* *

View file

@ -281,6 +281,19 @@ void ModMatrix::releaseVoice(NumericId<Voice> voiceId, NumericId<Region> regionI
} }
} }
void ModMatrix::cancelRelease(NumericId<Voice> voiceId, NumericId<Region> regionId, unsigned delay)
{
Impl& impl = *impl_;
ASSERT(regionId);
const auto idNumber = static_cast<size_t>(regionId.number());
for (auto idx: impl.sourceIndicesForRegion_[idNumber]) {
const Impl::Source& source = impl.sources_[idx];
source.gen->cancelRelease(source.key, voiceId, delay);
}
}
void ModMatrix::beginCycle(unsigned numFrames) void ModMatrix::beginCycle(unsigned numFrames)
{ {
Impl& impl = *impl_; Impl& impl = *impl_;

View file

@ -116,6 +116,11 @@ public:
*/ */
void releaseVoice(NumericId<Voice> voiceId, NumericId<Region> regionId, unsigned delay); void releaseVoice(NumericId<Voice> voiceId, NumericId<Region> regionId, unsigned delay);
/**
* @brief Cancel release for a given voice.
*/
void cancelRelease(NumericId<Voice> voiceId, NumericId<Region> regionId, unsigned delay);
/** /**
* @brief Start modulation processing for the entire cycle. * @brief Start modulation processing for the entire cycle.
* This clears all the buffers. * This clears all the buffers.

View file

@ -18,6 +18,52 @@ ADSREnvelopeSource::ADSREnvelopeSource(VoiceManager& manager, MidiState& state)
{ {
} }
ADSREnvelope* getEG(Voice* voice, const ModKey& key)
{
ADSREnvelope* eg = nullptr;
if (!voice)
return eg;
switch (key.id()) {
case ModId::AmpEG:
eg = voice->getAmplitudeEG();
break;
case ModId::PitchEG:
eg = voice->getPitchEG();
break;
case ModId::FilEG:
eg = voice->getFilterEG();
break;
default:
return eg;
}
return eg;
}
const EGDescription* getEGDescription(const Region* region, const ModKey& key)
{
const EGDescription* desc = nullptr;
if (!region)
return desc;
switch (key.id()) {
case ModId::AmpEG:
desc = &region->amplitudeEG;
break;
case ModId::PitchEG:
desc = &*region->pitchEG;
break;
case ModId::FilEG:
desc = &*region->filterEG;
break;
default:
return desc;
}
return desc;
}
void ADSREnvelopeSource::init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) void ADSREnvelopeSource::init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay)
{ {
Voice* voice = voiceManager_.getVoiceById(voiceId); Voice* voice = voiceManager_.getVoiceById(voiceId);
@ -27,29 +73,9 @@ void ADSREnvelopeSource::init(const ModKey& sourceKey, NumericId<Voice> voiceId,
} }
const Region* region = voice->getRegion(); const Region* region = voice->getRegion();
ADSREnvelope* eg = nullptr; ADSREnvelope* eg = getEG(voice, sourceKey);
const EGDescription* desc = nullptr; const EGDescription* desc = getEGDescription(region, sourceKey);
ASSERT(eg);
switch (sourceKey.id()) {
case ModId::AmpEG:
eg = voice->getAmplitudeEG();
ASSERT(eg);
desc = &region->amplitudeEG;
break;
case ModId::PitchEG:
eg = voice->getPitchEG();
ASSERT(eg);
desc = &*region->pitchEG;
break;
case ModId::FilEG:
eg = voice->getFilterEG();
ASSERT(eg);
desc = &*region->filterEG;
break;
default:
ASSERTFALSE;
return;
}
const TriggerEvent& triggerEvent = voice->getTriggerEvent(); const TriggerEvent& triggerEvent = voice->getTriggerEvent();
const float sampleRate = voice->getSampleRate(); const float sampleRate = voice->getSampleRate();
@ -64,27 +90,24 @@ void ADSREnvelopeSource::release(const ModKey& sourceKey, NumericId<Voice> voice
return; return;
} }
ADSREnvelope* eg = nullptr; ADSREnvelope* eg = getEG(voice, sourceKey);
ASSERT(eg);
switch (sourceKey.id()) { eg->startRelease(delay);
case ModId::AmpEG: }
eg = voice->getAmplitudeEG();
ASSERT(eg); void ADSREnvelopeSource::cancelRelease(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay)
break; {
case ModId::PitchEG: Voice* voice = voiceManager_.getVoiceById(voiceId);
eg = voice->getPitchEG(); if (!voice) {
ASSERT(eg);
break;
case ModId::FilEG:
eg = voice->getFilterEG();
ASSERT(eg);
break;
default:
ASSERTFALSE; ASSERTFALSE;
return; return;
} }
eg->startRelease(delay); ADSREnvelope* eg = getEG(voice, sourceKey);
ASSERT(eg);
eg->cancelRelease(delay);
} }
void ADSREnvelopeSource::generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) void ADSREnvelopeSource::generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer)
@ -95,25 +118,8 @@ void ADSREnvelopeSource::generate(const ModKey& sourceKey, NumericId<Voice> voic
return; return;
} }
ADSREnvelope* eg = nullptr; ADSREnvelope* eg = getEG(voice, sourceKey);
ASSERT(eg);
switch (sourceKey.id()) {
case ModId::AmpEG:
eg = voice->getAmplitudeEG();
ASSERT(eg);
break;
case ModId::PitchEG:
eg = voice->getPitchEG();
ASSERT(eg);
break;
case ModId::FilEG:
eg = voice->getFilterEG();
ASSERT(eg);
break;
default:
ASSERTFALSE;
return;
}
eg->getBlock(buffer); eg->getBlock(buffer);
} }

View file

@ -17,6 +17,7 @@ public:
explicit ADSREnvelopeSource(VoiceManager &manager, MidiState& state); explicit ADSREnvelopeSource(VoiceManager &manager, MidiState& state);
void init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override; void init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
void release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override; void release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
void cancelRelease(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
void generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) override; void generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) override;
private: private:

View file

@ -23,13 +23,6 @@ void ChannelAftertouchSource::init(const ModKey& sourceKey, NumericId<Voice> voi
UNUSED(delay); UNUSED(delay);
} }
void ChannelAftertouchSource::release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay)
{
UNUSED(sourceKey);
UNUSED(voiceId);
UNUSED(delay);
}
void ChannelAftertouchSource::generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) void ChannelAftertouchSource::generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer)
{ {
UNUSED(sourceKey); UNUSED(sourceKey);

View file

@ -16,7 +16,6 @@ class ChannelAftertouchSource : public ModGenerator {
public: public:
explicit ChannelAftertouchSource(VoiceManager &manager, MidiState& state); explicit ChannelAftertouchSource(VoiceManager &manager, MidiState& state);
void init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override; void init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
void release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
void generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) override; void generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) override;
private: private:

View file

@ -66,6 +66,26 @@ void FlexEnvelopeSource::release(const ModKey& sourceKey, NumericId<Voice> voice
eg->release(delay); eg->release(delay);
} }
void FlexEnvelopeSource::cancelRelease(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay)
{
unsigned egIndex = sourceKey.parameters().N;
Voice* voice = voiceManager_.getVoiceById(voiceId);
if (!voice) {
ASSERTFALSE;
return;
}
const Region* region = voice->getRegion();
if (egIndex >= region->flexEGs.size()) {
ASSERTFALSE;
return;
}
FlexEnvelope* eg = voice->getFlexEG(egIndex);
eg->cancelRelease(delay);
}
void FlexEnvelopeSource::generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) void FlexEnvelopeSource::generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer)
{ {
unsigned egIndex = sourceKey.parameters().N; unsigned egIndex = sourceKey.parameters().N;

View file

@ -16,6 +16,7 @@ public:
explicit FlexEnvelopeSource(VoiceManager& manager); explicit FlexEnvelopeSource(VoiceManager& manager);
void init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override; void init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
void release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override; void release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
void cancelRelease(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
void generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) override; void generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) override;
private: private:

View file

@ -23,13 +23,6 @@ void PolyAftertouchSource::init(const ModKey& sourceKey, NumericId<Voice> voiceI
UNUSED(delay); UNUSED(delay);
} }
void PolyAftertouchSource::release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay)
{
UNUSED(sourceKey);
UNUSED(voiceId);
UNUSED(delay);
}
void PolyAftertouchSource::generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) void PolyAftertouchSource::generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer)
{ {
UNUSED(sourceKey); UNUSED(sourceKey);

View file

@ -16,7 +16,6 @@ class PolyAftertouchSource : public ModGenerator {
public: public:
explicit PolyAftertouchSource(VoiceManager &manager, MidiState& state); explicit PolyAftertouchSource(VoiceManager &manager, MidiState& state);
void init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override; void init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
void release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
void generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) override; void generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) override;
private: private:

View file

@ -1829,6 +1829,95 @@ TEST_CASE("[Synth] Short empty files are turned into *silence")
REQUIRE(messageList == expected); REQUIRE(messageList == expected);
} }
TEST_CASE("[Synth] Sustain cancels release (Flex EG)")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<control> hint_sustain_cancels_release=1
<region> sample=*sine
eg01_ampeg=1 eg01_sustain=2
eg01_time1=0 eg01_level1=0.00
eg01_time2=0.06 eg01_level2=0.92
eg01_time3=1.00 eg01_level3=0.00 eg01_shape3=-3
)");
synth.noteOn(0, 60, 63 );
synth.renderBlock(buffer);
REQUIRE( playingSamples(synth) == std::vector<std::string> { "*sine" } );
synth.noteOff(0, 60, 0 );
synth.renderBlock(buffer);
REQUIRE( playingSamples(synth) == std::vector<std::string> { } );
synth.renderBlock(buffer);
synth.renderBlock(buffer);
synth.cc(0, 64, 127);
synth.renderBlock(buffer);
REQUIRE( playingSamples(synth) == std::vector<std::string> { "*sine" } );
}
TEST_CASE("[Synth] Sustain cancels release (Flex EG) is off by default")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<region> sample=*sine
eg01_ampeg=1 eg01_sustain=2
eg01_time1=0 eg01_level1=0.00
eg01_time2=0.06 eg01_level2=0.92
eg01_time3=1.00 eg01_level3=0.00 eg01_shape3=-3
)");
synth.noteOn(0, 60, 63 );
synth.renderBlock(buffer);
REQUIRE( playingSamples(synth) == std::vector<std::string> { "*sine" } );
synth.noteOff(0, 60, 0 );
synth.renderBlock(buffer);
REQUIRE( playingSamples(synth) == std::vector<std::string> { } );
synth.renderBlock(buffer);
synth.renderBlock(buffer);
synth.cc(0, 64, 127);
synth.renderBlock(buffer);
REQUIRE( playingSamples(synth) == std::vector<std::string> { } );
}
TEST_CASE("[Synth] Sustain cancels release")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<control> hint_sustain_cancels_release=1
<region> sample=*sine ampeg_release=10
)");
synth.noteOn(0, 60, 63 );
synth.renderBlock(buffer);
REQUIRE( playingSamples(synth) == std::vector<std::string> { "*sine" } );
synth.noteOff(0, 60, 0 );
synth.renderBlock(buffer);
REQUIRE( playingSamples(synth) == std::vector<std::string> { } );
synth.renderBlock(buffer);
synth.renderBlock(buffer);
synth.cc(0, 64, 127);
synth.renderBlock(buffer);
REQUIRE( playingSamples(synth) == std::vector<std::string> { "*sine" } );
}
TEST_CASE("[Synth] Sustain cancels release is off by default")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"(
<region> sample=*sine ampeg_release=10
)");
synth.noteOn(0, 60, 63 );
synth.renderBlock(buffer);
REQUIRE( playingSamples(synth) == std::vector<std::string> { "*sine" } );
synth.noteOff(0, 60, 0 );
synth.renderBlock(buffer);
REQUIRE( playingSamples(synth) == std::vector<std::string> { } );
synth.renderBlock(buffer);
synth.renderBlock(buffer);
synth.cc(0, 64, 127);
synth.renderBlock(buffer);
REQUIRE( playingSamples(synth) == std::vector<std::string> { } );
}
TEST_CASE("[Synth] Resets all controllers to default values") TEST_CASE("[Synth] Resets all controllers to default values")
{ {