Move all regional state into Layer

This commit is contained in:
Jean Pierre Cimalando 2021-03-25 21:46:21 +01:00
parent cc6337e827
commit da621bf5ec
18 changed files with 1078 additions and 865 deletions

View file

@ -35,7 +35,7 @@ public:
} }
sfz::MidiState midiState; sfz::MidiState midiState;
sfz::Region region{0, midiState}; sfz::Region region{0};
sfz::ADSREnvelope envelope; sfz::ADSREnvelope envelope;
std::vector<float> output; std::vector<float> output;
}; };

View file

@ -89,6 +89,7 @@ SFIZZ_SOURCES = \
src/sfizz/FlexEGDescription.cpp \ src/sfizz/FlexEGDescription.cpp \
src/sfizz/FlexEnvelope.cpp \ src/sfizz/FlexEnvelope.cpp \
src/sfizz/Interpolators.cpp \ src/sfizz/Interpolators.cpp \
src/sfizz/Layer.cpp \
src/sfizz/Logger.cpp \ src/sfizz/Logger.cpp \
src/sfizz/LFO.cpp \ src/sfizz/LFO.cpp \
src/sfizz/LFODescription.cpp \ src/sfizz/LFODescription.cpp \

View file

@ -75,6 +75,7 @@ set(SFIZZ_HEADERS
sfizz/HistoricalBuffer.h sfizz/HistoricalBuffer.h
sfizz/Interpolators.h sfizz/Interpolators.h
sfizz/Interpolators.hpp sfizz/Interpolators.hpp
sfizz/Layer.h
sfizz/Logger.h sfizz/Logger.h
sfizz/LFO.h sfizz/LFO.h
sfizz/LFOCommon.h sfizz/LFOCommon.h
@ -157,6 +158,7 @@ set(SFIZZ_SOURCES
sfizz/SynthMessaging.cpp sfizz/SynthMessaging.cpp
sfizz/WindowedSinc.cpp sfizz/WindowedSinc.cpp
sfizz/Interpolators.cpp sfizz/Interpolators.cpp
sfizz/Layer.cpp
sfizz/modulations/ModId.cpp sfizz/modulations/ModId.cpp
sfizz/modulations/ModKey.cpp sfizz/modulations/ModKey.cpp
sfizz/modulations/ModKeyHash.cpp sfizz/modulations/ModKeyHash.cpp

244
src/sfizz/Layer.cpp Normal file
View file

@ -0,0 +1,244 @@
// SPDX-License-Identifier: BSD-2-Clause
// This code is part of the sfizz library and is licensed under a BSD 2-clause
// license. You should have receive a LICENSE.md file along with the code.
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#include "Layer.h"
#include "Region.h"
#include "utility/Debug.h"
#include "utility/SwapAndPop.h"
#include <absl/algorithm/container.h>
namespace sfz {
Layer::Layer(std::unique_ptr<Region> region, const MidiState& midiState)
: Layer(region.get(), midiState)
{
regionOwned_ = true;
region.release();
}
Layer::Layer(Region* region, const MidiState& midiState)
: region_(region), regionOwned_(false), midiState_(midiState)
{
initializeActivations();
}
Layer::~Layer()
{
if (regionOwned_)
delete region_;
}
void Layer::initializeActivations()
{
const Region& region = *region_;
keySwitched_ = !region.usesKeySwitches;
previousKeySwitched_ = !region.usesPreviousKeySwitches;
sequenceSwitched_ = !region.usesSequenceSwitches;
pitchSwitched_ = true;
bpmSwitched_ = true;
aftertouchSwitched_ = true;
ccSwitched_.set();
}
bool Layer::isSwitchedOn() const noexcept
{
return keySwitched_ && previousKeySwitched_ && sequenceSwitched_ && pitchSwitched_ && bpmSwitched_ && aftertouchSwitched_ && ccSwitched_.all();
}
bool Layer::registerNoteOn(int noteNumber, float velocity, float randValue) noexcept
{
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
const Region& region = *region_;
const bool keyOk = region.keyRange.containsWithEnd(noteNumber);
if (keyOk) {
// Sequence activation
sequenceSwitched_ =
((sequenceCounter_++ % region.sequenceLength) == region.sequencePosition - 1);
}
if (!isSwitchedOn())
return false;
if (!region.triggerOnNote)
return false;
if (region.velocityOverride == VelocityOverride::previous)
velocity = midiState_.getLastVelocity();
const bool velOk = region.velocityRange.containsWithEnd(velocity);
const bool randOk = region.randRange.contains(randValue) || (randValue >= 1.0f && region.randRange.isValid() && region.randRange.getEnd() >= 1.0f);
const bool firstLegatoNote = (region.trigger == Trigger::first && midiState_.getActiveNotes() == 1);
const bool attackTrigger = (region.trigger == Trigger::attack);
const bool notFirstLegatoNote = (region.trigger == Trigger::legato && midiState_.getActiveNotes() > 1);
return keyOk && velOk && randOk && (attackTrigger || firstLegatoNote || notFirstLegatoNote);
}
bool Layer::registerNoteOff(int noteNumber, float velocity, float randValue) noexcept
{
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
const Region& region = *region_;
if (!isSwitchedOn())
return false;
if (!region.triggerOnNote)
return false;
// Prerequisites
const bool keyOk = region.keyRange.containsWithEnd(noteNumber);
const bool velOk = region.velocityRange.containsWithEnd(velocity);
const bool randOk = region.randRange.contains(randValue) || (randValue >= 1.0f && region.randRange.isValid() && region.randRange.getEnd() >= 1.0f);
if (!(velOk && keyOk && randOk))
return false;
// Release logic
if (region.trigger == Trigger::release_key)
return true;
if (region.trigger == Trigger::release) {
const bool sostenutoed = isNoteSostenutoed(noteNumber);
if (sostenutoed && !sostenutoPressed_) {
removeFromSostenutoReleases(noteNumber);
if (sustainPressed_)
delaySustainRelease(noteNumber, midiState_.getNoteVelocity(noteNumber));
}
if (!sostenutoPressed_ || !sostenutoed) {
if (sustainPressed_)
delaySustainRelease(noteNumber, midiState_.getNoteVelocity(noteNumber));
else
return true;
}
}
return false;
}
bool Layer::registerCC(int ccNumber, float ccValue) noexcept
{
ASSERT(ccValue >= 0.0f && ccValue <= 1.0f);
const Region& region = *region_;
if (ccNumber == region.sustainCC)
sustainPressed_ = region.checkSustain && ccValue >= region.sustainThreshold;
if (ccNumber == region.sostenutoCC) {
const bool newState = region.checkSostenuto && ccValue >= region.sostenutoThreshold;
if (!sostenutoPressed_ && newState)
storeSostenutoNotes();
if (!newState && sostenutoPressed_)
delayedSostenutoReleases_.clear();
sostenutoPressed_ = newState;
}
if (region.ccConditions.getWithDefault(ccNumber).containsWithEnd(ccValue))
ccSwitched_.set(ccNumber, true);
else
ccSwitched_.set(ccNumber, false);
if (!isSwitchedOn())
return false;
if (!region.triggerOnCC)
return false;
if (auto triggerRange = region.ccTriggers.get(ccNumber)) {
if (triggerRange->containsWithEnd(ccValue))
return true;
}
return false;
}
void Layer::registerPitchWheel(float pitch) noexcept
{
const Region& region = *region_;
if (region.bendRange.containsWithEnd(pitch))
pitchSwitched_ = true;
else
pitchSwitched_ = false;
}
void Layer::registerAftertouch(float aftertouch) noexcept
{
const Region& region = *region_;
if (region.aftertouchRange.containsWithEnd(aftertouch))
aftertouchSwitched_ = true;
else
aftertouchSwitched_ = false;
}
void Layer::registerTempo(float secondsPerQuarter) noexcept
{
const Region& region = *region_;
const float bpm = 60.0f / secondsPerQuarter;
if (region.bpmRange.containsWithEnd(bpm))
bpmSwitched_ = true;
else
bpmSwitched_ = false;
}
void Layer::delaySustainRelease(int noteNumber, float velocity) noexcept
{
if (delayedSustainReleases_.size() == delayedSustainReleases_.capacity())
return;
delayedSustainReleases_.emplace_back(noteNumber, velocity);
}
void Layer::delaySostenutoRelease(int noteNumber, float velocity) noexcept
{
if (delayedSostenutoReleases_.size() == delayedSostenutoReleases_.capacity())
return;
delayedSostenutoReleases_.emplace_back(noteNumber, velocity);
}
void Layer::removeFromSostenutoReleases(int noteNumber) noexcept
{
swapAndPopFirst(delayedSostenutoReleases_, [=](const std::pair<int, float>& p) {
return p.first == noteNumber;
});
}
void Layer::storeSostenutoNotes() noexcept
{
ASSERT(delayedSostenutoReleases_.empty());
const Region& region = *region_;
for (int note = region.keyRange.getStart(); note <= region.keyRange.getEnd(); ++note) {
if (midiState_.isNotePressed(note))
delaySostenutoRelease(note, midiState_.getNoteVelocity(note));
}
}
bool Layer::isNoteSustained(int noteNumber) const noexcept
{
return absl::c_find_if(delayedSustainReleases_, [=](const std::pair<int, float>& p) {
return p.first == noteNumber;
}) != delayedSustainReleases_.end();
}
bool Layer::isNoteSostenutoed(int noteNumber) const noexcept
{
return absl::c_find_if(delayedSostenutoReleases_, [=](const std::pair<int, float>& p) {
return p.first == noteNumber;
}) != delayedSostenutoReleases_.end();
}
} // namespace sfz

135
src/sfizz/Layer.h Normal file
View file

@ -0,0 +1,135 @@
// SPDX-License-Identifier: BSD-2-Clause
// This code is part of the sfizz library and is licensed under a BSD 2-clause
// license. You should have receive a LICENSE.md file along with the code.
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#pragma once
#include "Config.h"
#include "utility/NumericId.h"
#include "utility/LeakDetector.h"
#include <utility>
#include <string>
#include <vector>
#include <bitset>
#include <memory>
namespace sfz {
struct Region;
class MidiState;
struct Layer {
public:
Layer(std::unique_ptr<Region> region, const MidiState& midiState);
Layer(Region* region, const MidiState& midiState);
~Layer();
Layer(const Layer&) = delete;
Layer(Layer&&) = delete;
Layer& operator=(const Layer&) = delete;
Layer& operator=(Layer&&) = delete;
/**
* @brief Get the region that this layer operates on.
*/
const Region& getRegion() const noexcept { return *region_; }
/**
* @brief Get the region that this layer operates on.
*/
Region& getRegion() noexcept { return *region_; }
/**
* @brief Reset the activations to their initial states.
*/
void initializeActivations();
/**
* @brief Given the current midi state, is the region switched on?
*
* @return true
* @return false
*/
bool isSwitchedOn() const noexcept;
/**
* @brief Register a new note on event. The region may be switched on or off using keys so
* this function updates the keyswitches state.
*
* @param noteNumber
* @param velocity
* @param randValue a random value between 0 and 1 used to randomize a bit the region activations
* and vary the samples
* @return true if the region should trigger on this event.
* @return false
*/
bool registerNoteOn(int noteNumber, float velocity, float randValue) noexcept;
/**
* @brief Register a new note off event. The region may be switched on or off using keys so
* this function updates the keyswitches state.
*
* @param noteNumber
* @param velocity
* @param randValue a random value between 0 and 1 used to randomize a bit the region activations
* and vary the samples
* @return true if the region should trigger on this event.
* @return false
*/
bool registerNoteOff(int noteNumber, float velocity, float randValue) noexcept;
/**
* @brief Register a new CC event. The region may be switched on or off using CCs so
* this function checks if it indeeds need to activate or not.
*
* @param ccNumber
* @param ccValue
* @return true if the region should trigger on this event
* @return false
*/
bool registerCC(int ccNumber, float ccValue) noexcept;
/**
* @brief Register a new pitch wheel event.
*
* @param pitch
*/
void registerPitchWheel(float pitch) noexcept;
/**
* @brief Register a new aftertouch event.
*
* @param aftertouch
*/
void registerAftertouch(float aftertouch) noexcept;
/**
* @brief Register tempo
*
* @param secondsPerQuarter
*/
void registerTempo(float secondsPerQuarter) noexcept;
Region* const region_ {};
bool regionOwned_ = false;
// Started notes
bool sustainPressed_ { false };
bool sostenutoPressed_ { false };
std::vector<std::pair<int, float>> delayedSustainReleases_;
std::vector<std::pair<int, float>> delayedSostenutoReleases_;
void delaySustainRelease(int noteNumber, float velocity) noexcept;
void delaySostenutoRelease(int noteNumber, float velocity) noexcept;
void storeSostenutoNotes() noexcept;
void removeFromSostenutoReleases(int noteNumber) noexcept;
bool isNoteSustained(int noteNumber) const noexcept;
bool isNoteSostenutoed(int noteNumber) const noexcept;
const MidiState& midiState_;
bool keySwitched_ {};
bool previousKeySwitched_ {};
bool sequenceSwitched_ {};
bool pitchSwitched_ {};
bool bpmSwitched_ {};
bool aftertouchSwitched_ {};
std::bitset<config::numCCs> ccSwitched_;
int sequenceCounter_ { 0 };
LEAK_DETECTOR(Layer);
};
} // namespace sfz

View file

@ -6,6 +6,7 @@
#include "Region.h" #include "Region.h"
#include "Opcode.h" #include "Opcode.h"
#include "MidiState.h"
#include "MathHelpers.h" #include "MathHelpers.h"
#include "utility/SwapAndPop.h" #include "utility/SwapAndPop.h"
#include "utility/StringViewHelpers.h" #include "utility/StringViewHelpers.h"
@ -35,11 +36,9 @@ bool extendIfNecessary(std::vector<T>& vec, unsigned size, unsigned defaultCapac
return true; return true;
} }
sfz::Region::Region(int regionNumber, const MidiState& midiState, absl::string_view defaultPath) sfz::Region::Region(int regionNumber, absl::string_view defaultPath)
: id{regionNumber}, midiState(midiState), defaultPath(defaultPath) : id{regionNumber}, defaultPath(defaultPath)
{ {
ccSwitched.set();
gainToEffect.reserve(5); // sufficient room for main and fx1-4 gainToEffect.reserve(5); // sufficient room for main and fx1-4
gainToEffect.push_back(1.0); // contribute 100% into the main bus gainToEffect.push_back(1.0); // contribute 100% into the main bus
@ -260,7 +259,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
case hash("sw_last"): case hash("sw_last"):
if (!lastKeyswitchRange) { if (!lastKeyswitchRange) {
lastKeyswitch = opcode.readOptional(Default::key); lastKeyswitch = opcode.readOptional(Default::key);
keySwitched = !lastKeyswitch.has_value(); usesKeySwitches = lastKeyswitch.has_value();
} }
break; break;
case hash("sw_lolast"): case hash("sw_lolast"):
@ -271,7 +270,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
else else
lastKeyswitchRange->setStart(value); lastKeyswitchRange->setStart(value);
keySwitched = false; usesKeySwitches = true;
lastKeyswitch = absl::nullopt; lastKeyswitch = absl::nullopt;
} }
break; break;
@ -283,7 +282,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
else else
lastKeyswitchRange->setEnd(value); lastKeyswitchRange->setEnd(value);
keySwitched = false; usesKeySwitches = true;
lastKeyswitch = absl::nullopt; lastKeyswitch = absl::nullopt;
} }
break; break;
@ -292,14 +291,14 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
break; break;
case hash("sw_down"): case hash("sw_down"):
downKeyswitch = opcode.readOptional(Default::key); downKeyswitch = opcode.readOptional(Default::key);
keySwitched = !downKeyswitch.has_value(); usesKeySwitches = downKeyswitch.has_value();
break; break;
case hash("sw_up"): case hash("sw_up"):
upKeyswitch = opcode.readOptional(Default::key); upKeyswitch = opcode.readOptional(Default::key);
break; break;
case hash("sw_previous"): case hash("sw_previous"):
previousKeyswitch = opcode.readOptional(Default::key); previousKeyswitch = opcode.readOptional(Default::key);
previousKeySwitched = !previousKeyswitch.has_value(); usesPreviousKeySwitches = previousKeyswitch.has_value();
break; break;
case hash("sw_vel"): case hash("sw_vel"):
velocityOverride = velocityOverride =
@ -348,7 +347,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
break; break;
case hash("seq_position"): case hash("seq_position"):
sequencePosition = opcode.read(Default::sequence); sequencePosition = opcode.read(Default::sequence);
sequenceSwitched = false; usesSequenceSwitches = true;
break; break;
// Region logic: triggers // Region logic: triggers
case hash("trigger"): case hash("trigger"):
@ -1499,191 +1498,6 @@ bool sfz::Region::processGenericCc(const Opcode& opcode, OpcodeSpec<float> spec,
return true; return true;
} }
bool sfz::Region::isSwitchedOn() const noexcept
{
return keySwitched && previousKeySwitched && sequenceSwitched && pitchSwitched && bpmSwitched && aftertouchSwitched && ccSwitched.all();
}
void sfz::Region::delaySustainRelease(int noteNumber, float velocity) noexcept
{
if (delayedSustainReleases.size() == delayedSustainReleases.capacity())
return;
delayedSustainReleases.emplace_back(noteNumber, velocity);
}
void sfz::Region::delaySostenutoRelease(int noteNumber, float velocity) noexcept
{
if (delayedSostenutoReleases.size() == delayedSostenutoReleases.capacity())
return;
delayedSostenutoReleases.emplace_back(noteNumber, velocity);
}
void sfz::Region::removeFromSostenutoReleases(int noteNumber) noexcept
{
swapAndPopFirst(delayedSostenutoReleases, [=](const std::pair<int, float>& p) {
return p.first == noteNumber;
});
}
void sfz::Region::storeSostenutoNotes() noexcept
{
ASSERT(delayedSostenutoReleases.empty());
for (int note = keyRange.getStart(); note <= keyRange.getEnd(); ++note) {
if (midiState.isNotePressed(note))
delaySostenutoRelease(note, midiState.getNoteVelocity(note));
}
}
bool sfz::Region::isNoteSustained(int noteNumber) const noexcept
{
return absl::c_find_if(delayedSustainReleases, [=](const std::pair<int, float>& p) {
return p.first == noteNumber;
}) != delayedSustainReleases.end();
}
bool sfz::Region::isNoteSostenutoed(int noteNumber) const noexcept
{
return absl::c_find_if(delayedSostenutoReleases, [=](const std::pair<int, float>& p) {
return p.first == noteNumber;
}) != delayedSostenutoReleases.end();
}
bool sfz::Region::registerNoteOn(int noteNumber, float velocity, float randValue) noexcept
{
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
const bool keyOk = keyRange.containsWithEnd(noteNumber);
if (keyOk) {
// Sequence activation
sequenceSwitched =
((sequenceCounter++ % sequenceLength) == sequencePosition - 1);
}
if (!isSwitchedOn())
return false;
if (!triggerOnNote)
return false;
if (velocityOverride == VelocityOverride::previous)
velocity = midiState.getLastVelocity();
const bool velOk = velocityRange.containsWithEnd(velocity);
const bool randOk = randRange.contains(randValue) || (randValue >= 1.0f && randRange.isValid() && randRange.getEnd() >= 1.0f);
const bool firstLegatoNote = (trigger == Trigger::first && midiState.getActiveNotes() == 1);
const bool attackTrigger = (trigger == Trigger::attack);
const bool notFirstLegatoNote = (trigger == Trigger::legato && midiState.getActiveNotes() > 1);
return keyOk && velOk && randOk && (attackTrigger || firstLegatoNote || notFirstLegatoNote);
}
bool sfz::Region::registerNoteOff(int noteNumber, float velocity, float randValue) noexcept
{
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
if (!isSwitchedOn())
return false;
if (!triggerOnNote)
return false;
// Prerequisites
const bool keyOk = keyRange.containsWithEnd(noteNumber);
const bool velOk = velocityRange.containsWithEnd(velocity);
const bool randOk = randRange.contains(randValue) || (randValue >= 1.0f && randRange.isValid() && randRange.getEnd() >= 1.0f);
if (!(velOk && keyOk && randOk))
return false;
// Release logic
if (trigger == Trigger::release_key)
return true;
if (trigger == Trigger::release) {
const bool sostenutoed = isNoteSostenutoed(noteNumber);
if (sostenutoed && !sostenutoPressed) {
removeFromSostenutoReleases(noteNumber);
if (sustainPressed)
delaySustainRelease(noteNumber, midiState.getNoteVelocity(noteNumber));
}
if (!sostenutoPressed || !sostenutoed) {
if (sustainPressed)
delaySustainRelease(noteNumber, midiState.getNoteVelocity(noteNumber));
else
return true;
}
}
return false;
}
bool sfz::Region::registerCC(int ccNumber, float ccValue) noexcept
{
ASSERT(ccValue >= 0.0f && ccValue <= 1.0f);
if (ccNumber == sustainCC)
sustainPressed = checkSustain && ccValue >= sustainThreshold;
if (ccNumber == sostenutoCC) {
const bool newState = checkSostenuto && ccValue >= sostenutoThreshold;
if (!sostenutoPressed && newState)
storeSostenutoNotes();
if (!newState && sostenutoPressed)
delayedSostenutoReleases.clear();
sostenutoPressed = newState;
}
if (ccConditions.getWithDefault(ccNumber).containsWithEnd(ccValue))
ccSwitched.set(ccNumber, true);
else
ccSwitched.set(ccNumber, false);
if (!isSwitchedOn())
return false;
if (!triggerOnCC)
return false;
if (ccTriggers.contains(ccNumber) && ccTriggers[ccNumber].containsWithEnd(ccValue))
return true;
return false;
}
void sfz::Region::registerPitchWheel(float pitch) noexcept
{
if (bendRange.containsWithEnd(pitch))
pitchSwitched = true;
else
pitchSwitched = false;
}
void sfz::Region::registerAftertouch(float aftertouch) noexcept
{
if (aftertouchRange.containsWithEnd(aftertouch))
aftertouchSwitched = true;
else
aftertouchSwitched = false;
}
void sfz::Region::registerTempo(float secondsPerQuarter) noexcept
{
const float bpm = 60.0f / secondsPerQuarter;
if (bpmRange.containsWithEnd(bpm))
bpmSwitched = true;
else
bpmSwitched = false;
}
float sfz::Region::getBasePitchVariation(float noteNumber, float velocity) const noexcept float sfz::Region::getBasePitchVariation(float noteNumber, float velocity) const noexcept
{ {
ASSERT(velocity >= 0.0f && velocity <= 1.0f); ASSERT(velocity >= 0.0f && velocity <= 1.0f);
@ -1697,7 +1511,7 @@ float sfz::Region::getBasePitchVariation(float noteNumber, float velocity) const
return centsFactor(pitchVariationInCents); return centsFactor(pitchVariationInCents);
} }
float sfz::Region::getBaseVolumedB(int noteNumber) const noexcept float sfz::Region::getBaseVolumedB(const MidiState& midiState, int noteNumber) const noexcept
{ {
fast_real_distribution<float> volumeDistribution { 0.0f, ampRandom }; fast_real_distribution<float> volumeDistribution { 0.0f, ampRandom };
auto baseVolumedB = volume + volumeDistribution(Random::randomGenerator); auto baseVolumedB = volume + volumeDistribution(Random::randomGenerator);
@ -1732,7 +1546,7 @@ float sfz::Region::getPhase() const noexcept
return phase; return phase;
} }
uint64_t sfz::Region::getOffset(Oversampling factor) const noexcept uint64_t sfz::Region::getOffset(const MidiState& midiState, Oversampling factor) const noexcept
{ {
std::uniform_int_distribution<int64_t> offsetDistribution { 0, offsetRandom }; std::uniform_int_distribution<int64_t> offsetDistribution { 0, offsetRandom };
uint64_t finalOffset = offset + offsetDistribution(Random::randomGenerator); uint64_t finalOffset = offset + offsetDistribution(Random::randomGenerator);
@ -1741,7 +1555,7 @@ uint64_t sfz::Region::getOffset(Oversampling factor) const noexcept
return Default::offset.bounds.clamp(finalOffset) * static_cast<uint64_t>(factor); return Default::offset.bounds.clamp(finalOffset) * static_cast<uint64_t>(factor);
} }
float sfz::Region::getDelay() const noexcept float sfz::Region::getDelay(const MidiState& midiState) const noexcept
{ {
fast_real_distribution<float> delayDistribution { 0, delayRandom }; fast_real_distribution<float> delayDistribution { 0, delayRandom };
float finalDelay { delay }; float finalDelay { delay };
@ -1792,7 +1606,7 @@ float sfz::Region::getNoteGain(int noteNumber, float velocity) const noexcept
return baseGain; return baseGain;
} }
float sfz::Region::getCrossfadeGain() const noexcept float sfz::Region::getCrossfadeGain(const MidiState& midiState) const noexcept
{ {
float gain { 1.0f }; float gain { 1.0f };

View file

@ -15,7 +15,6 @@
#include "LFODescription.h" #include "LFODescription.h"
#include "Opcode.h" #include "Opcode.h"
#include "AudioBuffer.h" #include "AudioBuffer.h"
#include "MidiState.h"
#include "FileId.h" #include "FileId.h"
#include "utility/NumericId.h" #include "utility/NumericId.h"
#include "utility/LeakDetector.h" #include "utility/LeakDetector.h"
@ -29,6 +28,7 @@
namespace sfz { namespace sfz {
class RegionSet; class RegionSet;
class MidiState;
/** /**
* @brief Regions are the basic building blocks for the SFZ parsing and handling code. * @brief Regions are the basic building blocks for the SFZ parsing and handling code.
@ -43,7 +43,7 @@ class RegionSet;
* *
*/ */
struct Region { struct Region {
Region(int regionNumber, const MidiState& midiState, absl::string_view defaultPath = ""); explicit Region(int regionNumber, absl::string_view defaultPath = "");
Region(const Region&) = default; Region(const Region&) = default;
~Region() = default; ~Region() = default;
@ -98,65 +98,6 @@ struct Region {
* @return false * @return false
*/ */
bool shouldLoop() const noexcept { return (loopMode == LoopMode::loop_continuous || loopMode == LoopMode::loop_sustain); } bool shouldLoop() const noexcept { return (loopMode == LoopMode::loop_continuous || loopMode == LoopMode::loop_sustain); }
/**
* @brief Given the current midi state, is the region switched on?
*
* @return true
* @return false
*/
bool isSwitchedOn() const noexcept;
/**
* @brief Register a new note on event. The region may be switched on or off using keys so
* this function updates the keyswitches state.
*
* @param noteNumber
* @param velocity
* @param randValue a random value between 0 and 1 used to randomize a bit the region activations
* and vary the samples
* @return true if the region should trigger on this event.
* @return false
*/
bool registerNoteOn(int noteNumber, float velocity, float randValue) noexcept;
/**
* @brief Register a new note off event. The region may be switched on or off using keys so
* this function updates the keyswitches state.
*
* @param noteNumber
* @param velocity
* @param randValue a random value between 0 and 1 used to randomize a bit the region activations
* and vary the samples
* @return true if the region should trigger on this event.
* @return false
*/
bool registerNoteOff(int noteNumber, float velocity, float randValue) noexcept;
/**
* @brief Register a new CC event. The region may be switched on or off using CCs so
* this function checks if it indeeds need to activate or not.
*
* @param ccNumber
* @param ccValue
* @return true if the region should trigger on this event
* @return false
*/
bool registerCC(int ccNumber, float ccValue) noexcept;
/**
* @brief Register a new pitch wheel event.
*
* @param pitch
*/
void registerPitchWheel(float pitch) noexcept;
/**
* @brief Register a new aftertouch event.
*
* @param aftertouch
*/
void registerAftertouch(float aftertouch) noexcept;
/**
* @brief Register tempo
*
* @param secondsPerQuarter
*/
void registerTempo(float secondsPerQuarter) noexcept;
/** /**
* @brief Get the base pitch of the region depending on which note has been * @brief Get the base pitch of the region depending on which note has been
@ -180,18 +121,19 @@ struct Region {
* @brief Get the additional crossfade gain of the region depending on the * @brief Get the additional crossfade gain of the region depending on the
* CC values * CC values
* *
* @param ccState * @param midiState
* @return float * @return float
*/ */
float getCrossfadeGain() const noexcept; float getCrossfadeGain(const MidiState& midiState) const noexcept;
/** /**
* @brief Get the base volume of the region depending on which note has been * @brief Get the base volume of the region depending on which note has been
* pressed to trigger the region. * pressed to trigger the region.
* *
* @param midiState
* @param noteNumber * @param noteNumber
* @return float * @return float
*/ */
float getBaseVolumedB(int noteNumber) const noexcept; float getBaseVolumedB(const MidiState& midiState, int noteNumber) const noexcept;
/** /**
* @brief Get the base gain of the region. * @brief Get the base gain of the region.
* *
@ -221,15 +163,17 @@ struct Region {
/** /**
* @brief Get the region offset in samples * @brief Get the region offset in samples
* *
* @param midiState
* @return uint32_t * @return uint32_t
*/ */
uint64_t getOffset(Oversampling factor = Oversampling::x1) const noexcept; uint64_t getOffset(const MidiState& midiState, Oversampling factor = Oversampling::x1) const noexcept;
/** /**
* @brief Get the region delay in seconds * @brief Get the region delay in seconds
* *
* @param midiState
* @return float * @return float
*/ */
float getDelay() const noexcept; float getDelay(const MidiState& midiState) const noexcept;
/** /**
* @brief Get the index of the sample end, either natural end or forced * @brief Get the index of the sample end, either natural end or forced
* loop. * loop.
@ -410,6 +354,9 @@ struct Region {
float sustainThreshold { Default::sustainThreshold }; // sustain_cc float sustainThreshold { Default::sustainThreshold }; // sustain_cc
float sostenutoThreshold { Default::sostenutoThreshold }; // sustain_cc float sostenutoThreshold { Default::sostenutoThreshold }; // sustain_cc
bool usesKeySwitches { false };
bool usesPreviousKeySwitches { false };
// Region logic: internal conditions // Region logic: internal conditions
UncheckedRange<float> aftertouchRange { Default::loChannelAftertouch, Default::hiChannelAftertouch }; // hichanaft and lochanaft UncheckedRange<float> aftertouchRange { Default::loChannelAftertouch, Default::hiChannelAftertouch }; // hichanaft and lochanaft
UncheckedRange<float> bpmRange { Default::loBPM, Default::hiBPM }; // hibpm and lobpm UncheckedRange<float> bpmRange { Default::loBPM, Default::hiBPM }; // hibpm and lobpm
@ -417,6 +364,8 @@ struct Region {
uint8_t sequenceLength { Default::sequence }; // seq_length uint8_t sequenceLength { Default::sequence }; // seq_length
uint8_t sequencePosition { Default::sequence }; // seq_position uint8_t sequencePosition { Default::sequence }; // seq_position
bool usesSequenceSwitches { false };
// Region logic: triggers // Region logic: triggers
Trigger trigger { Default::trigger }; // trigger Trigger trigger { Default::trigger }; // trigger
CCMap<UncheckedRange<float>> ccTriggers {{ Default::loCC, Default::hiCC }}; // on_loccN on_hiccN CCMap<UncheckedRange<float>> ccTriggers {{ Default::loCC, Default::hiCC }}; // on_loccN on_hiccN
@ -507,29 +456,8 @@ struct Region {
// Parent // Parent
RegionSet* parent { nullptr }; RegionSet* parent { nullptr };
// Started notes std::string defaultPath;
bool sustainPressed { false };
bool sostenutoPressed { false };
std::vector<std::pair<int, float>> delayedSustainReleases;
std::vector<std::pair<int, float>> delayedSostenutoReleases;
void delaySustainRelease(int noteNumber, float velocity) noexcept;
void delaySostenutoRelease(int noteNumber, float velocity) noexcept;
void storeSostenutoNotes() noexcept;
void removeFromSostenutoReleases(int noteNumber) noexcept;
bool isNoteSustained(int noteNumber) const noexcept;
bool isNoteSostenutoed(int noteNumber) const noexcept;
const MidiState& midiState;
bool keySwitched { true };
bool previousKeySwitched { true };
bool sequenceSwitched { true };
bool pitchSwitched { true };
bool bpmSwitched { true };
bool aftertouchSwitched { true };
std::bitset<config::numCCs> ccSwitched;
std::string defaultPath { "" };
int sequenceCounter { 0 };
LEAK_DETECTOR(Region); LEAK_DETECTOR(Region);
}; };

View file

@ -136,8 +136,10 @@ void Synth::Impl::onParseWarning(const SourceRange& range, const std::string& me
void Synth::Impl::buildRegion(const std::vector<Opcode>& regionOpcodes) void Synth::Impl::buildRegion(const std::vector<Opcode>& regionOpcodes)
{ {
int regionNumber = static_cast<int>(regions_.size()); int regionNumber = static_cast<int>(layers_.size());
auto lastRegion = absl::make_unique<Region>(regionNumber, resources_.midiState, defaultPath_); Region* lastRegion = new Region(regionNumber, defaultPath_);
Layer* lastLayer = new Layer(std::unique_ptr<Region>(lastRegion), resources_.midiState);
layers_.emplace_back(lastLayer);
// //
auto parseOpcodes = [&](const std::vector<Opcode>& opcodes) { auto parseOpcodes = [&](const std::vector<Opcode>& opcodes) {
@ -171,22 +173,22 @@ void Synth::Impl::buildRegion(const std::vector<Opcode>& regionOpcodes)
lastRegion->offsetAllKeys(octaveOffset_ * 12 + noteOffset_); lastRegion->offsetAllKeys(octaveOffset_ * 12 + noteOffset_);
if (lastRegion->lastKeyswitch) if (lastRegion->lastKeyswitch)
lastKeyswitchLists_[*lastRegion->lastKeyswitch].push_back(lastRegion.get()); lastKeyswitchLists_[*lastRegion->lastKeyswitch].push_back(lastLayer);
if (lastRegion->lastKeyswitchRange) { if (lastRegion->lastKeyswitchRange) {
auto& range = *lastRegion->lastKeyswitchRange; auto& range = *lastRegion->lastKeyswitchRange;
for (uint8_t note = range.getStart(), end = range.getEnd(); note <= end; note++) for (uint8_t note = range.getStart(), end = range.getEnd(); note <= end; note++)
lastKeyswitchLists_[note].push_back(lastRegion.get()); lastKeyswitchLists_[note].push_back(lastLayer);
} }
if (lastRegion->upKeyswitch) if (lastRegion->upKeyswitch)
upKeyswitchLists_[*lastRegion->upKeyswitch].push_back(lastRegion.get()); upKeyswitchLists_[*lastRegion->upKeyswitch].push_back(lastLayer);
if (lastRegion->downKeyswitch) if (lastRegion->downKeyswitch)
downKeyswitchLists_[*lastRegion->downKeyswitch].push_back(lastRegion.get()); downKeyswitchLists_[*lastRegion->downKeyswitch].push_back(lastLayer);
if (lastRegion->previousKeyswitch) if (lastRegion->previousKeyswitch)
previousKeyswitchLists_.push_back(lastRegion.get()); previousKeyswitchLists_.push_back(lastLayer);
if (lastRegion->defaultSwitch) if (lastRegion->defaultSwitch)
currentSwitch_ = *lastRegion->defaultSwitch; currentSwitch_ = *lastRegion->defaultSwitch;
@ -201,18 +203,19 @@ void Synth::Impl::buildRegion(const std::vector<Opcode>& regionOpcodes)
if (currentSet_ != nullptr) { if (currentSet_ != nullptr) {
lastRegion->parent = currentSet_; lastRegion->parent = currentSet_;
currentSet_->addRegion(lastRegion.get()); currentSet_->addRegion(lastRegion);
} }
// Adapt the size of the delayed releases to avoid allocating later on // Adapt the size of the delayed releases to avoid allocating later on
if (lastRegion->trigger == Trigger::release) { if (lastRegion->trigger == Trigger::release) {
const auto keyLength = static_cast<unsigned>(lastRegion->keyRange.length()); const auto keyLength = static_cast<unsigned>(lastRegion->keyRange.length());
const auto size = max(config::delayedReleaseVoices, keyLength); const auto size = max(config::delayedReleaseVoices, keyLength);
lastRegion->delayedSustainReleases.reserve(size); lastLayer->delayedSustainReleases_.reserve(size);
lastRegion->delayedSostenutoReleases.reserve(size); lastLayer->delayedSostenutoReleases_.reserve(size);
} }
regions_.push_back(std::move(lastRegion)); // Initialize status of Key switches, CC switches, etc
lastLayer->initializeActivations();
} }
void Synth::Impl::clear() void Synth::Impl::clear()
@ -235,7 +238,7 @@ void Synth::Impl::clear()
currentSet_ = nullptr; currentSet_ = nullptr;
sets_.clear(); sets_.clear();
regions_.clear(); layers_.clear();
effectBuses_.clear(); effectBuses_.clear();
effectBuses_.emplace_back(new EffectBus); effectBuses_.emplace_back(new EffectBus);
effectBuses_[0]->setGainToMain(1.0); effectBuses_[0]->setGainToMain(1.0);
@ -502,7 +505,7 @@ bool Synth::loadSfzFile(const fs::path& file)
if (!loaderParsesPermissively) if (!loaderParsesPermissively)
success = parser.getErrorCount() == 0; success = parser.getErrorCount() == 0;
success = success && !impl.regions_.empty(); success = success && !impl.layers_.empty();
if (!success) { if (!success) {
parser.clear(); parser.clear();
@ -527,7 +530,7 @@ bool Synth::loadSfzString(const fs::path& path, absl::string_view text)
if (!loaderParsesPermissively) if (!loaderParsesPermissively)
success = parser.getErrorCount() == 0; success = parser.getErrorCount() == 0;
success = success && !impl.regions_.empty(); success = success && !impl.layers_.empty();
if (!success) { if (!success) {
parser.clear(); parser.clear();
@ -543,11 +546,12 @@ void Synth::Impl::finalizeSfzLoad()
resources_.filePool.setRootDirectory(parser_.originalDirectory()); resources_.filePool.setRootDirectory(parser_.originalDirectory());
size_t currentRegionIndex = 0; size_t currentRegionIndex = 0;
size_t currentRegionCount = regions_.size(); size_t currentRegionCount = layers_.size();
auto removeCurrentRegion = [this, &currentRegionIndex, &currentRegionCount]() { auto removeCurrentRegion = [this, &currentRegionIndex, &currentRegionCount]() {
DBG("Removing the region with sample " << *regions_[currentRegionIndex]->sampleId); const Region& region = layers_[currentRegionIndex]->getRegion();
regions_.erase(regions_.begin() + currentRegionIndex); DBG("Removing the region with sample " << *region.sampleId);
layers_.erase(layers_.begin() + currentRegionIndex);
--currentRegionCount; --currentRegionCount;
}; };
@ -564,155 +568,156 @@ void Synth::Impl::finalizeSfzLoad()
FlexEGs::clearUnusedCurves(); FlexEGs::clearUnusedCurves();
while (currentRegionIndex < currentRegionCount) { while (currentRegionIndex < currentRegionCount) {
auto region = regions_[currentRegionIndex].get(); Layer& layer = *layers_[currentRegionIndex];
Region& region = layer.getRegion();
absl::optional<FileInformation> fileInformation; absl::optional<FileInformation> fileInformation;
if (!region->isGenerator()) { if (!region.isGenerator()) {
if (!resources_.filePool.checkSampleId(*region->sampleId)) { if (!resources_.filePool.checkSampleId(*region.sampleId)) {
removeCurrentRegion(); removeCurrentRegion();
continue; continue;
} }
fileInformation = resources_.filePool.getFileInformation(*region->sampleId); fileInformation = resources_.filePool.getFileInformation(*region.sampleId);
if (!fileInformation) { if (!fileInformation) {
removeCurrentRegion(); removeCurrentRegion();
continue; continue;
} }
region->hasWavetableSample = fileInformation->wavetable || region.hasWavetableSample = fileInformation->wavetable ||
fileInformation->end < config::wavetableMaxFrames; fileInformation->end < config::wavetableMaxFrames;
} }
if (!region->isOscillator()) { if (!region.isOscillator()) {
region->sampleEnd = std::min(region->sampleEnd, fileInformation->end); region.sampleEnd = std::min(region.sampleEnd, fileInformation->end);
if (fileInformation->hasLoop) { if (fileInformation->hasLoop) {
if (region->loopRange.getStart() == Default::loopStart) if (region.loopRange.getStart() == Default::loopStart)
region->loopRange.setStart(fileInformation->loopStart); region.loopRange.setStart(fileInformation->loopStart);
if (region->loopRange.getEnd() == Default::loopEnd) if (region.loopRange.getEnd() == Default::loopEnd)
region->loopRange.setEnd(fileInformation->loopEnd); region.loopRange.setEnd(fileInformation->loopEnd);
if (!region->loopMode) if (!region.loopMode)
region->loopMode = LoopMode::loop_continuous; region.loopMode = LoopMode::loop_continuous;
} }
if (region->isRelease() && !region->loopMode) if (region.isRelease() && !region.loopMode)
region->loopMode = LoopMode::one_shot; region.loopMode = LoopMode::one_shot;
if (region->loopRange.getEnd() == Default::loopEnd) if (region.loopRange.getEnd() == Default::loopEnd)
region->loopRange.setEnd(region->sampleEnd); region.loopRange.setEnd(region.sampleEnd);
// if range is invalid, disable the loop // if range is invalid, disable the loop
if (!region->loopRange.isValid()) if (!region.loopRange.isValid())
region->loopMode = absl::nullopt; region.loopMode = absl::nullopt;
if (fileInformation->numChannels == 2) if (fileInformation->numChannels == 2)
region->hasStereoSample = true; region.hasStereoSample = true;
if (region->pitchKeycenterFromSample) if (region.pitchKeycenterFromSample)
region->pitchKeycenter = fileInformation->rootKey; region.pitchKeycenter = fileInformation->rootKey;
// TODO: adjust with LFO targets // TODO: adjust with LFO targets
const auto maxOffset = [region]() { const auto maxOffset = [region]() {
uint64_t sumOffsetCC = region->offset + region->offsetRandom; uint64_t sumOffsetCC = region.offset + region.offsetRandom;
for (const auto& offsets : region->offsetCC) for (const auto& offsets : region.offsetCC)
sumOffsetCC += offsets.data; sumOffsetCC += offsets.data;
return Default::offsetMod.bounds.clamp(sumOffsetCC); return Default::offsetMod.bounds.clamp(sumOffsetCC);
}(); }();
if (!resources_.filePool.preloadFile(*region->sampleId, maxOffset)) if (!resources_.filePool.preloadFile(*region.sampleId, maxOffset))
removeCurrentRegion(); removeCurrentRegion();
} }
else if (!region->isGenerator()) { else if (!region.isGenerator()) {
if (!resources_.wavePool.createFileWave(resources_.filePool, std::string(region->sampleId->filename()))) { if (!resources_.wavePool.createFileWave(resources_.filePool, std::string(region.sampleId->filename()))) {
removeCurrentRegion(); removeCurrentRegion();
continue; continue;
} }
} }
if (region->lastKeyswitch) { if (region.lastKeyswitch) {
if (currentSwitch_) if (currentSwitch_)
region->keySwitched = (*currentSwitch_ == *region->lastKeyswitch); layer.keySwitched_ = (*currentSwitch_ == *region.lastKeyswitch);
if (region->keyswitchLabel) if (region.keyswitchLabel)
setKeyswitchLabel(*region->lastKeyswitch, *region->keyswitchLabel); setKeyswitchLabel(*region.lastKeyswitch, *region.keyswitchLabel);
} }
if (region->lastKeyswitchRange) { if (region.lastKeyswitchRange) {
auto& range = *region->lastKeyswitchRange; auto& range = *region.lastKeyswitchRange;
if (currentSwitch_) if (currentSwitch_)
region->keySwitched = range.containsWithEnd(*currentSwitch_); layer.keySwitched_ = range.containsWithEnd(*currentSwitch_);
if (region->keyswitchLabel) { if (region.keyswitchLabel) {
for (uint8_t note = range.getStart(), end = range.getEnd(); note <= end; note++) for (uint8_t note = range.getStart(), end = range.getEnd(); note <= end; note++)
setKeyswitchLabel(note, *region->keyswitchLabel); setKeyswitchLabel(note, *region.keyswitchLabel);
} }
} }
for (auto note = 0; note < 128; note++) { for (auto note = 0; note < 128; note++) {
if (region->keyRange.containsWithEnd(note)) if (region.keyRange.containsWithEnd(note))
noteActivationLists_[note].push_back(region); noteActivationLists_[note].push_back(&layer);
} }
for (int cc = 0; cc < config::numCCs; cc++) { for (int cc = 0; cc < config::numCCs; cc++) {
if (region->ccTriggers.contains(cc) if (region.ccTriggers.contains(cc)
|| region->ccConditions.contains(cc) || region.ccConditions.contains(cc)
|| (cc == region->sustainCC && region->trigger == Trigger::release) || (cc == region.sustainCC && region.trigger == Trigger::release)
|| (cc == region->sostenutoCC && region->trigger == Trigger::release)) || (cc == region.sostenutoCC && region.trigger == Trigger::release))
ccActivationLists_[cc].push_back(region); ccActivationLists_[cc].push_back(&layer);
} }
// Defaults // Defaults
for (int cc = 0; cc < config::numCCs; cc++) { for (int cc = 0; cc < config::numCCs; cc++) {
region->registerCC(cc, resources_.midiState.getCCValue(cc)); layer.registerCC(cc, resources_.midiState.getCCValue(cc));
} }
// Set the default frequencies on equalizers if needed // Set the default frequencies on equalizers if needed
if (region->equalizers.size() > 0 if (region.equalizers.size() > 0
&& region->equalizers[0].frequency == Default::eqFrequency) { && region.equalizers[0].frequency == Default::eqFrequency) {
region->equalizers[0].frequency = Default::defaultEQFreq[0]; region.equalizers[0].frequency = Default::defaultEQFreq[0];
if (region->equalizers.size() > 1 if (region.equalizers.size() > 1
&& region->equalizers[1].frequency == Default::eqFrequency) { && region.equalizers[1].frequency == Default::eqFrequency) {
region->equalizers[1].frequency = Default::defaultEQFreq[1]; region.equalizers[1].frequency = Default::defaultEQFreq[1];
if (region->equalizers.size() > 2 if (region.equalizers.size() > 2
&& region->equalizers[2].frequency == Default::eqFrequency) { && region.equalizers[2].frequency == Default::eqFrequency) {
region->equalizers[2].frequency = Default::defaultEQFreq[2]; region.equalizers[2].frequency = Default::defaultEQFreq[2];
} }
} }
} }
if (!region->velocityPoints.empty()) if (!region.velocityPoints.empty())
region->velCurve = Curve::buildFromVelcurvePoints( region.velCurve = Curve::buildFromVelcurvePoints(
region->velocityPoints, Curve::Interpolator::Linear); region.velocityPoints, Curve::Interpolator::Linear);
region->registerPitchWheel(0); layer.registerPitchWheel(0);
region->registerAftertouch(0); layer.registerAftertouch(0);
region->registerTempo(2.0f); layer.registerTempo(2.0f);
maxFilters = max(maxFilters, region->filters.size()); maxFilters = max(maxFilters, region.filters.size());
maxEQs = max(maxEQs, region->equalizers.size()); maxEQs = max(maxEQs, region.equalizers.size());
maxLFOs = max(maxLFOs, region->lfos.size()); maxLFOs = max(maxLFOs, region.lfos.size());
maxFlexEGs = max(maxFlexEGs, region->flexEGs.size()); maxFlexEGs = max(maxFlexEGs, region.flexEGs.size());
havePitchEG = havePitchEG || region->pitchEG != absl::nullopt; havePitchEG = havePitchEG || region.pitchEG != absl::nullopt;
haveFilterEG = haveFilterEG || region->filterEG != absl::nullopt; haveFilterEG = haveFilterEG || region.filterEG != absl::nullopt;
haveAmplitudeLFO = haveAmplitudeLFO || region->amplitudeLFO != absl::nullopt; haveAmplitudeLFO = haveAmplitudeLFO || region.amplitudeLFO != absl::nullopt;
havePitchLFO = havePitchLFO || region->pitchLFO != absl::nullopt; havePitchLFO = havePitchLFO || region.pitchLFO != absl::nullopt;
haveFilterLFO = haveFilterLFO || region->filterLFO != absl::nullopt; haveFilterLFO = haveFilterLFO || region.filterLFO != absl::nullopt;
++currentRegionIndex; ++currentRegionIndex;
} }
if (currentRegionCount < regions_.size()) { if (currentRegionCount < layers_.size()) {
DBG("Removing " << (regions_.size() - currentRegionCount) DBG("Removing " << (layers_.size() - currentRegionCount)
<< " out of " << regions_.size() << " regions"); << " out of " << layers_.size() << " regions");
} }
regions_.resize(currentRegionCount); layers_.resize(currentRegionCount);
// collect all CCs used in regions, with matrix not yet connected // collect all CCs used in regions, with matrix not yet connected
BitArray<config::numCCs> usedCCs; BitArray<config::numCCs> usedCCs;
for (const RegionPtr& regionPtr : regions_) { for (const LayerPtr& layerPtr : layers_) {
const Region& region = *regionPtr; const Region& region = layerPtr->getRegion();
collectUsedCCsFromRegion(usedCCs, region); collectUsedCCsFromRegion(usedCCs, region);
for (const Region::Connection& connection : region.connections) { for (const Region::Connection& connection : region.connections) {
if (connection.source.id() == ModId::Controller) if (connection.source.id() == ModId::Controller)
@ -720,8 +725,8 @@ void Synth::Impl::finalizeSfzLoad()
} }
} }
// connect default controllers, except if these CC are already used // connect default controllers, except if these CC are already used
for (const RegionPtr& regionPtr : regions_) { for (const LayerPtr& layerPtr : layers_) {
Region& region = *regionPtr; Region& region = layerPtr->getRegion();
constexpr unsigned defaultSmoothness = 10; constexpr unsigned defaultSmoothness = 10;
if (!usedCCs.test(7)) { if (!usedCCs.test(7)) {
region.getOrCreateConnection( region.getOrCreateConnection(
@ -760,19 +765,21 @@ void Synth::Impl::finalizeSfzLoad()
currentUsedCCs_ = collectAllUsedCCs(); currentUsedCCs_ = collectAllUsedCCs();
// cache the set of keys assigned // cache the set of keys assigned
for (const RegionPtr& regionPtr : regions_) { for (const LayerPtr& layerPtr : layers_) {
UncheckedRange<uint8_t> keyRange = regionPtr->keyRange; const Region& region = layerPtr->getRegion();
UncheckedRange<uint8_t> keyRange = region.keyRange;
unsigned loKey = keyRange.getStart(); unsigned loKey = keyRange.getStart();
unsigned hiKey = keyRange.getEnd(); unsigned hiKey = keyRange.getEnd();
for (unsigned key = loKey; key <= hiKey; ++key) for (unsigned key = loKey; key <= hiKey; ++key)
keySlots_.set(key); keySlots_.set(key);
} }
// cache the set of keyswitches assigned // cache the set of keyswitches assigned
for (const RegionPtr& regionPtr : regions_) { for (const LayerPtr& layerPtr : layers_) {
if (absl::optional<uint8_t> sw = regionPtr->lastKeyswitch) { const Region& region = layerPtr->getRegion();
if (absl::optional<uint8_t> sw = region.lastKeyswitch) {
swLastSlots_.set(*sw); swLastSlots_.set(*sw);
} }
else if (absl::optional<UncheckedRange<uint8_t>> swRange = regionPtr->lastKeyswitchRange) { else if (absl::optional<UncheckedRange<uint8_t>> swRange = region.lastKeyswitchRange) {
unsigned loKey = swRange->getStart(); unsigned loKey = swRange->getStart();
unsigned hiKey = swRange->getEnd(); unsigned hiKey = swRange->getEnd();
for (unsigned key = loKey; key <= hiKey; ++key) for (unsigned key = loKey; key <= hiKey; ++key)
@ -1075,15 +1082,17 @@ void Synth::noteOff(int delay, int noteNumber, uint8_t velocity) noexcept
impl.resources_.midiState.noteOffEvent(delay, noteNumber, normalizedVelocity); impl.resources_.midiState.noteOffEvent(delay, noteNumber, normalizedVelocity);
} }
void Synth::Impl::startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept void Synth::Impl::startVoice(Layer* layer, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept
{ {
voiceManager_.checkPolyphony(region, delay, triggerEvent); const Region& region = layer->getRegion();
voiceManager_.checkPolyphony(&region, delay, triggerEvent);
Voice* selectedVoice = voiceManager_.findFreeVoice(); Voice* selectedVoice = voiceManager_.findFreeVoice();
if (selectedVoice == nullptr) if (selectedVoice == nullptr)
return; return;
ASSERT(selectedVoice->isFree()); ASSERT(selectedVoice->isFree());
if (selectedVoice->startVoice(region, delay, triggerEvent)) if (selectedVoice->startVoice(layer, delay, triggerEvent))
ring.addVoiceToRing(selectedVoice); ring.addVoiceToRing(selectedVoice);
} }
@ -1093,18 +1102,19 @@ void Synth::Impl::noteOffDispatch(int delay, int noteNumber, float velocity) noe
SisterVoiceRingBuilder ring; SisterVoiceRingBuilder ring;
const TriggerEvent triggerEvent { TriggerEventType::NoteOff, noteNumber, velocity }; const TriggerEvent triggerEvent { TriggerEventType::NoteOff, noteNumber, velocity };
for (auto& region : upKeyswitchLists_[noteNumber]) for (Layer* layer : upKeyswitchLists_[noteNumber])
region->keySwitched = true; layer->keySwitched_ = true;
for (auto& region : downKeyswitchLists_[noteNumber]) for (Layer* layer : downKeyswitchLists_[noteNumber])
region->keySwitched = false; layer->keySwitched_ = false;
for (auto& region : noteActivationLists_[noteNumber]) { for (Layer* layer : noteActivationLists_[noteNumber]) {
if (region->registerNoteOff(noteNumber, velocity, randValue)) { const Region& region = layer->getRegion();
if (region->trigger == Trigger::release && !region->rtDead && !voiceManager_.playingAttackVoice(region)) if (layer->registerNoteOff(noteNumber, velocity, randValue)) {
if (region.trigger == Trigger::release && !region.rtDead && !voiceManager_.playingAttackVoice(&region))
continue; continue;
startVoice(region, delay, triggerEvent, ring); startVoice(layer, delay, triggerEvent, ring);
} }
} }
} }
@ -1116,70 +1126,77 @@ void Synth::Impl::noteOnDispatch(int delay, int noteNumber, float velocity) noex
if (!lastKeyswitchLists_[noteNumber].empty()) { if (!lastKeyswitchLists_[noteNumber].empty()) {
if (currentSwitch_ && *currentSwitch_ != noteNumber) { if (currentSwitch_ && *currentSwitch_ != noteNumber) {
for (auto& region : lastKeyswitchLists_[*currentSwitch_]) for (Layer* layer : lastKeyswitchLists_[*currentSwitch_])
region->keySwitched = false; layer->keySwitched_ = false;
} }
currentSwitch_ = noteNumber; currentSwitch_ = noteNumber;
currentSwitchChanged_ = true; currentSwitchChanged_ = true;
} }
for (auto& region : lastKeyswitchLists_[noteNumber]) for (Layer* layer : lastKeyswitchLists_[noteNumber])
region->keySwitched = true; layer->keySwitched_ = true;
for (auto& region : upKeyswitchLists_[noteNumber]) for (Layer* layer : upKeyswitchLists_[noteNumber])
region->keySwitched = false; layer->keySwitched_ = false;
for (auto& region : downKeyswitchLists_[noteNumber]) for (Layer* layer : downKeyswitchLists_[noteNumber])
region->keySwitched = true; layer->keySwitched_ = true;
for (auto& region : noteActivationLists_[noteNumber]) { for (Layer* layer : noteActivationLists_[noteNumber]) {
if (region->registerNoteOn(noteNumber, velocity, randValue)) { if (layer->registerNoteOn(noteNumber, velocity, randValue)) {
const Region& region = layer->getRegion();
for (auto& voice : voiceManager_) { for (auto& voice : voiceManager_) {
if (voice.checkOffGroup(region, delay, noteNumber)) { if (voice.checkOffGroup(&region, delay, noteNumber)) {
const TriggerEvent& event = voice.getTriggerEvent(); const TriggerEvent& event = voice.getTriggerEvent();
noteOffDispatch(delay, event.number, event.value); noteOffDispatch(delay, event.number, event.value);
} }
} }
TriggerEvent triggerEvent { TriggerEventType::NoteOn, noteNumber, velocity }; TriggerEvent triggerEvent { TriggerEventType::NoteOn, noteNumber, velocity };
if (region->velocityOverride == VelocityOverride::previous) if (region.velocityOverride == VelocityOverride::previous)
triggerEvent.value = resources_.midiState.getLastVelocity(); triggerEvent.value = resources_.midiState.getLastVelocity();
startVoice(region, delay, triggerEvent, ring); startVoice(layer, delay, triggerEvent, ring);
} }
} }
for (auto& region : previousKeyswitchLists_) for (Layer* layer : previousKeyswitchLists_) {
region->previousKeySwitched = (*region->previousKeyswitch == noteNumber); const Region& region = layer->getRegion();
layer->previousKeySwitched_ = (region.previousKeyswitch == noteNumber);
}
} }
void Synth::Impl::startDelayedSustainReleases(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept void Synth::Impl::startDelayedSustainReleases(Layer* layer, int delay, SisterVoiceRingBuilder& ring) noexcept
{ {
if (!region->rtDead && !voiceManager_.playingAttackVoice(region)) { const Region& region = layer->getRegion();
region->delayedSustainReleases.clear();
if (!region.rtDead && !voiceManager_.playingAttackVoice(&region)) {
layer->delayedSustainReleases_.clear();
return; return;
} }
for (auto& note: region->delayedSustainReleases) { for (auto& note: layer->delayedSustainReleases_) {
const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second }; const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second };
startVoice(region, delay, noteOffEvent, ring); startVoice(layer, delay, noteOffEvent, ring);
} }
region->delayedSustainReleases.clear(); layer->delayedSustainReleases_.clear();
} }
void Synth::Impl::startDelayedSostenutoReleases(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept void Synth::Impl::startDelayedSostenutoReleases(Layer* layer, int delay, SisterVoiceRingBuilder& ring) noexcept
{ {
if (!region->rtDead && !voiceManager_.playingAttackVoice(region)) { const Region& region = layer->getRegion();
region->delayedSostenutoReleases.clear();
if (!region.rtDead && !voiceManager_.playingAttackVoice(&region)) {
layer->delayedSostenutoReleases_.clear();
return; return;
} }
for (auto& note: region->delayedSostenutoReleases) { for (auto& note: layer->delayedSostenutoReleases_) {
const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second }; const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second };
startVoice(region, delay, noteOffEvent, ring); startVoice(layer, delay, noteOffEvent, ring);
} }
region->delayedSostenutoReleases.clear(); layer->delayedSostenutoReleases_.clear();
} }
void Synth::cc(int delay, int ccNumber, uint8_t ccValue) noexcept void Synth::cc(int delay, int ccNumber, uint8_t ccValue) noexcept
@ -1192,23 +1209,25 @@ void Synth::Impl::ccDispatch(int delay, int ccNumber, float value) noexcept
{ {
SisterVoiceRingBuilder ring; SisterVoiceRingBuilder ring;
const TriggerEvent triggerEvent { TriggerEventType::CC, ccNumber, value }; const TriggerEvent triggerEvent { TriggerEventType::CC, ccNumber, value };
for (auto& region : ccActivationLists_[ccNumber]) { for (Layer* layer : ccActivationLists_[ccNumber]) {
if (region->checkSustain && ccNumber == region->sustainCC && value < region->sustainThreshold) const Region& region = layer->getRegion();
startDelayedSustainReleases(region, delay, ring);
if (region->checkSostenuto && ccNumber == region->sostenutoCC && value < region->sostenutoThreshold) { if (region.checkSustain && ccNumber == region.sustainCC && value < region.sustainThreshold)
if (region->sustainPressed) { startDelayedSustainReleases(layer, delay, ring);
for (const auto& v: region->delayedSostenutoReleases)
region->delaySustainRelease(v.first, v.second);
region->delayedSostenutoReleases.clear(); if (region.checkSostenuto && ccNumber == region.sostenutoCC && value < region.sostenutoThreshold) {
if (layer->sustainPressed_) {
for (const auto& v: layer->delayedSostenutoReleases_)
layer->delaySustainRelease(v.first, v.second);
layer->delayedSostenutoReleases_.clear();
} else { } else {
startDelayedSostenutoReleases(region, delay, ring); startDelayedSostenutoReleases(layer, delay, ring);
} }
} }
if (region->registerCC(ccNumber, value)) if (layer->registerCC(ccNumber, value))
startVoice(region, delay, triggerEvent, ring); startVoice(layer, delay, triggerEvent, ring);
} }
} }
@ -1288,8 +1307,8 @@ void Synth::pitchWheel(int delay, int pitch) noexcept
ScopedTiming logger { impl.dispatchDuration_, ScopedTiming::Operation::addToDuration }; ScopedTiming logger { impl.dispatchDuration_, ScopedTiming::Operation::addToDuration };
impl.resources_.midiState.pitchBendEvent(delay, normalizedPitch); impl.resources_.midiState.pitchBendEvent(delay, normalizedPitch);
for (auto& region : impl.regions_) { for (const Impl::LayerPtr& layer : impl.layers_) {
region->registerPitchWheel(normalizedPitch); layer->registerPitchWheel(normalizedPitch);
} }
for (auto& voice : impl.voiceManager_) { for (auto& voice : impl.voiceManager_) {
@ -1310,8 +1329,8 @@ void Synth::hdAftertouch(int delay, float normAftertouch) noexcept
impl.resources_.midiState.channelAftertouchEvent(delay, normAftertouch); impl.resources_.midiState.channelAftertouchEvent(delay, normAftertouch);
for (auto& region : impl.regions_) { for (const Impl::LayerPtr& layerPtr : impl.layers_) {
region->registerAftertouch(normAftertouch); layerPtr->registerAftertouch(normAftertouch);
} }
for (auto& voice : impl.voiceManager_) { for (auto& voice : impl.voiceManager_) {
@ -1365,7 +1384,7 @@ void Synth::playbackState(int delay, int playbackState)
int Synth::getNumRegions() const noexcept int Synth::getNumRegions() const noexcept
{ {
Impl& impl = *impl_; Impl& impl = *impl_;
return static_cast<int>(impl.regions_.size()); return static_cast<int>(impl.layers_.size());
} }
int Synth::getNumGroups() const noexcept int Synth::getNumGroups() const noexcept
@ -1490,10 +1509,16 @@ std::string Synth::exportMidnam(absl::string_view model) const
return std::move(writer.str()); return std::move(writer.str());
} }
const Region* Synth::getRegionView(int idx) const noexcept const Layer* Synth::getLayerView(int idx) const noexcept
{ {
Impl& impl = *impl_; Impl& impl = *impl_;
return (size_t)idx < impl.regions_.size() ? impl.regions_[idx].get() : nullptr; return (size_t)idx < impl.layers_.size() ? impl.layers_[idx].get() : nullptr;
}
const Region* Synth::getRegionView(int idx) const noexcept
{
const Layer* layer = getLayerView(idx);
return layer ? &layer->getRegion() : nullptr;
} }
const EffectBus* Synth::getEffectBusView(int idx) const noexcept const EffectBus* Synth::getEffectBusView(int idx) const noexcept
@ -1514,10 +1539,10 @@ const PolyphonyGroup* Synth::getPolyphonyGroupView(int idx) const noexcept
return impl.voiceManager_.getPolyphonyGroupView(idx); return impl.voiceManager_.getPolyphonyGroupView(idx);
} }
const Region* Synth::getRegionById(NumericId<Region> id) const noexcept Layer* Synth::getLayerById(NumericId<Region> id) noexcept
{ {
Impl& impl = *impl_; Impl& impl = *impl_;
const size_t size = impl.regions_.size(); const size_t size = impl.layers_.size();
if (size == 0 || !id.valid()) if (size == 0 || !id.valid())
return nullptr; return nullptr;
@ -1526,10 +1551,17 @@ const Region* Synth::getRegionById(NumericId<Region> id) const noexcept
size_t index = static_cast<size_t>(id.number()); size_t index = static_cast<size_t>(id.number());
index = std::min(index, size - 1); index = std::min(index, size - 1);
while (index > 0 && impl.regions_[index]->getId().number() > id.number()) while (index > 0 && impl.layers_[index]->getRegion().getId().number() > id.number())
--index; --index;
return (impl.regions_[index]->getId() == id) ? impl.regions_[index].get() : nullptr; return (impl.layers_[index]->getRegion().getId() == id) ?
impl.layers_[index].get() : nullptr;
}
const Region* Synth::getRegionById(NumericId<Region> id) const noexcept
{
Layer* layer = const_cast<Synth*>(this)->getLayerById(id);
return layer ? &layer->getRegion() : nullptr;
} }
const Voice* Synth::getVoiceView(int idx) const noexcept const Voice* Synth::getVoiceView(int idx) const noexcept
@ -1653,8 +1685,10 @@ void Synth::Impl::setupModMatrix()
{ {
ModMatrix& mm = resources_.modMatrix; ModMatrix& mm = resources_.modMatrix;
for (const RegionPtr& region : regions_) { for (const LayerPtr& layerPtr : layers_) {
for (const Region::Connection& conn : region->connections) { const Region& region = layerPtr->getRegion();
for (const Region::Connection& conn : region.connections) {
ModGenerator* gen = nullptr; ModGenerator* gen = nullptr;
ModKey sourceKey = conn.source; ModKey sourceKey = conn.source;
@ -1789,9 +1823,10 @@ void Synth::Impl::resetAllControllers(int delay) noexcept
voice.registerCC(delay, cc, 0.0f); voice.registerCC(delay, cc, 0.0f);
} }
for (auto& region : regions_) { for (const LayerPtr& layerPtr : layers_) {
Layer& layer = *layerPtr;
for (int cc = 0; cc < config::numCCs; ++cc) for (int cc = 0; cc < config::numCCs; ++cc)
region->registerCC(cc, 0.0f); layer.registerCC(cc, 0.0f);
} }
} }
@ -1929,8 +1964,8 @@ void Synth::Impl::collectUsedCCsFromModulations(BitArray<config::numCCs>& usedCC
BitArray<config::numCCs> Synth::Impl::collectAllUsedCCs() BitArray<config::numCCs> Synth::Impl::collectAllUsedCCs()
{ {
BitArray<config::numCCs> used; BitArray<config::numCCs> used;
for (const Impl::RegionPtr& region : regions_) for (const LayerPtr& layerPtr : layers_)
collectUsedCCsFromRegion(used, *region); collectUsedCCsFromRegion(used, layerPtr->getRegion());
collectUsedCCsFromModulations(used, resources_.modMatrix); collectUsedCCsFromModulations(used, resources_.modMatrix);
return used; return used;
} }

View file

@ -26,6 +26,7 @@ class RegionSet;
class PolyphonyGroup; class PolyphonyGroup;
class EffectBus; class EffectBus;
struct Region; struct Region;
struct Layer;
class Voice; class Voice;
/** /**
@ -189,6 +190,13 @@ public:
* @brief Export a MIDI Name document describing the loaded instrument * @brief Export a MIDI Name document describing the loaded instrument
*/ */
std::string exportMidnam(absl::string_view model = {}) const; std::string exportMidnam(absl::string_view model = {}) const;
/**
* @brief Find the layer which is associated with the given identifier.
*
* @param id
* @return Layer*
*/
Layer* getLayerById(NumericId<Region> id) noexcept;
/** /**
* @brief Find the region which is associated with the given identifier. * @brief Find the region which is associated with the given identifier.
* *
@ -196,6 +204,14 @@ public:
* @return const Region* * @return const Region*
*/ */
const Region* getRegionById(NumericId<Region> id) const noexcept; const Region* getRegionById(NumericId<Region> id) const noexcept;
/**
* @brief Get a raw view into a specific layer. This is mostly used
* for testing.
*
* @param idx
* @return const Layer*
*/
const Layer* getLayerView(int idx) const noexcept;
/** /**
* @brief Get a raw view into a specific region. This is mostly used * @brief Get a raw view into a specific region. This is mostly used
* for testing. * for testing.

View file

@ -28,9 +28,10 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co
if (extractMessage(p, path, indices) && !strcmp(sig, s)) if (extractMessage(p, path, indices) && !strcmp(sig, s))
#define GET_REGION_OR_BREAK(idx) \ #define GET_REGION_OR_BREAK(idx) \
if (idx >= impl.regions_.size()) \ if (idx >= impl.layers_.size()) \
break; \ break; \
const auto& region = *impl.regions_[idx]; Layer& layer = *impl.layers_[idx]; \
const Region& region = layer.getRegion();
MATCH("/hello", "") { MATCH("/hello", "") {
client.receive(delay, "/hello", "", nullptr); client.receive(delay, "/hello", "", nullptr);

View file

@ -5,6 +5,7 @@
#include "SisterVoiceRing.h" #include "SisterVoiceRing.h"
#include "TriggerEvent.h" #include "TriggerEvent.h"
#include "VoiceManager.h" #include "VoiceManager.h"
#include "Layer.h"
#include "BitArray.h" #include "BitArray.h"
#include "modulations/sources/ADSREnvelope.h" #include "modulations/sources/ADSREnvelope.h"
#include "modulations/sources/Controller.h" #include "modulations/sources/Controller.h"
@ -147,30 +148,30 @@ struct Synth::Impl final: public Parser::Listener {
* @brief Start a voice for a specific region. * @brief Start a voice for a specific region.
* This will do the needed polyphony checks and voice stealing. * This will do the needed polyphony checks and voice stealing.
* *
* @param region * @param layer
* @param delay * @param delay
* @param triggerEvent * @param triggerEvent
* @param ring * @param ring
*/ */
void startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept; void startVoice(Layer* layer, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept;
/** /**
* @brief Start all delayed sustain release voices of the region if necessary * @brief Start all delayed sustain release voices of the region if necessary
* *
* @param region * @param layer
* @param delay * @param delay
* @param ring * @param ring
*/ */
void startDelayedSustainReleases(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept; void startDelayedSustainReleases(Layer* layer, int delay, SisterVoiceRingBuilder& ring) noexcept;
/** /**
* @brief Start all delayed sostenuto release voices of the region if necessary * @brief Start all delayed sostenuto release voices of the region if necessary
* *
* @param region * @param layer
* @param delay * @param delay
* @param ring * @param ring
*/ */
void startDelayedSostenutoReleases(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept; void startDelayedSostenutoReleases(Layer* layer, int delay, SisterVoiceRingBuilder& ring) noexcept;
/** /**
* @brief Finalize SFZ loading, following a successful execution of the * @brief Finalize SFZ loading, following a successful execution of the
@ -242,22 +243,24 @@ struct Synth::Impl final: public Parser::Listener {
bool currentSwitchChanged_ = true; bool currentSwitchChanged_ = true;
std::vector<std::string> unknownOpcodes_; std::vector<std::string> unknownOpcodes_;
using RegionViewVector = std::vector<Region*>; using RegionViewVector = std::vector<Region*>;
using LayerViewVector = std::vector<Layer*>;
using VoiceViewVector = std::vector<Voice*>; using VoiceViewVector = std::vector<Voice*>;
using LayerPtr = std::unique_ptr<Layer>;
using RegionPtr = std::unique_ptr<Region>; using RegionPtr = std::unique_ptr<Region>;
using RegionSetPtr = std::unique_ptr<RegionSet>; using RegionSetPtr = std::unique_ptr<RegionSet>;
std::vector<RegionPtr> regions_; std::vector<LayerPtr> layers_;
VoiceManager voiceManager_; VoiceManager voiceManager_;
// These are more general "groups" than sfz and encapsulates the full hierarchy // These are more general "groups" than sfz and encapsulates the full hierarchy
RegionSet* currentSet_ { nullptr }; RegionSet* currentSet_ { nullptr };
std::vector<RegionSetPtr> sets_; std::vector<RegionSetPtr> sets_;
std::array<RegionViewVector, 128> lastKeyswitchLists_; std::array<LayerViewVector, 128> lastKeyswitchLists_;
std::array<RegionViewVector, 128> downKeyswitchLists_; std::array<LayerViewVector, 128> downKeyswitchLists_;
std::array<RegionViewVector, 128> upKeyswitchLists_; std::array<LayerViewVector, 128> upKeyswitchLists_;
RegionViewVector previousKeyswitchLists_; LayerViewVector previousKeyswitchLists_;
std::array<RegionViewVector, 128> noteActivationLists_; std::array<LayerViewVector, 128> noteActivationLists_;
std::array<RegionViewVector, config::numCCs> ccActivationLists_; std::array<LayerViewVector, config::numCCs> ccActivationLists_;
// Effect factory and buses // Effect factory and buses
EffectFactory effectFactory_; EffectFactory effectFactory_;

View file

@ -5,6 +5,7 @@
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#include "Voice.h" #include "Voice.h"
#include "Layer.h"
#include "AudioBuffer.h" #include "AudioBuffer.h"
#include "Config.h" #include "Config.h"
#include "Defaults.h" #include "Defaults.h"
@ -198,7 +199,7 @@ struct Voice::Impl
const NumericId<Voice> id_; const NumericId<Voice> id_;
StateListener* stateListener_ = nullptr; StateListener* stateListener_ = nullptr;
Region* region_ { nullptr }; const Region* region_ { nullptr };
State state_ { State::idle }; State state_ { State::idle };
bool noteIsOff_ { false }; bool noteIsOff_ { false };
@ -362,18 +363,21 @@ Voice::Impl::Impl(int voiceNumber, Resources& resources)
getSCurve(); getSCurve();
} }
bool Voice::startVoice(Region* region, int delay, const TriggerEvent& event) noexcept bool Voice::startVoice(Layer* layer, int delay, const TriggerEvent& event) noexcept
{ {
Impl& impl = *impl_; Impl& impl = *impl_;
ASSERT(event.value >= 0.0f && event.value <= 1.0f); ASSERT(event.value >= 0.0f && event.value <= 1.0f);
impl.region_ = region; Resources& resources = impl.resources_;
const Region& region = layer->getRegion();
impl.region_ = &region;
impl.triggerEvent_ = event; impl.triggerEvent_ = event;
if (impl.triggerEvent_.type == TriggerEventType::CC) if (impl.triggerEvent_.type == TriggerEventType::CC)
impl.triggerEvent_.number = region->pitchKeycenter; impl.triggerEvent_.number = region.pitchKeycenter;
if (region->disabled()) { if (region.disabled()) {
impl.switchState(State::cleanMeUp); impl.switchState(State::cleanMeUp);
return false; return false;
} }
@ -384,33 +388,33 @@ bool Voice::startVoice(Region* region, int delay, const TriggerEvent& event) noe
if (delay < 0) if (delay < 0)
delay = 0; delay = 0;
if (region->isOscillator()) { if (region.isOscillator()) {
const WavetableMulti* wave = nullptr; const WavetableMulti* wave = nullptr;
if (!region->isGenerator()) if (!region.isGenerator())
wave = impl.resources_.wavePool.getFileWave(region->sampleId->filename()); wave = resources.wavePool.getFileWave(region.sampleId->filename());
else { else {
switch (hash(region->sampleId->filename())) { switch (hash(region.sampleId->filename())) {
default: default:
case hash("*silence"): case hash("*silence"):
break; break;
case hash("*sine"): case hash("*sine"):
wave = impl.resources_.wavePool.getWaveSin(); wave = resources.wavePool.getWaveSin();
break; break;
case hash("*triangle"): // fallthrough case hash("*triangle"): // fallthrough
case hash("*tri"): case hash("*tri"):
wave = impl.resources_.wavePool.getWaveTriangle(); wave = resources.wavePool.getWaveTriangle();
break; break;
case hash("*square"): case hash("*square"):
wave = impl.resources_.wavePool.getWaveSquare(); wave = resources.wavePool.getWaveSquare();
break; break;
case hash("*saw"): case hash("*saw"):
wave = impl.resources_.wavePool.getWaveSaw(); wave = resources.wavePool.getWaveSaw();
break; break;
} }
} }
const float phase = region->getPhase(); const float phase = region.getPhase();
const int quality = const int quality =
region->oscillatorQuality.value_or(Default::oscillatorQuality); region.oscillatorQuality.value_or(Default::oscillatorQuality);
for (WavetableOscillator& osc : impl.waveOscillators_) { for (WavetableOscillator& osc : impl.waveOscillators_) {
osc.setWavetable(wave); osc.setWavetable(wave);
osc.setPhase(phase); osc.setPhase(phase);
@ -418,61 +422,61 @@ bool Voice::startVoice(Region* region, int delay, const TriggerEvent& event) noe
} }
impl.setupOscillatorUnison(); impl.setupOscillatorUnison();
} else { } else {
impl.currentPromise_ = impl.resources_.filePool.getFilePromise(region->sampleId); impl.currentPromise_ = resources.filePool.getFilePromise(region.sampleId);
if (!impl.currentPromise_) { if (!impl.currentPromise_) {
impl.switchState(State::cleanMeUp); impl.switchState(State::cleanMeUp);
return false; return false;
} }
impl.updateLoopInformation(); impl.updateLoopInformation();
impl.speedRatio_ = static_cast<float>(impl.currentPromise_->information.sampleRate / impl.sampleRate_); impl.speedRatio_ = static_cast<float>(impl.currentPromise_->information.sampleRate / impl.sampleRate_);
impl.sourcePosition_ = region->getOffset(impl.resources_.filePool.getOversamplingFactor()); impl.sourcePosition_ = region.getOffset(resources.midiState, resources.filePool.getOversamplingFactor());
} }
// do Scala retuning and reconvert the frequency into a 12TET key number // do Scala retuning and reconvert the frequency into a 12TET key number
const float numberRetuned = impl.resources_.tuning.getKeyFractional12TET(impl.triggerEvent_.number); const float numberRetuned = resources.tuning.getKeyFractional12TET(impl.triggerEvent_.number);
impl.pitchRatio_ = region->getBasePitchVariation(numberRetuned, impl.triggerEvent_.value); impl.pitchRatio_ = region.getBasePitchVariation(numberRetuned, impl.triggerEvent_.value);
// apply stretch tuning if set // apply stretch tuning if set
if (impl.resources_.stretch) if (resources.stretch)
impl.pitchRatio_ *= impl.resources_.stretch->getRatioForFractionalKey(numberRetuned); impl.pitchRatio_ *= resources.stretch->getRatioForFractionalKey(numberRetuned);
impl.baseVolumedB_ = region->getBaseVolumedB(impl.triggerEvent_.number); impl.baseVolumedB_ = region.getBaseVolumedB(resources.midiState, impl.triggerEvent_.number);
impl.baseGain_ = region->getBaseGain(); impl.baseGain_ = region.getBaseGain();
if (impl.triggerEvent_.type != TriggerEventType::CC) if (impl.triggerEvent_.type != TriggerEventType::CC)
impl.baseGain_ *= region->getNoteGain(impl.triggerEvent_.number, impl.triggerEvent_.value); impl.baseGain_ *= region.getNoteGain(impl.triggerEvent_.number, impl.triggerEvent_.value);
impl.gainSmoother_.reset(); impl.gainSmoother_.reset();
impl.resetCrossfades(); impl.resetCrossfades();
for (unsigned i = 0; i < region->filters.size(); ++i) { for (unsigned i = 0; i < region.filters.size(); ++i) {
impl.filters_[i].setup(*region, i, impl.triggerEvent_.number, impl.triggerEvent_.value); impl.filters_[i].setup(region, i, impl.triggerEvent_.number, impl.triggerEvent_.value);
} }
for (unsigned i = 0; i < region->equalizers.size(); ++i) { for (unsigned i = 0; i < region.equalizers.size(); ++i) {
impl.equalizers_[i].setup(*region, i, impl.triggerEvent_.value); impl.equalizers_[i].setup(region, i, impl.triggerEvent_.value);
} }
impl.triggerDelay_ = delay; impl.triggerDelay_ = delay;
impl.initialDelay_ = delay + static_cast<int>(region->getDelay() * impl.sampleRate_); impl.initialDelay_ = delay + static_cast<int>(region.getDelay(resources.midiState) * impl.sampleRate_);
impl.baseFrequency_ = impl.resources_.tuning.getFrequencyOfKey(impl.triggerEvent_.number); impl.baseFrequency_ = resources.tuning.getFrequencyOfKey(impl.triggerEvent_.number);
impl.sampleSize_ = region->trueSampleEnd(impl.resources_.filePool.getOversamplingFactor()) - impl.sourcePosition_ - 1; impl.sampleSize_ = region.trueSampleEnd(resources.filePool.getOversamplingFactor()) - impl.sourcePosition_ - 1;
impl.bendStepFactor_ = centsFactor(region->bendStep); impl.bendStepFactor_ = centsFactor(region.bendStep);
impl.bendSmoother_.setSmoothing(region->bendSmooth, impl.sampleRate_); impl.bendSmoother_.setSmoothing(region.bendSmooth, impl.sampleRate_);
impl.bendSmoother_.reset(centsFactor(region->getBendInCents(impl.resources_.midiState.getPitchBend()))); impl.bendSmoother_.reset(centsFactor(region.getBendInCents(resources.midiState.getPitchBend())));
impl.resources_.modMatrix.initVoice(impl.id_, region->getId(), impl.initialDelay_); resources.modMatrix.initVoice(impl.id_, region.getId(), impl.initialDelay_);
impl.saveModulationTargets(region); impl.saveModulationTargets(&region);
if (region->checkSustain) { if (region.checkSustain) {
const bool sustainPressed = const bool sustainPressed =
impl.resources_.midiState.getCCValue(region->sustainCC) >= region->sustainThreshold; resources.midiState.getCCValue(region.sustainCC) >= region.sustainThreshold;
impl.sustainState_ = impl.sustainState_ =
sustainPressed ? Impl::SustainState::Sustaining : Impl::SustainState::Up; sustainPressed ? Impl::SustainState::Sustaining : Impl::SustainState::Up;
} }
if (region->checkSostenuto) { if (region.checkSostenuto) {
const bool sostenutoPressed = const bool sostenutoPressed =
impl.resources_.midiState.getCCValue(region->sostenutoCC) >= region->sostenutoThreshold; resources.midiState.getCCValue(region.sostenutoCC) >= region.sostenutoThreshold;
impl.sostenutoState_ = impl.sostenutoState_ =
sostenutoPressed ? Impl::SostenutoState::PreviouslyDown : Impl::SostenutoState::Up; sostenutoPressed ? Impl::SostenutoState::PreviouslyDown : Impl::SostenutoState::Up;
} }

View file

@ -18,6 +18,7 @@ namespace sfz {
enum InterpolatorModel : int; enum InterpolatorModel : int;
class LFO; class LFO;
class FlexEnvelope; class FlexEnvelope;
struct Layer;
/** /**
* @brief The SFZ voice are the polyphony holders. They get activated by the synth * @brief The SFZ voice are the polyphony holders. They get activated by the synth
* and tasked to play a given region until the end, stopping on note-offs, off-groups * and tasked to play a given region until the end, stopping on note-offs, off-groups
@ -99,12 +100,12 @@ public:
/** /**
* @brief Start playing a region after a short delay for different triggers (note on, off, cc) * @brief Start playing a region after a short delay for different triggers (note on, off, cc)
* *
* @param region * @param layer
* @param delay * @param delay
* @param evebt * @param evebt
* @return bool * @return bool
*/ */
bool startVoice(Region* region, int delay, const TriggerEvent& event) noexcept; bool startVoice(Layer* layer, int delay, const TriggerEvent& event) noexcept;
/** /**
* @brief Get the sample quality determined by the active region. * @brief Get the sample quality determined by the active region.

View file

@ -7,6 +7,7 @@
#include "TestHelpers.h" #include "TestHelpers.h"
#include "sfizz/MidiState.h" #include "sfizz/MidiState.h"
#include "sfizz/Region.h" #include "sfizz/Region.h"
#include "sfizz/Layer.h"
#include "sfizz/SfzHelpers.h" #include "sfizz/SfzHelpers.h"
#include "sfizz/modulations/ModId.h" #include "sfizz/modulations/ModId.h"
#include "sfizz/modulations/ModKey.h" #include "sfizz/modulations/ModKey.h"
@ -18,8 +19,7 @@ using namespace sfz;
TEST_CASE("[Direct Region Tests] amp_velcurve") TEST_CASE("[Direct Region Tests] amp_velcurve")
{ {
MidiState midiState; Region region { 0 };
Region region { 0, midiState };
region.parseOpcode({ "amp_velcurve_6", "0.4" }); region.parseOpcode({ "amp_velcurve_6", "0.4" });
REQUIRE(region.velocityPoints.back() == std::pair<uint8_t, float>(6, 0.4f)); REQUIRE(region.velocityPoints.back() == std::pair<uint8_t, float>(6, 0.4f));
region.parseOpcode({ "amp_velcurve_127", "-1.0" }); region.parseOpcode({ "amp_velcurve_127", "-1.0" });
@ -33,87 +33,98 @@ TEST_CASE("[Direct Region Tests] amp_velcurve")
TEST_CASE("[Direct Region Tests] Release and release key") TEST_CASE("[Direct Region Tests] Release and release key")
{ {
MidiState midiState; MidiState midiState;
Region region { 0, midiState }; Region region { 0 };
region.parseOpcode({ "lokey", "63" }); region.parseOpcode({ "lokey", "63" });
region.parseOpcode({ "hikey", "65" }); region.parseOpcode({ "hikey", "65" });
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.delayedSustainReleases.reserve(config::delayedReleaseVoices);
SECTION("Release key without sustain") SECTION("Release key without sustain")
{ {
region.parseOpcode({ "trigger", "release_key" }); region.parseOpcode({ "trigger", "release_key" });
Layer layer { &region, midiState };
layer.delayedSustainReleases_.reserve(config::delayedReleaseVoices);
midiState.ccEvent(0, 64, 0.0f); midiState.ccEvent(0, 64, 0.0f);
region.registerCC(64, 0.0f); layer.registerCC(64, 0.0f);
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); REQUIRE( !layer.registerNoteOn(63, 0.5f, 0.0f) );
REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) ); REQUIRE( layer.registerNoteOff(63, 0.5f, 0.0f) );
} }
SECTION("Release key with sustain") SECTION("Release key with sustain")
{ {
region.parseOpcode({ "trigger", "release_key" }); region.parseOpcode({ "trigger", "release_key" });
Layer layer { &region, midiState };
layer.delayedSustainReleases_.reserve(config::delayedReleaseVoices);
midiState.ccEvent(0, 64, 1.0f); midiState.ccEvent(0, 64, 1.0f);
region.registerCC(64, 1.0f); layer.registerCC(64, 1.0f);
REQUIRE( !region.registerCC(64, 1.0f) ); REQUIRE( !layer.registerCC(64, 1.0f) );
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); REQUIRE( !layer.registerNoteOn(63, 0.5f, 0.0f) );
REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) ); REQUIRE( layer.registerNoteOff(63, 0.5f, 0.0f) );
} }
SECTION("Release without sustain") SECTION("Release without sustain")
{ {
region.parseOpcode({ "trigger", "release" }); region.parseOpcode({ "trigger", "release" });
Layer layer { &region, midiState };
layer.delayedSustainReleases_.reserve(config::delayedReleaseVoices);
midiState.ccEvent(0, 64, 0.0f); midiState.ccEvent(0, 64, 0.0f);
region.registerCC(64, 0.0f); layer.registerCC(64, 0.0f);
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); REQUIRE( !layer.registerNoteOn(63, 0.5f, 0.0f) );
REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) ); REQUIRE( layer.registerNoteOff(63, 0.5f, 0.0f) );
} }
SECTION("Release with sustain") SECTION("Release with sustain")
{ {
region.parseOpcode({ "trigger", "release" }); region.parseOpcode({ "trigger", "release" });
Layer layer { &region, midiState };
layer.delayedSustainReleases_.reserve(config::delayedReleaseVoices);
midiState.ccEvent(0, 64, 1.0f); midiState.ccEvent(0, 64, 1.0f);
region.registerCC(64, 1.0f); layer.registerCC(64, 1.0f);
midiState.noteOnEvent(0, 63, 0.5f); midiState.noteOnEvent(0, 63, 0.5f);
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); REQUIRE( !layer.registerNoteOn(63, 0.5f, 0.0f) );
REQUIRE( !region.registerNoteOff(63, 0.5f, 0.0f) ); REQUIRE( !layer.registerNoteOff(63, 0.5f, 0.0f) );
REQUIRE( region.delayedSustainReleases.size() == 1 ); REQUIRE( layer.delayedSustainReleases_.size() == 1 );
std::vector<std::pair<int, float>> expected = { std::vector<std::pair<int, float>> expected = {
{ 63, 0.5f } { 63, 0.5f }
}; };
REQUIRE( region.delayedSustainReleases == expected ); REQUIRE( layer.delayedSustainReleases_ == expected );
} }
SECTION("Release with sustain and 2 notes") SECTION("Release with sustain and 2 notes")
{ {
region.parseOpcode({ "trigger", "release" }); region.parseOpcode({ "trigger", "release" });
Layer layer { &region, midiState };
layer.delayedSustainReleases_.reserve(config::delayedReleaseVoices);
midiState.ccEvent(0, 64, 1.0f); midiState.ccEvent(0, 64, 1.0f);
region.registerCC(64, 1.0f); layer.registerCC(64, 1.0f);
midiState.noteOnEvent(0, 63, 0.5f); midiState.noteOnEvent(0, 63, 0.5f);
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); REQUIRE( !layer.registerNoteOn(63, 0.5f, 0.0f) );
midiState.noteOnEvent(0, 64, 0.6f); midiState.noteOnEvent(0, 64, 0.6f);
REQUIRE( !region.registerNoteOn(64, 0.6f, 0.0f) ); REQUIRE( !layer.registerNoteOn(64, 0.6f, 0.0f) );
REQUIRE( !region.registerNoteOff(63, 0.0f, 0.0f) ); REQUIRE( !layer.registerNoteOff(63, 0.0f, 0.0f) );
REQUIRE( !region.registerNoteOff(64, 0.2f, 0.0f) ); REQUIRE( !layer.registerNoteOff(64, 0.2f, 0.0f) );
REQUIRE( region.delayedSustainReleases.size() == 2 ); REQUIRE( layer.delayedSustainReleases_.size() == 2 );
std::vector<std::pair<int, float>> expected = { std::vector<std::pair<int, float>> expected = {
{ 63, 0.5f }, { 63, 0.5f },
{ 64, 0.6f } { 64, 0.6f }
}; };
REQUIRE( region.delayedSustainReleases == expected ); REQUIRE( layer.delayedSustainReleases_ == expected );
} }
SECTION("Release with sustain and 2 notes but 1 outside") SECTION("Release with sustain and 2 notes but 1 outside")
{ {
region.parseOpcode({ "trigger", "release" }); region.parseOpcode({ "trigger", "release" });
Layer layer { &region, midiState };
layer.delayedSustainReleases_.reserve(config::delayedReleaseVoices);
midiState.ccEvent(0, 64, 1.0f); midiState.ccEvent(0, 64, 1.0f);
region.registerCC(64, 1.0f); layer.registerCC(64, 1.0f);
midiState.noteOnEvent(0, 63, 0.5f); midiState.noteOnEvent(0, 63, 0.5f);
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); REQUIRE( !layer.registerNoteOn(63, 0.5f, 0.0f) );
midiState.noteOnEvent(0, 66, 0.6f); midiState.noteOnEvent(0, 66, 0.6f);
REQUIRE( !region.registerNoteOn(66, 0.6f, 0.0f) ); REQUIRE( !layer.registerNoteOn(66, 0.6f, 0.0f) );
REQUIRE( !region.registerNoteOff(63, 0.0f, 0.0f) ); REQUIRE( !layer.registerNoteOff(63, 0.0f, 0.0f) );
REQUIRE( !region.registerNoteOff(66, 0.2f, 0.0f) ); REQUIRE( !layer.registerNoteOff(66, 0.2f, 0.0f) );
REQUIRE( region.delayedSustainReleases.size() == 1 ); REQUIRE( layer.delayedSustainReleases_.size() == 1 );
std::vector<std::pair<int, float>> expected = { std::vector<std::pair<int, float>> expected = {
{ 63, 0.5f } { 63, 0.5f }
}; };
REQUIRE( region.delayedSustainReleases == expected ); REQUIRE( layer.delayedSustainReleases_ == expected );
} }
} }

View file

@ -4,6 +4,7 @@
// license. You should have receive a LICENSE.md file along with the code. // license. You should have receive a LICENSE.md file along with the code.
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#include "sfizz/Layer.h"
#include "sfizz/Region.h" #include "sfizz/Region.h"
#include "sfizz/Synth.h" #include "sfizz/Synth.h"
#include "sfizz/SfzHelpers.h" #include "sfizz/SfzHelpers.h"
@ -14,33 +15,35 @@ using namespace sfz::literals;
TEST_CASE("Region activation", "Region tests") TEST_CASE("Region activation", "Region tests")
{ {
sfz::MidiState midiState; sfz::MidiState midiState;
sfz::Region region { 0, midiState }; sfz::Region region { 0 };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
SECTION("Basic state") SECTION("Basic state")
{ {
region.registerCC(4, 0_norm); sfz::Layer layer { &region, midiState };
REQUIRE(region.isSwitchedOn()); layer.registerCC(4, 0_norm);
REQUIRE(layer.isSwitchedOn());
} }
SECTION("Single CC range") SECTION("Single CC range")
{ {
region.parseOpcode({ "locc4", "56" }); region.parseOpcode({ "locc4", "56" });
region.parseOpcode({ "hicc4", "59" }); region.parseOpcode({ "hicc4", "59" });
region.registerCC(4, 0_norm); sfz::Layer layer { &region, midiState };
REQUIRE(!region.isSwitchedOn()); layer.registerCC(4, 0_norm);
region.registerCC(4, 57_norm); REQUIRE(!layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerCC(4, 57_norm);
region.registerCC(4, 56_norm); REQUIRE(layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerCC(4, 56_norm);
region.registerCC(4, 59_norm); REQUIRE(layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerCC(4, 59_norm);
region.registerCC(4, 43_norm); REQUIRE(layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerCC(4, 43_norm);
region.registerCC(4, 65_norm); REQUIRE(!layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerCC(4, 65_norm);
region.registerCC(6, 57_norm); REQUIRE(!layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerCC(6, 57_norm);
REQUIRE(!layer.isSwitchedOn());
} }
SECTION("Multiple CC ranges") SECTION("Multiple CC ranges")
@ -49,69 +52,73 @@ TEST_CASE("Region activation", "Region tests")
region.parseOpcode({ "hicc4", "59" }); region.parseOpcode({ "hicc4", "59" });
region.parseOpcode({ "locc54", "18" }); region.parseOpcode({ "locc54", "18" });
region.parseOpcode({ "hicc54", "27" }); region.parseOpcode({ "hicc54", "27" });
region.registerCC(4, 0_norm); sfz::Layer layer { &region, midiState };
region.registerCC(54, 0_norm); layer.registerCC(4, 0_norm);
REQUIRE(!region.isSwitchedOn()); layer.registerCC(54, 0_norm);
region.registerCC(4, 57_norm); REQUIRE(!layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerCC(4, 57_norm);
region.registerCC(54, 19_norm); REQUIRE(!layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerCC(54, 19_norm);
region.registerCC(54, 17_norm); REQUIRE(layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerCC(54, 17_norm);
region.registerCC(54, 27_norm); REQUIRE(!layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerCC(54, 27_norm);
region.registerCC(4, 56_norm); REQUIRE(layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerCC(4, 56_norm);
region.registerCC(4, 59_norm); REQUIRE(layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerCC(4, 59_norm);
region.registerCC(54, 2_norm); REQUIRE(layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerCC(54, 2_norm);
region.registerCC(54, 26_norm); REQUIRE(!layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerCC(54, 26_norm);
region.registerCC(4, 65_norm); REQUIRE(layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerCC(4, 65_norm);
REQUIRE(!layer.isSwitchedOn());
} }
SECTION("Bend ranges") SECTION("Bend ranges")
{ {
region.parseOpcode({ "lobend", "56" }); region.parseOpcode({ "lobend", "56" });
region.parseOpcode({ "hibend", "243" }); region.parseOpcode({ "hibend", "243" });
region.registerPitchWheel(0); sfz::Layer layer { &region, midiState };
REQUIRE(!region.isSwitchedOn()); layer.registerPitchWheel(0);
region.registerPitchWheel(sfz::normalizeBend(56)); REQUIRE(!layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerPitchWheel(sfz::normalizeBend(56));
region.registerPitchWheel(sfz::normalizeBend(243)); REQUIRE(layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerPitchWheel(sfz::normalizeBend(243));
region.registerPitchWheel(sfz::normalizeBend(245)); REQUIRE(layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerPitchWheel(sfz::normalizeBend(245));
REQUIRE(!layer.isSwitchedOn());
} }
SECTION("Aftertouch ranges") SECTION("Aftertouch ranges")
{ {
region.parseOpcode({ "lochanaft", "56" }); region.parseOpcode({ "lochanaft", "56" });
region.parseOpcode({ "hichanaft", "68" }); region.parseOpcode({ "hichanaft", "68" });
region.registerAftertouch(sfz::normalize7Bits(0)); sfz::Layer layer { &region, midiState };
REQUIRE(!region.isSwitchedOn()); layer.registerAftertouch(sfz::normalize7Bits(0));
region.registerAftertouch(sfz::normalize7Bits(56)); REQUIRE(!layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerAftertouch(sfz::normalize7Bits(56));
region.registerAftertouch(sfz::normalize7Bits(68)); REQUIRE(layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerAftertouch(sfz::normalize7Bits(68));
region.registerAftertouch(sfz::normalize7Bits(98)); REQUIRE(layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerAftertouch(sfz::normalize7Bits(98));
REQUIRE(!layer.isSwitchedOn());
} }
SECTION("BPM ranges") SECTION("BPM ranges")
{ {
region.parseOpcode({ "lobpm", "56" }); region.parseOpcode({ "lobpm", "56" });
region.parseOpcode({ "hibpm", "68" }); region.parseOpcode({ "hibpm", "68" });
region.registerTempo(2.0f); sfz::Layer layer { &region, midiState };
REQUIRE(!region.isSwitchedOn()); layer.registerTempo(2.0f);
region.registerTempo(0.90f); REQUIRE(!layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerTempo(0.90f);
region.registerTempo(1.01f); REQUIRE(layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerTempo(1.01f);
region.registerTempo(1.1f); REQUIRE(layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerTempo(1.1f);
REQUIRE(!layer.isSwitchedOn());
} }
SECTION("Sequences: length 2, default position") SECTION("Sequences: length 2, default position")
@ -119,61 +126,64 @@ TEST_CASE("Region activation", "Region tests")
region.parseOpcode({ "seq_length", "2" }); region.parseOpcode({ "seq_length", "2" });
region.parseOpcode({ "seq_position", "1" }); region.parseOpcode({ "seq_position", "1" });
region.parseOpcode({ "key", "40" }); region.parseOpcode({ "key", "40" });
REQUIRE(!region.isSwitchedOn()); sfz::Layer layer { &region, midiState };
region.registerNoteOn(40, 64_norm, 0.5f); REQUIRE(!layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerNoteOn(40, 64_norm, 0.5f);
region.registerNoteOff(40, 0_norm, 0.5f); REQUIRE(layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerNoteOff(40, 0_norm, 0.5f);
region.registerNoteOn(40, 64_norm, 0.5f); REQUIRE(layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerNoteOn(40, 64_norm, 0.5f);
region.registerNoteOff(40, 0_norm, 0.5f); REQUIRE(!layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerNoteOff(40, 0_norm, 0.5f);
region.registerNoteOn(40, 64_norm, 0.5f); REQUIRE(!layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerNoteOn(40, 64_norm, 0.5f);
region.registerNoteOff(40, 0_norm, 0.5f); REQUIRE(layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerNoteOff(40, 0_norm, 0.5f);
REQUIRE(layer.isSwitchedOn());
} }
SECTION("Sequences: length 2, position 2") SECTION("Sequences: length 2, position 2")
{ {
region.parseOpcode({ "seq_length", "2" }); region.parseOpcode({ "seq_length", "2" });
region.parseOpcode({ "seq_position", "2" }); region.parseOpcode({ "seq_position", "2" });
region.parseOpcode({ "key", "40" }); region.parseOpcode({ "key", "40" });
REQUIRE(!region.isSwitchedOn()); sfz::Layer layer { &region, midiState };
region.registerNoteOn(40, 64_norm, 0.5f); REQUIRE(!layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerNoteOn(40, 64_norm, 0.5f);
region.registerNoteOff(40, 0_norm, 0.5f); REQUIRE(!layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerNoteOff(40, 0_norm, 0.5f);
region.registerNoteOn(40, 64_norm, 0.5f); REQUIRE(!layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerNoteOn(40, 64_norm, 0.5f);
region.registerNoteOff(40, 0_norm, 0.5f); REQUIRE(layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerNoteOff(40, 0_norm, 0.5f);
region.registerNoteOn(40, 64_norm, 0.5f); REQUIRE(layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerNoteOn(40, 64_norm, 0.5f);
region.registerNoteOff(40, 0_norm, 0.5f); REQUIRE(!layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerNoteOff(40, 0_norm, 0.5f);
REQUIRE(!layer.isSwitchedOn());
} }
SECTION("Sequences: length 3, position 2") SECTION("Sequences: length 3, position 2")
{ {
region.parseOpcode({ "seq_length", "3" }); region.parseOpcode({ "seq_length", "3" });
region.parseOpcode({ "seq_position", "2" }); region.parseOpcode({ "seq_position", "2" });
region.parseOpcode({ "key", "40" }); region.parseOpcode({ "key", "40" });
REQUIRE(!region.isSwitchedOn()); sfz::Layer layer { &region, midiState };
region.registerNoteOn(40, 64_norm, 0.5f); REQUIRE(!layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerNoteOn(40, 64_norm, 0.5f);
region.registerNoteOff(40, 0_norm, 0.5f); REQUIRE(!layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerNoteOff(40, 0_norm, 0.5f);
region.registerNoteOn(40, 64_norm, 0.5f); REQUIRE(!layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerNoteOn(40, 64_norm, 0.5f);
region.registerNoteOff(40, 0_norm, 0.5f); REQUIRE(layer.isSwitchedOn());
REQUIRE(region.isSwitchedOn()); layer.registerNoteOff(40, 0_norm, 0.5f);
region.registerNoteOn(40, 64_norm, 0.5f); REQUIRE(layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerNoteOn(40, 64_norm, 0.5f);
region.registerNoteOff(40, 0_norm, 0.5f); REQUIRE(!layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerNoteOff(40, 0_norm, 0.5f);
region.registerNoteOn(40, 64_norm, 0.5f); REQUIRE(!layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerNoteOn(40, 64_norm, 0.5f);
region.registerNoteOff(40, 0_norm, 0.5f); REQUIRE(!layer.isSwitchedOn());
REQUIRE(!region.isSwitchedOn()); layer.registerNoteOff(40, 0_norm, 0.5f);
REQUIRE(!layer.isSwitchedOn());
} }
} }
@ -341,10 +351,10 @@ TEST_CASE("[Keyswitches] sw_default")
<region> sw_last=40 key=54 sample=*sine <region> sw_last=40 key=54 sample=*sine
)"); )");
REQUIRE( synth.getNumRegions() == 4 ); REQUIRE( synth.getNumRegions() == 4 );
REQUIRE( !synth.getRegionView(0)->isSwitchedOn() ); REQUIRE( !synth.getLayerView(0)->isSwitchedOn() );
REQUIRE( synth.getRegionView(1)->isSwitchedOn() ); REQUIRE( synth.getLayerView(1)->isSwitchedOn() );
REQUIRE( !synth.getRegionView(2)->isSwitchedOn() ); REQUIRE( !synth.getLayerView(2)->isSwitchedOn() );
REQUIRE( synth.getRegionView(3)->isSwitchedOn() ); REQUIRE( synth.getLayerView(3)->isSwitchedOn() );
} }
TEST_CASE("[Keyswitches] sw_default and playing with switches") TEST_CASE("[Keyswitches] sw_default and playing with switches")
@ -358,22 +368,22 @@ TEST_CASE("[Keyswitches] sw_default and playing with switches")
<region> sw_last=40 key=54 sample=*sine <region> sw_last=40 key=54 sample=*sine
)"); )");
REQUIRE( synth.getNumRegions() == 4 ); REQUIRE( synth.getNumRegions() == 4 );
REQUIRE( !synth.getRegionView(0)->isSwitchedOn() ); REQUIRE( !synth.getLayerView(0)->isSwitchedOn() );
REQUIRE( synth.getRegionView(1)->isSwitchedOn() ); REQUIRE( synth.getLayerView(1)->isSwitchedOn() );
REQUIRE( !synth.getRegionView(2)->isSwitchedOn() ); REQUIRE( !synth.getLayerView(2)->isSwitchedOn() );
REQUIRE( synth.getRegionView(3)->isSwitchedOn() ); REQUIRE( synth.getLayerView(3)->isSwitchedOn() );
synth.noteOn(0, 41, 64); synth.noteOn(0, 41, 64);
synth.noteOff(0, 41, 0); synth.noteOff(0, 41, 0);
REQUIRE( synth.getRegionView(0)->isSwitchedOn() ); REQUIRE( synth.getLayerView(0)->isSwitchedOn() );
REQUIRE( !synth.getRegionView(1)->isSwitchedOn() ); REQUIRE( !synth.getLayerView(1)->isSwitchedOn() );
REQUIRE( synth.getRegionView(2)->isSwitchedOn() ); REQUIRE( synth.getLayerView(2)->isSwitchedOn() );
REQUIRE( !synth.getRegionView(3)->isSwitchedOn() ); REQUIRE( !synth.getLayerView(3)->isSwitchedOn() );
synth.noteOn(0, 40, 64); synth.noteOn(0, 40, 64);
synth.noteOff(0, 40, 64); synth.noteOff(0, 40, 64);
REQUIRE( !synth.getRegionView(0)->isSwitchedOn() ); REQUIRE( !synth.getLayerView(0)->isSwitchedOn() );
REQUIRE( synth.getRegionView(1)->isSwitchedOn() ); REQUIRE( synth.getLayerView(1)->isSwitchedOn() );
REQUIRE( !synth.getRegionView(2)->isSwitchedOn() ); REQUIRE( !synth.getLayerView(2)->isSwitchedOn() );
REQUIRE( synth.getRegionView(3)->isSwitchedOn() ); REQUIRE( synth.getLayerView(3)->isSwitchedOn() );
} }
TEST_CASE("[Keyswitches] sw_previous in range") TEST_CASE("[Keyswitches] sw_previous in range")
@ -385,21 +395,21 @@ TEST_CASE("[Keyswitches] sw_previous in range")
// Note: sforzando seems to activate by default if sw_previous is indeed 60, // Note: sforzando seems to activate by default if sw_previous is indeed 60,
// but not any other value. As it does not seem really useful at this point // but not any other value. As it does not seem really useful at this point
// the test assumes that sw_previous regions are disabled by default // the test assumes that sw_previous regions are disabled by default
REQUIRE(!synth.getRegionView(0)->isSwitchedOn()); REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
synth.noteOn(0, 51, 64); synth.noteOn(0, 51, 64);
REQUIRE(synth.getNumActiveVoices() == 0); REQUIRE(synth.getNumActiveVoices() == 0);
synth.noteOn(0, 60, 64); synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices() == 0); REQUIRE(synth.getNumActiveVoices() == 0);
REQUIRE(synth.getRegionView(0)->isSwitchedOn()); REQUIRE(synth.getLayerView(0)->isSwitchedOn());
synth.noteOn(0, 51, 64); synth.noteOn(0, 51, 64);
REQUIRE(synth.getNumActiveVoices() == 1); REQUIRE(synth.getNumActiveVoices() == 1);
REQUIRE(!synth.getRegionView(0)->isSwitchedOn()); REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
synth.noteOn(0, 60, 64); synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices() == 1); REQUIRE(synth.getNumActiveVoices() == 1);
REQUIRE(synth.getRegionView(0)->isSwitchedOn()); REQUIRE(synth.getLayerView(0)->isSwitchedOn());
synth.noteOn(0, 60, 64); synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices() == 2); REQUIRE(synth.getNumActiveVoices() == 2);
REQUIRE(synth.getRegionView(0)->isSwitchedOn()); REQUIRE(synth.getLayerView(0)->isSwitchedOn());
} }
TEST_CASE("[Keyswitches] sw_previous out of range") TEST_CASE("[Keyswitches] sw_previous out of range")
@ -409,20 +419,20 @@ TEST_CASE("[Keyswitches] sw_previous out of range")
synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_previous.sfz", R"( synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_previous.sfz", R"(
<region> sample=*saw sw_previous=60 lokey=50 hikey=55 <region> sample=*saw sw_previous=60 lokey=50 hikey=55
)"); )");
REQUIRE(!synth.getRegionView(0)->isSwitchedOn()); REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
synth.noteOn(0, 51, 64); synth.noteOn(0, 51, 64);
REQUIRE(synth.getNumActiveVoices() == 0); REQUIRE(synth.getNumActiveVoices() == 0);
synth.noteOn(0, 60, 64); synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices() == 0); REQUIRE(synth.getNumActiveVoices() == 0);
REQUIRE(synth.getRegionView(0)->isSwitchedOn()); REQUIRE(synth.getLayerView(0)->isSwitchedOn());
synth.noteOn(0, 51, 64); synth.noteOn(0, 51, 64);
REQUIRE(synth.getNumActiveVoices() == 1); REQUIRE(synth.getNumActiveVoices() == 1);
REQUIRE(!synth.getRegionView(0)->isSwitchedOn()); REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
synth.noteOn(0, 60, 64); synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices() == 1); REQUIRE(synth.getNumActiveVoices() == 1);
REQUIRE(synth.getRegionView(0)->isSwitchedOn()); REQUIRE(synth.getLayerView(0)->isSwitchedOn());
synth.noteOn(0, 61, 64); synth.noteOn(0, 61, 64);
REQUIRE(!synth.getRegionView(0)->isSwitchedOn()); REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
} }
TEST_CASE("[Keyswitches] sw_lolast and sw_hilast") TEST_CASE("[Keyswitches] sw_lolast and sw_hilast")
@ -432,29 +442,29 @@ TEST_CASE("[Keyswitches] sw_lolast and sw_hilast")
<region> sw_lolast=57 sw_hilast=59 key=70 sample=*saw <region> sw_lolast=57 sw_hilast=59 key=70 sample=*saw
<region> sw_lolast=60 sw_hilast=62 key=72 sample=*sine <region> sw_lolast=60 sw_hilast=62 key=72 sample=*sine
)"); )");
REQUIRE(!synth.getRegionView(0)->isSwitchedOn()); REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getRegionView(1)->isSwitchedOn()); REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 51, 64); synth.noteOn(0, 51, 64);
REQUIRE(!synth.getRegionView(0)->isSwitchedOn()); REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getRegionView(1)->isSwitchedOn()); REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 57, 64); synth.noteOn(0, 57, 64);
REQUIRE(synth.getRegionView(0)->isSwitchedOn()); REQUIRE(synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getRegionView(1)->isSwitchedOn()); REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 60, 64); synth.noteOn(0, 60, 64);
REQUIRE(!synth.getRegionView(0)->isSwitchedOn()); REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
REQUIRE(synth.getRegionView(1)->isSwitchedOn()); REQUIRE(synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 58, 64); synth.noteOn(0, 58, 64);
REQUIRE(synth.getRegionView(0)->isSwitchedOn()); REQUIRE(synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getRegionView(1)->isSwitchedOn()); REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 61, 64); synth.noteOn(0, 61, 64);
REQUIRE(!synth.getRegionView(0)->isSwitchedOn()); REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
REQUIRE(synth.getRegionView(1)->isSwitchedOn()); REQUIRE(synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 59, 64); synth.noteOn(0, 59, 64);
REQUIRE(synth.getRegionView(0)->isSwitchedOn()); REQUIRE(synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getRegionView(1)->isSwitchedOn()); REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 62, 64); synth.noteOn(0, 62, 64);
REQUIRE(!synth.getRegionView(0)->isSwitchedOn()); REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
REQUIRE(synth.getRegionView(1)->isSwitchedOn()); REQUIRE(synth.getLayerView(1)->isSwitchedOn());
} }
TEST_CASE("[Keyswitches] sw_lolast and sw_hilast with sw_last") TEST_CASE("[Keyswitches] sw_lolast and sw_hilast with sw_last")
@ -464,26 +474,26 @@ TEST_CASE("[Keyswitches] sw_lolast and sw_hilast with sw_last")
<region> sw_last=40 sw_lolast=57 sw_hilast=59 key=70 sample=*saw <region> sw_last=40 sw_lolast=57 sw_hilast=59 key=70 sample=*saw
<region> sw_lolast=60 sw_hilast=62 sw_last=41 key=72 sample=*sine <region> sw_lolast=60 sw_hilast=62 sw_last=41 key=72 sample=*sine
)"); )");
REQUIRE(!synth.getRegionView(0)->isSwitchedOn()); REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getRegionView(1)->isSwitchedOn()); REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 40, 64); synth.noteOn(0, 40, 64);
REQUIRE(!synth.getRegionView(0)->isSwitchedOn()); REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getRegionView(1)->isSwitchedOn()); REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 41, 64); synth.noteOn(0, 41, 64);
REQUIRE(!synth.getRegionView(0)->isSwitchedOn()); REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getRegionView(1)->isSwitchedOn()); REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 57, 64); synth.noteOn(0, 57, 64);
REQUIRE(synth.getRegionView(0)->isSwitchedOn()); REQUIRE(synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getRegionView(1)->isSwitchedOn()); REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 41, 64); synth.noteOn(0, 41, 64);
REQUIRE(synth.getRegionView(0)->isSwitchedOn()); REQUIRE(synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getRegionView(1)->isSwitchedOn()); REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 60, 64); synth.noteOn(0, 60, 64);
REQUIRE(!synth.getRegionView(0)->isSwitchedOn()); REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
REQUIRE(synth.getRegionView(1)->isSwitchedOn()); REQUIRE(synth.getLayerView(1)->isSwitchedOn());
synth.noteOn(0, 40, 64); synth.noteOn(0, 40, 64);
REQUIRE(!synth.getRegionView(0)->isSwitchedOn()); REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
REQUIRE(synth.getRegionView(1)->isSwitchedOn()); REQUIRE(synth.getLayerView(1)->isSwitchedOn());
} }
TEST_CASE("[Keyswitches] sw_lolast and sw_hilast with sw_default") TEST_CASE("[Keyswitches] sw_lolast and sw_hilast with sw_default")
@ -494,8 +504,8 @@ TEST_CASE("[Keyswitches] sw_lolast and sw_hilast with sw_default")
<region> sw_lolast=57 sw_hilast=59 key=70 sample=*saw <region> sw_lolast=57 sw_hilast=59 key=70 sample=*saw
<region> sw_lolast=60 sw_hilast=62 key=72 sample=*sine <region> sw_lolast=60 sw_hilast=62 key=72 sample=*sine
)"); )");
REQUIRE(synth.getRegionView(0)->isSwitchedOn()); REQUIRE(synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getRegionView(1)->isSwitchedOn()); REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
} }
TEST_CASE("[Keyswitches] Multiple sw_default") TEST_CASE("[Keyswitches] Multiple sw_default")
@ -509,10 +519,10 @@ TEST_CASE("[Keyswitches] Multiple sw_default")
<master> sw_default=59 <master> sw_default=59
<region> sw_last=62 key=73 sample=*saw <region> sw_last=62 key=73 sample=*saw
)"); )");
REQUIRE(!synth.getRegionView(0)->isSwitchedOn()); REQUIRE(!synth.getLayerView(0)->isSwitchedOn());
// Only the last one is taken into account // Only the last one is taken into account
REQUIRE(synth.getRegionView(1)->isSwitchedOn()); REQUIRE(synth.getLayerView(1)->isSwitchedOn());
REQUIRE(!synth.getRegionView(2)->isSwitchedOn()); REQUIRE(!synth.getLayerView(2)->isSwitchedOn());
} }
TEST_CASE("[Keyswitches] Multiple sw_default, in region") TEST_CASE("[Keyswitches] Multiple sw_default, in region")
@ -523,7 +533,7 @@ TEST_CASE("[Keyswitches] Multiple sw_default, in region")
<region> sw_last=58 key=70 sample=*saw <region> sw_last=58 key=70 sample=*saw
<region> sw_default=58 sw_last=59 key=72 sample=*saw <region> sw_default=58 sw_last=59 key=72 sample=*saw
)"); )");
REQUIRE(synth.getRegionView(0)->isSwitchedOn()); REQUIRE(synth.getLayerView(0)->isSwitchedOn());
REQUIRE(!synth.getRegionView(1)->isSwitchedOn()); REQUIRE(!synth.getLayerView(1)->isSwitchedOn());
} }

View file

@ -7,6 +7,7 @@
#include "TestHelpers.h" #include "TestHelpers.h"
#include "sfizz/Synth.h" #include "sfizz/Synth.h"
#include "sfizz/Region.h" #include "sfizz/Region.h"
#include "sfizz/Layer.h"
#include "sfizz/SfzHelpers.h" #include "sfizz/SfzHelpers.h"
#include "catch2/catch.hpp" #include "catch2/catch.hpp"
using namespace Catch::literals; using namespace Catch::literals;
@ -16,50 +17,54 @@ using namespace sfz;
TEST_CASE("Basic triggers", "Region triggers") TEST_CASE("Basic triggers", "Region triggers")
{ {
MidiState midiState; MidiState midiState;
Region region { 0, midiState }; Region region { 0 };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
SECTION("key") SECTION("key")
{ {
region.parseOpcode({ "key", "40" }); region.parseOpcode({ "key", "40" });
REQUIRE(region.registerNoteOn(40, 64_norm, 0.5f)); Layer layer { &region, midiState };
REQUIRE(!region.registerNoteOff(40, 64_norm, 0.5f)); REQUIRE(layer.registerNoteOn(40, 64_norm, 0.5f));
REQUIRE(!region.registerNoteOn(41, 64_norm, 0.5f)); REQUIRE(!layer.registerNoteOff(40, 64_norm, 0.5f));
REQUIRE(!region.registerCC(63, 64_norm)); REQUIRE(!layer.registerNoteOn(41, 64_norm, 0.5f));
REQUIRE(!layer.registerCC(63, 64_norm));
} }
SECTION("lokey and hikey") SECTION("lokey and hikey")
{ {
region.parseOpcode({ "lokey", "40" }); region.parseOpcode({ "lokey", "40" });
region.parseOpcode({ "hikey", "42" }); region.parseOpcode({ "hikey", "42" });
REQUIRE(!region.registerNoteOn(39, 64_norm, 0.5f)); Layer layer { &region, midiState };
REQUIRE(region.registerNoteOn(40, 64_norm, 0.5f)); REQUIRE(!layer.registerNoteOn(39, 64_norm, 0.5f));
REQUIRE(!region.registerNoteOff(40, 64_norm, 0.5f)); REQUIRE(layer.registerNoteOn(40, 64_norm, 0.5f));
REQUIRE(region.registerNoteOn(41, 64_norm, 0.5f)); REQUIRE(!layer.registerNoteOff(40, 64_norm, 0.5f));
REQUIRE(region.registerNoteOn(42, 64_norm, 0.5f)); REQUIRE(layer.registerNoteOn(41, 64_norm, 0.5f));
REQUIRE(!region.registerNoteOn(43, 64_norm, 0.5f)); REQUIRE(layer.registerNoteOn(42, 64_norm, 0.5f));
REQUIRE(!region.registerNoteOff(42, 64_norm, 0.5f)); REQUIRE(!layer.registerNoteOn(43, 64_norm, 0.5f));
REQUIRE(!region.registerNoteOff(42, 64_norm, 0.5f)); REQUIRE(!layer.registerNoteOff(42, 64_norm, 0.5f));
REQUIRE(!region.registerCC(63, 64_norm)); REQUIRE(!layer.registerNoteOff(42, 64_norm, 0.5f));
REQUIRE(!layer.registerCC(63, 64_norm));
} }
SECTION("key and release trigger") SECTION("key and release trigger")
{ {
region.parseOpcode({ "key", "40" }); region.parseOpcode({ "key", "40" });
region.parseOpcode({ "trigger", "release" }); region.parseOpcode({ "trigger", "release" });
REQUIRE(!region.registerNoteOn(40, 64_norm, 0.5f)); Layer layer { &region, midiState };
REQUIRE(region.registerNoteOff(40, 64_norm, 0.5f)); REQUIRE(!layer.registerNoteOn(40, 64_norm, 0.5f));
REQUIRE(!region.registerNoteOn(41, 64_norm, 0.5f)); REQUIRE(layer.registerNoteOff(40, 64_norm, 0.5f));
REQUIRE(!region.registerNoteOff(41, 64_norm, 0.5f)); REQUIRE(!layer.registerNoteOn(41, 64_norm, 0.5f));
REQUIRE(!region.registerCC(63, 64_norm)); REQUIRE(!layer.registerNoteOff(41, 64_norm, 0.5f));
REQUIRE(!layer.registerCC(63, 64_norm));
} }
SECTION("key and release_key trigger") SECTION("key and release_key trigger")
{ {
region.parseOpcode({ "key", "40" }); region.parseOpcode({ "key", "40" });
region.parseOpcode({ "trigger", "release_key" }); region.parseOpcode({ "trigger", "release_key" });
REQUIRE(!region.registerNoteOn(40, 64_norm, 0.5f)); Layer layer { &region, midiState };
REQUIRE(region.registerNoteOff(40, 64_norm, 0.5f)); REQUIRE(!layer.registerNoteOn(40, 64_norm, 0.5f));
REQUIRE(!region.registerNoteOn(41, 64_norm, 0.5f)); REQUIRE(layer.registerNoteOff(40, 64_norm, 0.5f));
REQUIRE(!region.registerNoteOff(41, 64_norm, 0.5f)); REQUIRE(!layer.registerNoteOn(41, 64_norm, 0.5f));
REQUIRE(!region.registerCC(63, 64_norm)); REQUIRE(!layer.registerNoteOff(41, 64_norm, 0.5f));
REQUIRE(!layer.registerCC(63, 64_norm));
} }
// TODO: first and legato triggers // TODO: first and legato triggers
SECTION("lovel and hivel") SECTION("lovel and hivel")
@ -67,11 +72,12 @@ TEST_CASE("Basic triggers", "Region triggers")
region.parseOpcode({ "key", "40" }); region.parseOpcode({ "key", "40" });
region.parseOpcode({ "lovel", "60" }); region.parseOpcode({ "lovel", "60" });
region.parseOpcode({ "hivel", "70" }); region.parseOpcode({ "hivel", "70" });
REQUIRE(region.registerNoteOn(40, 64_norm, 0.5f)); Layer layer { &region, midiState };
REQUIRE(region.registerNoteOn(40, 60_norm, 0.5f)); REQUIRE(layer.registerNoteOn(40, 64_norm, 0.5f));
REQUIRE(region.registerNoteOn(40, 70_norm, 0.5f)); REQUIRE(layer.registerNoteOn(40, 60_norm, 0.5f));
REQUIRE(!region.registerNoteOn(41, 71_norm, 0.5f)); REQUIRE(layer.registerNoteOn(40, 70_norm, 0.5f));
REQUIRE(!region.registerNoteOn(41, 59_norm, 0.5f)); REQUIRE(!layer.registerNoteOn(41, 71_norm, 0.5f));
REQUIRE(!layer.registerNoteOn(41, 59_norm, 0.5f));
} }
SECTION("lorand and hirand") SECTION("lorand and hirand")
@ -79,54 +85,63 @@ TEST_CASE("Basic triggers", "Region triggers")
region.parseOpcode({ "key", "40" }); region.parseOpcode({ "key", "40" });
region.parseOpcode({ "lorand", "0.35" }); region.parseOpcode({ "lorand", "0.35" });
region.parseOpcode({ "hirand", "0.40" }); region.parseOpcode({ "hirand", "0.40" });
REQUIRE(!region.registerNoteOn(40, 64_norm, 0.34f)); Layer layer { &region, midiState };
REQUIRE(region.registerNoteOn(40, 64_norm, 0.35f)); REQUIRE(!layer.registerNoteOn(40, 64_norm, 0.34f));
REQUIRE(region.registerNoteOn(40, 64_norm, 0.36f)); REQUIRE(layer.registerNoteOn(40, 64_norm, 0.35f));
REQUIRE(region.registerNoteOn(40, 64_norm, 0.37f)); REQUIRE(layer.registerNoteOn(40, 64_norm, 0.36f));
REQUIRE(region.registerNoteOn(40, 64_norm, 0.38f)); REQUIRE(layer.registerNoteOn(40, 64_norm, 0.37f));
REQUIRE(region.registerNoteOn(40, 64_norm, 0.39f)); REQUIRE(layer.registerNoteOn(40, 64_norm, 0.38f));
REQUIRE(!region.registerNoteOn(40, 64_norm, 0.40f)); REQUIRE(layer.registerNoteOn(40, 64_norm, 0.39f));
REQUIRE(!region.registerNoteOn(40, 64_norm, 0.41f)); REQUIRE(!layer.registerNoteOn(40, 64_norm, 0.40f));
REQUIRE(!layer.registerNoteOn(40, 64_norm, 0.41f));
} }
SECTION("lorand and hirand on 1.0f") SECTION("lorand and hirand on 1.0f")
{ {
region.parseOpcode({ "key", "40" }); region.parseOpcode({ "key", "40" });
region.parseOpcode({ "lorand", "0.35" }); region.parseOpcode({ "lorand", "0.35" });
REQUIRE(!region.registerNoteOn(40, 64_norm, 0.34f)); Layer layer { &region, midiState };
REQUIRE(region.registerNoteOn(40, 64_norm, 0.35f)); REQUIRE(!layer.registerNoteOn(40, 64_norm, 0.34f));
REQUIRE(region.registerNoteOn(40, 64_norm, 1.0f)); REQUIRE(layer.registerNoteOn(40, 64_norm, 0.35f));
REQUIRE(layer.registerNoteOn(40, 64_norm, 1.0f));
} }
SECTION("Disable key trigger") SECTION("Disable key trigger")
{ {
region.parseOpcode({ "key", "40" }); region.parseOpcode({ "key", "40" });
REQUIRE(region.registerNoteOn(40, 64_norm, 1.0f)); Layer layer1 { &region, midiState };
REQUIRE(layer1.registerNoteOn(40, 64_norm, 1.0f));
region.parseOpcode({ "hikey", "-1" }); region.parseOpcode({ "hikey", "-1" });
REQUIRE(!region.registerNoteOn(40, 64_norm, 1.0f)); Layer layer2 { &region, midiState };
REQUIRE(!layer2.registerNoteOn(40, 64_norm, 1.0f));
region.parseOpcode({ "hikey", "40" }); region.parseOpcode({ "hikey", "40" });
REQUIRE(region.registerNoteOn(40, 64_norm, 1.0f)); Layer layer3 { &region, midiState };
REQUIRE(layer3.registerNoteOn(40, 64_norm, 1.0f));
Layer layer4 { &region, midiState };
region.parseOpcode({ "key", "-1" }); region.parseOpcode({ "key", "-1" });
REQUIRE(!region.registerNoteOn(40, 64_norm, 1.0f)); REQUIRE(!layer4.registerNoteOn(40, 64_norm, 1.0f));
Layer layer5 { &region, midiState };
region.parseOpcode({ "key", "40" }); region.parseOpcode({ "key", "40" });
REQUIRE(region.registerNoteOn(40, 64_norm, 1.0f)); REQUIRE(layer5.registerNoteOn(40, 64_norm, 1.0f));
} }
SECTION("on_loccN, on_hiccN") SECTION("on_loccN, on_hiccN")
{ {
region.parseOpcode({ "on_locc47", "64" }); region.parseOpcode({ "on_locc47", "64" });
region.parseOpcode({ "on_hicc47", "68" }); region.parseOpcode({ "on_hicc47", "68" });
REQUIRE(!region.registerCC(47, 63_norm)); Layer layer1 { &region, midiState };
REQUIRE(region.registerCC(47, 64_norm)); REQUIRE(!layer1.registerCC(47, 63_norm));
REQUIRE(region.registerCC(47, 65_norm)); REQUIRE(layer1.registerCC(47, 64_norm));
REQUIRE(layer1.registerCC(47, 65_norm));
region.parseOpcode({ "hikey", "-1" }); region.parseOpcode({ "hikey", "-1" });
REQUIRE(region.registerCC(47, 64_norm)); Layer layer2 { &region, midiState };
REQUIRE(region.registerCC(47, 65_norm)); REQUIRE(layer2.registerCC(47, 64_norm));
REQUIRE(region.registerCC(47, 66_norm)); REQUIRE(layer2.registerCC(47, 65_norm));
REQUIRE(region.registerCC(47, 67_norm)); REQUIRE(layer2.registerCC(47, 66_norm));
REQUIRE(region.registerCC(47, 68_norm)); REQUIRE(layer2.registerCC(47, 67_norm));
REQUIRE(!region.registerCC(47, 69_norm)); REQUIRE(layer2.registerCC(47, 68_norm));
REQUIRE(!region.registerCC(40, 64_norm)); REQUIRE(!layer2.registerCC(47, 69_norm));
REQUIRE(!layer2.registerCC(40, 64_norm));
} }
SECTION("on_loccN does not disable key triggering") SECTION("on_loccN does not disable key triggering")
@ -134,10 +149,11 @@ TEST_CASE("Basic triggers", "Region triggers")
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "on_locc1", "127" }); region.parseOpcode({ "on_locc1", "127" });
region.parseOpcode({ "on_hicc1", "127" }); region.parseOpcode({ "on_hicc1", "127" });
REQUIRE(!region.registerCC(1, 126_norm)); Layer layer { &region, midiState };
REQUIRE(!region.registerCC(2, 127_norm)); REQUIRE(!layer.registerCC(1, 126_norm));
REQUIRE(region.registerCC(1, 127_norm)); REQUIRE(!layer.registerCC(2, 127_norm));
REQUIRE(region.registerNoteOn(64, 127_norm, 0.5f)); REQUIRE(layer.registerCC(1, 127_norm));
REQUIRE(layer.registerNoteOn(64, 127_norm, 0.5f));
} }
SECTION("on_loccN does not disable key triggering, but adding key=-1 does") SECTION("on_loccN does not disable key triggering, but adding key=-1 does")
@ -146,9 +162,10 @@ TEST_CASE("Basic triggers", "Region triggers")
region.parseOpcode({ "on_locc1", "127" }); region.parseOpcode({ "on_locc1", "127" });
region.parseOpcode({ "on_hicc1", "127" }); region.parseOpcode({ "on_hicc1", "127" });
region.parseOpcode({ "key", "-1" }); region.parseOpcode({ "key", "-1" });
REQUIRE(!region.registerCC(1, 126_norm)); Layer layer { &region, midiState };
REQUIRE(region.registerCC(1, 127_norm)); REQUIRE(!layer.registerCC(1, 126_norm));
REQUIRE(!region.registerNoteOn(64, 127_norm, 0.5f)); REQUIRE(layer.registerCC(1, 127_norm));
REQUIRE(!layer.registerNoteOn(64, 127_norm, 0.5f));
} }
SECTION("on_loccN does not disable key triggering, but adding hikey=-1 does") SECTION("on_loccN does not disable key triggering, but adding hikey=-1 does")
@ -157,33 +174,35 @@ TEST_CASE("Basic triggers", "Region triggers")
region.parseOpcode({ "on_locc1", "127" }); region.parseOpcode({ "on_locc1", "127" });
region.parseOpcode({ "on_hicc1", "127" }); region.parseOpcode({ "on_hicc1", "127" });
region.parseOpcode({ "hikey", "-1" }); region.parseOpcode({ "hikey", "-1" });
REQUIRE(!region.registerCC(1, 126_norm)); Layer layer { &region, midiState };
REQUIRE(!region.registerCC(2, 127_norm)); REQUIRE(!layer.registerCC(1, 126_norm));
REQUIRE(region.registerCC(1, 127_norm)); REQUIRE(!layer.registerCC(2, 127_norm));
REQUIRE(!region.registerNoteOn(64, 127_norm, 0.5f)); REQUIRE(layer.registerCC(1, 127_norm));
REQUIRE(!layer.registerNoteOn(64, 127_norm, 0.5f));
} }
} }
TEST_CASE("Legato triggers", "Region triggers") TEST_CASE("Legato triggers", "Region triggers")
{ {
MidiState midiState; MidiState midiState;
Region region { 0, midiState }; Region region { 0 };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
SECTION("First note playing") SECTION("First note playing")
{ {
region.parseOpcode({ "lokey", "40" }); region.parseOpcode({ "lokey", "40" });
region.parseOpcode({ "hikey", "50" }); region.parseOpcode({ "hikey", "50" });
region.parseOpcode({ "trigger", "first" }); region.parseOpcode({ "trigger", "first" });
Layer layer { &region, midiState };
midiState.noteOnEvent(0, 40, 64_norm); midiState.noteOnEvent(0, 40, 64_norm);
REQUIRE(region.registerNoteOn(40, 64_norm, 0.5f)); REQUIRE(layer.registerNoteOn(40, 64_norm, 0.5f));
midiState.noteOnEvent(0, 41, 64_norm); midiState.noteOnEvent(0, 41, 64_norm);
REQUIRE(!region.registerNoteOn(41, 64_norm, 0.5f)); REQUIRE(!layer.registerNoteOn(41, 64_norm, 0.5f));
midiState.noteOffEvent(0, 40, 0_norm); midiState.noteOffEvent(0, 40, 0_norm);
region.registerNoteOff(40, 0_norm, 0.5f); layer.registerNoteOff(40, 0_norm, 0.5f);
midiState.noteOffEvent(0, 41, 0_norm); midiState.noteOffEvent(0, 41, 0_norm);
region.registerNoteOff(41, 0_norm, 0.5f); layer.registerNoteOff(41, 0_norm, 0.5f);
midiState.noteOnEvent(0, 42, 64_norm); midiState.noteOnEvent(0, 42, 64_norm);
REQUIRE(region.registerNoteOn(42, 64_norm, 0.5f)); REQUIRE(layer.registerNoteOn(42, 64_norm, 0.5f));
} }
SECTION("Second note playing") SECTION("Second note playing")
@ -191,16 +210,17 @@ TEST_CASE("Legato triggers", "Region triggers")
region.parseOpcode({ "lokey", "40" }); region.parseOpcode({ "lokey", "40" });
region.parseOpcode({ "hikey", "50" }); region.parseOpcode({ "hikey", "50" });
region.parseOpcode({ "trigger", "legato" }); region.parseOpcode({ "trigger", "legato" });
Layer layer { &region, midiState };
midiState.noteOnEvent(0, 40, 64_norm); midiState.noteOnEvent(0, 40, 64_norm);
REQUIRE(!region.registerNoteOn(40, 64_norm, 0.5f)); REQUIRE(!layer.registerNoteOn(40, 64_norm, 0.5f));
midiState.noteOnEvent(0, 41, 64_norm); midiState.noteOnEvent(0, 41, 64_norm);
REQUIRE(region.registerNoteOn(41, 64_norm, 0.5f)); REQUIRE(layer.registerNoteOn(41, 64_norm, 0.5f));
midiState.noteOffEvent(0, 40, 64_norm); midiState.noteOffEvent(0, 40, 64_norm);
region.registerNoteOff(40, 0_norm, 0.5f); layer.registerNoteOff(40, 0_norm, 0.5f);
midiState.noteOffEvent(0, 41, 64_norm); midiState.noteOffEvent(0, 41, 64_norm);
region.registerNoteOff(41, 0_norm, 0.5f); layer.registerNoteOff(41, 0_norm, 0.5f);
midiState.noteOnEvent(0, 42, 64_norm); midiState.noteOnEvent(0, 42, 64_norm);
REQUIRE(!region.registerNoteOn(42, 64_norm, 0.5f)); REQUIRE(!layer.registerNoteOn(42, 64_norm, 0.5f));
} }
} }

View file

@ -18,8 +18,7 @@ constexpr int numRandomTests { 64 };
TEST_CASE("[Region] Crossfade in on key") TEST_CASE("[Region] Crossfade in on key")
{ {
MidiState midiState; Region region { 0 };
Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfin_lokey", "1" }); region.parseOpcode({ "xfin_lokey", "1" });
region.parseOpcode({ "xfin_hikey", "3" }); region.parseOpcode({ "xfin_hikey", "3" });
@ -30,8 +29,7 @@ TEST_CASE("[Region] Crossfade in on key")
TEST_CASE("[Region] Crossfade in on key - 2") TEST_CASE("[Region] Crossfade in on key - 2")
{ {
MidiState midiState; Region region { 0 };
Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfin_lokey", "1" }); region.parseOpcode({ "xfin_lokey", "1" });
region.parseOpcode({ "xfin_hikey", "5" }); region.parseOpcode({ "xfin_hikey", "5" });
@ -45,8 +43,7 @@ TEST_CASE("[Region] Crossfade in on key - 2")
TEST_CASE("[Region] Crossfade in on key - gain") TEST_CASE("[Region] Crossfade in on key - gain")
{ {
MidiState midiState; Region region { 0 };
Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfin_lokey", "1" }); region.parseOpcode({ "xfin_lokey", "1" });
region.parseOpcode({ "xfin_hikey", "5" }); region.parseOpcode({ "xfin_hikey", "5" });
@ -60,8 +57,7 @@ TEST_CASE("[Region] Crossfade in on key - gain")
TEST_CASE("[Region] Crossfade out on key") TEST_CASE("[Region] Crossfade out on key")
{ {
MidiState midiState; Region region { 0 };
Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfout_lokey", "51" }); region.parseOpcode({ "xfout_lokey", "51" });
region.parseOpcode({ "xfout_hikey", "55" }); region.parseOpcode({ "xfout_hikey", "55" });
@ -76,8 +72,7 @@ TEST_CASE("[Region] Crossfade out on key")
TEST_CASE("[Region] Crossfade out on key - gain") TEST_CASE("[Region] Crossfade out on key - gain")
{ {
MidiState midiState; Region region { 0 };
Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfout_lokey", "51" }); region.parseOpcode({ "xfout_lokey", "51" });
region.parseOpcode({ "xfout_hikey", "55" }); region.parseOpcode({ "xfout_hikey", "55" });
@ -93,8 +88,7 @@ TEST_CASE("[Region] Crossfade out on key - gain")
TEST_CASE("[Region] Crossfade in on velocity") TEST_CASE("[Region] Crossfade in on velocity")
{ {
MidiState midiState; Region region { 0 };
Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfin_lovel", "20" }); region.parseOpcode({ "xfin_lovel", "20" });
region.parseOpcode({ "xfin_hivel", "24" }); region.parseOpcode({ "xfin_hivel", "24" });
@ -110,8 +104,7 @@ TEST_CASE("[Region] Crossfade in on velocity")
TEST_CASE("[Region] Crossfade in on vel - gain") TEST_CASE("[Region] Crossfade in on vel - gain")
{ {
MidiState midiState; Region region { 0 };
Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfin_lovel", "20" }); region.parseOpcode({ "xfin_lovel", "20" });
region.parseOpcode({ "xfin_hivel", "24" }); region.parseOpcode({ "xfin_hivel", "24" });
@ -128,8 +121,7 @@ TEST_CASE("[Region] Crossfade in on vel - gain")
TEST_CASE("[Region] Crossfade out on vel") TEST_CASE("[Region] Crossfade out on vel")
{ {
MidiState midiState; Region region { 0 };
Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfout_lovel", "51" }); region.parseOpcode({ "xfout_lovel", "51" });
region.parseOpcode({ "xfout_hivel", "55" }); region.parseOpcode({ "xfout_hivel", "55" });
@ -145,8 +137,7 @@ TEST_CASE("[Region] Crossfade out on vel")
TEST_CASE("[Region] Crossfade out on vel - gain") TEST_CASE("[Region] Crossfade out on vel - gain")
{ {
MidiState midiState; Region region { 0 };
Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfout_lovel", "51" }); region.parseOpcode({ "xfout_lovel", "51" });
region.parseOpcode({ "xfout_hivel", "55" }); region.parseOpcode({ "xfout_hivel", "55" });
@ -164,104 +155,103 @@ TEST_CASE("[Region] Crossfade out on vel - gain")
TEST_CASE("[Region] Crossfade in on CC") TEST_CASE("[Region] Crossfade in on CC")
{ {
MidiState midiState; MidiState midiState;
Region region { 0, midiState }; Region region { 0 };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfin_locc24", "20" }); region.parseOpcode({ "xfin_locc24", "20" });
region.parseOpcode({ "xfin_hicc24", "24" }); region.parseOpcode({ "xfin_hicc24", "24" });
region.parseOpcode({ "amp_veltrack", "0" }); region.parseOpcode({ "amp_veltrack", "0" });
midiState.ccEvent(0, 24, 19_norm); midiState.ccEvent(0, 24, 19_norm);
REQUIRE(region.getCrossfadeGain() == 0.0_a); REQUIRE(region.getCrossfadeGain(midiState) == 0.0_a);
midiState.ccEvent(0, 24, 20_norm); midiState.ccEvent(0, 24, 20_norm);
REQUIRE(region.getCrossfadeGain() == 0.0_a); REQUIRE(region.getCrossfadeGain(midiState) == 0.0_a);
midiState.ccEvent(0, 24, 21_norm); midiState.ccEvent(0, 24, 21_norm);
REQUIRE(region.getCrossfadeGain() == 0.5_a); REQUIRE(region.getCrossfadeGain(midiState) == 0.5_a);
midiState.ccEvent(0, 24, 22_norm); midiState.ccEvent(0, 24, 22_norm);
REQUIRE(region.getCrossfadeGain() == 0.70711_a); REQUIRE(region.getCrossfadeGain(midiState) == 0.70711_a);
midiState.ccEvent(0, 24, 23_norm); midiState.ccEvent(0, 24, 23_norm);
REQUIRE(region.getCrossfadeGain() == 0.86603_a); REQUIRE(region.getCrossfadeGain(midiState) == 0.86603_a);
midiState.ccEvent(0, 24, 24_norm); midiState.ccEvent(0, 24, 24_norm);
REQUIRE(region.getCrossfadeGain() == 1.0_a); REQUIRE(region.getCrossfadeGain(midiState) == 1.0_a);
midiState.ccEvent(0, 24, 25_norm); midiState.ccEvent(0, 24, 25_norm);
REQUIRE(region.getCrossfadeGain() == 1.0_a); REQUIRE(region.getCrossfadeGain(midiState) == 1.0_a);
} }
TEST_CASE("[Region] Crossfade in on CC - gain") TEST_CASE("[Region] Crossfade in on CC - gain")
{ {
MidiState midiState; MidiState midiState;
Region region { 0, midiState }; Region region { 0 };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfin_locc24", "20" }); region.parseOpcode({ "xfin_locc24", "20" });
region.parseOpcode({ "xfin_hicc24", "24" }); region.parseOpcode({ "xfin_hicc24", "24" });
region.parseOpcode({ "amp_veltrack", "0" }); region.parseOpcode({ "amp_veltrack", "0" });
region.parseOpcode({ "xf_cccurve", "gain" }); region.parseOpcode({ "xf_cccurve", "gain" });
midiState.ccEvent(0, 24, 19_norm); midiState.ccEvent(0, 24, 19_norm);
REQUIRE(region.getCrossfadeGain() == 0.0_a); REQUIRE(region.getCrossfadeGain(midiState) == 0.0_a);
midiState.ccEvent(0, 24, 20_norm); midiState.ccEvent(0, 24, 20_norm);
REQUIRE(region.getCrossfadeGain() == 0.0_a); REQUIRE(region.getCrossfadeGain(midiState) == 0.0_a);
midiState.ccEvent(0, 24, 21_norm); midiState.ccEvent(0, 24, 21_norm);
REQUIRE(region.getCrossfadeGain() == 0.25_a); REQUIRE(region.getCrossfadeGain(midiState) == 0.25_a);
midiState.ccEvent(0, 24, 22_norm); midiState.ccEvent(0, 24, 22_norm);
REQUIRE(region.getCrossfadeGain() == 0.5_a); REQUIRE(region.getCrossfadeGain(midiState) == 0.5_a);
midiState.ccEvent(0, 24, 23_norm); midiState.ccEvent(0, 24, 23_norm);
REQUIRE(region.getCrossfadeGain() == 0.75_a); REQUIRE(region.getCrossfadeGain(midiState) == 0.75_a);
midiState.ccEvent(0, 24, 24_norm); midiState.ccEvent(0, 24, 24_norm);
REQUIRE(region.getCrossfadeGain() == 1.0_a); REQUIRE(region.getCrossfadeGain(midiState) == 1.0_a);
midiState.ccEvent(0, 24, 25_norm); midiState.ccEvent(0, 24, 25_norm);
REQUIRE(region.getCrossfadeGain() == 1.0_a); REQUIRE(region.getCrossfadeGain(midiState) == 1.0_a);
} }
TEST_CASE("[Region] Crossfade out on CC") TEST_CASE("[Region] Crossfade out on CC")
{ {
MidiState midiState; MidiState midiState;
Region region { 0, midiState }; Region region { 0 };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfout_locc24", "20" }); region.parseOpcode({ "xfout_locc24", "20" });
region.parseOpcode({ "xfout_hicc24", "24" }); region.parseOpcode({ "xfout_hicc24", "24" });
region.parseOpcode({ "amp_veltrack", "0" }); region.parseOpcode({ "amp_veltrack", "0" });
midiState.ccEvent(0, 24, 19_norm); midiState.ccEvent(0, 24, 19_norm);
REQUIRE(region.getCrossfadeGain() == 1.0_a); REQUIRE(region.getCrossfadeGain(midiState) == 1.0_a);
midiState.ccEvent(0, 24, 20_norm); midiState.ccEvent(0, 24, 20_norm);
REQUIRE(region.getCrossfadeGain() == 1.0_a); REQUIRE(region.getCrossfadeGain(midiState) == 1.0_a);
midiState.ccEvent(0, 24, 21_norm); midiState.ccEvent(0, 24, 21_norm);
REQUIRE(region.getCrossfadeGain() == 0.86603_a); REQUIRE(region.getCrossfadeGain(midiState) == 0.86603_a);
midiState.ccEvent(0, 24, 22_norm); midiState.ccEvent(0, 24, 22_norm);
REQUIRE(region.getCrossfadeGain() == 0.70711_a); REQUIRE(region.getCrossfadeGain(midiState) == 0.70711_a);
midiState.ccEvent(0, 24, 23_norm); midiState.ccEvent(0, 24, 23_norm);
REQUIRE(region.getCrossfadeGain() == 0.5_a); REQUIRE(region.getCrossfadeGain(midiState) == 0.5_a);
midiState.ccEvent(0, 24, 24_norm); midiState.ccEvent(0, 24, 24_norm);
REQUIRE(region.getCrossfadeGain() == 0.0_a); REQUIRE(region.getCrossfadeGain(midiState) == 0.0_a);
midiState.ccEvent(0, 24, 25_norm); midiState.ccEvent(0, 24, 25_norm);
REQUIRE(region.getCrossfadeGain() == 0.0_a); REQUIRE(region.getCrossfadeGain(midiState) == 0.0_a);
} }
TEST_CASE("[Region] Crossfade out on CC - gain") TEST_CASE("[Region] Crossfade out on CC - gain")
{ {
MidiState midiState; MidiState midiState;
Region region { 0, midiState }; Region region { 0 };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfout_locc24", "20" }); region.parseOpcode({ "xfout_locc24", "20" });
region.parseOpcode({ "xfout_hicc24", "24" }); region.parseOpcode({ "xfout_hicc24", "24" });
region.parseOpcode({ "amp_veltrack", "0" }); region.parseOpcode({ "amp_veltrack", "0" });
region.parseOpcode({ "xf_cccurve", "gain" }); region.parseOpcode({ "xf_cccurve", "gain" });
midiState.ccEvent(0, 24, 19_norm); midiState.ccEvent(0, 24, 19_norm);
REQUIRE(region.getCrossfadeGain() == 1.0_a); REQUIRE(region.getCrossfadeGain(midiState) == 1.0_a);
midiState.ccEvent(0, 24, 20_norm); midiState.ccEvent(0, 24, 20_norm);
REQUIRE(region.getCrossfadeGain() == 1.0_a); REQUIRE(region.getCrossfadeGain(midiState) == 1.0_a);
midiState.ccEvent(0, 24, 21_norm); midiState.ccEvent(0, 24, 21_norm);
REQUIRE(region.getCrossfadeGain() == 0.75_a); REQUIRE(region.getCrossfadeGain(midiState) == 0.75_a);
midiState.ccEvent(0, 24, 22_norm); midiState.ccEvent(0, 24, 22_norm);
REQUIRE(region.getCrossfadeGain() == 0.5_a); REQUIRE(region.getCrossfadeGain(midiState) == 0.5_a);
midiState.ccEvent(0, 24, 23_norm); midiState.ccEvent(0, 24, 23_norm);
REQUIRE(region.getCrossfadeGain() == 0.25_a); REQUIRE(region.getCrossfadeGain(midiState) == 0.25_a);
midiState.ccEvent(0, 24, 24_norm); midiState.ccEvent(0, 24, 24_norm);
REQUIRE(region.getCrossfadeGain() == 0.0_a); REQUIRE(region.getCrossfadeGain(midiState) == 0.0_a);
midiState.ccEvent(0, 24, 25_norm); midiState.ccEvent(0, 24, 25_norm);
REQUIRE(region.getCrossfadeGain() == 0.0_a); REQUIRE(region.getCrossfadeGain(midiState) == 0.0_a);
} }
TEST_CASE("[Region] Velocity bug for extreme values - veltrack at 0") TEST_CASE("[Region] Velocity bug for extreme values - veltrack at 0")
{ {
MidiState midiState; Region region { 0 };
Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "amp_veltrack", "0" }); region.parseOpcode({ "amp_veltrack", "0" });
REQUIRE(region.getNoteGain(64, 127_norm) == 1.0_a); REQUIRE(region.getNoteGain(64, 127_norm) == 1.0_a);
@ -271,8 +261,7 @@ TEST_CASE("[Region] Velocity bug for extreme values - veltrack at 0")
TEST_CASE("[Region] Velocity bug for extreme values - positive veltrack") TEST_CASE("[Region] Velocity bug for extreme values - positive veltrack")
{ {
MidiState midiState; Region region { 0 };
Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "amp_veltrack", "100" }); region.parseOpcode({ "amp_veltrack", "100" });
REQUIRE(region.getNoteGain(64, 127_norm) == 1.0_a); REQUIRE(region.getNoteGain(64, 127_norm) == 1.0_a);
@ -281,8 +270,7 @@ TEST_CASE("[Region] Velocity bug for extreme values - positive veltrack")
TEST_CASE("[Region] Velocity bug for extreme values - negative veltrack") TEST_CASE("[Region] Velocity bug for extreme values - negative veltrack")
{ {
MidiState midiState; Region region { 0 };
Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "amp_veltrack", "-100" }); region.parseOpcode({ "amp_veltrack", "-100" });
REQUIRE(region.getNoteGain(64, 127_norm) == Approx(0.0).margin(0.0001)); REQUIRE(region.getNoteGain(64, 127_norm) == Approx(0.0).margin(0.0001));
@ -293,35 +281,35 @@ TEST_CASE("[Region] rt_decay")
{ {
MidiState midiState; MidiState midiState;
midiState.setSampleRate(1000); midiState.setSampleRate(1000);
Region region { 0, midiState }; Region region { 0 };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "trigger", "release" }); region.parseOpcode({ "trigger", "release" });
region.parseOpcode({ "rt_decay", "10" }); region.parseOpcode({ "rt_decay", "10" });
midiState.noteOnEvent(0, 64, 64_norm); midiState.noteOnEvent(0, 64, 64_norm);
midiState.advanceTime(100); midiState.advanceTime(100);
REQUIRE( region.getBaseVolumedB(64) == Approx(Default::volume - 1.0f).margin(0.1) ); REQUIRE( region.getBaseVolumedB(midiState, 64) == Approx(Default::volume - 1.0f).margin(0.1) );
region.parseOpcode({ "rt_decay", "20" }); region.parseOpcode({ "rt_decay", "20" });
midiState.noteOnEvent(0, 64, 64_norm); midiState.noteOnEvent(0, 64, 64_norm);
midiState.advanceTime(100); midiState.advanceTime(100);
REQUIRE( region.getBaseVolumedB(64) == Approx(Default::volume - 2.0f).margin(0.1) ); REQUIRE( region.getBaseVolumedB(midiState, 64) == Approx(Default::volume - 2.0f).margin(0.1) );
region.parseOpcode({ "trigger", "attack" }); region.parseOpcode({ "trigger", "attack" });
midiState.noteOnEvent(0, 64, 64_norm); midiState.noteOnEvent(0, 64, 64_norm);
midiState.advanceTime(100); midiState.advanceTime(100);
REQUIRE( region.getBaseVolumedB(64) == Approx(Default::volume).margin(0.1) ); REQUIRE( region.getBaseVolumedB(midiState, 64) == Approx(Default::volume).margin(0.1) );
} }
TEST_CASE("[Region] Base delay") TEST_CASE("[Region] Base delay")
{ {
MidiState midiState; MidiState midiState;
Region region { 0, midiState }; Region region { 0 };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "delay", "10" }); region.parseOpcode({ "delay", "10" });
REQUIRE( region.getDelay() == 10.0f ); REQUIRE( region.getDelay(midiState) == 10.0f );
region.parseOpcode({ "delay_random", "10" }); region.parseOpcode({ "delay_random", "10" });
Random::randomGenerator.seed(42); Random::randomGenerator.seed(42);
for (int i = 0; i < numRandomTests; ++i) for (int i = 0; i < numRandomTests; ++i)
{ {
auto delay = region.getDelay(); auto delay = region.getDelay(midiState);
REQUIRE( (delay >= 10.0 && delay <= 20.0) ); REQUIRE( (delay >= 10.0 && delay <= 20.0) );
} }
} }
@ -329,25 +317,24 @@ TEST_CASE("[Region] Base delay")
TEST_CASE("[Region] Offsets with CCs") TEST_CASE("[Region] Offsets with CCs")
{ {
MidiState midiState; MidiState midiState;
Region region { 0, midiState }; Region region { 0 };
region.parseOpcode({ "offset_cc4", "255" }); region.parseOpcode({ "offset_cc4", "255" });
region.parseOpcode({ "offset", "10" }); region.parseOpcode({ "offset", "10" });
REQUIRE( region.getOffset() == 10 ); REQUIRE( region.getOffset(midiState) == 10 );
midiState.ccEvent(0, 4, 127_norm); midiState.ccEvent(0, 4, 127_norm);
REQUIRE( region.getOffset() == 265 ); REQUIRE( region.getOffset(midiState) == 265 );
midiState.ccEvent(0, 4, 100_norm); midiState.ccEvent(0, 4, 100_norm);
REQUIRE( region.getOffset() == 210 ); REQUIRE( region.getOffset(midiState) == 210 );
midiState.ccEvent(0, 4, 10_norm); midiState.ccEvent(0, 4, 10_norm);
REQUIRE( region.getOffset() == 30 ); REQUIRE( region.getOffset(midiState) == 30 );
midiState.ccEvent(0, 4, 0); midiState.ccEvent(0, 4, 0);
REQUIRE( region.getOffset() == 10 ); REQUIRE( region.getOffset(midiState) == 10 );
} }
TEST_CASE("[Region] Pitch variation with veltrack") TEST_CASE("[Region] Pitch variation with veltrack")
{ {
MidiState midiState; Region region { 0 };
Region region { 0, midiState };
REQUIRE(region.getBasePitchVariation(60.0, 0_norm) == 1.0); REQUIRE(region.getBasePitchVariation(60.0, 0_norm) == 1.0);
REQUIRE(region.getBasePitchVariation(60.0, 64_norm) == 1.0); REQUIRE(region.getBasePitchVariation(60.0, 64_norm) == 1.0);
@ -357,4 +344,3 @@ TEST_CASE("[Region] Pitch variation with veltrack")
REQUIRE(region.getBasePitchVariation(60.0, 64_norm) == Approx(centsFactor(600.0)).margin(0.01f)); REQUIRE(region.getBasePitchVariation(60.0, 64_norm) == Approx(centsFactor(600.0)).margin(0.01f));
REQUIRE(region.getBasePitchVariation(60.0, 127_norm) == Approx(centsFactor(1200.0)).margin(0.01f)); REQUIRE(region.getBasePitchVariation(60.0, 127_norm) == Approx(centsFactor(1200.0)).margin(0.01f));
} }

View file

@ -5,13 +5,15 @@
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#include "sfizz/Synth.h" #include "sfizz/Synth.h"
#include "sfizz/Region.h"
#include "sfizz/Layer.h"
#include "sfizz/SisterVoiceRing.h" #include "sfizz/SisterVoiceRing.h"
#include "sfizz/SfzHelpers.h" #include "sfizz/SfzHelpers.h"
#include "sfizz/utility/NumericId.h" #include "sfizz/utility/NumericId.h"
#include "BitArray.h" #include "BitArray.h"
#include "TestHelpers.h" #include "TestHelpers.h"
#include <algorithm>
#include "catch2/catch.hpp" #include "catch2/catch.hpp"
#include <algorithm>
using namespace Catch::literals; using namespace Catch::literals;
using namespace sfz::literals; using namespace sfz::literals;
@ -1111,7 +1113,7 @@ TEST_CASE("[Synth] Release (Multiple notes, release, cleared the delayed voices
sortAll(requiredVelocities, actualVelocities); sortAll(requiredVelocities, actualVelocities);
REQUIRE( requiredVelocities == actualVelocities ); REQUIRE( requiredVelocities == actualVelocities );
REQUIRE( synth.getRegionView(1)->delayedSustainReleases.empty() ); REQUIRE( synth.getLayerView(1)->delayedSustainReleases_.empty() );
} }
TEST_CASE("[Synth] Release (Multiple notes after pedal is down, release, cleared the delayed voices after)") TEST_CASE("[Synth] Release (Multiple notes after pedal is down, release, cleared the delayed voices after)")
@ -1141,7 +1143,7 @@ TEST_CASE("[Synth] Release (Multiple notes after pedal is down, release, cleared
sortAll(requiredVelocities, actualVelocities); sortAll(requiredVelocities, actualVelocities);
REQUIRE( requiredVelocities == actualVelocities ); REQUIRE( requiredVelocities == actualVelocities );
REQUIRE( synth.getRegionView(1)->delayedSustainReleases.empty() ); REQUIRE( synth.getLayerView(1)->delayedSustainReleases_.empty() );
} }
TEST_CASE("[Synth] Release (Multiple note ons during pedal down)") TEST_CASE("[Synth] Release (Multiple note ons during pedal down)")
@ -1168,7 +1170,7 @@ TEST_CASE("[Synth] Release (Multiple note ons during pedal down)")
} }
sortAll(requiredVelocities, actualVelocities); sortAll(requiredVelocities, actualVelocities);
REQUIRE( requiredVelocities == actualVelocities ); REQUIRE( requiredVelocities == actualVelocities );
REQUIRE( synth.getRegionView(1)->delayedSustainReleases.empty() ); REQUIRE( synth.getLayerView(1)->delayedSustainReleases_.empty() );
} }
TEST_CASE("[Synth] No release sample after the main sample stopped sounding by default") TEST_CASE("[Synth] No release sample after the main sample stopped sounding by default")
@ -1203,7 +1205,7 @@ TEST_CASE("[Synth] No release sample after the main sample stopped sounding by d
synth.cc(0, 64, 0); synth.cc(0, 64, 0);
REQUIRE( synth.getNumActiveVoices() == 0 ); REQUIRE( synth.getNumActiveVoices() == 0 );
REQUIRE( synth.getRegionView(1)->delayedSustainReleases.empty() ); REQUIRE( synth.getLayerView(1)->delayedSustainReleases_.empty() );
} }
TEST_CASE("[Synth] If rt_dead is active the release sample can sound after the attack sample died") TEST_CASE("[Synth] If rt_dead is active the release sample can sound after the attack sample died")
@ -1238,7 +1240,7 @@ TEST_CASE("[Synth] If rt_dead is active the release sample can sound after the a
synth.cc(0, 64, 0); synth.cc(0, 64, 0);
REQUIRE( synth.getNumActiveVoices() == 0 ); REQUIRE( synth.getNumActiveVoices() == 0 );
REQUIRE( synth.getRegionView(1)->delayedSustainReleases.empty() ); REQUIRE( synth.getLayerView(1)->delayedSustainReleases_.empty() );
} }
TEST_CASE("[Synth] sw_default works at a global level") TEST_CASE("[Synth] sw_default works at a global level")