Merge branch 'develop' into release-key
This commit is contained in:
commit
c810e81ade
28 changed files with 1320 additions and 222 deletions
101
lv2/sfizz.c
101
lv2/sfizz.c
|
|
@ -179,6 +179,20 @@ enum
|
||||||
SFIZZ_STRETCH_TUNING = 11,
|
SFIZZ_STRETCH_TUNING = 11,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static void
|
||||||
|
sfizz_lv2_state_free_path(LV2_State_Free_Path_Handle handle,
|
||||||
|
char *path)
|
||||||
|
{
|
||||||
|
(void)handle;
|
||||||
|
free(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static LV2_State_Free_Path sfizz_State_Free_Path =
|
||||||
|
{
|
||||||
|
.handle = NULL,
|
||||||
|
.free_path = &sfizz_lv2_state_free_path,
|
||||||
|
};
|
||||||
|
|
||||||
static void
|
static void
|
||||||
sfizz_lv2_map_required_uris(sfizz_plugin_t *self)
|
sfizz_lv2_map_required_uris(sfizz_plugin_t *self)
|
||||||
{
|
{
|
||||||
|
|
@ -911,9 +925,18 @@ restore(LV2_Handle instance,
|
||||||
const LV2_Feature *const *features)
|
const LV2_Feature *const *features)
|
||||||
{
|
{
|
||||||
UNUSED(flags);
|
UNUSED(flags);
|
||||||
UNUSED(features);
|
|
||||||
sfizz_plugin_t *self = (sfizz_plugin_t *)instance;
|
sfizz_plugin_t *self = (sfizz_plugin_t *)instance;
|
||||||
|
|
||||||
|
LV2_State_Map_Path *map_path = NULL;
|
||||||
|
LV2_State_Free_Path *free_path = &sfizz_State_Free_Path;
|
||||||
|
for (const LV2_Feature *const *f = features; *f; ++f)
|
||||||
|
{
|
||||||
|
if (!strcmp((*f)->URI, LV2_STATE__mapPath))
|
||||||
|
map_path = (LV2_State_Map_Path *)(**f).data;
|
||||||
|
else if (!strcmp((*f)->URI, LV2_STATE__freePath))
|
||||||
|
free_path = (LV2_State_Free_Path *)(**f).data;
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch back the saved file path, if any
|
// Fetch back the saved file path, if any
|
||||||
size_t size;
|
size_t size;
|
||||||
uint32_t type;
|
uint32_t type;
|
||||||
|
|
@ -922,24 +945,46 @@ restore(LV2_Handle instance,
|
||||||
value = retrieve(handle, self->sfizz_sfz_file_uri, &size, &type, &val_flags);
|
value = retrieve(handle, self->sfizz_sfz_file_uri, &size, &type, &val_flags);
|
||||||
if (value)
|
if (value)
|
||||||
{
|
{
|
||||||
lv2_log_note(&self->logger, "[sfizz] Restoring the file %s\n", (const char *)value);
|
const char *path = (const char *)value;
|
||||||
sfizz_lv2_load_file(instance, (const char *)value);
|
if (map_path)
|
||||||
|
{
|
||||||
|
path = map_path->absolute_path(map_path->handle, path);
|
||||||
|
if (!path)
|
||||||
|
return LV2_STATE_ERR_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
lv2_log_note(&self->logger, "[sfizz] Restoring the file %s\n", path);
|
||||||
|
sfizz_lv2_load_file(instance, path);
|
||||||
|
|
||||||
|
if (map_path)
|
||||||
|
free_path->free_path(free_path->handle, (char *)path);
|
||||||
}
|
}
|
||||||
|
|
||||||
value = retrieve(handle, self->sfizz_scala_file_uri, &size, &type, &val_flags);
|
value = retrieve(handle, self->sfizz_scala_file_uri, &size, &type, &val_flags);
|
||||||
if (value)
|
if (value)
|
||||||
{
|
{
|
||||||
if (sfizz_load_scala_file(self->synth, (const char *)value))
|
const char *path = (const char *)value;
|
||||||
|
if (map_path)
|
||||||
|
{
|
||||||
|
path = map_path->absolute_path(map_path->handle, path);
|
||||||
|
if (!path)
|
||||||
|
return LV2_STATE_ERR_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sfizz_load_scala_file(self->synth, path))
|
||||||
{
|
{
|
||||||
lv2_log_note(&self->logger,
|
lv2_log_note(&self->logger,
|
||||||
"[sfizz] Restoring the scale %s\n", (const char *)value);
|
"[sfizz] Restoring the scale %s\n", path);
|
||||||
strcpy(self->scala_file_path, (const char *)value);
|
strcpy(self->scala_file_path, path);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
lv2_log_error(&self->logger,
|
lv2_log_error(&self->logger,
|
||||||
"[sfizz] Error while restoring the scale %s\n", (const char *)value);
|
"[sfizz] Error while restoring the scale %s\n", path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (map_path)
|
||||||
|
free_path->free_path(free_path->handle, (char *)path);
|
||||||
}
|
}
|
||||||
|
|
||||||
value = retrieve(handle, self->sfizz_num_voices_uri, &size, &type, &val_flags);
|
value = retrieve(handle, self->sfizz_num_voices_uri, &size, &type, &val_flags);
|
||||||
|
|
@ -988,23 +1033,55 @@ save(LV2_Handle instance,
|
||||||
const LV2_Feature *const *features)
|
const LV2_Feature *const *features)
|
||||||
{
|
{
|
||||||
UNUSED(flags);
|
UNUSED(flags);
|
||||||
UNUSED(features);
|
|
||||||
sfizz_plugin_t *self = (sfizz_plugin_t *)instance;
|
sfizz_plugin_t *self = (sfizz_plugin_t *)instance;
|
||||||
|
|
||||||
|
LV2_State_Map_Path *map_path = NULL;
|
||||||
|
LV2_State_Free_Path *free_path = &sfizz_State_Free_Path;
|
||||||
|
for (const LV2_Feature *const *f = features; *f; ++f)
|
||||||
|
{
|
||||||
|
if (!strcmp((*f)->URI, LV2_STATE__mapPath))
|
||||||
|
map_path = (LV2_State_Map_Path *)(**f).data;
|
||||||
|
else if (!strcmp((*f)->URI, LV2_STATE__freePath))
|
||||||
|
free_path = (LV2_State_Free_Path *)(**f).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *path;
|
||||||
|
|
||||||
// Save the file path
|
// Save the file path
|
||||||
|
path = self->sfz_file_path;
|
||||||
|
if (map_path)
|
||||||
|
{
|
||||||
|
path = map_path->abstract_path(map_path->handle, path);
|
||||||
|
if (!path)
|
||||||
|
return LV2_STATE_ERR_UNKNOWN;
|
||||||
|
}
|
||||||
store(handle,
|
store(handle,
|
||||||
self->sfizz_sfz_file_uri,
|
self->sfizz_sfz_file_uri,
|
||||||
self->sfz_file_path,
|
path,
|
||||||
strlen(self->sfz_file_path) + 1,
|
strlen(path) + 1,
|
||||||
self->atom_path_uri,
|
self->atom_path_uri,
|
||||||
LV2_STATE_IS_POD);
|
LV2_STATE_IS_POD);
|
||||||
|
if (map_path)
|
||||||
|
free_path->free_path(free_path->handle, (char *)path);
|
||||||
|
|
||||||
// Save the scala file path
|
// Save the scala file path
|
||||||
|
path = self->scala_file_path;
|
||||||
|
if (map_path)
|
||||||
|
{
|
||||||
|
path = map_path->abstract_path(map_path->handle, path);
|
||||||
|
if (!path)
|
||||||
|
return LV2_STATE_ERR_UNKNOWN;
|
||||||
|
}
|
||||||
|
if (!path)
|
||||||
|
return LV2_STATE_ERR_UNKNOWN;
|
||||||
store(handle,
|
store(handle,
|
||||||
self->sfizz_scala_file_uri,
|
self->sfizz_scala_file_uri,
|
||||||
self->scala_file_path,
|
path,
|
||||||
strlen(self->scala_file_path) + 1,
|
strlen(path) + 1,
|
||||||
self->atom_path_uri,
|
self->atom_path_uri,
|
||||||
LV2_STATE_IS_POD);
|
LV2_STATE_IS_POD);
|
||||||
|
if (map_path)
|
||||||
|
free_path->free_path(free_path->handle, (char *)path);
|
||||||
|
|
||||||
// Save the number of voices
|
// Save the number of voices
|
||||||
store(handle,
|
store(handle,
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
||||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||||
@prefix state: <http://lv2plug.in/ns/ext/state#> .
|
@prefix state: <http://lv2plug.in/ns/ext/state#> .
|
||||||
|
@prefix time: <http://lv2plug.in/ns/ext/time#> .
|
||||||
@prefix units: <http://lv2plug.in/ns/extensions/units#> .
|
@prefix units: <http://lv2plug.in/ns/extensions/units#> .
|
||||||
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
|
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
|
||||||
@prefix work: <http://lv2plug.in/ns/ext/worker#> .
|
@prefix work: <http://lv2plug.in/ns/ext/worker#> .
|
||||||
|
|
@ -67,7 +68,7 @@ midnam:update a lv2:Feature .
|
||||||
lv2:microVersion @LV2PLUGIN_VERSION_MICRO@ ;
|
lv2:microVersion @LV2PLUGIN_VERSION_MICRO@ ;
|
||||||
|
|
||||||
lv2:requiredFeature urid:map, bufsize:boundedBlockLength, work:schedule ;
|
lv2:requiredFeature urid:map, bufsize:boundedBlockLength, work:schedule ;
|
||||||
lv2:optionalFeature lv2:hardRTCapable, opts:options ;
|
lv2:optionalFeature lv2:hardRTCapable, opts:options, state:mapPath, state:freePath ;
|
||||||
lv2:extensionData opts:interface, state:interface, work:interface ;
|
lv2:extensionData opts:interface, state:interface, work:interface ;
|
||||||
|
|
||||||
lv2:optionalFeature midnam:update ;
|
lv2:optionalFeature midnam:update ;
|
||||||
|
|
@ -82,7 +83,7 @@ midnam:update a lv2:Feature .
|
||||||
lv2:port [
|
lv2:port [
|
||||||
a lv2:InputPort, atom:AtomPort ;
|
a lv2:InputPort, atom:AtomPort ;
|
||||||
atom:bufferType atom:Sequence ;
|
atom:bufferType atom:Sequence ;
|
||||||
atom:supports patch:Message, midi:MidiEvent ;
|
atom:supports patch:Message, midi:MidiEvent, time:Position ;
|
||||||
lv2:designation lv2:control ;
|
lv2:designation lv2:control ;
|
||||||
lv2:index 0 ;
|
lv2:index 0 ;
|
||||||
lv2:symbol "control" ;
|
lv2:symbol "control" ;
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ set (SFIZZ_SOURCES
|
||||||
sfizz/Smoothers.cpp
|
sfizz/Smoothers.cpp
|
||||||
sfizz/Wavetables.cpp
|
sfizz/Wavetables.cpp
|
||||||
sfizz/Tuning.cpp
|
sfizz/Tuning.cpp
|
||||||
|
sfizz/RegionSet.cpp
|
||||||
|
sfizz/PolyphonyGroup.cpp
|
||||||
|
sfizz/VoiceStealing.cpp
|
||||||
sfizz/RTSemaphore.cpp
|
sfizz/RTSemaphore.cpp
|
||||||
sfizz/Panning.cpp
|
sfizz/Panning.cpp
|
||||||
sfizz/Effects.cpp
|
sfizz/Effects.cpp
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ namespace config {
|
||||||
constexpr int numVoices { 64 };
|
constexpr int numVoices { 64 };
|
||||||
constexpr unsigned maxVoices { 256 };
|
constexpr unsigned maxVoices { 256 };
|
||||||
constexpr unsigned smoothingSteps { 512 };
|
constexpr unsigned smoothingSteps { 512 };
|
||||||
constexpr uint8_t gainSmoothing { 5 };
|
constexpr uint8_t gainSmoothing { 0 };
|
||||||
constexpr unsigned powerTableSizeExponent { 11 };
|
constexpr unsigned powerTableSizeExponent { 11 };
|
||||||
constexpr int maxFilePromises { maxVoices };
|
constexpr int maxFilePromises { maxVoices };
|
||||||
constexpr int allSoundOffCC { 120 };
|
constexpr int allSoundOffCC { 120 };
|
||||||
|
|
@ -77,6 +77,7 @@ namespace config {
|
||||||
constexpr int filtersPerVoice { 2 };
|
constexpr int filtersPerVoice { 2 };
|
||||||
constexpr int eqsPerVoice { 3 };
|
constexpr int eqsPerVoice { 3 };
|
||||||
constexpr int oscillatorsPerVoice { 9 };
|
constexpr int oscillatorsPerVoice { 9 };
|
||||||
|
constexpr float uniformNoiseBounds { 0.25f };
|
||||||
constexpr float noiseVariance { 0.25f };
|
constexpr float noiseVariance { 0.25f };
|
||||||
/**
|
/**
|
||||||
Minimum interval in frames between recomputations of coefficients of the
|
Minimum interval in frames between recomputations of coefficients of the
|
||||||
|
|
@ -98,6 +99,10 @@ namespace config {
|
||||||
static constexpr double amplitudeTriangle = 0.625;
|
static constexpr double amplitudeTriangle = 0.625;
|
||||||
static constexpr double amplitudeSaw = 0.515;
|
static constexpr double amplitudeSaw = 0.515;
|
||||||
static constexpr double amplitudeSquare = 0.515;
|
static constexpr double amplitudeSquare = 0.515;
|
||||||
|
/**
|
||||||
|
Background file loading
|
||||||
|
*/
|
||||||
|
static constexpr int backgroundLoaderPthreadPriority = 50; // expressed in %
|
||||||
} // namespace config
|
} // namespace config
|
||||||
|
|
||||||
} // namespace sfz
|
} // namespace sfz
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ namespace Default
|
||||||
// Performance parameters: amplifier
|
// Performance parameters: amplifier
|
||||||
constexpr float globalVolume { -7.35f };
|
constexpr float globalVolume { -7.35f };
|
||||||
constexpr float volume { 0.0f };
|
constexpr float volume { 0.0f };
|
||||||
constexpr Range<float> volumeRange { -144.0, 6.0 };
|
constexpr Range<float> volumeRange { -144.0, 48.0 };
|
||||||
constexpr Range<float> volumeCCRange { -144.0, 48.0 };
|
constexpr Range<float> volumeCCRange { -144.0, 48.0 };
|
||||||
constexpr float amplitude { 100.0 };
|
constexpr float amplitude { 100.0 };
|
||||||
constexpr Range<float> amplitudeRange { 0.0, 100.0 };
|
constexpr Range<float> amplitudeRange { 0.0, 100.0 };
|
||||||
|
|
|
||||||
|
|
@ -80,13 +80,13 @@ struct EGDescription {
|
||||||
float vel2sustain { Default::vel2sustain };
|
float vel2sustain { Default::vel2sustain };
|
||||||
int vel2depth { Default::depth };
|
int vel2depth { Default::depth };
|
||||||
|
|
||||||
absl::optional<CCData<float>> ccAttack;
|
CCMap<float> ccAttack;
|
||||||
absl::optional<CCData<float>> ccDecay;
|
CCMap<float> ccDecay;
|
||||||
absl::optional<CCData<float>> ccDelay;
|
CCMap<float> ccDelay;
|
||||||
absl::optional<CCData<float>> ccHold;
|
CCMap<float> ccHold;
|
||||||
absl::optional<CCData<float>> ccRelease;
|
CCMap<float> ccRelease;
|
||||||
absl::optional<CCData<float>> ccStart;
|
CCMap<float> ccStart;
|
||||||
absl::optional<CCData<float>> ccSustain;
|
CCMap<float> ccSustain;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the attack with possibly a CC modifier and a velocity modifier
|
* @brief Get the attack with possibly a CC modifier and a velocity modifier
|
||||||
|
|
@ -98,7 +98,11 @@ struct EGDescription {
|
||||||
float getAttack(const MidiState& state, float velocity) const noexcept
|
float getAttack(const MidiState& state, float velocity) const noexcept
|
||||||
{
|
{
|
||||||
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
|
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
|
||||||
return Default::egTimeRange.clamp(ccSwitchedValue(state, ccAttack, attack) + velocity * vel2attack);
|
float returnedValue { attack + velocity * vel2attack };
|
||||||
|
for (auto& mod: ccAttack) {
|
||||||
|
returnedValue += state.getCCValue(mod.cc) * mod.data;
|
||||||
|
}
|
||||||
|
return Default::egTimeRange.clamp(returnedValue);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @brief Get the decay with possibly a CC modifier and a velocity modifier
|
* @brief Get the decay with possibly a CC modifier and a velocity modifier
|
||||||
|
|
@ -110,7 +114,11 @@ struct EGDescription {
|
||||||
float getDecay(const MidiState& state, float velocity) const noexcept
|
float getDecay(const MidiState& state, float velocity) const noexcept
|
||||||
{
|
{
|
||||||
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
|
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
|
||||||
return Default::egTimeRange.clamp(ccSwitchedValue(state, ccDecay, decay) + velocity * vel2decay);
|
float returnedValue { decay + velocity * vel2decay };
|
||||||
|
for (auto& mod: ccDecay) {
|
||||||
|
returnedValue += state.getCCValue(mod.cc) * mod.data;
|
||||||
|
}
|
||||||
|
return Default::egTimeRange.clamp(returnedValue);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @brief Get the delay with possibly a CC modifier and a velocity modifier
|
* @brief Get the delay with possibly a CC modifier and a velocity modifier
|
||||||
|
|
@ -122,7 +130,11 @@ struct EGDescription {
|
||||||
float getDelay(const MidiState& state, float velocity) const noexcept
|
float getDelay(const MidiState& state, float velocity) const noexcept
|
||||||
{
|
{
|
||||||
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
|
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
|
||||||
return Default::egTimeRange.clamp(ccSwitchedValue(state, ccDelay, delay) + velocity * vel2delay);
|
float returnedValue { delay + velocity * vel2delay };
|
||||||
|
for (auto& mod: ccDelay) {
|
||||||
|
returnedValue += state.getCCValue(mod.cc) * mod.data;
|
||||||
|
}
|
||||||
|
return Default::egTimeRange.clamp(returnedValue);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @brief Get the holding duration with possibly a CC modifier and a velocity modifier
|
* @brief Get the holding duration with possibly a CC modifier and a velocity modifier
|
||||||
|
|
@ -134,7 +146,11 @@ struct EGDescription {
|
||||||
float getHold(const MidiState& state, float velocity) const noexcept
|
float getHold(const MidiState& state, float velocity) const noexcept
|
||||||
{
|
{
|
||||||
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
|
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
|
||||||
return Default::egTimeRange.clamp(ccSwitchedValue(state, ccHold, hold) + velocity * vel2hold);
|
float returnedValue { hold + velocity * vel2hold };
|
||||||
|
for (auto& mod: ccHold) {
|
||||||
|
returnedValue += state.getCCValue(mod.cc) * mod.data;
|
||||||
|
}
|
||||||
|
return Default::egTimeRange.clamp(returnedValue);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @brief Get the release duration with possibly a CC modifier and a velocity modifier
|
* @brief Get the release duration with possibly a CC modifier and a velocity modifier
|
||||||
|
|
@ -146,7 +162,11 @@ struct EGDescription {
|
||||||
float getRelease(const MidiState& state, float velocity) const noexcept
|
float getRelease(const MidiState& state, float velocity) const noexcept
|
||||||
{
|
{
|
||||||
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
|
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
|
||||||
return Default::egTimeRange.clamp(ccSwitchedValue(state, ccRelease, release) + velocity * vel2release);
|
float returnedValue { release + velocity * vel2release };
|
||||||
|
for (auto& mod: ccRelease) {
|
||||||
|
returnedValue += state.getCCValue(mod.cc) * mod.data;
|
||||||
|
}
|
||||||
|
return Default::egTimeRange.clamp(returnedValue);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @brief Get the starting level with possibly a CC modifier and a velocity modifier
|
* @brief Get the starting level with possibly a CC modifier and a velocity modifier
|
||||||
|
|
@ -158,7 +178,11 @@ struct EGDescription {
|
||||||
float getStart(const MidiState& state, float velocity) const noexcept
|
float getStart(const MidiState& state, float velocity) const noexcept
|
||||||
{
|
{
|
||||||
UNUSED(velocity);
|
UNUSED(velocity);
|
||||||
return Default::egPercentRange.clamp(ccSwitchedValue(state, ccStart, start));
|
float returnedValue { start };
|
||||||
|
for (auto& mod: ccStart) {
|
||||||
|
returnedValue += state.getCCValue(mod.cc) * mod.data;
|
||||||
|
}
|
||||||
|
return Default::egPercentRange.clamp(returnedValue);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @brief Get the sustain level with possibly a CC modifier and a velocity modifier
|
* @brief Get the sustain level with possibly a CC modifier and a velocity modifier
|
||||||
|
|
@ -170,7 +194,11 @@ struct EGDescription {
|
||||||
float getSustain(const MidiState& state, float velocity) const noexcept
|
float getSustain(const MidiState& state, float velocity) const noexcept
|
||||||
{
|
{
|
||||||
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
|
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
|
||||||
return Default::egPercentRange.clamp(ccSwitchedValue(state, ccSustain, sustain) + velocity * vel2sustain);
|
float returnedValue { sustain + velocity * vel2sustain };
|
||||||
|
for (auto& mod: ccSustain) {
|
||||||
|
returnedValue += state.getCCValue(mod.cc) * mod.data;
|
||||||
|
}
|
||||||
|
return Default::egPercentRange.clamp(returnedValue);
|
||||||
}
|
}
|
||||||
LEAK_DETECTOR(EGDescription);
|
LEAK_DETECTOR(EGDescription);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,14 @@
|
||||||
#include "absl/memory/memory.h"
|
#include "absl/memory/memory.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <sndfile.hh>
|
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <system_error>
|
||||||
|
#include <sndfile.hh>
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
|
#include <pthread.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
void readBaseFile(SndfileHandle& sndFile, sfz::FileAudioBuffer& output, uint32_t numFrames, bool reverse)
|
void readBaseFile(SndfileHandle& sndFile, sfz::FileAudioBuffer& output, uint32_t numFrames, bool reverse)
|
||||||
{
|
{
|
||||||
|
|
@ -125,11 +131,16 @@ sfz::FilePool::~FilePool()
|
||||||
{
|
{
|
||||||
quitThread = true;
|
quitThread = true;
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
|
||||||
for (unsigned i = 0; i < threadPool.size(); ++i) {
|
for (unsigned i = 0; i < threadPool.size(); ++i) {
|
||||||
std::error_code ec;
|
ec = std::error_code();
|
||||||
workerBarrier.post(ec);
|
workerBarrier.post(ec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ec = std::error_code();
|
||||||
|
semClearingRequest.post(ec);
|
||||||
|
|
||||||
for (auto& thread: threadPool)
|
for (auto& thread: threadPool)
|
||||||
thread.join();
|
thread.join();
|
||||||
}
|
}
|
||||||
|
|
@ -360,28 +371,36 @@ void sfz::FilePool::tryToClearPromises()
|
||||||
|
|
||||||
void sfz::FilePool::clearingThread()
|
void sfz::FilePool::clearingThread()
|
||||||
{
|
{
|
||||||
while (!quitThread) {
|
raiseCurrentThreadPriority();
|
||||||
|
|
||||||
|
RTSemaphore& request = semClearingRequest;
|
||||||
|
do {
|
||||||
|
request.wait();
|
||||||
|
if (quitThread)
|
||||||
|
return;
|
||||||
tryToClearPromises();
|
tryToClearPromises();
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
} while (1);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void sfz::FilePool::loadingThread() noexcept
|
void sfz::FilePool::loadingThread() noexcept
|
||||||
{
|
{
|
||||||
|
raiseCurrentThreadPriority();
|
||||||
|
|
||||||
FilePromisePtr promise;
|
FilePromisePtr promise;
|
||||||
while (!quitThread) {
|
do {
|
||||||
|
workerBarrier.wait();
|
||||||
|
|
||||||
if (emptyQueue) {
|
if (emptyQueue) {
|
||||||
while(promiseQueue.try_pop(promise)) {
|
while (promiseQueue.try_pop(promise)) {
|
||||||
// We're just dequeuing
|
// We're just dequeuing
|
||||||
}
|
}
|
||||||
emptyQueue = false;
|
emptyQueue = false;
|
||||||
|
semEmptyQueueFinished.post();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::error_code ec;
|
if (quitThread)
|
||||||
workerBarrier.wait(ec);
|
return;
|
||||||
ASSERT(!ec);
|
|
||||||
|
|
||||||
if (!promiseQueue.try_pop(promise)) {
|
if (!promiseQueue.try_pop(promise)) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -406,13 +425,11 @@ void sfz::FilePool::loadingThread() noexcept
|
||||||
|
|
||||||
threadsLoading--;
|
threadsLoading--;
|
||||||
|
|
||||||
while (!filledPromiseQueue.try_push(promise)) {
|
semFilledPromiseQueueAvailable.wait();
|
||||||
DBG("[sfizz] Error enqueuing the promise for " << promise->fileId << " in the filledPromiseQueue");
|
filledPromiseQueue.push(promise);
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
promise.reset();
|
promise.reset();
|
||||||
}
|
} while (1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void sfz::FilePool::clear()
|
void sfz::FilePool::clear()
|
||||||
|
|
@ -438,12 +455,15 @@ void sfz::FilePool::cleanupPromises() noexcept
|
||||||
// Remove the promises from the filled queue and put them in a linear
|
// Remove the promises from the filled queue and put them in a linear
|
||||||
// storage
|
// storage
|
||||||
FilePromisePtr promise;
|
FilePromisePtr promise;
|
||||||
while (filledPromiseQueue.try_pop(promise))
|
while (filledPromiseQueue.try_pop(promise)) {
|
||||||
|
semFilledPromiseQueueAvailable.post();
|
||||||
temporaryFilePromises.push_back(promise);
|
temporaryFilePromises.push_back(promise);
|
||||||
|
}
|
||||||
|
|
||||||
auto promiseUsedOnce = [](FilePromisePtr& p) { return p.use_count() == 1; };
|
auto promiseUsedOnce = [](FilePromisePtr& p) { return p.use_count() == 1; };
|
||||||
auto moveToClear = [&](FilePromisePtr& p) { return promisesToClear.push_back(p); };
|
auto moveToClear = [&](FilePromisePtr& p) { return promisesToClear.push_back(p); };
|
||||||
swapAndPopAll(temporaryFilePromises, promiseUsedOnce, moveToClear);
|
if (swapAndPopAll(temporaryFilePromises, promiseUsedOnce, moveToClear) > 0)
|
||||||
|
semClearingRequest.post();
|
||||||
}
|
}
|
||||||
|
|
||||||
void sfz::FilePool::setOversamplingFactor(sfz::Oversampling factor) noexcept
|
void sfz::FilePool::setOversamplingFactor(sfz::Oversampling factor) noexcept
|
||||||
|
|
@ -474,12 +494,8 @@ uint32_t sfz::FilePool::getPreloadSize() const noexcept
|
||||||
void sfz::FilePool::emptyFileLoadingQueues() noexcept
|
void sfz::FilePool::emptyFileLoadingQueues() noexcept
|
||||||
{
|
{
|
||||||
emptyQueue = true;
|
emptyQueue = true;
|
||||||
std::error_code ec;
|
workerBarrier.post();
|
||||||
workerBarrier.post(ec);
|
semEmptyQueueFinished.wait();
|
||||||
ASSERT(!ec);
|
|
||||||
|
|
||||||
while (emptyQueue)
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void sfz::FilePool::waitForBackgroundLoading() noexcept
|
void sfz::FilePool::waitForBackgroundLoading() noexcept
|
||||||
|
|
@ -496,3 +512,35 @@ void sfz::FilePool::waitForBackgroundLoading() noexcept
|
||||||
std::this_thread::sleep_for(std::chrono::microseconds(100));
|
std::this_thread::sleep_for(std::chrono::microseconds(100));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void sfz::FilePool::raiseCurrentThreadPriority() noexcept
|
||||||
|
{
|
||||||
|
#if defined(_WIN32)
|
||||||
|
HANDLE thread = GetCurrentThread();
|
||||||
|
const int priority = THREAD_PRIORITY_ABOVE_NORMAL; /*THREAD_PRIORITY_HIGHEST*/
|
||||||
|
if (!SetThreadPriority(thread, priority)) {
|
||||||
|
std::system_error error(GetLastError(), std::system_category());
|
||||||
|
DBG("[sfizz] Cannot set current thread priority: " << error.what());
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
pthread_t thread = pthread_self();
|
||||||
|
int policy;
|
||||||
|
sched_param param;
|
||||||
|
|
||||||
|
if (pthread_getschedparam(thread, &policy, ¶m) != 0) {
|
||||||
|
DBG("[sfizz] Cannot get current thread scheduling parameters");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
policy = SCHED_RR;
|
||||||
|
const int minprio = sched_get_priority_min(policy);
|
||||||
|
const int maxprio = sched_get_priority_max(policy);
|
||||||
|
param.sched_priority = minprio +
|
||||||
|
config::backgroundLoaderPthreadPriority * (maxprio - minprio) / 100;
|
||||||
|
|
||||||
|
if (pthread_setschedparam(thread, policy, ¶m) != 0) {
|
||||||
|
DBG("[sfizz] Cannot set current thread scheduling parameters");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -259,6 +259,11 @@ public:
|
||||||
* in the queue.
|
* in the queue.
|
||||||
*/
|
*/
|
||||||
void waitForBackgroundLoading() noexcept;
|
void waitForBackgroundLoading() noexcept;
|
||||||
|
/**
|
||||||
|
* @brief Assign the current thread a priority which is appropriate
|
||||||
|
* for background sample file processing.
|
||||||
|
*/
|
||||||
|
static void raiseCurrentThreadPriority() noexcept;
|
||||||
private:
|
private:
|
||||||
Logger& logger;
|
Logger& logger;
|
||||||
fs::path rootDirectory;
|
fs::path rootDirectory;
|
||||||
|
|
@ -268,13 +273,16 @@ private:
|
||||||
|
|
||||||
atomic_queue::AtomicQueue2<FilePromisePtr, config::maxVoices> promiseQueue;
|
atomic_queue::AtomicQueue2<FilePromisePtr, config::maxVoices> promiseQueue;
|
||||||
atomic_queue::AtomicQueue2<FilePromisePtr, config::maxVoices> filledPromiseQueue;
|
atomic_queue::AtomicQueue2<FilePromisePtr, config::maxVoices> filledPromiseQueue;
|
||||||
|
RTSemaphore semFilledPromiseQueueAvailable { config::maxVoices };
|
||||||
uint32_t preloadSize { config::preloadSize };
|
uint32_t preloadSize { config::preloadSize };
|
||||||
Oversampling oversamplingFactor { config::defaultOversamplingFactor };
|
Oversampling oversamplingFactor { config::defaultOversamplingFactor };
|
||||||
// Signals
|
// Signals
|
||||||
volatile bool quitThread { false };
|
volatile bool quitThread { false };
|
||||||
volatile bool emptyQueue { false };
|
volatile bool emptyQueue { false };
|
||||||
|
RTSemaphore semEmptyQueueFinished;
|
||||||
std::atomic<int> threadsLoading { 0 };
|
std::atomic<int> threadsLoading { 0 };
|
||||||
RTSemaphore workerBarrier;
|
RTSemaphore workerBarrier;
|
||||||
|
RTSemaphore semClearingRequest;
|
||||||
|
|
||||||
// File promises data structures along with their guards.
|
// File promises data structures along with their guards.
|
||||||
std::vector<FilePromisePtr> emptyPromises;
|
std::vector<FilePromisePtr> emptyPromises;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
#include "SIMDConfig.h"
|
#include "SIMDConfig.h"
|
||||||
#include "absl/types/span.h"
|
#include "absl/types/span.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ enum OpcodeScope {
|
||||||
kOpcodeScopeGlobal,
|
kOpcodeScopeGlobal,
|
||||||
//! control scope
|
//! control scope
|
||||||
kOpcodeScopeControl,
|
kOpcodeScopeControl,
|
||||||
|
//! Master scope
|
||||||
|
kOpcodeScopeMaster,
|
||||||
//! group scope
|
//! group scope
|
||||||
kOpcodeScopeGroup,
|
kOpcodeScopeGroup,
|
||||||
//! region scope
|
//! region scope
|
||||||
|
|
|
||||||
18
src/sfizz/PolyphonyGroup.cpp
Normal file
18
src/sfizz/PolyphonyGroup.cpp
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#include "PolyphonyGroup.h"
|
||||||
|
|
||||||
|
void sfz::PolyphonyGroup::setPolyphonyLimit(unsigned limit) noexcept
|
||||||
|
{
|
||||||
|
polyphonyLimit = limit;
|
||||||
|
voices.reserve(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sfz::PolyphonyGroup::registerVoice(Voice* voice) noexcept
|
||||||
|
{
|
||||||
|
if (absl::c_find(voices, voice) == voices.end())
|
||||||
|
voices.push_back(voice);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sfz::PolyphonyGroup::removeVoice(const Voice* voice) noexcept
|
||||||
|
{
|
||||||
|
swapAndPopFirst(voices, [voice](const Voice* v) { return v == voice; });
|
||||||
|
}
|
||||||
60
src/sfizz/PolyphonyGroup.h
Normal file
60
src/sfizz/PolyphonyGroup.h
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
// 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 "Region.h"
|
||||||
|
#include "Voice.h"
|
||||||
|
#include "SwapAndPop.h"
|
||||||
|
#include "absl/algorithm/container.h"
|
||||||
|
|
||||||
|
namespace sfz
|
||||||
|
{
|
||||||
|
class PolyphonyGroup {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Set the polyphony limit for this polyphony group.
|
||||||
|
*
|
||||||
|
* @param limit
|
||||||
|
*/
|
||||||
|
void setPolyphonyLimit(unsigned limit) noexcept;
|
||||||
|
/**
|
||||||
|
* @brief Register an active voice in this polyphony group.
|
||||||
|
*
|
||||||
|
* @param voice
|
||||||
|
*/
|
||||||
|
void registerVoice(Voice* voice) noexcept;
|
||||||
|
/**
|
||||||
|
* @brief Remove a voice from this polyphony group.
|
||||||
|
* If the voice was not registered before, this has no effect.
|
||||||
|
*
|
||||||
|
* @param voice
|
||||||
|
*/
|
||||||
|
void removeVoice(const Voice* voice) noexcept;
|
||||||
|
/**
|
||||||
|
* @brief Get the polyphony limit for this group
|
||||||
|
*
|
||||||
|
* @return unsigned
|
||||||
|
*/
|
||||||
|
unsigned getPolyphonyLimit() const noexcept { return polyphonyLimit; }
|
||||||
|
/**
|
||||||
|
* @brief Get the active voices
|
||||||
|
*
|
||||||
|
* @return const std::vector<Voice*>&
|
||||||
|
*/
|
||||||
|
const std::vector<Voice*>& getActiveVoices() const noexcept { return voices; }
|
||||||
|
/**
|
||||||
|
* @brief Get the active voices
|
||||||
|
*
|
||||||
|
* @return std::vector<Voice*>&
|
||||||
|
*/
|
||||||
|
std::vector<Voice*>& getActiveVoices() noexcept { return voices; }
|
||||||
|
private:
|
||||||
|
unsigned polyphonyLimit { config::maxVoices };
|
||||||
|
std::vector<Voice*> voices;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -164,6 +164,10 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
|
||||||
DBG("Unkown off mode:" << std::string(opcode.value));
|
DBG("Unkown off mode:" << std::string(opcode.value));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case hash("polyphony"):
|
||||||
|
if (auto value = readOpcode(opcode.value, Default::polyphonyRange))
|
||||||
|
polyphony = *value;
|
||||||
|
break;
|
||||||
case hash("note_polyphony"):
|
case hash("note_polyphony"):
|
||||||
if (auto value = readOpcode(opcode.value, Default::polyphonyRange))
|
if (auto value = readOpcode(opcode.value, Default::polyphonyRange))
|
||||||
notePolyphony = *value;
|
notePolyphony = *value;
|
||||||
|
|
@ -408,8 +412,9 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
|
||||||
if (opcode.parameters.back() > 127)
|
if (opcode.parameters.back() > 127)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
const auto inputVelocity = static_cast<uint8_t>(opcode.parameters.back());
|
||||||
if (value)
|
if (value)
|
||||||
velocityPoints.emplace_back(opcode.parameters.back(), *value);
|
velocityPoints.emplace_back(inputVelocity, *value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case hash("xfin_lokey"):
|
case hash("xfin_lokey"):
|
||||||
|
|
@ -819,25 +824,60 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
|
||||||
setValueFromOpcode(opcode, amplitudeEG.vel2sustain, Default::egOnCCPercentRange);
|
setValueFromOpcode(opcode, amplitudeEG.vel2sustain, Default::egOnCCPercentRange);
|
||||||
break;
|
break;
|
||||||
case hash("ampeg_attack_oncc&"): // also ampeg_attackcc&
|
case hash("ampeg_attack_oncc&"): // also ampeg_attackcc&
|
||||||
setCCPairFromOpcode(opcode, amplitudeEG.ccAttack, Default::egOnCCTimeRange);
|
if (opcode.parameters.back() >= config::numCCs)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange))
|
||||||
|
amplitudeEG.ccAttack[opcode.parameters.back()] = *value;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case hash("ampeg_decay_oncc&"): // also ampeg_decaycc&
|
case hash("ampeg_decay_oncc&"): // also ampeg_decaycc&
|
||||||
setCCPairFromOpcode(opcode, amplitudeEG.ccDecay, Default::egOnCCTimeRange);
|
if (opcode.parameters.back() >= config::numCCs)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange))
|
||||||
|
amplitudeEG.ccDecay[opcode.parameters.back()] = *value;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case hash("ampeg_delay_oncc&"): // also ampeg_delaycc&
|
case hash("ampeg_delay_oncc&"): // also ampeg_delaycc&
|
||||||
setCCPairFromOpcode(opcode, amplitudeEG.ccDelay, Default::egOnCCTimeRange);
|
if (opcode.parameters.back() >= config::numCCs)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange))
|
||||||
|
amplitudeEG.ccDelay[opcode.parameters.back()] = *value;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case hash("ampeg_hold_oncc&"): // also ampeg_holdcc&
|
case hash("ampeg_hold_oncc&"): // also ampeg_holdcc&
|
||||||
setCCPairFromOpcode(opcode, amplitudeEG.ccHold, Default::egOnCCTimeRange);
|
if (opcode.parameters.back() >= config::numCCs)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange))
|
||||||
|
amplitudeEG.ccHold[opcode.parameters.back()] = *value;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case hash("ampeg_release_oncc&"): // also ampeg_releasecc&
|
case hash("ampeg_release_oncc&"): // also ampeg_releasecc&
|
||||||
setCCPairFromOpcode(opcode, amplitudeEG.ccRelease, Default::egOnCCTimeRange);
|
if (opcode.parameters.back() >= config::numCCs)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange))
|
||||||
|
amplitudeEG.ccRelease[opcode.parameters.back()] = *value;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case hash("ampeg_start_oncc&"): // also ampeg_startcc&
|
case hash("ampeg_start_oncc&"): // also ampeg_startcc&
|
||||||
setCCPairFromOpcode(opcode, amplitudeEG.ccStart, Default::egOnCCPercentRange);
|
if (opcode.parameters.back() >= config::numCCs)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (auto value = readOpcode(opcode.value, Default::egOnCCPercentRange))
|
||||||
|
amplitudeEG.ccStart[opcode.parameters.back()] = *value;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case hash("ampeg_sustain_oncc&"): // also ampeg_sustaincc&
|
case hash("ampeg_sustain_oncc&"): // also ampeg_sustaincc&
|
||||||
setCCPairFromOpcode(opcode, amplitudeEG.ccSustain, Default::egOnCCPercentRange);
|
if (opcode.parameters.back() >= config::numCCs)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (auto value = readOpcode(opcode.value, Default::egOnCCPercentRange))
|
||||||
|
amplitudeEG.ccSustain[opcode.parameters.back()] = *value;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case hash("effect&"):
|
case hash("effect&"):
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,9 @@
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace sfz {
|
namespace sfz {
|
||||||
|
|
||||||
|
class RegionSet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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.
|
||||||
* All SFZ files are made of regions that are activated when a key is pressed or a CC
|
* All SFZ files are made of regions that are activated when a key is pressed or a CC
|
||||||
|
|
@ -282,7 +285,8 @@ struct Region {
|
||||||
uint32_t group { Default::group }; // group
|
uint32_t group { Default::group }; // group
|
||||||
absl::optional<uint32_t> offBy {}; // off_by
|
absl::optional<uint32_t> offBy {}; // off_by
|
||||||
SfzOffMode offMode { Default::offMode }; // off_mode
|
SfzOffMode offMode { Default::offMode }; // off_mode
|
||||||
absl::optional<uint32_t> notePolyphony {};
|
absl::optional<uint32_t> notePolyphony {}; // note_polyphony
|
||||||
|
unsigned polyphony { config::maxVoices }; // polyphony
|
||||||
SfzSelfMask selfMask { Default::selfMask };
|
SfzSelfMask selfMask { Default::selfMask };
|
||||||
|
|
||||||
// Region logic: key mapping
|
// Region logic: key mapping
|
||||||
|
|
@ -367,6 +371,8 @@ struct Region {
|
||||||
ModifierArray<CCMap<Modifier>> modifiers;
|
ModifierArray<CCMap<Modifier>> modifiers;
|
||||||
|
|
||||||
bool triggerOnCC { false }; // whether the region triggers on CC events or note events
|
bool triggerOnCC { false }; // whether the region triggers on CC events or note events
|
||||||
|
// Parent
|
||||||
|
RegionSet* parent { nullptr };
|
||||||
private:
|
private:
|
||||||
const MidiState& midiState;
|
const MidiState& midiState;
|
||||||
bool keySwitched { true };
|
bool keySwitched { true };
|
||||||
|
|
|
||||||
48
src/sfizz/RegionSet.cpp
Normal file
48
src/sfizz/RegionSet.cpp
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
#include "RegionSet.h"
|
||||||
|
|
||||||
|
void sfz::RegionSet::setPolyphonyLimit(unsigned limit) noexcept
|
||||||
|
{
|
||||||
|
polyphonyLimit = limit;
|
||||||
|
voices.reserve(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sfz::RegionSet::addRegion(Region* region) noexcept
|
||||||
|
{
|
||||||
|
if (absl::c_find(regions, region) == regions.end())
|
||||||
|
regions.push_back(region);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sfz::RegionSet::addSubset(RegionSet* group) noexcept
|
||||||
|
{
|
||||||
|
if (absl::c_find(subsets, group) == subsets.end())
|
||||||
|
subsets.push_back(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sfz::RegionSet::registerVoice(Voice* voice) noexcept
|
||||||
|
{
|
||||||
|
if (absl::c_find(voices, voice) == voices.end())
|
||||||
|
voices.push_back(voice);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sfz::RegionSet::removeVoice(const Voice* voice) noexcept
|
||||||
|
{
|
||||||
|
swapAndPopFirst(voices, [voice](const Voice* v) { return v == voice; });
|
||||||
|
}
|
||||||
|
|
||||||
|
void sfz::RegionSet::registerVoiceInHierarchy(const Region* region, Voice* voice) noexcept
|
||||||
|
{
|
||||||
|
auto* parent = region->parent;
|
||||||
|
while (parent != nullptr) {
|
||||||
|
parent->registerVoice(voice);
|
||||||
|
parent = parent->getParent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sfz::RegionSet::removeVoiceFromHierarchy(const Region* region, const Voice* voice) noexcept
|
||||||
|
{
|
||||||
|
auto* parent = region->parent;
|
||||||
|
while (parent != nullptr) {
|
||||||
|
parent->removeVoice(voice);
|
||||||
|
parent = parent->getParent();
|
||||||
|
}
|
||||||
|
}
|
||||||
114
src/sfizz/RegionSet.h
Normal file
114
src/sfizz/RegionSet.h
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
// 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 "Region.h"
|
||||||
|
#include "Voice.h"
|
||||||
|
#include "SwapAndPop.h"
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace sfz
|
||||||
|
{
|
||||||
|
|
||||||
|
class RegionSet {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Set the polyphony limit for the set
|
||||||
|
*
|
||||||
|
* @param limit
|
||||||
|
*/
|
||||||
|
void setPolyphonyLimit(unsigned limit) noexcept;
|
||||||
|
/**
|
||||||
|
* @brief Add a region to the set
|
||||||
|
*
|
||||||
|
* @param region
|
||||||
|
*/
|
||||||
|
void addRegion(Region* region) noexcept;
|
||||||
|
/**
|
||||||
|
* @brief Add a subset to the set
|
||||||
|
*
|
||||||
|
* @param group
|
||||||
|
*/
|
||||||
|
void addSubset(RegionSet* group) noexcept;
|
||||||
|
/**
|
||||||
|
* @brief Register a voice as active in this set
|
||||||
|
*
|
||||||
|
* @param voice
|
||||||
|
*/
|
||||||
|
void registerVoice(Voice* voice) noexcept;
|
||||||
|
/**
|
||||||
|
* @brief Remove an active voice for this set.
|
||||||
|
* If the voice was not registered this has no effect.
|
||||||
|
*
|
||||||
|
* @param voice
|
||||||
|
*/
|
||||||
|
void removeVoice(const Voice* voice) noexcept;
|
||||||
|
/**
|
||||||
|
* @brief Register a voice in the whole parent hierarchy of the region
|
||||||
|
*
|
||||||
|
* @param region
|
||||||
|
* @param voice
|
||||||
|
*/
|
||||||
|
static void registerVoiceInHierarchy(const Region* region, Voice* voice) noexcept;
|
||||||
|
/**
|
||||||
|
* @brief Remove an active voice from the whole parent hierarchy of the region.
|
||||||
|
*
|
||||||
|
* @param region
|
||||||
|
* @param voice
|
||||||
|
*/
|
||||||
|
static void removeVoiceFromHierarchy(const Region* region, const Voice* voice) noexcept;
|
||||||
|
/**
|
||||||
|
* @brief Get the polyphony limit
|
||||||
|
*
|
||||||
|
* @return unsigned
|
||||||
|
*/
|
||||||
|
unsigned getPolyphonyLimit() const noexcept { return polyphonyLimit; }
|
||||||
|
/**
|
||||||
|
* @brief Get the parent set
|
||||||
|
*
|
||||||
|
* @return RegionSet*
|
||||||
|
*/
|
||||||
|
RegionSet* getParent() const noexcept { return parent; }
|
||||||
|
/**
|
||||||
|
* @brief Set the parent set
|
||||||
|
*
|
||||||
|
* @param parent
|
||||||
|
*/
|
||||||
|
void setParent(RegionSet* parent) noexcept { this->parent = parent; }
|
||||||
|
/**
|
||||||
|
* @brief Get the active voices
|
||||||
|
*
|
||||||
|
* @return const std::vector<Voice*>&
|
||||||
|
*/
|
||||||
|
const std::vector<Voice*>& getActiveVoices() const noexcept { return voices; }
|
||||||
|
/**
|
||||||
|
* @brief Get the active voices
|
||||||
|
*
|
||||||
|
* @return std::vector<Voice*>&
|
||||||
|
*/
|
||||||
|
std::vector<Voice*>& getActiveVoices() noexcept { return voices; }
|
||||||
|
/**
|
||||||
|
* @brief Get the regions in the set
|
||||||
|
*
|
||||||
|
* @return const std::vector<Region*>&
|
||||||
|
*/
|
||||||
|
const std::vector<Region*>& getRegions() const noexcept { return regions; }
|
||||||
|
/**
|
||||||
|
* @brief Get the region subsets in this set
|
||||||
|
*
|
||||||
|
* @return const std::vector<RegionSet*>&
|
||||||
|
*/
|
||||||
|
const std::vector<RegionSet*>& getSubsets() const noexcept { return subsets; }
|
||||||
|
private:
|
||||||
|
RegionSet* parent { nullptr };
|
||||||
|
std::vector<Region*> regions;
|
||||||
|
std::vector<RegionSet*> subsets;
|
||||||
|
std::vector<Voice*> voices;
|
||||||
|
unsigned polyphonyLimit { config::maxVoices };
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
// This code is part of the sfizz library and is licensed under a 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.
|
// 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
|
||||||
|
#pragma once
|
||||||
|
|
||||||
#include "Voice.h"
|
#include "Voice.h"
|
||||||
#include "absl/meta/type_traits.h"
|
#include "absl/meta/type_traits.h"
|
||||||
|
|
@ -11,9 +12,17 @@ namespace sfz
|
||||||
{
|
{
|
||||||
|
|
||||||
struct SisterVoiceRing {
|
struct SisterVoiceRing {
|
||||||
|
/**
|
||||||
|
* @brief Apply a lambda function to all sisters in a ring.
|
||||||
|
* This function should be robust enough to be able to kill the voice
|
||||||
|
* in the lambda.
|
||||||
|
*
|
||||||
|
* @param voice
|
||||||
|
* @param lambda
|
||||||
|
*/
|
||||||
template<class F, class T,
|
template<class F, class T,
|
||||||
absl::enable_if_t<std::is_same<Voice, absl::remove_const_t<T>>::value, int> = 0>
|
absl::enable_if_t<std::is_same<Voice, absl::remove_const_t<T>>::value, int> = 0>
|
||||||
static void applyToRing(T* voice, F&& lambda)
|
static void applyToRing(T* voice, F&& lambda) noexcept
|
||||||
{
|
{
|
||||||
auto v = voice->getNextSisterVoice();
|
auto v = voice->getNextSisterVoice();
|
||||||
while (v != voice) {
|
while (v != voice) {
|
||||||
|
|
@ -24,7 +33,13 @@ struct SisterVoiceRing {
|
||||||
lambda(voice);
|
lambda(voice);
|
||||||
}
|
}
|
||||||
|
|
||||||
static unsigned countSisterVoices(const Voice* start)
|
/**
|
||||||
|
* @brief Count the number of sister voices in a ring
|
||||||
|
*
|
||||||
|
* @param start
|
||||||
|
* @return unsigned
|
||||||
|
*/
|
||||||
|
static unsigned countSisterVoices(const Voice* start) noexcept
|
||||||
{
|
{
|
||||||
if (!start)
|
if (!start)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -40,6 +55,54 @@ struct SisterVoiceRing {
|
||||||
ASSERT(count < config::maxVoices);
|
ASSERT(count < config::maxVoices);
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if a sister voice ring is well formed
|
||||||
|
*
|
||||||
|
* @param start
|
||||||
|
* @return true
|
||||||
|
* @return false
|
||||||
|
*/
|
||||||
|
static bool checkRingValidity(const Voice* start) noexcept
|
||||||
|
{
|
||||||
|
if (start == nullptr)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
unsigned idx { 0 };
|
||||||
|
const Voice* ring[config::maxVoices];
|
||||||
|
ring[idx] = start;
|
||||||
|
while (idx < config::maxVoices) {
|
||||||
|
const auto* newVoice = ring[idx]->getNextSisterVoice();
|
||||||
|
|
||||||
|
if (newVoice == nullptr) {
|
||||||
|
DBG("Error in ring: " << static_cast<const void*>(ring[idx])
|
||||||
|
<< " next sister is null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newVoice->getPreviousSisterVoice() != ring[idx]) {
|
||||||
|
DBG("Error in ring: " << static_cast<const void*>(newVoice)
|
||||||
|
<< " refers " << static_cast<const void*>(newVoice->getPreviousSisterVoice())
|
||||||
|
<< " as previous sister voice instead of "
|
||||||
|
<< static_cast<const void*>(ring[idx]));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newVoice == start)
|
||||||
|
break;
|
||||||
|
|
||||||
|
for (unsigned i = 1; i < idx; ++i) {
|
||||||
|
if (ring[i] == newVoice) {
|
||||||
|
DBG("Error in ring: " << static_cast<const void*>(newVoice)
|
||||||
|
<< " already present in ring at index " << i);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ring[++idx] = newVoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -49,7 +112,7 @@ struct SisterVoiceRing {
|
||||||
*/
|
*/
|
||||||
class SisterVoiceRingBuilder {
|
class SisterVoiceRingBuilder {
|
||||||
public:
|
public:
|
||||||
~SisterVoiceRingBuilder() {
|
~SisterVoiceRingBuilder() noexcept {
|
||||||
if (lastStartedVoice != nullptr) {
|
if (lastStartedVoice != nullptr) {
|
||||||
ASSERT(firstStartedVoice);
|
ASSERT(firstStartedVoice);
|
||||||
lastStartedVoice->setNextSisterVoice(firstStartedVoice);
|
lastStartedVoice->setNextSisterVoice(firstStartedVoice);
|
||||||
|
|
@ -62,7 +125,7 @@ public:
|
||||||
*
|
*
|
||||||
* @param voice
|
* @param voice
|
||||||
*/
|
*/
|
||||||
void addVoiceToRing(Voice* voice) {
|
void addVoiceToRing(Voice* voice) noexcept {
|
||||||
if (firstStartedVoice == nullptr)
|
if (firstStartedVoice == nullptr)
|
||||||
firstStartedVoice = voice;
|
firstStartedVoice = voice;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,14 +51,30 @@ void sfz::Synth::onVoiceStateChanged(NumericId<Voice> id, Voice::State state)
|
||||||
{
|
{
|
||||||
(void)id;
|
(void)id;
|
||||||
(void)state;
|
(void)state;
|
||||||
DBG("Voice " << id.number << ": state " << static_cast<int>(state));
|
if (state == Voice::State::idle) {
|
||||||
|
auto voice = getVoiceById(id);
|
||||||
|
RegionSet::removeVoiceFromHierarchy(voice->getRegion(), voice);
|
||||||
|
polyphonyGroups[voice->getRegion()->group].removeVoice(voice);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void sfz::Synth::onParseFullBlock(const std::string& header, const std::vector<Opcode>& members)
|
void sfz::Synth::onParseFullBlock(const std::string& header, const std::vector<Opcode>& members)
|
||||||
{
|
{
|
||||||
|
const auto newRegionSet = [&](RegionSet* parentSet) {
|
||||||
|
ASSERT(parentSet != nullptr);
|
||||||
|
sets.emplace_back(new RegionSet);
|
||||||
|
auto newSet = sets.back().get();
|
||||||
|
parentSet->addSubset(newSet);
|
||||||
|
newSet->setParent(parentSet);
|
||||||
|
currentSet = newSet;
|
||||||
|
};
|
||||||
|
|
||||||
switch (hash(header)) {
|
switch (hash(header)) {
|
||||||
case hash("global"):
|
case hash("global"):
|
||||||
globalOpcodes = members;
|
globalOpcodes = members;
|
||||||
|
currentSet = sets.front().get();
|
||||||
|
lastHeader = OpcodeScope::kOpcodeScopeGlobal;
|
||||||
groupOpcodes.clear();
|
groupOpcodes.clear();
|
||||||
masterOpcodes.clear();
|
masterOpcodes.clear();
|
||||||
handleGlobalOpcodes(members);
|
handleGlobalOpcodes(members);
|
||||||
|
|
@ -69,11 +85,19 @@ void sfz::Synth::onParseFullBlock(const std::string& header, const std::vector<O
|
||||||
break;
|
break;
|
||||||
case hash("master"):
|
case hash("master"):
|
||||||
masterOpcodes = members;
|
masterOpcodes = members;
|
||||||
|
newRegionSet(sets.front().get());
|
||||||
groupOpcodes.clear();
|
groupOpcodes.clear();
|
||||||
|
lastHeader = OpcodeScope::kOpcodeScopeMaster;
|
||||||
|
handleMasterOpcodes(members);
|
||||||
numMasters++;
|
numMasters++;
|
||||||
break;
|
break;
|
||||||
case hash("group"):
|
case hash("group"):
|
||||||
groupOpcodes = members;
|
groupOpcodes = members;
|
||||||
|
if (lastHeader == OpcodeScope::kOpcodeScopeGroup)
|
||||||
|
newRegionSet(currentSet->getParent());
|
||||||
|
else
|
||||||
|
newRegionSet(currentSet);
|
||||||
|
lastHeader = OpcodeScope::kOpcodeScopeGroup;
|
||||||
handleGroupOpcodes(members, masterOpcodes);
|
handleGroupOpcodes(members, masterOpcodes);
|
||||||
numGroups++;
|
numGroups++;
|
||||||
break;
|
break;
|
||||||
|
|
@ -105,6 +129,8 @@ void sfz::Synth::onParseWarning(const SourceRange& range, const std::string& mes
|
||||||
|
|
||||||
void sfz::Synth::buildRegion(const std::vector<Opcode>& regionOpcodes)
|
void sfz::Synth::buildRegion(const std::vector<Opcode>& regionOpcodes)
|
||||||
{
|
{
|
||||||
|
ASSERT(currentSet != nullptr);
|
||||||
|
|
||||||
int regionNumber = static_cast<int>(regions.size());
|
int regionNumber = static_cast<int>(regions.size());
|
||||||
auto lastRegion = absl::make_unique<Region>(regionNumber, resources.midiState, defaultPath);
|
auto lastRegion = absl::make_unique<Region>(regionNumber, resources.midiState, defaultPath);
|
||||||
|
|
||||||
|
|
@ -128,6 +154,13 @@ void sfz::Synth::buildRegion(const std::vector<Opcode>& regionOpcodes)
|
||||||
if (octaveOffset != 0 || noteOffset != 0)
|
if (octaveOffset != 0 || noteOffset != 0)
|
||||||
lastRegion->offsetAllKeys(octaveOffset * 12 + noteOffset);
|
lastRegion->offsetAllKeys(octaveOffset * 12 + noteOffset);
|
||||||
|
|
||||||
|
// There was a combination of group= and polyphony= on a region, so set the group polyphony
|
||||||
|
if (lastRegion->group != Default::group && lastRegion->polyphony != config::maxVoices)
|
||||||
|
setGroupPolyphony(lastRegion->group, lastRegion->polyphony);
|
||||||
|
|
||||||
|
lastRegion->parent = currentSet;
|
||||||
|
currentSet->addRegion(lastRegion.get());
|
||||||
|
|
||||||
regions.push_back(std::move(lastRegion));
|
regions.push_back(std::move(lastRegion));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,6 +175,10 @@ void sfz::Synth::clear()
|
||||||
for (auto& list : ccActivationLists)
|
for (auto& list : ccActivationLists)
|
||||||
list.clear();
|
list.clear();
|
||||||
|
|
||||||
|
lastHeader = OpcodeScope::kOpcodeScopeGlobal;
|
||||||
|
sets.clear();
|
||||||
|
sets.emplace_back(new RegionSet);
|
||||||
|
currentSet = sets.front().get();
|
||||||
regions.clear();
|
regions.clear();
|
||||||
effectBuses.clear();
|
effectBuses.clear();
|
||||||
effectBuses.emplace_back(new EffectBus);
|
effectBuses.emplace_back(new EffectBus);
|
||||||
|
|
@ -162,17 +199,38 @@ void sfz::Synth::clear()
|
||||||
masterOpcodes.clear();
|
masterOpcodes.clear();
|
||||||
groupOpcodes.clear();
|
groupOpcodes.clear();
|
||||||
unknownOpcodes.clear();
|
unknownOpcodes.clear();
|
||||||
groupMaxPolyphony.clear();
|
polyphonyGroups.clear();
|
||||||
groupMaxPolyphony.push_back(config::maxVoices);
|
polyphonyGroups.emplace_back();
|
||||||
|
polyphonyGroups.back().setPolyphonyLimit(config::maxVoices);
|
||||||
modificationTime = fs::file_time_type::min();
|
modificationTime = fs::file_time_type::min();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void sfz::Synth::handleMasterOpcodes(const std::vector<Opcode>& members)
|
||||||
|
{
|
||||||
|
for (auto& rawMember : members) {
|
||||||
|
const Opcode member = rawMember.cleanUp(kOpcodeScopeGlobal);
|
||||||
|
|
||||||
|
switch (member.lettersOnlyHash) {
|
||||||
|
case hash("polyphony"):
|
||||||
|
ASSERT(currentSet != nullptr);
|
||||||
|
if (auto value = readOpcode(member.value, Default::polyphonyRange))
|
||||||
|
currentSet->setPolyphonyLimit(*value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void sfz::Synth::handleGlobalOpcodes(const std::vector<Opcode>& members)
|
void sfz::Synth::handleGlobalOpcodes(const std::vector<Opcode>& members)
|
||||||
{
|
{
|
||||||
for (auto& rawMember : members) {
|
for (auto& rawMember : members) {
|
||||||
const Opcode member = rawMember.cleanUp(kOpcodeScopeGlobal);
|
const Opcode member = rawMember.cleanUp(kOpcodeScopeGlobal);
|
||||||
|
|
||||||
switch (member.lettersOnlyHash) {
|
switch (member.lettersOnlyHash) {
|
||||||
|
case hash("polyphony"):
|
||||||
|
ASSERT(currentSet != nullptr);
|
||||||
|
if (auto value = readOpcode(member.value, Default::polyphonyRange))
|
||||||
|
currentSet->setPolyphonyLimit(*value);
|
||||||
|
break;
|
||||||
case hash("sw_default"):
|
case hash("sw_default"):
|
||||||
setValueFromOpcode(member, defaultSwitch, Default::keyRange);
|
setValueFromOpcode(member, defaultSwitch, Default::keyRange);
|
||||||
break;
|
break;
|
||||||
|
|
@ -187,7 +245,7 @@ void sfz::Synth::handleGlobalOpcodes(const std::vector<Opcode>& members)
|
||||||
void sfz::Synth::handleGroupOpcodes(const std::vector<Opcode>& members, const std::vector<Opcode>& masterMembers)
|
void sfz::Synth::handleGroupOpcodes(const std::vector<Opcode>& members, const std::vector<Opcode>& masterMembers)
|
||||||
{
|
{
|
||||||
absl::optional<unsigned> groupIdx;
|
absl::optional<unsigned> groupIdx;
|
||||||
unsigned maxPolyphony { config::maxVoices };
|
absl::optional<unsigned> maxPolyphony;
|
||||||
|
|
||||||
const auto parseOpcode = [&](const Opcode& rawMember) {
|
const auto parseOpcode = [&](const Opcode& rawMember) {
|
||||||
const Opcode member = rawMember.cleanUp(kOpcodeScopeGroup);
|
const Opcode member = rawMember.cleanUp(kOpcodeScopeGroup);
|
||||||
|
|
@ -197,7 +255,7 @@ void sfz::Synth::handleGroupOpcodes(const std::vector<Opcode>& members, const st
|
||||||
setValueFromOpcode(member, groupIdx, Default::groupRange);
|
setValueFromOpcode(member, groupIdx, Default::groupRange);
|
||||||
break;
|
break;
|
||||||
case hash("polyphony"):
|
case hash("polyphony"):
|
||||||
setValueFromOpcode(member, maxPolyphony, Range<unsigned>(0, config::maxVoices));
|
setValueFromOpcode(member, maxPolyphony, Default::polyphonyRange);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -208,8 +266,14 @@ void sfz::Synth::handleGroupOpcodes(const std::vector<Opcode>& members, const st
|
||||||
for (auto& member : members)
|
for (auto& member : members)
|
||||||
parseOpcode(member);
|
parseOpcode(member);
|
||||||
|
|
||||||
if (groupIdx)
|
if (groupIdx && maxPolyphony) {
|
||||||
setGroupPolyphony(*groupIdx, maxPolyphony);
|
setGroupPolyphony(*groupIdx, *maxPolyphony);
|
||||||
|
} else if (maxPolyphony) {
|
||||||
|
ASSERT(currentSet != nullptr);
|
||||||
|
currentSet->setPolyphonyLimit(*maxPolyphony);
|
||||||
|
} else if (groupIdx && *groupIdx > polyphonyGroups.size()) {
|
||||||
|
setGroupPolyphony(*groupIdx, config::maxVoices);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void sfz::Synth::handleControlOpcodes(const std::vector<Opcode>& members)
|
void sfz::Synth::handleControlOpcodes(const std::vector<Opcode>& members)
|
||||||
|
|
@ -237,8 +301,10 @@ void sfz::Synth::handleControlOpcodes(const std::vector<Opcode>& members)
|
||||||
ccLabels.emplace_back(member.parameters.back(), std::string(member.value));
|
ccLabels.emplace_back(member.parameters.back(), std::string(member.value));
|
||||||
break;
|
break;
|
||||||
case hash("label_key&"):
|
case hash("label_key&"):
|
||||||
if (Default::keyRange.containsWithEnd(member.parameters.back()))
|
if (member.parameters.back() <= Default::keyRange.getEnd()) {
|
||||||
keyLabels.emplace_back(member.parameters.back(), std::string(member.value));
|
const auto noteNumber = static_cast<uint8_t>(member.parameters.back());
|
||||||
|
keyLabels.emplace_back(noteNumber, std::string(member.value));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case hash("default_path"):
|
case hash("default_path"):
|
||||||
defaultPath = absl::StrReplaceAll(trim(member.value), { { "\\", "/" } });
|
defaultPath = absl::StrReplaceAll(trim(member.value), { { "\\", "/" } });
|
||||||
|
|
@ -331,7 +397,11 @@ bool sfz::Synth::loadSfzFile(const fs::path& file)
|
||||||
clear();
|
clear();
|
||||||
|
|
||||||
const std::lock_guard<std::mutex> disableCallback { callbackGuard };
|
const std::lock_guard<std::mutex> disableCallback { callbackGuard };
|
||||||
parser.parseFile(file);
|
|
||||||
|
std::error_code ec;
|
||||||
|
fs::path realFile = fs::canonical(file, ec);
|
||||||
|
|
||||||
|
parser.parseFile(ec ? file : realFile);
|
||||||
if (parser.getErrorCount() > 0)
|
if (parser.getErrorCount() > 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|
@ -437,8 +507,10 @@ void sfz::Synth::finalizeSfzLoad()
|
||||||
keyswitchLabels.push_back({ *region->keyswitch, *region->keyswitchLabel });
|
keyswitchLabels.push_back({ *region->keyswitch, *region->keyswitchLabel });
|
||||||
|
|
||||||
// Some regions had group number but no "group-level" opcodes handled the polyphony
|
// Some regions had group number but no "group-level" opcodes handled the polyphony
|
||||||
while (groupMaxPolyphony.size() <= region->group)
|
while (polyphonyGroups.size() <= region->group) {
|
||||||
groupMaxPolyphony.push_back(config::maxVoices);
|
polyphonyGroups.emplace_back();
|
||||||
|
polyphonyGroups.back().setPolyphonyLimit(config::maxVoices);
|
||||||
|
}
|
||||||
|
|
||||||
for (auto note = 0; note < 128; note++) {
|
for (auto note = 0; note < 128; note++) {
|
||||||
if (region->keyRange.containsWithEnd(note) || (region->hasKeyswitches() && region->keyswitchRange.containsWithEnd(note)))
|
if (region->keyRange.containsWithEnd(note) || (region->hasKeyswitches() && region->keyswitchRange.containsWithEnd(note)))
|
||||||
|
|
@ -546,52 +618,11 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept
|
||||||
auto freeVoice = absl::c_find_if(voices, [](const std::unique_ptr<Voice>& voice) {
|
auto freeVoice = absl::c_find_if(voices, [](const std::unique_ptr<Voice>& voice) {
|
||||||
return voice->isFree();
|
return voice->isFree();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (freeVoice != voices.end())
|
if (freeVoice != voices.end())
|
||||||
return freeVoice->get();
|
return freeVoice->get();
|
||||||
|
|
||||||
// Start of the voice stealing algorithm
|
return {};
|
||||||
absl::c_sort(voiceViewArray, voiceOrdering);
|
|
||||||
|
|
||||||
const auto sumEnvelope = absl::c_accumulate(voiceViewArray, 0.0f, [](float sum, const Voice* v) {
|
|
||||||
return sum + v->getAverageEnvelope();
|
|
||||||
});
|
|
||||||
const auto envThreshold = sumEnvelope
|
|
||||||
/ static_cast<float>(voiceViewArray.size()) * config::stealingEnvelopeCoeff;
|
|
||||||
const auto ageThreshold = voiceViewArray.front()->getAge() * config::stealingAgeCoeff;
|
|
||||||
|
|
||||||
Voice* returnedVoice = voiceViewArray.front();
|
|
||||||
unsigned idx = 0;
|
|
||||||
while (idx < voiceViewArray.size()) {
|
|
||||||
const auto ref = voiceViewArray[idx];
|
|
||||||
|
|
||||||
if (ref->getAge() < ageThreshold) {
|
|
||||||
// Went too far, we'll kill the oldest note.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
float maxEnvelope { 0.0f };
|
|
||||||
SisterVoiceRing::applyToRing(ref, [&](Voice* v) {
|
|
||||||
maxEnvelope = max(maxEnvelope, v->getAverageEnvelope());
|
|
||||||
});
|
|
||||||
|
|
||||||
if (maxEnvelope < envThreshold) {
|
|
||||||
returnedVoice = ref;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Jump over the sister voices in the set
|
|
||||||
do { idx++; }
|
|
||||||
while (idx < voiceViewArray.size() && sisterVoices(ref, voiceViewArray[idx]));
|
|
||||||
}
|
|
||||||
|
|
||||||
auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock);
|
|
||||||
SisterVoiceRing::applyToRing(returnedVoice, [&] (Voice* v) {
|
|
||||||
renderVoiceToOutputs(*v, *tempSpan);
|
|
||||||
v->reset();
|
|
||||||
});
|
|
||||||
ASSERT(returnedVoice->isFree());
|
|
||||||
|
|
||||||
return returnedVoice;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int sfz::Synth::getNumActiveVoices() const noexcept
|
int sfz::Synth::getNumActiveVoices() const noexcept
|
||||||
|
|
@ -652,7 +683,6 @@ void sfz::Synth::renderVoiceToOutputs(Voice& voice, AudioSpan<float>& tempSpan)
|
||||||
bus->addToInputs(tempSpan, addGain, tempSpan.getNumFrames());
|
bus->addToInputs(tempSpan, addGain, tempSpan.getNumFrames());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
||||||
|
|
@ -708,7 +738,7 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
||||||
callbackBreakdown.panning += voice->getLastPanningDuration();
|
callbackBreakdown.panning += voice->getLastPanningDuration();
|
||||||
|
|
||||||
if (voice->toBeCleanedUp())
|
if (voice->toBeCleanedUp())
|
||||||
voice->reset();
|
voice->reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -811,6 +841,8 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex
|
||||||
|
|
||||||
voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOff);
|
voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOff);
|
||||||
ring.addVoiceToRing(voice);
|
ring.addVoiceToRing(voice);
|
||||||
|
RegionSet::registerVoiceInHierarchy(region, voice);
|
||||||
|
polyphonyGroups[region->group].registerVoice(voice);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -822,21 +854,25 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc
|
||||||
|
|
||||||
for (auto& region : noteActivationLists[noteNumber]) {
|
for (auto& region : noteActivationLists[noteNumber]) {
|
||||||
if (region->registerNoteOn(noteNumber, velocity, randValue)) {
|
if (region->registerNoteOn(noteNumber, velocity, randValue)) {
|
||||||
unsigned activeNotesInGroup { 0 };
|
unsigned notePolyphonyCounter { 0 };
|
||||||
unsigned activeNotes { 0 };
|
|
||||||
Voice* selfMaskCandidate { nullptr };
|
Voice* selfMaskCandidate { nullptr };
|
||||||
|
Voice* selectedVoice { nullptr };
|
||||||
|
regionPolyphonyArray.clear();
|
||||||
|
|
||||||
for (auto& voice : voices) {
|
for (auto& voice : voices) {
|
||||||
const auto voiceRegion = voice->getRegion();
|
if (voice->isFree()) {
|
||||||
if (voiceRegion == nullptr)
|
if (selectedVoice == nullptr)
|
||||||
|
selectedVoice = voice.get();
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (voiceRegion->group == region->group)
|
if (voice->getRegion() == region) {
|
||||||
activeNotesInGroup += 1;
|
regionPolyphonyArray.push_back(voice.get());
|
||||||
|
}
|
||||||
|
|
||||||
if (region->notePolyphony) {
|
if (region->notePolyphony) {
|
||||||
if (voice->getTriggerNumber() == noteNumber && voice->getTriggerType() == Voice::TriggerType::NoteOn) {
|
if (voice->getTriggerNumber() == noteNumber && voice->getTriggerType() == Voice::TriggerType::NoteOn) {
|
||||||
activeNotes += 1;
|
notePolyphonyCounter += 1;
|
||||||
switch (region->selfMask) {
|
switch (region->selfMask) {
|
||||||
case SfzSelfMask::mask:
|
case SfzSelfMask::mask:
|
||||||
if (voice->getTriggerValue() < velocity) {
|
if (voice->getTriggerValue() < velocity) {
|
||||||
|
|
@ -856,22 +892,62 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc
|
||||||
noteOffDispatch(delay, voice->getTriggerNumber(), voice->getTriggerValue());
|
noteOffDispatch(delay, voice->getTriggerNumber(), voice->getTriggerValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeNotesInGroup >= groupMaxPolyphony[region->group])
|
// Polyphony reached on note_polyphony
|
||||||
continue;
|
if (region->notePolyphony && notePolyphonyCounter >= *region->notePolyphony) {
|
||||||
|
|
||||||
if (region->notePolyphony && activeNotes >= *region->notePolyphony) {
|
|
||||||
if (selfMaskCandidate != nullptr)
|
if (selfMaskCandidate != nullptr)
|
||||||
selfMaskCandidate->release(delay);
|
selfMaskCandidate->release(delay);
|
||||||
else // We're the lowest velocity guy here
|
else // We're the lowest velocity guy here
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto voice = findFreeVoice();
|
auto parent = region->parent;
|
||||||
if (voice == nullptr)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn);
|
// Polyphony reached on region
|
||||||
ring.addVoiceToRing(voice);
|
if (regionPolyphonyArray.size() >= region->polyphony) {
|
||||||
|
selectedVoice = stealer.steal(absl::MakeSpan(regionPolyphonyArray));
|
||||||
|
goto render;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Polyphony reached on polyphony group
|
||||||
|
if (polyphonyGroups[region->group].getActiveVoices().size()
|
||||||
|
== polyphonyGroups[region->group].getPolyphonyLimit()) {
|
||||||
|
const auto activeVoices = absl::MakeSpan(polyphonyGroups[region->group].getActiveVoices());
|
||||||
|
selectedVoice = stealer.steal(activeVoices);
|
||||||
|
goto render;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Polyphony reached some parent group/master/etc
|
||||||
|
while (parent != nullptr) {
|
||||||
|
if (parent->getActiveVoices().size() >= parent->getPolyphonyLimit()) {
|
||||||
|
const auto activeVoices = absl::MakeSpan(parent->getActiveVoices());
|
||||||
|
selectedVoice = stealer.steal(activeVoices);
|
||||||
|
goto render;
|
||||||
|
}
|
||||||
|
parent = parent->getParent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Engine polyphony reached, we're stealing something
|
||||||
|
if (selectedVoice == nullptr) {
|
||||||
|
selectedVoice = stealer.steal(absl::MakeSpan(voiceViewArray));
|
||||||
|
}
|
||||||
|
|
||||||
|
render:
|
||||||
|
// Kill voice if necessary, pre-rendering it into the output buffers
|
||||||
|
ASSERT(selectedVoice);
|
||||||
|
if (!selectedVoice->isFree()) {
|
||||||
|
auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock);
|
||||||
|
SisterVoiceRing::applyToRing(selectedVoice, [&] (Voice* v) {
|
||||||
|
renderVoiceToOutputs(*v, *tempSpan);
|
||||||
|
v->reset();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Voice should be free now
|
||||||
|
ASSERT(selectedVoice->isFree());
|
||||||
|
selectedVoice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn);
|
||||||
|
ring.addVoiceToRing(selectedVoice);
|
||||||
|
RegionSet::registerVoiceInHierarchy(region, selectedVoice);
|
||||||
|
polyphonyGroups[region->group].registerVoice(selectedVoice);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -926,6 +1002,8 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept
|
||||||
}
|
}
|
||||||
|
|
||||||
ring.addVoiceToRing(voice);
|
ring.addVoiceToRing(voice);
|
||||||
|
RegionSet::registerVoiceInHierarchy(region, voice);
|
||||||
|
polyphonyGroups[region->group].registerVoice(voice);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1088,6 +1166,16 @@ const sfz::EffectBus* sfz::Synth::getEffectBusView(int idx) const noexcept
|
||||||
return (size_t)idx < effectBuses.size() ? effectBuses[idx].get() : nullptr;
|
return (size_t)idx < effectBuses.size() ? effectBuses[idx].get() : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sfz::RegionSet* sfz::Synth::getRegionSetView(int idx) const noexcept
|
||||||
|
{
|
||||||
|
return (size_t)idx < sets.size() ? sets[idx].get() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sfz::PolyphonyGroup* sfz::Synth::getPolyphonyGroupView(int idx) const noexcept
|
||||||
|
{
|
||||||
|
return (size_t)idx < polyphonyGroups.size() ? &polyphonyGroups[idx] : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
const sfz::Region* sfz::Synth::getRegionById(NumericId<Region> id) const noexcept
|
const sfz::Region* sfz::Synth::getRegionById(NumericId<Region> id) const noexcept
|
||||||
{
|
{
|
||||||
const size_t size = regions.size();
|
const size_t size = regions.size();
|
||||||
|
|
@ -1127,6 +1215,11 @@ const sfz::Voice* sfz::Synth::getVoiceView(int idx) const noexcept
|
||||||
return (size_t)idx < voices.size() ? voices[idx].get() : nullptr;
|
return (size_t)idx < voices.size() ? voices[idx].get() : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsigned sfz::Synth::getNumPolyphonyGroups() const noexcept
|
||||||
|
{
|
||||||
|
return polyphonyGroups.size();
|
||||||
|
}
|
||||||
|
|
||||||
const std::vector<std::string>& sfz::Synth::getUnknownOpcodes() const noexcept
|
const std::vector<std::string>& sfz::Synth::getUnknownOpcodes() const noexcept
|
||||||
{
|
{
|
||||||
return unknownOpcodes;
|
return unknownOpcodes;
|
||||||
|
|
@ -1207,6 +1300,9 @@ void sfz::Synth::resetVoices(int numVoices)
|
||||||
voiceViewArray.clear();
|
voiceViewArray.clear();
|
||||||
voiceViewArray.reserve(numVoices);
|
voiceViewArray.reserve(numVoices);
|
||||||
|
|
||||||
|
regionPolyphonyArray.clear();
|
||||||
|
regionPolyphonyArray.reserve(numVoices);
|
||||||
|
|
||||||
for (auto& voice : voices) {
|
for (auto& voice : voices) {
|
||||||
voice->setSampleRate(this->sampleRate);
|
voice->setSampleRate(this->sampleRate);
|
||||||
voice->setSamplesPerBlock(this->samplesPerBlock);
|
voice->setSamplesPerBlock(this->samplesPerBlock);
|
||||||
|
|
@ -1235,8 +1331,10 @@ void sfz::Synth::setOversamplingFactor(sfz::Oversampling factor) noexcept
|
||||||
if (factor == oversamplingFactor)
|
if (factor == oversamplingFactor)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
for (auto& voice : voices)
|
for (auto& voice : voices) {
|
||||||
|
|
||||||
voice->reset();
|
voice->reset();
|
||||||
|
}
|
||||||
|
|
||||||
resources.filePool.emptyFileLoadingQueues();
|
resources.filePool.emptyFileLoadingQueues();
|
||||||
resources.filePool.setOversamplingFactor(factor);
|
resources.filePool.setOversamplingFactor(factor);
|
||||||
|
|
@ -1348,8 +1446,8 @@ void sfz::Synth::allSoundOff() noexcept
|
||||||
|
|
||||||
void sfz::Synth::setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept
|
void sfz::Synth::setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept
|
||||||
{
|
{
|
||||||
while (groupMaxPolyphony.size() <= groupIdx)
|
while (polyphonyGroups.size() <= groupIdx)
|
||||||
groupMaxPolyphony.push_back(config::maxVoices);
|
polyphonyGroups.emplace_back();
|
||||||
|
|
||||||
groupMaxPolyphony[groupIdx] = polyphony;
|
polyphonyGroups[groupIdx].setPolyphonyLimit(polyphony);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,14 @@
|
||||||
#include "Parser.h"
|
#include "Parser.h"
|
||||||
#include "Voice.h"
|
#include "Voice.h"
|
||||||
#include "Region.h"
|
#include "Region.h"
|
||||||
|
#include "RegionSet.h"
|
||||||
|
#include "PolyphonyGroup.h"
|
||||||
#include "Effects.h"
|
#include "Effects.h"
|
||||||
#include "LeakDetector.h"
|
#include "LeakDetector.h"
|
||||||
#include "MidiState.h"
|
#include "MidiState.h"
|
||||||
#include "AudioSpan.h"
|
#include "AudioSpan.h"
|
||||||
#include "parser/Parser.h"
|
#include "parser/Parser.h"
|
||||||
|
#include "VoiceStealing.h"
|
||||||
#include "absl/types/span.h"
|
#include "absl/types/span.h"
|
||||||
#include <absl/types/optional.h>
|
#include <absl/types/optional.h>
|
||||||
#include <random>
|
#include <random>
|
||||||
|
|
@ -220,17 +223,39 @@ public:
|
||||||
* for testing.
|
* for testing.
|
||||||
*
|
*
|
||||||
* @param idx
|
* @param idx
|
||||||
* @return const Region*
|
* @return const Voice*
|
||||||
*/
|
*/
|
||||||
const Voice* getVoiceView(int idx) const noexcept;
|
const Voice* getVoiceView(int idx) const noexcept;
|
||||||
/**
|
/**
|
||||||
* @brief Get a raw view into a specific voice. This is mostly used
|
* @brief Get a raw view into a specific effect bus. This is mostly used
|
||||||
* for testing.
|
* for testing.
|
||||||
*
|
*
|
||||||
* @param idx
|
* @param idx
|
||||||
* @return const Region*
|
* @return const EffectBus*
|
||||||
*/
|
*/
|
||||||
const EffectBus* getEffectBusView(int idx) const noexcept;
|
const EffectBus* getEffectBusView(int idx) const noexcept;
|
||||||
|
/**
|
||||||
|
* @brief Get a raw view into a specific set of regions. This is mostly used
|
||||||
|
* for testing.
|
||||||
|
*
|
||||||
|
* @param idx
|
||||||
|
* @return const RegionSet*
|
||||||
|
*/
|
||||||
|
const RegionSet* getRegionSetView(int idx) const noexcept;
|
||||||
|
/**
|
||||||
|
* @brief Get a raw view into a specific polyphony group. This is mostly used
|
||||||
|
* for testing.
|
||||||
|
*
|
||||||
|
* @param idx
|
||||||
|
* @return const PolyphonyGroup*
|
||||||
|
*/
|
||||||
|
const PolyphonyGroup* getPolyphonyGroupView(int idx) const noexcept;
|
||||||
|
/**
|
||||||
|
* @brief Get the number of polyphony groups
|
||||||
|
*
|
||||||
|
* @return unsigned
|
||||||
|
*/
|
||||||
|
unsigned getNumPolyphonyGroups() const noexcept;
|
||||||
/**
|
/**
|
||||||
* @brief Get a list of unknown opcodes. The lifetime of the
|
* @brief Get a list of unknown opcodes. The lifetime of the
|
||||||
* string views in the code are linked to the currently loaded
|
* string views in the code are linked to the currently loaded
|
||||||
|
|
@ -572,7 +597,6 @@ private:
|
||||||
* @param polyphone the max polyphony
|
* @param polyphone the max polyphony
|
||||||
*/
|
*/
|
||||||
void setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept;
|
void setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept;
|
||||||
std::vector<unsigned> groupMaxPolyphony { config::maxVoices };
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Reset all CCs; to be used on CC 121
|
* @brief Reset all CCs; to be used on CC 121
|
||||||
|
|
@ -598,6 +622,12 @@ private:
|
||||||
* @param members the opcodes of the <global> block
|
* @param members the opcodes of the <global> block
|
||||||
*/
|
*/
|
||||||
void handleGlobalOpcodes(const std::vector<Opcode>& members);
|
void handleGlobalOpcodes(const std::vector<Opcode>& members);
|
||||||
|
/**
|
||||||
|
* @brief Helper function to dispatch <master> opcodes
|
||||||
|
*
|
||||||
|
* @param members the opcodes of the <master> block
|
||||||
|
*/
|
||||||
|
void handleMasterOpcodes(const std::vector<Opcode>& members);
|
||||||
/**
|
/**
|
||||||
* @brief Helper function to dispatch <group> opcodes
|
* @brief Helper function to dispatch <group> opcodes
|
||||||
*
|
*
|
||||||
|
|
@ -649,8 +679,6 @@ private:
|
||||||
void noteOnDispatch(int delay, int noteNumber, float velocity) noexcept;
|
void noteOnDispatch(int delay, int noteNumber, float velocity) noexcept;
|
||||||
void noteOffDispatch(int delay, int noteNumber, float velocity) noexcept;
|
void noteOffDispatch(int delay, int noteNumber, float velocity) noexcept;
|
||||||
|
|
||||||
unsigned killSisterVoices(const Voice* voiceToKill) noexcept;
|
|
||||||
|
|
||||||
// Opcode memory; these are used to build regions, as a new region
|
// Opcode memory; these are used to build regions, as a new region
|
||||||
// will integrate opcodes from the group, master and global block
|
// will integrate opcodes from the group, master and global block
|
||||||
std::vector<Opcode> globalOpcodes;
|
std::vector<Opcode> globalOpcodes;
|
||||||
|
|
@ -672,15 +700,27 @@ private:
|
||||||
// Default active switch if multiple keyswitchable regions are present
|
// Default active switch if multiple keyswitchable regions are present
|
||||||
absl::optional<uint8_t> defaultSwitch;
|
absl::optional<uint8_t> defaultSwitch;
|
||||||
std::vector<std::string> unknownOpcodes;
|
std::vector<std::string> unknownOpcodes;
|
||||||
using RegionPtrVector = std::vector<Region*>;
|
using RegionViewVector = std::vector<Region*>;
|
||||||
using VoicePtrVector = std::vector<Voice*>;
|
using VoiceViewVector = std::vector<Voice*>;
|
||||||
std::vector<std::unique_ptr<Region>> regions;
|
using VoicePtr = std::unique_ptr<Voice>;
|
||||||
std::vector<std::unique_ptr<Voice>> voices;
|
using RegionPtr = std::unique_ptr<Region>;
|
||||||
|
using RegionSetPtr = std::unique_ptr<RegionSet>;
|
||||||
|
std::vector<RegionPtr> regions;
|
||||||
|
std::vector<VoicePtr> voices;
|
||||||
|
// These are more general "groups" than sfz and encapsulates the full hierarchy
|
||||||
|
RegionSet* currentSet;
|
||||||
|
OpcodeScope lastHeader { OpcodeScope::kOpcodeScopeGlobal };
|
||||||
|
std::vector<RegionSetPtr> sets;
|
||||||
|
// These are the `group=` groups where you can off voices
|
||||||
|
std::vector<PolyphonyGroup> polyphonyGroups;
|
||||||
// Views to speed up iteration over the regions and voices when events
|
// Views to speed up iteration over the regions and voices when events
|
||||||
// occur in the audio callback
|
// occur in the audio callback
|
||||||
VoicePtrVector voiceViewArray;
|
VoiceViewVector regionPolyphonyArray;
|
||||||
std::array<RegionPtrVector, 128> noteActivationLists;
|
VoiceStealing stealer;
|
||||||
std::array<RegionPtrVector, config::numCCs> ccActivationLists;
|
|
||||||
|
VoiceViewVector voiceViewArray;
|
||||||
|
std::array<RegionViewVector, 128> noteActivationLists;
|
||||||
|
std::array<RegionViewVector, config::numCCs> ccActivationLists;
|
||||||
|
|
||||||
// Effect factory and buses
|
// Effect factory and buses
|
||||||
EffectFactory effectFactory;
|
EffectFactory effectFactory;
|
||||||
|
|
|
||||||
|
|
@ -136,21 +136,24 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value,
|
||||||
for (auto& modId : allModifiers) {
|
for (auto& modId : allModifiers) {
|
||||||
ASSERT(modifierSmoothers[modId].size() >= region->modifiers[modId].size());
|
ASSERT(modifierSmoothers[modId].size() >= region->modifiers[modId].size());
|
||||||
forEachWithSmoother(modId, [modId, this](const CCData<Modifier>& mod, Smoother& smoother) {
|
forEachWithSmoother(modId, [modId, this](const CCData<Modifier>& mod, Smoother& smoother) {
|
||||||
|
const auto ccValue = resources.midiState.getCCValue(mod.cc);
|
||||||
|
const auto curve = resources.curves.getCurve(mod.data.curve);
|
||||||
|
const auto finalValue = curve.evalNormalized(ccValue) * mod.data.value;
|
||||||
switch (modId) {
|
switch (modId) {
|
||||||
case Mod::volume:
|
case Mod::volume:
|
||||||
smoother.reset(db2mag(resources.midiState.getCCValue(mod.cc) * mod.data.value));
|
smoother.reset(db2mag(finalValue));
|
||||||
break;
|
break;
|
||||||
case Mod::pitch:
|
case Mod::pitch:
|
||||||
smoother.reset(centsFactor(resources.midiState.getCCValue(mod.cc) * mod.data.value));
|
smoother.reset(centsFactor(finalValue));
|
||||||
break;
|
break;
|
||||||
case Mod::amplitude:
|
case Mod::amplitude:
|
||||||
case Mod::pan:
|
case Mod::pan:
|
||||||
case Mod::width:
|
case Mod::width:
|
||||||
case Mod::position:
|
case Mod::position:
|
||||||
smoother.reset(normalizePercents(resources.midiState.getCCValue(mod.cc) * mod.data.value));
|
smoother.reset(normalizePercents(finalValue));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
smoother.reset(resources.midiState.getCCValue(mod.cc) * mod.data.value);
|
smoother.reset(finalValue);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
smoother.setSmoothing(mod.data.smooth, sampleRate);
|
smoother.setSmoothing(mod.data.smooth, sampleRate);
|
||||||
|
|
@ -606,8 +609,20 @@ void sfz::Voice::fillWithGenerator(AudioSpan<float> buffer) noexcept
|
||||||
const auto rightSpan = buffer.getSpan(1);
|
const auto rightSpan = buffer.getSpan(1);
|
||||||
|
|
||||||
if (region->sampleId.filename() == "*noise") {
|
if (region->sampleId.filename() == "*noise") {
|
||||||
absl::c_generate(leftSpan, noiseDist);
|
auto gen = [&]() {
|
||||||
absl::c_generate(rightSpan, noiseDist);
|
return uniformNoiseDist(Random::randomGenerator);
|
||||||
|
};
|
||||||
|
absl::c_generate(leftSpan, gen);
|
||||||
|
absl::c_generate(rightSpan, gen);
|
||||||
|
} else if (region->sampleId.filename() == "*gnoise") {
|
||||||
|
// You need to wrap in a lambda, otherwise generate will
|
||||||
|
// make a copy of the gaussian distribution *along with its state*
|
||||||
|
// leading to periodic behavior....
|
||||||
|
auto gen = [&]() {
|
||||||
|
return gaussianNoiseDist();
|
||||||
|
};
|
||||||
|
absl::c_generate(leftSpan, gen);
|
||||||
|
absl::c_generate(rightSpan, gen);
|
||||||
} else {
|
} else {
|
||||||
const auto numFrames = buffer.getNumFrames();
|
const auto numFrames = buffer.getNumFrames();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -467,7 +467,8 @@ private:
|
||||||
Voice* nextSisterVoice { this };
|
Voice* nextSisterVoice { this };
|
||||||
Voice* previousSisterVoice { this };
|
Voice* previousSisterVoice { this };
|
||||||
|
|
||||||
fast_gaussian_generator<float> noiseDist { 0.0f, config::noiseVariance };
|
fast_real_distribution<float> uniformNoiseDist { -config::uniformNoiseBounds, config::uniformNoiseBounds };
|
||||||
|
fast_gaussian_generator<float> gaussianNoiseDist { 0.0f, config::noiseVariance };
|
||||||
|
|
||||||
ModifierArray<std::vector<Smoother>> modifierSmoothers;
|
ModifierArray<std::vector<Smoother>> modifierSmoothers;
|
||||||
Smoother gainSmoother;
|
Smoother gainSmoother;
|
||||||
|
|
|
||||||
53
src/sfizz/VoiceStealing.cpp
Normal file
53
src/sfizz/VoiceStealing.cpp
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
#include "VoiceStealing.h"
|
||||||
|
|
||||||
|
sfz::VoiceStealing::VoiceStealing()
|
||||||
|
{
|
||||||
|
voiceScores.reserve(config::maxVoices);
|
||||||
|
}
|
||||||
|
|
||||||
|
sfz::Voice* sfz::VoiceStealing::steal(absl::Span<sfz::Voice*> voices) noexcept
|
||||||
|
{
|
||||||
|
// Start of the voice stealing algorithm
|
||||||
|
absl::c_sort(voices, voiceOrdering);
|
||||||
|
|
||||||
|
const auto sumEnvelope = absl::c_accumulate(voices, 0.0f, [](float sum, const Voice* v) {
|
||||||
|
return sum + v->getAverageEnvelope();
|
||||||
|
});
|
||||||
|
// We are checking the envelope to try and kill voices with relative low contribution
|
||||||
|
// to the output compared to the rest.
|
||||||
|
const auto envThreshold = sumEnvelope
|
||||||
|
/ static_cast<float>(voices.size()) * config::stealingEnvelopeCoeff;
|
||||||
|
// We are checking the age so that voices have the time to build up attack
|
||||||
|
// This is not perfect because pad-type voices will take a long time to output
|
||||||
|
// their sound, but it's reasonable for sounds with a quick attack and longer
|
||||||
|
// release.
|
||||||
|
const auto ageThreshold = voices.front()->getAge() * config::stealingAgeCoeff;
|
||||||
|
// This needs to be positive
|
||||||
|
ASSERT(ageThreshold >= 0);
|
||||||
|
|
||||||
|
Voice* returnedVoice = voices.front();
|
||||||
|
unsigned idx = 0;
|
||||||
|
while (idx < voices.size()) {
|
||||||
|
const auto ref = voices[idx];
|
||||||
|
|
||||||
|
if (ref->getAge() <= ageThreshold) {
|
||||||
|
// Went too far, we'll kill the oldest note.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
float maxEnvelope { 0.0f };
|
||||||
|
SisterVoiceRing::applyToRing(ref, [&](Voice* v) {
|
||||||
|
maxEnvelope = max(maxEnvelope, v->getAverageEnvelope());
|
||||||
|
});
|
||||||
|
|
||||||
|
if (maxEnvelope < envThreshold) {
|
||||||
|
returnedVoice = ref;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jump over the sister voices in the set
|
||||||
|
do { idx++; }
|
||||||
|
while (idx < voices.size() && sisterVoices(ref, voices[idx]));
|
||||||
|
}
|
||||||
|
return returnedVoice;
|
||||||
|
}
|
||||||
54
src/sfizz/VoiceStealing.h
Normal file
54
src/sfizz/VoiceStealing.h
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
// 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 "Voice.h"
|
||||||
|
#include "SisterVoiceRing.h"
|
||||||
|
#include <vector>
|
||||||
|
#include "absl/types/span.h"
|
||||||
|
|
||||||
|
namespace sfz
|
||||||
|
{
|
||||||
|
class VoiceStealing
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
VoiceStealing();
|
||||||
|
/**
|
||||||
|
* @brief Propose a voice to steal from a set of voices
|
||||||
|
*
|
||||||
|
* @param voices
|
||||||
|
* @return Voice*
|
||||||
|
*/
|
||||||
|
Voice* steal(absl::Span<Voice*> voices) noexcept;
|
||||||
|
private:
|
||||||
|
struct VoiceScore
|
||||||
|
{
|
||||||
|
Voice* voice;
|
||||||
|
double score;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VoiceScoreComparator
|
||||||
|
{
|
||||||
|
bool operator()(const VoiceScore& voiceScore, const double& score)
|
||||||
|
{
|
||||||
|
return (voiceScore.score < score);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator()(const double& score, const VoiceScore& voiceScore)
|
||||||
|
{
|
||||||
|
return (score < voiceScore.score);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator()(const VoiceScore& lhs, const VoiceScore& rhs)
|
||||||
|
{
|
||||||
|
return (lhs.score < rhs.score);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
std::vector<VoiceScore> voiceScores;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,7 @@ set(SFIZZ_TEST_SOURCES
|
||||||
MidiStateT.cpp
|
MidiStateT.cpp
|
||||||
InterpolatorsT.cpp
|
InterpolatorsT.cpp
|
||||||
SmoothersT.cpp
|
SmoothersT.cpp
|
||||||
|
PolyphonyT.cpp
|
||||||
RegionActivationT.cpp
|
RegionActivationT.cpp
|
||||||
RegionValueComputationsT.cpp
|
RegionValueComputationsT.cpp
|
||||||
# If we're tweaking the curves this kind of tests does not make sense
|
# If we're tweaking the curves this kind of tests does not make sense
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,21 @@ TEST_CASE("[EGDescription] Attack range")
|
||||||
sfz::MidiState state;
|
sfz::MidiState state;
|
||||||
eg.attack = 1;
|
eg.attack = 1;
|
||||||
eg.vel2attack = -1.27f;
|
eg.vel2attack = -1.27f;
|
||||||
eg.ccAttack = { 63, 1.27f };
|
eg.ccAttack[63] = 1.27f;
|
||||||
REQUIRE(eg.getAttack(state, 0_norm) == 1.0f);
|
REQUIRE(eg.getAttack(state, 0_norm) == 1.0f);
|
||||||
REQUIRE(eg.getAttack(state, 127_norm) == 0.0f);
|
REQUIRE(eg.getAttack(state, 127_norm) == 0.0f);
|
||||||
state.ccEvent(0, 63, 127_norm);
|
state.ccEvent(0, 63, 127_norm);
|
||||||
REQUIRE(eg.getAttack(state, 127_norm) == 1.0f);
|
REQUIRE(eg.getAttack(state, 127_norm) == 1.0f);
|
||||||
REQUIRE(eg.getAttack(state, 0_norm) == 2.27f);
|
REQUIRE(eg.getAttack(state, 0_norm) == 2.27f);
|
||||||
eg.ccAttack = { 63, 127.0f };
|
eg.ccAttack[63] = 127.0f;
|
||||||
REQUIRE(eg.getAttack(state, 0_norm) == 100.0f);
|
REQUIRE(eg.getAttack(state, 0_norm) == 100.0f);
|
||||||
|
eg.ccAttack[63] = 1.27f;
|
||||||
|
eg.ccAttack[65] = 1.0f;
|
||||||
|
REQUIRE(eg.getAttack(state, 0_norm) == 2.27f);
|
||||||
|
REQUIRE(eg.getAttack(state, 127_norm) == 1.0f);
|
||||||
|
state.ccEvent(0, 65, 127_norm);
|
||||||
|
REQUIRE(eg.getAttack(state, 0_norm) == 3.27f);
|
||||||
|
REQUIRE(eg.getAttack(state, 127_norm) == 2.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("[EGDescription] Delay range")
|
TEST_CASE("[EGDescription] Delay range")
|
||||||
|
|
@ -33,14 +40,21 @@ TEST_CASE("[EGDescription] Delay range")
|
||||||
sfz::MidiState state;
|
sfz::MidiState state;
|
||||||
eg.delay = 1;
|
eg.delay = 1;
|
||||||
eg.vel2delay = -1.27f;
|
eg.vel2delay = -1.27f;
|
||||||
eg.ccDelay = { 63, 1.27f };
|
eg.ccDelay[63] = 1.27f;
|
||||||
REQUIRE(eg.getDelay(state, 0_norm) == 1.0f);
|
REQUIRE(eg.getDelay(state, 0_norm) == 1.0f);
|
||||||
REQUIRE(eg.getDelay(state, 127_norm) == 0.0f);
|
REQUIRE(eg.getDelay(state, 127_norm) == 0.0f);
|
||||||
state.ccEvent(0, 63, 127_norm);
|
state.ccEvent(0, 63, 127_norm);
|
||||||
REQUIRE(eg.getDelay(state, 127_norm) == 1.0f);
|
REQUIRE(eg.getDelay(state, 127_norm) == 1.0f);
|
||||||
REQUIRE(eg.getDelay(state, 0_norm) == 2.27f);
|
REQUIRE(eg.getDelay(state, 0_norm) == 2.27f);
|
||||||
eg.ccDelay = { 63, 127.0f };
|
eg.ccDelay[63] = 127.0f;
|
||||||
REQUIRE(eg.getDelay(state, 0_norm) == 100.0f);
|
REQUIRE(eg.getDelay(state, 0_norm) == 100.0f);
|
||||||
|
eg.ccDelay[63] = 1.27f;
|
||||||
|
eg.ccDelay[65] = 1.0f;
|
||||||
|
REQUIRE(eg.getDelay(state, 0_norm) == 2.27f);
|
||||||
|
REQUIRE(eg.getDelay(state, 127_norm) == 1.0f);
|
||||||
|
state.ccEvent(0, 65, 127_norm);
|
||||||
|
REQUIRE(eg.getDelay(state, 0_norm) == 3.27f);
|
||||||
|
REQUIRE(eg.getDelay(state, 127_norm) == 2.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("[EGDescription] Decay range")
|
TEST_CASE("[EGDescription] Decay range")
|
||||||
|
|
@ -49,14 +63,21 @@ TEST_CASE("[EGDescription] Decay range")
|
||||||
sfz::MidiState state;
|
sfz::MidiState state;
|
||||||
eg.decay = 1.0f;
|
eg.decay = 1.0f;
|
||||||
eg.vel2decay = -1.27f;
|
eg.vel2decay = -1.27f;
|
||||||
eg.ccDecay = { 63, 1.27f };
|
eg.ccDecay[63] = 1.27f;
|
||||||
REQUIRE(eg.getDecay(state, 0_norm) == 1.0f);
|
REQUIRE(eg.getDecay(state, 0_norm) == 1.0f);
|
||||||
REQUIRE(eg.getDecay(state, 127_norm) == 0.0f);
|
REQUIRE(eg.getDecay(state, 127_norm) == 0.0f);
|
||||||
state.ccEvent(0, 63, 127_norm);
|
state.ccEvent(0, 63, 127_norm);
|
||||||
REQUIRE(eg.getDecay(state, 127_norm) == 1.0f);
|
REQUIRE(eg.getDecay(state, 127_norm) == 1.0f);
|
||||||
REQUIRE(eg.getDecay(state, 0_norm) == 2.27f);
|
REQUIRE(eg.getDecay(state, 0_norm) == 2.27f);
|
||||||
eg.ccDecay = { 63, 127.0f };
|
eg.ccDecay[63] = 127.0f;
|
||||||
REQUIRE(eg.getDecay(state, 0_norm) == 100.0f);
|
REQUIRE(eg.getDecay(state, 0_norm) == 100.0f);
|
||||||
|
eg.ccDecay[63] = 1.27f;
|
||||||
|
eg.ccDecay[65] = 1.0f;
|
||||||
|
REQUIRE(eg.getDecay(state, 0_norm) == 2.27f);
|
||||||
|
REQUIRE(eg.getDecay(state, 127_norm) == 1.0f);
|
||||||
|
state.ccEvent(0, 65, 127_norm);
|
||||||
|
REQUIRE(eg.getDecay(state, 0_norm) == 3.27f);
|
||||||
|
REQUIRE(eg.getDecay(state, 127_norm) == 2.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("[EGDescription] Release range")
|
TEST_CASE("[EGDescription] Release range")
|
||||||
|
|
@ -65,14 +86,21 @@ TEST_CASE("[EGDescription] Release range")
|
||||||
sfz::MidiState state;
|
sfz::MidiState state;
|
||||||
eg.release = 1;
|
eg.release = 1;
|
||||||
eg.vel2release = -1.27f;
|
eg.vel2release = -1.27f;
|
||||||
eg.ccRelease = { 63, 1.27f };
|
eg.ccRelease[63] = 1.27f;
|
||||||
REQUIRE(eg.getRelease(state, 0_norm) == 1.0f);
|
REQUIRE(eg.getRelease(state, 0_norm) == 1.0f);
|
||||||
REQUIRE(eg.getRelease(state, 127_norm) == 0.0f);
|
REQUIRE(eg.getRelease(state, 127_norm) == 0.0f);
|
||||||
state.ccEvent(0, 63, 127_norm);
|
state.ccEvent(0, 63, 127_norm);
|
||||||
REQUIRE(eg.getRelease(state, 127_norm) == 1.0f);
|
REQUIRE(eg.getRelease(state, 127_norm) == 1.0f);
|
||||||
REQUIRE(eg.getRelease(state, 0_norm) == 2.27f);
|
REQUIRE(eg.getRelease(state, 0_norm) == 2.27f);
|
||||||
eg.ccRelease = { 63, 127.0f };
|
eg.ccRelease[63] = 127.0f;
|
||||||
REQUIRE(eg.getRelease(state, 0_norm) == 100.0f);
|
REQUIRE(eg.getRelease(state, 0_norm) == 100.0f);
|
||||||
|
eg.ccRelease[63] = 1.27f;
|
||||||
|
eg.ccRelease[65] = 1.0f;
|
||||||
|
REQUIRE(eg.getRelease(state, 0_norm) == 2.27f);
|
||||||
|
REQUIRE(eg.getRelease(state, 127_norm) == 1.0f);
|
||||||
|
state.ccEvent(0, 65, 127_norm);
|
||||||
|
REQUIRE(eg.getRelease(state, 0_norm) == 3.27f);
|
||||||
|
REQUIRE(eg.getRelease(state, 127_norm) == 2.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("[EGDescription] Hold range")
|
TEST_CASE("[EGDescription] Hold range")
|
||||||
|
|
@ -81,14 +109,21 @@ TEST_CASE("[EGDescription] Hold range")
|
||||||
sfz::MidiState state;
|
sfz::MidiState state;
|
||||||
eg.hold = 1;
|
eg.hold = 1;
|
||||||
eg.vel2hold = -1.27f;
|
eg.vel2hold = -1.27f;
|
||||||
eg.ccHold = { 63, 1.27f };
|
eg.ccHold[63] = 1.27f;
|
||||||
REQUIRE(eg.getHold(state, 0_norm) == 1.0f);
|
REQUIRE(eg.getHold(state, 0_norm) == 1.0f);
|
||||||
REQUIRE(eg.getHold(state, 127_norm) == 0.0f);
|
REQUIRE(eg.getHold(state, 127_norm) == 0.0f);
|
||||||
state.ccEvent(0, 63, 127_norm);
|
state.ccEvent(0, 63, 127_norm);
|
||||||
REQUIRE(eg.getHold(state, 127_norm) == 1.0f);
|
REQUIRE(eg.getHold(state, 127_norm) == 1.0f);
|
||||||
REQUIRE(eg.getHold(state, 0_norm) == 2.27f);
|
REQUIRE(eg.getHold(state, 0_norm) == 2.27f);
|
||||||
eg.ccHold = { 63, 127.0f };
|
eg.ccHold[63] = 127.0f;
|
||||||
REQUIRE(eg.getHold(state, 0_norm) == 100.0f);
|
REQUIRE(eg.getHold(state, 0_norm) == 100.0f);
|
||||||
|
eg.ccHold[63] = 1.27f;
|
||||||
|
eg.ccHold[65] = 1.0f;
|
||||||
|
REQUIRE(eg.getHold(state, 0_norm) == 2.27f);
|
||||||
|
REQUIRE(eg.getHold(state, 127_norm) == 1.0f);
|
||||||
|
state.ccEvent(0, 65, 127_norm);
|
||||||
|
REQUIRE(eg.getHold(state, 0_norm) == 3.27f);
|
||||||
|
REQUIRE(eg.getHold(state, 127_norm) == 2.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("[EGDescription] Sustain level")
|
TEST_CASE("[EGDescription] Sustain level")
|
||||||
|
|
@ -97,13 +132,21 @@ TEST_CASE("[EGDescription] Sustain level")
|
||||||
sfz::MidiState state;
|
sfz::MidiState state;
|
||||||
eg.sustain = 50;
|
eg.sustain = 50;
|
||||||
eg.vel2sustain = -100;
|
eg.vel2sustain = -100;
|
||||||
eg.ccSustain = { 63, 100.0f };
|
eg.ccSustain[63] = 100.0f;
|
||||||
REQUIRE(eg.getSustain(state, 0_norm) == 50.0f);
|
REQUIRE(eg.getSustain(state, 0_norm) == 50.0f);
|
||||||
REQUIRE(eg.getSustain(state, 127_norm) == 0.0f);
|
REQUIRE(eg.getSustain(state, 127_norm) == 0.0f);
|
||||||
state.ccEvent(0, 63, 127_norm);
|
state.ccEvent(0, 63, 127_norm);
|
||||||
REQUIRE(eg.getSustain(state, 127_norm) == 50.0f);
|
REQUIRE(eg.getSustain(state, 127_norm) == 50.0f);
|
||||||
eg.ccSustain = { 63, 200.0f };
|
eg.ccSustain[63] = 200.0f;
|
||||||
REQUIRE(eg.getSustain(state, 0_norm) == 100.0f);
|
REQUIRE(eg.getSustain(state, 0_norm) == 100.0f);
|
||||||
|
eg.sustain = 0;
|
||||||
|
eg.ccSustain[63] = 50.0f;
|
||||||
|
eg.ccSustain[65] = 50.0f;
|
||||||
|
REQUIRE(eg.getSustain(state, 0_norm) == 50.0f);
|
||||||
|
REQUIRE(eg.getSustain(state, 127_norm) == 0.0f);
|
||||||
|
state.ccEvent(0, 65, 127_norm);
|
||||||
|
REQUIRE(eg.getSustain(state, 0_norm) == 100.0f);
|
||||||
|
REQUIRE(eg.getSustain(state, 127_norm) == 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("[EGDescription] Start level")
|
TEST_CASE("[EGDescription] Start level")
|
||||||
|
|
@ -111,11 +154,19 @@ TEST_CASE("[EGDescription] Start level")
|
||||||
sfz::EGDescription eg;
|
sfz::EGDescription eg;
|
||||||
sfz::MidiState state;
|
sfz::MidiState state;
|
||||||
eg.start = 0;
|
eg.start = 0;
|
||||||
eg.ccStart = { 63, 127.0f };
|
eg.ccStart[63] = 127.0f;
|
||||||
REQUIRE(eg.getStart(state, 0_norm) == 0.0f);
|
REQUIRE(eg.getStart(state, 0_norm) == 0.0f);
|
||||||
REQUIRE(eg.getStart(state, 127_norm) == 0.0f);
|
REQUIRE(eg.getStart(state, 127_norm) == 0.0f);
|
||||||
state.ccEvent(0, 63, 127_norm);
|
state.ccEvent(0, 63, 127_norm);
|
||||||
REQUIRE(eg.getStart(state, 0_norm) == 100.0f);
|
REQUIRE(eg.getStart(state, 0_norm) == 100.0f);
|
||||||
eg.ccStart = { 63, -127.0f };
|
eg.ccStart[63] = -127.0f;
|
||||||
REQUIRE(eg.getStart(state, 0_norm) == 0.0f);
|
REQUIRE(eg.getStart(state, 0_norm) == 0.0f);
|
||||||
|
eg.start = 0;
|
||||||
|
eg.ccStart[63] = 50.0f;
|
||||||
|
eg.ccStart[65] = 50.0f;
|
||||||
|
REQUIRE(eg.getStart(state, 0_norm) == 50.0f);
|
||||||
|
REQUIRE(eg.getStart(state, 127_norm) == 50.0f);
|
||||||
|
state.ccEvent(0, 65, 127_norm);
|
||||||
|
REQUIRE(eg.getStart(state, 0_norm) == 100.0f);
|
||||||
|
REQUIRE(eg.getStart(state, 127_norm) == 100.0f);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@
|
||||||
|
|
||||||
#include "sfizz/Interpolators.h"
|
#include "sfizz/Interpolators.h"
|
||||||
#include "catch2/catch.hpp"
|
#include "catch2/catch.hpp"
|
||||||
#include <algorithm>
|
#include <array>
|
||||||
|
#include <numeric>
|
||||||
using namespace Catch::literals;
|
using namespace Catch::literals;
|
||||||
|
|
||||||
TEST_CASE("[Interpolators] Sample at points")
|
TEST_CASE("[Interpolators] Sample at points")
|
||||||
|
|
|
||||||
226
tests/PolyphonyT.cpp
Normal file
226
tests/PolyphonyT.cpp
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
// 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 "sfizz/Synth.h"
|
||||||
|
#include "sfizz/SfzHelpers.h"
|
||||||
|
#include "catch2/catch.hpp"
|
||||||
|
|
||||||
|
using namespace Catch::literals;
|
||||||
|
using namespace sfz::literals;
|
||||||
|
|
||||||
|
constexpr int blockSize { 256 };
|
||||||
|
|
||||||
|
|
||||||
|
TEST_CASE("[Polyphony] Polyphony in hierarchy")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzString(fs::current_path(), R"(
|
||||||
|
<region> key=61 sample=*sine polyphony=2
|
||||||
|
<group> polyphony=2
|
||||||
|
<region> key=62 sample=*sine
|
||||||
|
<master> polyphony=3
|
||||||
|
<region> key=63 sample=*sine
|
||||||
|
<region> key=63 sample=*sine
|
||||||
|
<region> key=63 sample=*sine
|
||||||
|
<group> polyphony=4
|
||||||
|
<region> key=64 sample=*sine polyphony=5
|
||||||
|
<region> key=64 sample=*sine
|
||||||
|
<region> key=64 sample=*sine
|
||||||
|
<region> key=64 sample=*sine
|
||||||
|
)");
|
||||||
|
REQUIRE( synth.getRegionView(0)->polyphony == 2 );
|
||||||
|
REQUIRE( synth.getRegionSetView(1)->getPolyphonyLimit() == 2 );
|
||||||
|
REQUIRE( synth.getRegionView(1)->polyphony == 2 );
|
||||||
|
REQUIRE( synth.getRegionSetView(2)->getPolyphonyLimit() == 3 );
|
||||||
|
REQUIRE( synth.getRegionSetView(2)->getRegions()[0]->polyphony == 3 );
|
||||||
|
REQUIRE( synth.getRegionSetView(3)->getPolyphonyLimit() == 4 );
|
||||||
|
REQUIRE( synth.getRegionSetView(3)->getRegions()[0]->polyphony == 5 );
|
||||||
|
REQUIRE( synth.getRegionSetView(3)->getRegions()[1]->polyphony == 4 );
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Polyphony] Polyphony groups")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzString(fs::current_path(), R"(
|
||||||
|
<group> polyphony=2
|
||||||
|
<region> key=62 sample=*sine
|
||||||
|
<group> group=1 polyphony=3
|
||||||
|
<region> key=63 sample=*sine
|
||||||
|
<region> key=63 sample=*sine group=2 polyphony=4
|
||||||
|
<region> key=63 sample=*sine group=4 polyphony=5
|
||||||
|
<group> group=4
|
||||||
|
<region> key=62 sample=*sine
|
||||||
|
)");
|
||||||
|
REQUIRE( synth.getNumPolyphonyGroups() == 5 );
|
||||||
|
REQUIRE( synth.getNumRegions() == 5 );
|
||||||
|
REQUIRE( synth.getRegionView(0)->group == 0 );
|
||||||
|
REQUIRE( synth.getRegionView(1)->group == 1 );
|
||||||
|
REQUIRE( synth.getRegionView(2)->group == 2 );
|
||||||
|
REQUIRE( synth.getRegionView(3)->group == 4 );
|
||||||
|
REQUIRE( synth.getRegionView(3)->polyphony == 5 );
|
||||||
|
REQUIRE( synth.getRegionView(4)->group == 4 );
|
||||||
|
REQUIRE( synth.getPolyphonyGroupView(1)->getPolyphonyLimit() == 3 );
|
||||||
|
REQUIRE( synth.getPolyphonyGroupView(2)->getPolyphonyLimit() == 4 );
|
||||||
|
REQUIRE( synth.getPolyphonyGroupView(3)->getPolyphonyLimit() == sfz::config::maxVoices );
|
||||||
|
REQUIRE( synth.getPolyphonyGroupView(4)->getPolyphonyLimit() == 5 );
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Polyphony] group polyphony limits")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzString(fs::current_path(), R"(
|
||||||
|
<group> group=1 polyphony=2
|
||||||
|
<region> sample=*sine key=65
|
||||||
|
)");
|
||||||
|
synth.noteOn(0, 65, 64);
|
||||||
|
synth.noteOn(0, 65, 64);
|
||||||
|
synth.noteOn(0, 65, 64);
|
||||||
|
REQUIRE(synth.getNumActiveVoices() == 2); // group polyphony should block the last note
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Polyphony] Hierarchy polyphony limits")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzString(fs::current_path(), R"(
|
||||||
|
<group> polyphony=2
|
||||||
|
<region> sample=*sine key=65
|
||||||
|
)");
|
||||||
|
synth.noteOn(0, 65, 64);
|
||||||
|
synth.noteOn(0, 65, 64);
|
||||||
|
synth.noteOn(0, 65, 64);
|
||||||
|
REQUIRE(synth.getNumActiveVoices() == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Polyphony] Hierarchy polyphony limits (group)")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzString(fs::current_path(), R"(
|
||||||
|
<group> polyphony=2
|
||||||
|
<region> sample=*sine key=65
|
||||||
|
)");
|
||||||
|
synth.noteOn(0, 65, 64);
|
||||||
|
synth.noteOn(0, 65, 64);
|
||||||
|
synth.noteOn(0, 65, 64);
|
||||||
|
REQUIRE(synth.getNumActiveVoices() == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Polyphony] Hierarchy polyphony limits (master)")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzString(fs::current_path(), R"(
|
||||||
|
<master> polyphony=2
|
||||||
|
<group> polyphony=5
|
||||||
|
<region> sample=*sine key=65
|
||||||
|
)");
|
||||||
|
synth.noteOn(0, 65, 64);
|
||||||
|
synth.noteOn(0, 65, 64);
|
||||||
|
synth.noteOn(0, 65, 64);
|
||||||
|
REQUIRE(synth.getNumActiveVoices() == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Polyphony] Hierarchy polyphony limits (limit in another master)")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzString(fs::current_path(), R"(
|
||||||
|
<master> polyphony=2
|
||||||
|
<region> sample=*saw key=65
|
||||||
|
<master>
|
||||||
|
<group> polyphony=5
|
||||||
|
<region> sample=*sine key=66
|
||||||
|
)");
|
||||||
|
synth.noteOn(0, 65, 64);
|
||||||
|
synth.noteOn(0, 65, 64);
|
||||||
|
synth.noteOn(0, 65, 64);
|
||||||
|
synth.noteOn(0, 66, 64);
|
||||||
|
synth.noteOn(0, 66, 64);
|
||||||
|
synth.noteOn(0, 66, 64);
|
||||||
|
REQUIRE(synth.getNumActiveVoices() == 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Polyphony] Hierarchy polyphony limits (global)")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzString(fs::current_path(), R"(
|
||||||
|
<global> polyphony=2
|
||||||
|
<group> polyphony=5
|
||||||
|
<region> sample=*sine key=65
|
||||||
|
)");
|
||||||
|
synth.noteOn(0, 65, 64);
|
||||||
|
synth.noteOn(0, 65, 64);
|
||||||
|
synth.noteOn(0, 65, 64);
|
||||||
|
REQUIRE(synth.getNumActiveVoices() == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Polyphony] Polyphony in master")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.setSamplesPerBlock(blockSize);
|
||||||
|
sfz::AudioBuffer<float> buffer { 2, blockSize };
|
||||||
|
synth.loadSfzString(fs::current_path(), R"(
|
||||||
|
<master> polyphony=2
|
||||||
|
<group> group=2
|
||||||
|
<region> sample=*sine key=65
|
||||||
|
<group> group=3
|
||||||
|
<region> sample=*sine key=63
|
||||||
|
<master> // Empty master resets the polyphony
|
||||||
|
<region> sample=*sine key=61
|
||||||
|
)");
|
||||||
|
synth.noteOn(0, 65, 64);
|
||||||
|
synth.noteOn(0, 65, 64);
|
||||||
|
synth.noteOn(0, 65, 64);
|
||||||
|
REQUIRE(synth.getNumActiveVoices() == 2); // group polyphony should block the last note
|
||||||
|
synth.allSoundOff();
|
||||||
|
synth.renderBlock(buffer);
|
||||||
|
REQUIRE(synth.getNumActiveVoices() == 0);
|
||||||
|
synth.noteOn(0, 63, 64);
|
||||||
|
synth.noteOn(0, 63, 64);
|
||||||
|
synth.noteOn(0, 63, 64);
|
||||||
|
REQUIRE(synth.getNumActiveVoices() == 2); // group polyphony should block the last note
|
||||||
|
synth.allSoundOff();
|
||||||
|
synth.renderBlock(buffer);
|
||||||
|
REQUIRE(synth.getNumActiveVoices() == 0);
|
||||||
|
synth.noteOn(0, 61, 64);
|
||||||
|
synth.noteOn(0, 61, 64);
|
||||||
|
synth.noteOn(0, 61, 64);
|
||||||
|
REQUIRE(synth.getNumActiveVoices() == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST_CASE("[Polyphony] Self-masking")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzString(fs::current_path(), R"(
|
||||||
|
<region> sample=*sine key=64 note_polyphony=2
|
||||||
|
)");
|
||||||
|
synth.noteOn(0, 64, 63);
|
||||||
|
synth.noteOn(0, 64, 62);
|
||||||
|
synth.noteOn(0, 64, 64);
|
||||||
|
REQUIRE(synth.getNumActiveVoices() == 3); // One of these is releasing
|
||||||
|
REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm);
|
||||||
|
REQUIRE(!synth.getVoiceView(0)->releasedOrFree());
|
||||||
|
REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm);
|
||||||
|
REQUIRE(synth.getVoiceView(1)->releasedOrFree()); // The lowest velocity voice is the masking candidate
|
||||||
|
REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm);
|
||||||
|
REQUIRE(!synth.getVoiceView(2)->releasedOrFree());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Polyphony] Not self-masking")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzString(fs::current_path(), R"(
|
||||||
|
<region> sample=*sine key=66 note_polyphony=2 note_selfmask=off
|
||||||
|
)");
|
||||||
|
synth.noteOn(0, 66, 63);
|
||||||
|
synth.noteOn(0, 66, 62);
|
||||||
|
synth.noteOn(0, 66, 64);
|
||||||
|
REQUIRE(synth.getNumActiveVoices() == 3); // One of these is releasing
|
||||||
|
REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm);
|
||||||
|
REQUIRE(synth.getVoiceView(0)->releasedOrFree()); // The first encountered voice is the masking candidate
|
||||||
|
REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm);
|
||||||
|
REQUIRE(!synth.getVoiceView(1)->releasedOrFree());
|
||||||
|
REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm);
|
||||||
|
REQUIRE(!synth.getVoiceView(2)->releasedOrFree());
|
||||||
|
}
|
||||||
|
|
@ -522,8 +522,8 @@ TEST_CASE("[Region] Parsing opcodes")
|
||||||
REQUIRE(region.volume == -123.0f);
|
REQUIRE(region.volume == -123.0f);
|
||||||
region.parseOpcode({ "volume", "-185" });
|
region.parseOpcode({ "volume", "-185" });
|
||||||
REQUIRE(region.volume == -144.0f);
|
REQUIRE(region.volume == -144.0f);
|
||||||
region.parseOpcode({ "volume", "19" });
|
region.parseOpcode({ "volume", "79" });
|
||||||
REQUIRE(region.volume == 6.0f);
|
REQUIRE(region.volume == 48.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("pan")
|
SECTION("pan")
|
||||||
|
|
@ -1083,13 +1083,13 @@ TEST_CASE("[Region] Parsing opcodes")
|
||||||
SECTION("ampeg_XX_onccNN")
|
SECTION("ampeg_XX_onccNN")
|
||||||
{
|
{
|
||||||
// Defaults
|
// Defaults
|
||||||
REQUIRE(!region.amplitudeEG.ccAttack);
|
REQUIRE(region.amplitudeEG.ccAttack.empty());
|
||||||
REQUIRE(!region.amplitudeEG.ccDecay);
|
REQUIRE(region.amplitudeEG.ccDecay.empty());
|
||||||
REQUIRE(!region.amplitudeEG.ccDelay);
|
REQUIRE(region.amplitudeEG.ccDelay.empty());
|
||||||
REQUIRE(!region.amplitudeEG.ccHold);
|
REQUIRE(region.amplitudeEG.ccHold.empty());
|
||||||
REQUIRE(!region.amplitudeEG.ccRelease);
|
REQUIRE(region.amplitudeEG.ccRelease.empty());
|
||||||
REQUIRE(!region.amplitudeEG.ccStart);
|
REQUIRE(region.amplitudeEG.ccStart.empty());
|
||||||
REQUIRE(!region.amplitudeEG.ccSustain);
|
REQUIRE(region.amplitudeEG.ccSustain.empty());
|
||||||
//
|
//
|
||||||
region.parseOpcode({ "ampeg_attack_oncc1", "1" });
|
region.parseOpcode({ "ampeg_attack_oncc1", "1" });
|
||||||
region.parseOpcode({ "ampeg_decay_oncc2", "2" });
|
region.parseOpcode({ "ampeg_decay_oncc2", "2" });
|
||||||
|
|
@ -1098,27 +1098,20 @@ TEST_CASE("[Region] Parsing opcodes")
|
||||||
region.parseOpcode({ "ampeg_release_oncc5", "5" });
|
region.parseOpcode({ "ampeg_release_oncc5", "5" });
|
||||||
region.parseOpcode({ "ampeg_start_oncc6", "6" });
|
region.parseOpcode({ "ampeg_start_oncc6", "6" });
|
||||||
region.parseOpcode({ "ampeg_sustain_oncc7", "7" });
|
region.parseOpcode({ "ampeg_sustain_oncc7", "7" });
|
||||||
REQUIRE(region.amplitudeEG.ccAttack);
|
REQUIRE(region.amplitudeEG.ccAttack.contains(1));
|
||||||
REQUIRE(region.amplitudeEG.ccDecay);
|
REQUIRE(region.amplitudeEG.ccDecay.contains(2));
|
||||||
REQUIRE(region.amplitudeEG.ccDelay);
|
REQUIRE(region.amplitudeEG.ccDelay.contains(3));
|
||||||
REQUIRE(region.amplitudeEG.ccHold);
|
REQUIRE(region.amplitudeEG.ccHold.contains(4));
|
||||||
REQUIRE(region.amplitudeEG.ccRelease);
|
REQUIRE(region.amplitudeEG.ccRelease.contains(5));
|
||||||
REQUIRE(region.amplitudeEG.ccStart);
|
REQUIRE(region.amplitudeEG.ccStart.contains(6));
|
||||||
REQUIRE(region.amplitudeEG.ccSustain);
|
REQUIRE(region.amplitudeEG.ccSustain.contains(7));
|
||||||
REQUIRE(region.amplitudeEG.ccAttack->cc == 1);
|
REQUIRE(region.amplitudeEG.ccAttack[1] == 1.0f);
|
||||||
REQUIRE(region.amplitudeEG.ccDecay->cc == 2);
|
REQUIRE(region.amplitudeEG.ccDecay[2] == 2.0f);
|
||||||
REQUIRE(region.amplitudeEG.ccDelay->cc == 3);
|
REQUIRE(region.amplitudeEG.ccDelay[3] == 3.0f);
|
||||||
REQUIRE(region.amplitudeEG.ccHold->cc == 4);
|
REQUIRE(region.amplitudeEG.ccHold[4] == 4.0f);
|
||||||
REQUIRE(region.amplitudeEG.ccRelease->cc == 5);
|
REQUIRE(region.amplitudeEG.ccRelease[5] == 5.0f);
|
||||||
REQUIRE(region.amplitudeEG.ccStart->cc == 6);
|
REQUIRE(region.amplitudeEG.ccStart[6] == 6.0f);
|
||||||
REQUIRE(region.amplitudeEG.ccSustain->cc == 7);
|
REQUIRE(region.amplitudeEG.ccSustain[7] == 7.0f);
|
||||||
REQUIRE(region.amplitudeEG.ccAttack->data == 1.0f);
|
|
||||||
REQUIRE(region.amplitudeEG.ccDecay->data == 2.0f);
|
|
||||||
REQUIRE(region.amplitudeEG.ccDelay->data == 3.0f);
|
|
||||||
REQUIRE(region.amplitudeEG.ccHold->data == 4.0f);
|
|
||||||
REQUIRE(region.amplitudeEG.ccRelease->data == 5.0f);
|
|
||||||
REQUIRE(region.amplitudeEG.ccStart->data == 6.0f);
|
|
||||||
REQUIRE(region.amplitudeEG.ccSustain->data == 7.0f);
|
|
||||||
//
|
//
|
||||||
region.parseOpcode({ "ampeg_attack_oncc1", "101" });
|
region.parseOpcode({ "ampeg_attack_oncc1", "101" });
|
||||||
region.parseOpcode({ "ampeg_decay_oncc2", "101" });
|
region.parseOpcode({ "ampeg_decay_oncc2", "101" });
|
||||||
|
|
@ -1127,13 +1120,13 @@ TEST_CASE("[Region] Parsing opcodes")
|
||||||
region.parseOpcode({ "ampeg_release_oncc5", "101" });
|
region.parseOpcode({ "ampeg_release_oncc5", "101" });
|
||||||
region.parseOpcode({ "ampeg_start_oncc6", "101" });
|
region.parseOpcode({ "ampeg_start_oncc6", "101" });
|
||||||
region.parseOpcode({ "ampeg_sustain_oncc7", "101" });
|
region.parseOpcode({ "ampeg_sustain_oncc7", "101" });
|
||||||
REQUIRE(region.amplitudeEG.ccAttack->data == 100.0f);
|
REQUIRE(region.amplitudeEG.ccAttack[1] == 100.0f);
|
||||||
REQUIRE(region.amplitudeEG.ccDecay->data == 100.0f);
|
REQUIRE(region.amplitudeEG.ccDecay[2] == 100.0f);
|
||||||
REQUIRE(region.amplitudeEG.ccDelay->data == 100.0f);
|
REQUIRE(region.amplitudeEG.ccDelay[3] == 100.0f);
|
||||||
REQUIRE(region.amplitudeEG.ccHold->data == 100.0f);
|
REQUIRE(region.amplitudeEG.ccHold[4] == 100.0f);
|
||||||
REQUIRE(region.amplitudeEG.ccRelease->data == 100.0f);
|
REQUIRE(region.amplitudeEG.ccRelease[5] == 100.0f);
|
||||||
REQUIRE(region.amplitudeEG.ccStart->data == 100.0f);
|
REQUIRE(region.amplitudeEG.ccStart[6] == 100.0f);
|
||||||
REQUIRE(region.amplitudeEG.ccSustain->data == 100.0f);
|
REQUIRE(region.amplitudeEG.ccSustain[7] == 100.0f);
|
||||||
//
|
//
|
||||||
region.parseOpcode({ "ampeg_attack_oncc1", "-101" });
|
region.parseOpcode({ "ampeg_attack_oncc1", "-101" });
|
||||||
region.parseOpcode({ "ampeg_decay_oncc2", "-101" });
|
region.parseOpcode({ "ampeg_decay_oncc2", "-101" });
|
||||||
|
|
@ -1142,13 +1135,56 @@ TEST_CASE("[Region] Parsing opcodes")
|
||||||
region.parseOpcode({ "ampeg_release_oncc5", "-101" });
|
region.parseOpcode({ "ampeg_release_oncc5", "-101" });
|
||||||
region.parseOpcode({ "ampeg_start_oncc6", "-101" });
|
region.parseOpcode({ "ampeg_start_oncc6", "-101" });
|
||||||
region.parseOpcode({ "ampeg_sustain_oncc7", "-101" });
|
region.parseOpcode({ "ampeg_sustain_oncc7", "-101" });
|
||||||
REQUIRE(region.amplitudeEG.ccAttack->data == -100.0f);
|
REQUIRE(region.amplitudeEG.ccAttack[1] == -100.0f);
|
||||||
REQUIRE(region.amplitudeEG.ccDecay->data == -100.0f);
|
REQUIRE(region.amplitudeEG.ccDecay[2] == -100.0f);
|
||||||
REQUIRE(region.amplitudeEG.ccDelay->data == -100.0f);
|
REQUIRE(region.amplitudeEG.ccDelay[3] == -100.0f);
|
||||||
REQUIRE(region.amplitudeEG.ccHold->data == -100.0f);
|
REQUIRE(region.amplitudeEG.ccHold[4] == -100.0f);
|
||||||
REQUIRE(region.amplitudeEG.ccRelease->data == -100.0f);
|
REQUIRE(region.amplitudeEG.ccRelease[5] == -100.0f);
|
||||||
REQUIRE(region.amplitudeEG.ccStart->data == -100.0f);
|
REQUIRE(region.amplitudeEG.ccStart[6] == -100.0f);
|
||||||
REQUIRE(region.amplitudeEG.ccSustain->data == -100.0f);
|
REQUIRE(region.amplitudeEG.ccSustain[7] == -100.0f);
|
||||||
|
//
|
||||||
|
region.parseOpcode({ "ampeg_attack_oncc1", "1" });
|
||||||
|
region.parseOpcode({ "ampeg_decay_oncc2", "2" });
|
||||||
|
region.parseOpcode({ "ampeg_delay_oncc3", "3" });
|
||||||
|
region.parseOpcode({ "ampeg_hold_oncc4", "4" });
|
||||||
|
region.parseOpcode({ "ampeg_release_oncc5", "5" });
|
||||||
|
region.parseOpcode({ "ampeg_start_oncc6", "6" });
|
||||||
|
region.parseOpcode({ "ampeg_sustain_oncc7", "7" });
|
||||||
|
region.parseOpcode({ "ampeg_attack_oncc2", "2" });
|
||||||
|
region.parseOpcode({ "ampeg_decay_oncc3", "3" });
|
||||||
|
region.parseOpcode({ "ampeg_delay_oncc4", "4" });
|
||||||
|
region.parseOpcode({ "ampeg_hold_oncc5", "5" });
|
||||||
|
region.parseOpcode({ "ampeg_release_oncc6", "6" });
|
||||||
|
region.parseOpcode({ "ampeg_start_oncc7", "7" });
|
||||||
|
region.parseOpcode({ "ampeg_sustain_oncc8", "8" });
|
||||||
|
REQUIRE(region.amplitudeEG.ccAttack.contains(1));
|
||||||
|
REQUIRE(region.amplitudeEG.ccDecay.contains(2));
|
||||||
|
REQUIRE(region.amplitudeEG.ccDelay.contains(3));
|
||||||
|
REQUIRE(region.amplitudeEG.ccHold.contains(4));
|
||||||
|
REQUIRE(region.amplitudeEG.ccRelease.contains(5));
|
||||||
|
REQUIRE(region.amplitudeEG.ccStart.contains(6));
|
||||||
|
REQUIRE(region.amplitudeEG.ccSustain.contains(7));
|
||||||
|
REQUIRE(region.amplitudeEG.ccAttack.contains(2));
|
||||||
|
REQUIRE(region.amplitudeEG.ccDecay.contains(3));
|
||||||
|
REQUIRE(region.amplitudeEG.ccDelay.contains(4));
|
||||||
|
REQUIRE(region.amplitudeEG.ccHold.contains(5));
|
||||||
|
REQUIRE(region.amplitudeEG.ccRelease.contains(6));
|
||||||
|
REQUIRE(region.amplitudeEG.ccStart.contains(7));
|
||||||
|
REQUIRE(region.amplitudeEG.ccSustain.contains(8));
|
||||||
|
REQUIRE(region.amplitudeEG.ccAttack[1] == 1.0f);
|
||||||
|
REQUIRE(region.amplitudeEG.ccDecay[2] == 2.0f);
|
||||||
|
REQUIRE(region.amplitudeEG.ccDelay[3] == 3.0f);
|
||||||
|
REQUIRE(region.amplitudeEG.ccHold[4] == 4.0f);
|
||||||
|
REQUIRE(region.amplitudeEG.ccRelease[5] == 5.0f);
|
||||||
|
REQUIRE(region.amplitudeEG.ccStart[6] == 6.0f);
|
||||||
|
REQUIRE(region.amplitudeEG.ccSustain[7] == 7.0f);
|
||||||
|
REQUIRE(region.amplitudeEG.ccAttack[2] == 2.0f);
|
||||||
|
REQUIRE(region.amplitudeEG.ccDecay[3] == 3.0f);
|
||||||
|
REQUIRE(region.amplitudeEG.ccDelay[4] == 4.0f);
|
||||||
|
REQUIRE(region.amplitudeEG.ccHold[5] == 5.0f);
|
||||||
|
REQUIRE(region.amplitudeEG.ccRelease[6] == 6.0f);
|
||||||
|
REQUIRE(region.amplitudeEG.ccStart[7] == 7.0f);
|
||||||
|
REQUIRE(region.amplitudeEG.ccSustain[8] == 8.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("sustain_sw and sostenuto_sw")
|
SECTION("sustain_sw and sostenuto_sw")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue