commit
df5b3a10e4
14 changed files with 505 additions and 38 deletions
|
|
@ -183,6 +183,11 @@ namespace config {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
constexpr int playheadMovedFrames { 16 };
|
constexpr int playheadMovedFrames { 16 };
|
||||||
|
/**
|
||||||
|
* @brief Max number of voices to start on release pedal up
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
constexpr unsigned delayedReleaseVoices { 16 };
|
||||||
} // namespace config
|
} // namespace config
|
||||||
|
|
||||||
} // namespace sfz
|
} // namespace sfz
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,9 @@ UInt16Spec ccNumber { 0, {0, config::numCCs}, 0 };
|
||||||
UInt16Spec smoothCC { 0, {0, 100}, kPermissiveUpperBound };
|
UInt16Spec smoothCC { 0, {0, 100}, kPermissiveUpperBound };
|
||||||
UInt8Spec curveCC { 0, {0, 255}, 0 };
|
UInt8Spec curveCC { 0, {0, 255}, 0 };
|
||||||
UInt8Spec sustainCC { 64, {0, 127}, 0 };
|
UInt8Spec sustainCC { 64, {0, 127}, 0 };
|
||||||
|
UInt8Spec sostenutoCC { 66, {0, 127}, 0 };
|
||||||
FloatSpec sustainThreshold { 1.0f, {0.0f, 127.0f}, kNormalizeMidi|kPermissiveBounds };
|
FloatSpec sustainThreshold { 1.0f, {0.0f, 127.0f}, kNormalizeMidi|kPermissiveBounds };
|
||||||
|
FloatSpec sostenutoThreshold { 1.0f, {0.0f, 127.0f}, kNormalizeMidi|kPermissiveBounds };
|
||||||
BoolSpec checkSustain { true, {0, 1}, kEnforceBounds };
|
BoolSpec checkSustain { true, {0, 1}, kEnforceBounds };
|
||||||
BoolSpec checkSostenuto { true, {0, 1}, kEnforceBounds };
|
BoolSpec checkSostenuto { true, {0, 1}, kEnforceBounds };
|
||||||
FloatSpec loBPM { 0.0f, {0.0f, 500.0f}, kPermissiveBounds };
|
FloatSpec loBPM { 0.0f, {0.0f, 500.0f}, kPermissiveBounds };
|
||||||
|
|
|
||||||
|
|
@ -161,9 +161,11 @@ namespace Default
|
||||||
extern const OpcodeSpec<uint8_t> curveCC;
|
extern const OpcodeSpec<uint8_t> curveCC;
|
||||||
extern const OpcodeSpec<uint16_t> smoothCC;
|
extern const OpcodeSpec<uint16_t> smoothCC;
|
||||||
extern const OpcodeSpec<uint8_t> sustainCC;
|
extern const OpcodeSpec<uint8_t> sustainCC;
|
||||||
|
extern const OpcodeSpec<uint8_t> sostenutoCC;
|
||||||
extern const OpcodeSpec<bool> checkSustain;
|
extern const OpcodeSpec<bool> checkSustain;
|
||||||
extern const OpcodeSpec<bool> checkSostenuto;
|
extern const OpcodeSpec<bool> checkSostenuto;
|
||||||
extern const OpcodeSpec<float> sustainThreshold;
|
extern const OpcodeSpec<float> sustainThreshold;
|
||||||
|
extern const OpcodeSpec<float> sostenutoThreshold;
|
||||||
extern const OpcodeSpec<float> loBPM;
|
extern const OpcodeSpec<float> loBPM;
|
||||||
extern const OpcodeSpec<float> hiBPM;
|
extern const OpcodeSpec<float> hiBPM;
|
||||||
extern const OpcodeSpec<uint8_t> sequence;
|
extern const OpcodeSpec<uint8_t> sequence;
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ void sfz::MidiState::noteOnEvent(int delay, int noteNumber, float velocity) noex
|
||||||
noteOnTimes[noteNumber] = internalClock + static_cast<unsigned>(delay);
|
noteOnTimes[noteNumber] = internalClock + static_cast<unsigned>(delay);
|
||||||
lastNotePlayed = noteNumber;
|
lastNotePlayed = noteNumber;
|
||||||
activeNotes++;
|
activeNotes++;
|
||||||
|
noteStates[noteNumber] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -37,6 +38,7 @@ void sfz::MidiState::noteOffEvent(int delay, int noteNumber, float velocity) noe
|
||||||
noteOffTimes[noteNumber] = internalClock + static_cast<unsigned>(delay);
|
noteOffTimes[noteNumber] = internalClock + static_cast<unsigned>(delay);
|
||||||
if (activeNotes > 0)
|
if (activeNotes > 0)
|
||||||
activeNotes--;
|
activeNotes--;
|
||||||
|
noteStates[noteNumber] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -181,6 +183,7 @@ void sfz::MidiState::reset() noexcept
|
||||||
activeNotes = 0;
|
activeNotes = 0;
|
||||||
internalClock = 0;
|
internalClock = 0;
|
||||||
lastNotePlayed = 0;
|
lastNotePlayed = 0;
|
||||||
|
noteStates.reset();
|
||||||
absl::c_fill(noteOnTimes, 0);
|
absl::c_fill(noteOnTimes, 0);
|
||||||
absl::c_fill(noteOffTimes, 0);
|
absl::c_fill(noteOffTimes, 0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <bitset>
|
||||||
#include "CCMap.h"
|
#include "CCMap.h"
|
||||||
#include "Range.h"
|
#include "Range.h"
|
||||||
|
|
||||||
|
|
@ -142,6 +143,15 @@ public:
|
||||||
*/
|
*/
|
||||||
void flushEvents() noexcept;
|
void flushEvents() noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if a note is currently depressed
|
||||||
|
*
|
||||||
|
* @param noteNumber
|
||||||
|
* @return true
|
||||||
|
* @return false
|
||||||
|
*/
|
||||||
|
bool isNotePressed(int noteNumber) const noexcept { return noteStates[noteNumber]; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the CC value for CC number
|
* @brief Get the CC value for CC number
|
||||||
*
|
*
|
||||||
|
|
@ -191,6 +201,12 @@ private:
|
||||||
|
|
||||||
MidiNoteArray<unsigned> noteOffTimes { {} };
|
MidiNoteArray<unsigned> noteOffTimes { {} };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Store the note states
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
std::bitset<128> noteStates;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Stores the velocity of the note ons for currently
|
* @brief Stores the velocity of the note ons for currently
|
||||||
* depressed notes.
|
* depressed notes.
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
#include "Macros.h"
|
#include "Macros.h"
|
||||||
#include "Debug.h"
|
#include "Debug.h"
|
||||||
#include "Opcode.h"
|
#include "Opcode.h"
|
||||||
|
#include "SwapAndPop.h"
|
||||||
#include "StringViewHelpers.h"
|
#include "StringViewHelpers.h"
|
||||||
#include "ModifierHelpers.h"
|
#include "ModifierHelpers.h"
|
||||||
#include "modulations/ModId.h"
|
#include "modulations/ModId.h"
|
||||||
|
|
@ -308,9 +309,15 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
|
||||||
case hash("sustain_cc"):
|
case hash("sustain_cc"):
|
||||||
sustainCC = opcode.read(Default::sustainCC);
|
sustainCC = opcode.read(Default::sustainCC);
|
||||||
break;
|
break;
|
||||||
|
case hash("sostenuto_cc"):
|
||||||
|
sostenutoCC = opcode.read(Default::sostenutoCC);
|
||||||
|
break;
|
||||||
case hash("sustain_lo"):
|
case hash("sustain_lo"):
|
||||||
sustainThreshold = opcode.read(Default::sustainThreshold);
|
sustainThreshold = opcode.read(Default::sustainThreshold);
|
||||||
break;
|
break;
|
||||||
|
case hash("sostenuto_lo"):
|
||||||
|
sostenutoThreshold = opcode.read(Default::sostenutoThreshold);
|
||||||
|
break;
|
||||||
case hash("sustain_sw"):
|
case hash("sustain_sw"):
|
||||||
checkSustain = opcode.read(Default::checkSustain);
|
checkSustain = opcode.read(Default::checkSustain);
|
||||||
break;
|
break;
|
||||||
|
|
@ -1497,6 +1504,53 @@ bool sfz::Region::isSwitchedOn() const noexcept
|
||||||
return keySwitched && previousKeySwitched && sequenceSwitched && pitchSwitched && bpmSwitched && aftertouchSwitched && ccSwitched.all();
|
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
|
bool sfz::Region::registerNoteOn(int noteNumber, float velocity, float randValue) noexcept
|
||||||
{
|
{
|
||||||
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
|
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
|
||||||
|
|
@ -1551,13 +1605,20 @@ bool sfz::Region::registerNoteOff(int noteNumber, float velocity, float randValu
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (trigger == Trigger::release) {
|
if (trigger == Trigger::release) {
|
||||||
if (midiState.getCCValue(sustainCC) < sustainThreshold)
|
const bool sostenutoed = isNoteSostenutoed(noteNumber);
|
||||||
return true;
|
|
||||||
|
|
||||||
// If we reach this part, we're storing the notes to delay their release on CC up
|
if (sostenutoed && !sostenutoPressed) {
|
||||||
// This is handled by the Synth object
|
removeFromSostenutoReleases(noteNumber);
|
||||||
|
if (sustainPressed)
|
||||||
|
delaySustainRelease(noteNumber, midiState.getNoteVelocity(noteNumber));
|
||||||
|
}
|
||||||
|
|
||||||
delayedReleases.emplace_back(noteNumber, midiState.getNoteVelocity(noteNumber));
|
if (!sostenutoPressed || !sostenutoed) {
|
||||||
|
if (sustainPressed)
|
||||||
|
delaySustainRelease(noteNumber, midiState.getNoteVelocity(noteNumber));
|
||||||
|
else
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -1567,6 +1628,20 @@ bool sfz::Region::registerCC(int ccNumber, float ccValue) noexcept
|
||||||
{
|
{
|
||||||
ASSERT(ccValue >= 0.0f && ccValue <= 1.0f);
|
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))
|
if (ccConditions.getWithDefault(ccNumber).containsWithEnd(ccValue))
|
||||||
ccSwitched.set(ccNumber, true);
|
ccSwitched.set(ccNumber, true);
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -406,7 +406,9 @@ struct Region {
|
||||||
bool checkSustain { Default::checkSustain }; // sustain_sw
|
bool checkSustain { Default::checkSustain }; // sustain_sw
|
||||||
bool checkSostenuto { Default::checkSostenuto }; // sostenuto_sw
|
bool checkSostenuto { Default::checkSostenuto }; // sostenuto_sw
|
||||||
uint16_t sustainCC { Default::sustainCC }; // sustain_cc
|
uint16_t sustainCC { Default::sustainCC }; // sustain_cc
|
||||||
|
uint16_t sostenutoCC { Default::sostenutoCC }; // sustain_cc
|
||||||
float sustainThreshold { Default::sustainThreshold }; // sustain_cc
|
float sustainThreshold { Default::sustainThreshold }; // sustain_cc
|
||||||
|
float sostenutoThreshold { Default::sostenutoThreshold }; // sustain_cc
|
||||||
|
|
||||||
// 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
|
||||||
|
|
@ -506,7 +508,16 @@ struct Region {
|
||||||
RegionSet* parent { nullptr };
|
RegionSet* parent { nullptr };
|
||||||
|
|
||||||
// Started notes
|
// Started notes
|
||||||
std::vector<std::pair<int, float>> delayedReleases;
|
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;
|
const MidiState& midiState;
|
||||||
bool keySwitched { true };
|
bool keySwitched { true };
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,12 @@ void Synth::Impl::buildRegion(const std::vector<Opcode>& regionOpcodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adapt the size of the delayed releases to avoid allocating later on
|
// Adapt the size of the delayed releases to avoid allocating later on
|
||||||
lastRegion->delayedReleases.reserve(lastRegion->keyRange.length());
|
if (lastRegion->trigger == Trigger::release) {
|
||||||
|
const auto keyLength = static_cast<unsigned>(lastRegion->keyRange.length());
|
||||||
|
const auto size = max(config::delayedReleaseVoices, keyLength);
|
||||||
|
lastRegion->delayedSustainReleases.reserve(size);
|
||||||
|
lastRegion->delayedSostenutoReleases.reserve(size);
|
||||||
|
}
|
||||||
|
|
||||||
regions_.push_back(std::move(lastRegion));
|
regions_.push_back(std::move(lastRegion));
|
||||||
}
|
}
|
||||||
|
|
@ -654,7 +659,8 @@ void Synth::Impl::finalizeSfzLoad()
|
||||||
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))
|
||||||
ccActivationLists_[cc].push_back(region);
|
ccActivationLists_[cc].push_back(region);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1147,21 +1153,34 @@ void Synth::Impl::noteOnDispatch(int delay, int noteNumber, float velocity) noex
|
||||||
region->previousKeySwitched = (*region->previousKeyswitch == noteNumber);
|
region->previousKeySwitched = (*region->previousKeyswitch == noteNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Synth::Impl::startDelayedReleaseVoices(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept
|
void Synth::Impl::startDelayedSustainReleases(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept
|
||||||
{
|
{
|
||||||
if (!region->rtDead && !voiceManager_.playingAttackVoice(region)) {
|
if (!region->rtDead && !voiceManager_.playingAttackVoice(region)) {
|
||||||
region->delayedReleases.clear();
|
region->delayedSustainReleases.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto& note: region->delayedReleases) {
|
for (auto& note: region->delayedSustainReleases) {
|
||||||
// FIXME: we really need to have some form of common method to find and start voices...
|
|
||||||
const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second };
|
const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second };
|
||||||
startVoice(region, delay, noteOffEvent, ring);
|
startVoice(region, delay, noteOffEvent, ring);
|
||||||
}
|
}
|
||||||
region->delayedReleases.clear();
|
|
||||||
|
region->delayedSustainReleases.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Synth::Impl::startDelayedSostenutoReleases(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept
|
||||||
|
{
|
||||||
|
if (!region->rtDead && !voiceManager_.playingAttackVoice(region)) {
|
||||||
|
region->delayedSostenutoReleases.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& note: region->delayedSostenutoReleases) {
|
||||||
|
const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second };
|
||||||
|
startVoice(region, delay, noteOffEvent, ring);
|
||||||
|
}
|
||||||
|
region->delayedSostenutoReleases.clear();
|
||||||
|
}
|
||||||
|
|
||||||
void Synth::cc(int delay, int ccNumber, uint8_t ccValue) noexcept
|
void Synth::cc(int delay, int ccNumber, uint8_t ccValue) noexcept
|
||||||
{
|
{
|
||||||
|
|
@ -1174,8 +1193,19 @@ 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 (auto& region : ccActivationLists_[ccNumber]) {
|
||||||
if (ccNumber == region->sustainCC)
|
if (region->checkSustain && ccNumber == region->sustainCC && value < region->sustainThreshold)
|
||||||
startDelayedReleaseVoices(region, delay, ring);
|
startDelayedSustainReleases(region, delay, ring);
|
||||||
|
|
||||||
|
if (region->checkSostenuto && ccNumber == region->sostenutoCC && value < region->sostenutoThreshold) {
|
||||||
|
if (region->sustainPressed) {
|
||||||
|
for (const auto& v: region->delayedSostenutoReleases)
|
||||||
|
region->delaySustainRelease(v.first, v.second);
|
||||||
|
|
||||||
|
region->delayedSostenutoReleases.clear();
|
||||||
|
} else {
|
||||||
|
startDelayedSostenutoReleases(region, delay, ring);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (region->registerCC(ccNumber, value))
|
if (region->registerCC(ccNumber, value))
|
||||||
startVoice(region, delay, triggerEvent, ring);
|
startVoice(region, delay, triggerEvent, ring);
|
||||||
|
|
|
||||||
|
|
@ -1042,11 +1042,21 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co
|
||||||
client.receive<'i'>(delay, path, region.sustainCC);
|
client.receive<'i'>(delay, path, region.sustainCC);
|
||||||
} break;
|
} break;
|
||||||
|
|
||||||
|
MATCH("/region&/sostenuto_cc", "") {
|
||||||
|
GET_REGION_OR_BREAK(indices[0])
|
||||||
|
client.receive<'i'>(delay, path, region.sostenutoCC);
|
||||||
|
} break;
|
||||||
|
|
||||||
MATCH("/region&/sustain_lo", "") {
|
MATCH("/region&/sustain_lo", "") {
|
||||||
GET_REGION_OR_BREAK(indices[0])
|
GET_REGION_OR_BREAK(indices[0])
|
||||||
client.receive<'f'>(delay, path, region.sustainThreshold);
|
client.receive<'f'>(delay, path, region.sustainThreshold);
|
||||||
} break;
|
} break;
|
||||||
|
|
||||||
|
MATCH("/region&/sostenuto_lo", "") {
|
||||||
|
GET_REGION_OR_BREAK(indices[0])
|
||||||
|
client.receive<'f'>(delay, path, region.sostenutoThreshold);
|
||||||
|
} break;
|
||||||
|
|
||||||
MATCH("/region&/oscillator_phase", "") {
|
MATCH("/region&/oscillator_phase", "") {
|
||||||
GET_REGION_OR_BREAK(indices[0])
|
GET_REGION_OR_BREAK(indices[0])
|
||||||
client.receive<'f'>(delay, path, region.oscillatorPhase);
|
client.receive<'f'>(delay, path, region.oscillatorPhase);
|
||||||
|
|
|
||||||
|
|
@ -155,13 +155,22 @@ struct Synth::Impl final: public Parser::Listener {
|
||||||
void startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept;
|
void startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Start all delayed release voices of the region if necessary
|
* @brief Start all delayed sustain release voices of the region if necessary
|
||||||
*
|
*
|
||||||
* @param region
|
* @param region
|
||||||
* @param delay
|
* @param delay
|
||||||
* @param ring
|
* @param ring
|
||||||
*/
|
*/
|
||||||
void startDelayedReleaseVoices(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept;
|
void startDelayedSustainReleases(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Start all delayed sostenuto release voices of the region if necessary
|
||||||
|
*
|
||||||
|
* @param region
|
||||||
|
* @param delay
|
||||||
|
* @param ring
|
||||||
|
*/
|
||||||
|
void startDelayedSostenutoReleases(Region* region, 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
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,10 @@ struct Voice::Impl
|
||||||
|
|
||||||
State state_ { State::idle };
|
State state_ { State::idle };
|
||||||
bool noteIsOff_ { false };
|
bool noteIsOff_ { false };
|
||||||
|
enum class SustainState { Up, Sustaining };
|
||||||
|
SustainState sustainState_ { SustainState::Up };
|
||||||
|
enum class SostenutoState { Up, Sustaining, PreviouslyDown };
|
||||||
|
SostenutoState sostenutoState_ { SostenutoState::Up };
|
||||||
|
|
||||||
TriggerEvent triggerEvent_;
|
TriggerEvent triggerEvent_;
|
||||||
absl::optional<int> triggerDelay_;
|
absl::optional<int> triggerDelay_;
|
||||||
|
|
@ -459,6 +463,20 @@ bool Voice::startVoice(Region* region, int delay, const TriggerEvent& event) noe
|
||||||
impl.resources_.modMatrix.initVoice(impl.id_, region->getId(), impl.initialDelay_);
|
impl.resources_.modMatrix.initVoice(impl.id_, region->getId(), impl.initialDelay_);
|
||||||
impl.saveModulationTargets(region);
|
impl.saveModulationTargets(region);
|
||||||
|
|
||||||
|
if (region->checkSustain) {
|
||||||
|
const bool sustainPressed =
|
||||||
|
impl.resources_.midiState.getCCValue(region->sustainCC) >= region->sustainThreshold;
|
||||||
|
impl.sustainState_ =
|
||||||
|
sustainPressed ? Impl::SustainState::Sustaining : Impl::SustainState::Up;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (region->checkSostenuto) {
|
||||||
|
const bool sostenutoPressed =
|
||||||
|
impl.resources_.midiState.getCCValue(region->sostenutoCC) >= region->sostenutoThreshold;
|
||||||
|
impl.sostenutoState_ =
|
||||||
|
sostenutoPressed ? Impl::SostenutoState::PreviouslyDown : Impl::SostenutoState::Up;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -543,8 +561,13 @@ void Voice::registerNoteOff(int delay, int noteNumber, float velocity) noexcept
|
||||||
if (impl.region_->loopMode == LoopMode::one_shot)
|
if (impl.region_->loopMode == LoopMode::one_shot)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!impl.region_->checkSustain
|
const bool sustainPedalReleaseCondition = !impl.region_->checkSustain
|
||||||
|| impl.resources_.midiState.getCCValue(impl.region_->sustainCC) < impl.region_->sustainThreshold)
|
|| impl.sustainState_ != Impl::SustainState::Sustaining;
|
||||||
|
|
||||||
|
const bool sostenutoPedalReleaseCondition = !impl.region_->checkSostenuto
|
||||||
|
|| impl.sostenutoState_ != Impl::SostenutoState::Sustaining;
|
||||||
|
|
||||||
|
if (sustainPedalReleaseCondition && sostenutoPedalReleaseCondition)
|
||||||
release(delay);
|
release(delay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -559,10 +582,29 @@ void Voice::registerCC(int delay, int ccNumber, float ccValue) noexcept
|
||||||
if (impl.state_ != State::playing)
|
if (impl.state_ != State::playing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (impl.region_->checkSustain
|
if (impl.region_->checkSustain && (ccNumber == impl.region_->sostenutoCC)) {
|
||||||
&& impl.noteIsOff_
|
if (ccValue < impl.region_->sostenutoThreshold) {
|
||||||
&& ccNumber == impl.region_->sustainCC
|
impl.sostenutoState_ = Impl::SostenutoState::Up;
|
||||||
&& ccValue < impl.region_->sustainThreshold)
|
} else if (impl.sostenutoState_ == Impl::SostenutoState::Up) {
|
||||||
|
impl.sostenutoState_ = Impl::SostenutoState::Sustaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (impl.region_->checkSostenuto && (ccNumber == impl.region_->sustainCC)) {
|
||||||
|
if (ccValue < impl.region_->sustainThreshold) {
|
||||||
|
impl.sustainState_ = Impl::SustainState::Up;
|
||||||
|
} else {
|
||||||
|
impl.sustainState_ = Impl::SustainState::Sustaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool sustainPedalReleaseCondition = !impl.region_->checkSustain
|
||||||
|
|| (impl.sustainState_ != Impl::SustainState::Sustaining);
|
||||||
|
|
||||||
|
const bool sostenutoPedalReleaseCondition = !impl.region_->checkSostenuto
|
||||||
|
|| (impl.sostenutoState_ != Impl::SostenutoState::Sustaining);
|
||||||
|
|
||||||
|
if (impl.noteIsOff_ && sostenutoPedalReleaseCondition && sustainPedalReleaseCondition)
|
||||||
release(delay);
|
release(delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1512,6 +1554,7 @@ void Voice::reset() noexcept
|
||||||
impl.count_ = 1;
|
impl.count_ = 1;
|
||||||
impl.floatPositionOffset_ = 0.0f;
|
impl.floatPositionOffset_ = 0.0f;
|
||||||
impl.noteIsOff_ = false;
|
impl.noteIsOff_ = false;
|
||||||
|
impl.sostenutoState_ = Impl::SostenutoState::Up;
|
||||||
|
|
||||||
impl.resetLoopInformation();
|
impl.resetLoopInformation();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,12 @@ TEST_CASE("[Direct Region Tests] Release and release key")
|
||||||
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" });
|
||||||
midiState.ccEvent(0, 64, 0.0f);
|
midiState.ccEvent(0, 64, 0.0f);
|
||||||
|
region.registerCC(64, 0.0f);
|
||||||
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) );
|
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) );
|
||||||
REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) );
|
REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) );
|
||||||
}
|
}
|
||||||
|
|
@ -48,6 +50,7 @@ TEST_CASE("[Direct Region Tests] Release and release key")
|
||||||
{
|
{
|
||||||
region.parseOpcode({ "trigger", "release_key" });
|
region.parseOpcode({ "trigger", "release_key" });
|
||||||
midiState.ccEvent(0, 64, 1.0f);
|
midiState.ccEvent(0, 64, 1.0f);
|
||||||
|
region.registerCC(64, 1.0f);
|
||||||
REQUIRE( !region.registerCC(64, 1.0f) );
|
REQUIRE( !region.registerCC(64, 1.0f) );
|
||||||
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) );
|
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) );
|
||||||
REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) );
|
REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) );
|
||||||
|
|
@ -57,6 +60,7 @@ TEST_CASE("[Direct Region Tests] Release and release key")
|
||||||
{
|
{
|
||||||
region.parseOpcode({ "trigger", "release" });
|
region.parseOpcode({ "trigger", "release" });
|
||||||
midiState.ccEvent(0, 64, 0.0f);
|
midiState.ccEvent(0, 64, 0.0f);
|
||||||
|
region.registerCC(64, 0.0f);
|
||||||
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) );
|
REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) );
|
||||||
REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) );
|
REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) );
|
||||||
}
|
}
|
||||||
|
|
@ -65,48 +69,51 @@ TEST_CASE("[Direct Region Tests] Release and release key")
|
||||||
{
|
{
|
||||||
region.parseOpcode({ "trigger", "release" });
|
region.parseOpcode({ "trigger", "release" });
|
||||||
midiState.ccEvent(0, 64, 1.0f);
|
midiState.ccEvent(0, 64, 1.0f);
|
||||||
|
region.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( !region.registerNoteOn(63, 0.5f, 0.0f) );
|
||||||
REQUIRE( !region.registerNoteOff(63, 0.5f, 0.0f) );
|
REQUIRE( !region.registerNoteOff(63, 0.5f, 0.0f) );
|
||||||
REQUIRE( region.delayedReleases.size() == 1 );
|
REQUIRE( region.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.delayedReleases == expected );
|
REQUIRE( region.delayedSustainReleases == expected );
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("Release with sustain and 2 notes")
|
SECTION("Release with sustain and 2 notes")
|
||||||
{
|
{
|
||||||
region.parseOpcode({ "trigger", "release" });
|
region.parseOpcode({ "trigger", "release" });
|
||||||
midiState.ccEvent(0, 64, 1.0f);
|
midiState.ccEvent(0, 64, 1.0f);
|
||||||
|
region.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( !region.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( !region.registerNoteOn(64, 0.6f, 0.0f) );
|
||||||
REQUIRE( !region.registerNoteOff(63, 0.0f, 0.0f) );
|
REQUIRE( !region.registerNoteOff(63, 0.0f, 0.0f) );
|
||||||
REQUIRE( !region.registerNoteOff(64, 0.2f, 0.0f) );
|
REQUIRE( !region.registerNoteOff(64, 0.2f, 0.0f) );
|
||||||
REQUIRE( region.delayedReleases.size() == 2 );
|
REQUIRE( region.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.delayedReleases == expected );
|
REQUIRE( region.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" });
|
||||||
midiState.ccEvent(0, 64, 1.0f);
|
midiState.ccEvent(0, 64, 1.0f);
|
||||||
|
region.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( !region.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( !region.registerNoteOn(66, 0.6f, 0.0f) );
|
||||||
REQUIRE( !region.registerNoteOff(63, 0.0f, 0.0f) );
|
REQUIRE( !region.registerNoteOff(63, 0.0f, 0.0f) );
|
||||||
REQUIRE( !region.registerNoteOff(66, 0.2f, 0.0f) );
|
REQUIRE( !region.registerNoteOff(66, 0.2f, 0.0f) );
|
||||||
REQUIRE( region.delayedReleases.size() == 1 );
|
REQUIRE( region.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.delayedReleases == expected );
|
REQUIRE( region.delayedSustainReleases == expected );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2425,6 +2425,52 @@ TEST_CASE("[Values] Sustain low")
|
||||||
REQUIRE(messageList == expected);
|
REQUIRE(messageList == expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Values] Sostenuto CC")
|
||||||
|
{
|
||||||
|
Synth synth;
|
||||||
|
std::vector<std::string> messageList;
|
||||||
|
Client client(&messageList);
|
||||||
|
client.setReceiveCallback(&simpleMessageReceiver);
|
||||||
|
|
||||||
|
synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"(
|
||||||
|
<region> sample=kick.wav
|
||||||
|
<region> sample=kick.wav sostenuto_cc=10
|
||||||
|
<region> sample=kick.wav sostenuto_cc=20 sostenuto_cc=-1
|
||||||
|
)");
|
||||||
|
synth.dispatchMessage(client, 0, "/region0/sostenuto_cc", "", nullptr);
|
||||||
|
synth.dispatchMessage(client, 0, "/region1/sostenuto_cc", "", nullptr);
|
||||||
|
synth.dispatchMessage(client, 0, "/region2/sostenuto_cc", "", nullptr);
|
||||||
|
std::vector<std::string> expected {
|
||||||
|
"/region0/sostenuto_cc,i : { 66 }",
|
||||||
|
"/region1/sostenuto_cc,i : { 10 }",
|
||||||
|
"/region2/sostenuto_cc,i : { 66 }",
|
||||||
|
};
|
||||||
|
REQUIRE(messageList == expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Values] Sostenuto low")
|
||||||
|
{
|
||||||
|
Synth synth;
|
||||||
|
std::vector<std::string> messageList;
|
||||||
|
Client client(&messageList);
|
||||||
|
client.setReceiveCallback(&simpleMessageReceiver);
|
||||||
|
|
||||||
|
synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"(
|
||||||
|
<region> sample=kick.wav
|
||||||
|
<region> sample=kick.wav sostenuto_lo=10
|
||||||
|
<region> sample=kick.wav sostenuto_lo=10 sostenuto_lo=-1
|
||||||
|
)");
|
||||||
|
synth.dispatchMessage(client, 0, "/region0/sostenuto_lo", "", nullptr);
|
||||||
|
synth.dispatchMessage(client, 0, "/region1/sostenuto_lo", "", nullptr);
|
||||||
|
synth.dispatchMessage(client, 0, "/region2/sostenuto_lo", "", nullptr);
|
||||||
|
std::vector<std::string> expected {
|
||||||
|
"/region0/sostenuto_lo,f : { 0.00787402 }",
|
||||||
|
"/region1/sostenuto_lo,f : { 0.0787402 }",
|
||||||
|
"/region2/sostenuto_lo,f : { -0.00787402 }",
|
||||||
|
};
|
||||||
|
REQUIRE(messageList == expected);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("[Values] Oscillator phase")
|
TEST_CASE("[Values] Oscillator phase")
|
||||||
{
|
{
|
||||||
Synth synth;
|
Synth synth;
|
||||||
|
|
|
||||||
226
tests/SynthT.cpp
226
tests/SynthT.cpp
|
|
@ -771,8 +771,6 @@ TEST_CASE("[Synth] Release (pedal was already down)")
|
||||||
REQUIRE( synth.getNumActiveVoices() == 2 );
|
REQUIRE( synth.getNumActiveVoices() == 2 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
TEST_CASE("[Synth] Release samples don't play unless there is another playing region that matches")
|
TEST_CASE("[Synth] Release samples don't play unless there is another playing region that matches")
|
||||||
{
|
{
|
||||||
sfz::Synth synth;
|
sfz::Synth synth;
|
||||||
|
|
@ -793,7 +791,7 @@ TEST_CASE("[Synth] Release key (Different sustain CC)")
|
||||||
{
|
{
|
||||||
sfz::Synth synth;
|
sfz::Synth synth;
|
||||||
synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"(
|
synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"(
|
||||||
<global>sustain_cc=54
|
<global> sustain_cc=54
|
||||||
<region> key=62 sample=*sine trigger=release_key
|
<region> key=62 sample=*sine trigger=release_key
|
||||||
)");
|
)");
|
||||||
synth.noteOn(0, 62, 85);
|
synth.noteOn(0, 62, 85);
|
||||||
|
|
@ -806,7 +804,7 @@ TEST_CASE("[Synth] Release (Different sustain CC)")
|
||||||
{
|
{
|
||||||
sfz::Synth synth;
|
sfz::Synth synth;
|
||||||
synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"(
|
synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"(
|
||||||
<global>sustain_cc=54
|
<global> sustain_cc=54
|
||||||
<region> key=62 sample=*silence
|
<region> key=62 sample=*silence
|
||||||
<region> key=62 sample=*sine trigger=release
|
<region> key=62 sample=*sine trigger=release
|
||||||
)");
|
)");
|
||||||
|
|
@ -818,6 +816,114 @@ TEST_CASE("[Synth] Release (Different sustain CC)")
|
||||||
REQUIRE( synth.getNumActiveVoices() == 2 );
|
REQUIRE( synth.getNumActiveVoices() == 2 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Synth] Release (don't check sustain)")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"(
|
||||||
|
<global>sustain_cc=54 sustain_sw=off
|
||||||
|
<region> key=62 sample=*silence
|
||||||
|
<region> key=62 sample=*sine trigger=release
|
||||||
|
)");
|
||||||
|
synth.noteOn(0, 62, 85);
|
||||||
|
synth.cc(0, 54, 127);
|
||||||
|
synth.noteOff(0, 62, 85);
|
||||||
|
REQUIRE( synth.getNumActiveVoices() == 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Synth] Release key (Different sostenuto CC)")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"(
|
||||||
|
<global> sostenuto_cc=54
|
||||||
|
<region> key=62 sample=*sine trigger=release_key
|
||||||
|
)");
|
||||||
|
synth.noteOn(0, 62, 85);
|
||||||
|
synth.cc(0, 54, 127);
|
||||||
|
synth.noteOff(0, 62, 85);
|
||||||
|
REQUIRE( synth.getNumActiveVoices() == 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Synth] Release (don't check sostenuto)")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"(
|
||||||
|
<global> sostenuto_cc=54 sostenuto_sw=off
|
||||||
|
<region> key=62 sample=*silence
|
||||||
|
<region> key=62 sample=*sine trigger=release
|
||||||
|
)");
|
||||||
|
synth.noteOn(0, 62, 85);
|
||||||
|
synth.cc(0, 54, 127);
|
||||||
|
synth.noteOff(0, 62, 85);
|
||||||
|
REQUIRE( synth.getNumActiveVoices() == 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Synth] Release (Different sostenuto CC)")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"(
|
||||||
|
<global> sostenuto_cc=54
|
||||||
|
<region> key=62 sample=*silence
|
||||||
|
<region> key=62 sample=*sine trigger=release
|
||||||
|
<region> key=64 sample=*silence
|
||||||
|
<region> key=64 sample=*sine trigger=release
|
||||||
|
)");
|
||||||
|
SECTION("One note with sostenuto")
|
||||||
|
{
|
||||||
|
synth.noteOn(0, 62, 85);
|
||||||
|
synth.cc(1, 54, 127);
|
||||||
|
synth.noteOff(2, 62, 85);
|
||||||
|
REQUIRE( synth.getNumActiveVoices() == 1 );
|
||||||
|
synth.cc(3, 54, 0);
|
||||||
|
REQUIRE( synth.getNumActiveVoices() == 2 );
|
||||||
|
}
|
||||||
|
SECTION("Two notes, only one with sostenuto")
|
||||||
|
{
|
||||||
|
synth.noteOn(0, 62, 85);
|
||||||
|
synth.cc(1, 54, 127);
|
||||||
|
synth.noteOn(2, 64, 85);
|
||||||
|
synth.noteOff(3, 62, 85);
|
||||||
|
synth.noteOff(3, 64, 85);
|
||||||
|
REQUIRE( synth.getNumActiveVoices() == 3 );
|
||||||
|
synth.cc(4, 54, 0);
|
||||||
|
REQUIRE( synth.getNumActiveVoices() == 4 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Synth] Release (sustain + sostenuto)")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"(
|
||||||
|
<region> key=62 sample=*silence
|
||||||
|
<region> key=62 sample=*sine trigger=release
|
||||||
|
<region> key=64 sample=*silence
|
||||||
|
<region> key=64 sample=*sine trigger=release
|
||||||
|
)");
|
||||||
|
SECTION("Sustain up first")
|
||||||
|
{
|
||||||
|
synth.noteOn(0, 62, 85);
|
||||||
|
synth.cc(1, 66, 127);
|
||||||
|
synth.cc(1, 64, 127);
|
||||||
|
synth.noteOff(2, 62, 85);
|
||||||
|
REQUIRE( synth.getNumActiveVoices() == 1 );
|
||||||
|
synth.cc(3, 64, 0);
|
||||||
|
REQUIRE( synth.getNumActiveVoices() == 1 );
|
||||||
|
synth.cc(4, 66, 0);
|
||||||
|
REQUIRE( synth.getNumActiveVoices() == 2 );
|
||||||
|
}
|
||||||
|
SECTION("Sostenuto up first")
|
||||||
|
{
|
||||||
|
synth.noteOn(0, 62, 85);
|
||||||
|
synth.cc(1, 66, 127);
|
||||||
|
synth.cc(1, 64, 127);
|
||||||
|
synth.noteOff(2, 62, 85);
|
||||||
|
REQUIRE( synth.getNumActiveVoices() == 1 );
|
||||||
|
synth.cc(3, 66, 0);
|
||||||
|
REQUIRE( synth.getNumActiveVoices() == 1 );
|
||||||
|
synth.cc(4, 64, 0);
|
||||||
|
REQUIRE( synth.getNumActiveVoices() == 2 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("[Synth] Sustain threshold default")
|
TEST_CASE("[Synth] Sustain threshold default")
|
||||||
{
|
{
|
||||||
sfz::Synth synth;
|
sfz::Synth synth;
|
||||||
|
|
@ -852,6 +958,108 @@ TEST_CASE("[Synth] Sustain threshold")
|
||||||
REQUIRE( synth.getNumActiveVoices() == 5 );
|
REQUIRE( synth.getNumActiveVoices() == 5 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Synth] Sustain")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzString(fs::current_path() / "tests/TestFiles/sostenuto.sfz", R"(
|
||||||
|
<region> sample=*sine
|
||||||
|
)");
|
||||||
|
|
||||||
|
SECTION("1 note")
|
||||||
|
{
|
||||||
|
synth.noteOn(0, 62, 85);
|
||||||
|
REQUIRE( numPlayingVoices(synth) == 1 );
|
||||||
|
synth.cc(1, 64, 1);
|
||||||
|
synth.noteOff(2, 62, 85);
|
||||||
|
REQUIRE( numPlayingVoices(synth) == 1 );
|
||||||
|
synth.cc(3, 64, 0);
|
||||||
|
REQUIRE( numPlayingVoices(synth) == 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("2 notes")
|
||||||
|
{
|
||||||
|
synth.noteOn(0, 62, 85);
|
||||||
|
synth.noteOn(0, 64, 85);
|
||||||
|
synth.cc(1, 64, 1);
|
||||||
|
synth.noteOff(2, 62, 85);
|
||||||
|
synth.noteOff(2, 64, 85);
|
||||||
|
REQUIRE( numPlayingVoices(synth) == 2 );
|
||||||
|
synth.cc(3, 64, 0);
|
||||||
|
REQUIRE( numPlayingVoices(synth) == 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("1 note after the pedal is depressed")
|
||||||
|
{
|
||||||
|
synth.cc(0, 64, 1);
|
||||||
|
synth.noteOn(1, 62, 85);
|
||||||
|
synth.noteOff(2, 62, 85);
|
||||||
|
REQUIRE( numPlayingVoices(synth) == 1 );
|
||||||
|
synth.cc(3, 64, 0);
|
||||||
|
REQUIRE( numPlayingVoices(synth) == 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("2 notes, 1 after the pedal is depressed")
|
||||||
|
{
|
||||||
|
synth.noteOn(0, 62, 85);
|
||||||
|
synth.cc(1, 64, 1);
|
||||||
|
synth.noteOn(2, 64, 85);
|
||||||
|
synth.noteOff(3, 62, 85);
|
||||||
|
synth.noteOff(3, 64, 85);
|
||||||
|
REQUIRE( numPlayingVoices(synth) == 2 );
|
||||||
|
synth.cc(4, 64, 0);
|
||||||
|
REQUIRE( numPlayingVoices(synth) == 0 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Synth] Sostenuto")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzString(fs::current_path() / "tests/TestFiles/sostenuto.sfz", R"(
|
||||||
|
<region> sample=*sine
|
||||||
|
)");
|
||||||
|
|
||||||
|
SECTION("1 note")
|
||||||
|
{
|
||||||
|
synth.noteOn(0, 62, 85);
|
||||||
|
synth.cc(1, 66, 1);
|
||||||
|
synth.noteOff(2, 62, 85);
|
||||||
|
REQUIRE( numPlayingVoices(synth) == 1 );
|
||||||
|
synth.cc(3, 66, 0);
|
||||||
|
REQUIRE( numPlayingVoices(synth) == 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("2 notes")
|
||||||
|
{
|
||||||
|
synth.noteOn(0, 62, 85);
|
||||||
|
synth.noteOn(0, 64, 85);
|
||||||
|
synth.cc(1, 66, 1);
|
||||||
|
synth.noteOff(2, 62, 85);
|
||||||
|
synth.noteOff(2, 64, 85);
|
||||||
|
REQUIRE( numPlayingVoices(synth) == 2 );
|
||||||
|
synth.cc(3, 66, 0);
|
||||||
|
REQUIRE( numPlayingVoices(synth) == 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("1 note but after the pedal is depressed")
|
||||||
|
{
|
||||||
|
synth.cc(0, 66, 1);
|
||||||
|
synth.noteOn(1, 62, 85);
|
||||||
|
synth.noteOff(2, 62, 85);
|
||||||
|
REQUIRE( numPlayingVoices(synth) == 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("2 notes, 1 after the pedal is depressed")
|
||||||
|
{
|
||||||
|
synth.noteOn(0, 62, 85);
|
||||||
|
synth.cc(1, 66, 1);
|
||||||
|
synth.noteOn(2, 64, 85);
|
||||||
|
synth.noteOff(3, 62, 85);
|
||||||
|
synth.noteOff(3, 64, 85);
|
||||||
|
REQUIRE( numPlayingVoices(synth) == 1 );
|
||||||
|
REQUIRE( getActiveVoices(synth)[0]->getTriggerEvent().number == 62 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("[Synth] Release (Multiple notes, release_key ignores the pedal)")
|
TEST_CASE("[Synth] Release (Multiple notes, release_key ignores the pedal)")
|
||||||
{
|
{
|
||||||
sfz::Synth synth;
|
sfz::Synth synth;
|
||||||
|
|
@ -903,7 +1111,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)->delayedReleases.empty() );
|
REQUIRE( synth.getRegionView(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)")
|
||||||
|
|
@ -933,7 +1141,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)->delayedReleases.empty() );
|
REQUIRE( synth.getRegionView(1)->delayedSustainReleases.empty() );
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("[Synth] Release (Multiple note ons during pedal down)")
|
TEST_CASE("[Synth] Release (Multiple note ons during pedal down)")
|
||||||
|
|
@ -960,7 +1168,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)->delayedReleases.empty() );
|
REQUIRE( synth.getRegionView(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")
|
||||||
|
|
@ -995,7 +1203,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)->delayedReleases.empty() );
|
REQUIRE( synth.getRegionView(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")
|
||||||
|
|
@ -1030,7 +1238,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)->delayedReleases.empty() );
|
REQUIRE( synth.getRegionView(1)->delayedSustainReleases.empty() );
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("[Synth] sw_default works at a global level")
|
TEST_CASE("[Synth] sw_default works at a global level")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue