Merge pull request #214 from paulfd/note-stealing
Improve the note stealing
This commit is contained in:
commit
d6c24be064
19 changed files with 455 additions and 205 deletions
|
|
@ -8,6 +8,7 @@
|
|||
#include "OnePoleFilter.h"
|
||||
#include "SfzFilter.h"
|
||||
#include "ScopedFTZ.h"
|
||||
#include "SfzHelpers.h"
|
||||
#include <benchmark/benchmark.h>
|
||||
#include <random>
|
||||
#include <numeric>
|
||||
|
|
@ -57,7 +58,7 @@ BENCHMARK_DEFINE_F(FilterFixture, OnePole_VA)(benchmark::State& state) {
|
|||
const auto sentinel = cutoff.data() + blockSize;
|
||||
while (cutoffPtr < sentinel)
|
||||
{
|
||||
const auto gain = sfz::OnePoleFilter<float>::normalizedGain(*cutoffPtr, sampleRate);
|
||||
const auto gain = sfz::vaGain(*cutoffPtr, sampleRate);
|
||||
filter.setGain(gain);
|
||||
filter.processLowpass({ inputPtr, step }, { outputPtr, step } );
|
||||
cutoffPtr += step;
|
||||
|
|
|
|||
51
lv2/sfizz.c
51
lv2/sfizz.c
|
|
@ -74,7 +74,7 @@
|
|||
#define PITCH_BUILD_AND_CENTER(first_byte, last_byte) (int)(((unsigned int)last_byte << 7) + (unsigned int)first_byte) - 8192
|
||||
#define MAX_BLOCK_SIZE 8192
|
||||
#define MAX_PATH_SIZE 1024
|
||||
#define MAX_VOICES 256
|
||||
#define MAX_VOICES 512
|
||||
#define DEFAULT_VOICES 64
|
||||
#define DEFAULT_OVERSAMPLING SFIZZ_OVERSAMPLING_X1
|
||||
#define DEFAULT_PRELOAD 8192
|
||||
|
|
@ -584,7 +584,7 @@ sfizz_lv2_check_num_voices(sfizz_plugin_t* self)
|
|||
num_voices_atom.body = num_voices;
|
||||
if (self->worker->schedule_work(self->worker->handle,
|
||||
lv2_atom_total_size((LV2_Atom *)&num_voices_atom),
|
||||
&num_voices_atom) == LV2_WORKER_SUCCESS)
|
||||
&num_voices_atom) != LV2_WORKER_SUCCESS)
|
||||
{
|
||||
lv2_log_error(&self->logger, "[sfizz] There was an issue changing the number of voices\n");
|
||||
}
|
||||
|
|
@ -906,8 +906,7 @@ work(LV2_Handle instance,
|
|||
const void *data)
|
||||
{
|
||||
sfizz_plugin_t *self = (sfizz_plugin_t *)instance;
|
||||
if (!data)
|
||||
{
|
||||
if (!data) {
|
||||
lv2_log_error(&self->logger, "[sfizz] Ignoring empty data in the worker thread\n");
|
||||
return LV2_WORKER_ERR_UNKNOWN;
|
||||
}
|
||||
|
|
@ -922,12 +921,9 @@ work(LV2_Handle instance,
|
|||
|
||||
const char *sfz_file_path = LV2_ATOM_BODY_CONST(atom);
|
||||
self->changing_state = true;
|
||||
if (sfizz_load_file(self->synth, sfz_file_path))
|
||||
{
|
||||
if (sfizz_load_file(self->synth, sfz_file_path)) {
|
||||
sfizz_lv2_update_file_info(self, sfz_file_path);
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
lv2_log_error(&self->logger, "[sfizz] Error with %s; no file should be loaded\n", sfz_file_path);
|
||||
}
|
||||
self->changing_state = false;
|
||||
|
|
@ -939,12 +935,18 @@ work(LV2_Handle instance,
|
|||
return LV2_WORKER_SUCCESS;
|
||||
}
|
||||
|
||||
self->changing_state = true;
|
||||
const int num_voices = *(const int *)LV2_ATOM_BODY_CONST(atom);
|
||||
if (sfizz_get_num_voices(self->synth) == num_voices) {
|
||||
return LV2_WORKER_SUCCESS; // Nothing to do
|
||||
}
|
||||
|
||||
self->changing_state = true;
|
||||
sfizz_set_num_voices(self->synth, num_voices);
|
||||
if (sfizz_get_num_voices(self->synth) == num_voices) {
|
||||
self->num_voices = num_voices;
|
||||
lv2_log_note(&self->logger, "[sfizz] Number of voices changed to: %d\n", num_voices);
|
||||
} else {
|
||||
lv2_log_error(&self->logger, "[sfizz] Error changing the number of voices\n");
|
||||
}
|
||||
self->changing_state = false;
|
||||
}
|
||||
|
|
@ -955,12 +957,18 @@ work(LV2_Handle instance,
|
|||
return LV2_WORKER_SUCCESS;
|
||||
}
|
||||
|
||||
self->changing_state = true;
|
||||
const unsigned int preload_size = *(const unsigned int *)LV2_ATOM_BODY_CONST(atom);
|
||||
if (sfizz_get_preload_size(self->synth) == preload_size) {
|
||||
return LV2_WORKER_SUCCESS; // Nothing to do
|
||||
}
|
||||
|
||||
self->changing_state = true;
|
||||
sfizz_set_preload_size(self->synth, preload_size);
|
||||
if (sfizz_get_preload_size(self->synth) == preload_size) {
|
||||
self->preload_size = preload_size;
|
||||
lv2_log_note(&self->logger, "[sfizz] Preload size changed to: %d\n", preload_size);
|
||||
} else {
|
||||
lv2_log_error(&self->logger, "[sfizz] Error changing the preload size\n");
|
||||
}
|
||||
self->changing_state = false;
|
||||
}
|
||||
|
|
@ -970,13 +978,20 @@ work(LV2_Handle instance,
|
|||
respond(handle, size, data); // send back so that we reschedule the check
|
||||
return LV2_WORKER_SUCCESS;
|
||||
}
|
||||
self->changing_state = true;
|
||||
|
||||
const sfizz_oversampling_factor_t oversampling =
|
||||
*(const sfizz_oversampling_factor_t *)LV2_ATOM_BODY_CONST(atom);
|
||||
if (sfizz_get_oversampling_factor(self->synth) == oversampling) {
|
||||
return LV2_WORKER_SUCCESS; // Nothing to do
|
||||
}
|
||||
|
||||
self->changing_state = true;
|
||||
sfizz_set_oversampling_factor(self->synth, oversampling);
|
||||
if (sfizz_get_oversampling_factor(self->synth) == oversampling) {
|
||||
self->oversampling = oversampling;
|
||||
lv2_log_note(&self->logger, "[sfizz] Oversampling changed to: %d\n", oversampling);
|
||||
} else {
|
||||
lv2_log_error(&self->logger, "[sfizz] Error changing the oversampling\n");
|
||||
}
|
||||
self->changing_state = false;
|
||||
}
|
||||
|
|
@ -992,13 +1007,12 @@ work(LV2_Handle instance,
|
|||
if (self->changing_state)
|
||||
return LV2_WORKER_SUCCESS;
|
||||
|
||||
lv2_log_note(&self->logger, "[sfizz] File %s seems to have been updated, reloading\n", self->sfz_file_path);
|
||||
if (sfizz_load_file(self->synth, self->sfz_file_path))
|
||||
{
|
||||
lv2_log_note(&self->logger,
|
||||
"[sfizz] File %s seems to have been updated, reloading\n",
|
||||
self->sfz_file_path);
|
||||
if (sfizz_load_file(self->synth, self->sfz_file_path)) {
|
||||
sfizz_lv2_update_file_info(self, self->sfz_file_path);
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
lv2_log_error(&self->logger, "[sfizz] Error with %s; no file should be loaded\n", self->sfz_file_path);
|
||||
}
|
||||
}
|
||||
|
|
@ -1012,7 +1026,6 @@ work(LV2_Handle instance,
|
|||
self->unmap->unmap(self->unmap->handle, atom->type));
|
||||
return LV2_WORKER_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
return LV2_WORKER_SUCCESS;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ midnam:update a lv2:Feature .
|
|||
lv2:portProperty pprop:expensive ;
|
||||
lv2:portProperty lv2:integer ;
|
||||
lv2:portProperty lv2:enumeration ;
|
||||
lv2:default 64 ;
|
||||
lv2:default 256 ;
|
||||
lv2:minimum 8 ;
|
||||
lv2:maximum 256 ;
|
||||
lv2:scalePoint [ rdfs:label "8 voices",
|
||||
|
|
@ -147,6 +147,11 @@ midnam:update a lv2:Feature .
|
|||
"256 Voci"@it;
|
||||
rdf:value 256
|
||||
] ;
|
||||
lv2:scalePoint [ rdfs:label "512 voices",
|
||||
"512 voix"@fr ,
|
||||
"512 Voci"@it;
|
||||
rdf:value 512
|
||||
] ;
|
||||
] , [
|
||||
a lv2:InputPort, lv2:ControlPort ;
|
||||
lv2:index 6 ;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include "Config.h"
|
||||
#include "Debug.h"
|
||||
#include "LeakDetector.h"
|
||||
#include "SIMDHelpers.h"
|
||||
#include "absl/types/span.h"
|
||||
#include "absl/memory/memory.h"
|
||||
#include <array>
|
||||
|
|
@ -259,6 +260,15 @@ public:
|
|||
numChannels = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes zeros in the buffer
|
||||
*/
|
||||
void clear()
|
||||
{
|
||||
for (size_t i = 0; i < numChannels; ++i)
|
||||
fill<Type>(getSpan(i), Type{ 0.0 });
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Add a positive number of channels to the buffer
|
||||
*
|
||||
|
|
@ -271,6 +281,22 @@ public:
|
|||
addChannel();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert implicitly to a pointer of channels
|
||||
*/
|
||||
operator const float* const*() const noexcept
|
||||
{
|
||||
return buffers.data();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert implicitly to a pointer of channels
|
||||
*/
|
||||
operator float* const*() noexcept
|
||||
{
|
||||
return buffers.data();
|
||||
}
|
||||
|
||||
private:
|
||||
using buffer_type = Buffer<Type, Alignment>;
|
||||
using buffer_ptr = std::unique_ptr<buffer_type>;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ namespace config {
|
|||
constexpr float defaultSampleRate { 48000 };
|
||||
constexpr int defaultSamplesPerBlock { 1024 };
|
||||
constexpr int maxBlockSize { 8192 };
|
||||
constexpr int bufferPoolSize { 4 };
|
||||
constexpr int bufferPoolSize { 6 };
|
||||
constexpr int stereoBufferPoolSize { 4 };
|
||||
constexpr int indexBufferPoolSize { 2 };
|
||||
constexpr int preloadSize { 8192 };
|
||||
|
|
@ -37,9 +37,9 @@ namespace config {
|
|||
constexpr bool loggingEnabled { false };
|
||||
constexpr size_t numChannels { 2 };
|
||||
constexpr int numBackgroundThreads { 4 };
|
||||
constexpr int numVoices { 64 };
|
||||
constexpr unsigned maxVoices { 256 };
|
||||
constexpr int maxFilePromises { maxVoices * 2 };
|
||||
constexpr int numVoices { 256 };
|
||||
constexpr unsigned maxVoices { 512 };
|
||||
constexpr int maxFilePromises { maxVoices };
|
||||
constexpr int sustainCC { 64 };
|
||||
constexpr int allSoundOffCC { 120 };
|
||||
constexpr int resetCC { 121 };
|
||||
|
|
@ -54,11 +54,21 @@ namespace config {
|
|||
constexpr Oversampling defaultOversamplingFactor { Oversampling::x1 };
|
||||
constexpr float A440 { 440.0 };
|
||||
constexpr size_t powerHistoryLength { 16 };
|
||||
constexpr float voiceStealingThreshold { 0.00001f };
|
||||
constexpr float filteredEnvelopeCutoff { 5 };
|
||||
constexpr uint16_t numCCs { 512 };
|
||||
constexpr int maxCurves { 256 };
|
||||
constexpr int chunkSize { 1024 };
|
||||
constexpr int filtersInPool { maxVoices * 2 };
|
||||
/**
|
||||
* @brief The threshold for age stealing.
|
||||
* In percentage of the voice's max age.
|
||||
*/
|
||||
constexpr float stealingAgeCoeff { 0.5f };
|
||||
/**
|
||||
* @brief The threshold for envelope stealing.
|
||||
* In percentage of the sum of all envelopes.
|
||||
*/
|
||||
constexpr float stealingEnvelopeCoeff { 0.5f };
|
||||
constexpr int filtersPerVoice { 2 };
|
||||
constexpr int eqsPerVoice { 3 };
|
||||
constexpr int oscillatorsPerVoice { 9 };
|
||||
|
|
|
|||
|
|
@ -109,6 +109,17 @@ void EffectBus::addToInputs(const float* const addInput[], float addGain, unsign
|
|||
}
|
||||
}
|
||||
|
||||
void EffectBus::applyGain(const float* gain, unsigned nframes)
|
||||
{
|
||||
if (!gain)
|
||||
return;
|
||||
|
||||
absl::Span<const float> gainSpan { gain, nframes };
|
||||
for (unsigned c = 0; c < EffectChannels; ++c) {
|
||||
sfz::applyGain<float>(gainSpan, _inputs.getSpan(c));
|
||||
}
|
||||
}
|
||||
|
||||
void EffectBus::setSampleRate(double sampleRate)
|
||||
{
|
||||
for (const auto& effectPtr : _effects)
|
||||
|
|
|
|||
|
|
@ -136,6 +136,11 @@ public:
|
|||
*/
|
||||
void addToInputs(const float* const addInput[], float addGain, unsigned nframes);
|
||||
|
||||
/**
|
||||
@brief Apply a gain to the inputs
|
||||
*/
|
||||
void applyGain(const float* gain, unsigned nframes);
|
||||
|
||||
/**
|
||||
@brief Initializes all effects in the bus with the given sample rate.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ public:
|
|||
buffer.resize(size);
|
||||
fill<ValueType>(absl::MakeSpan(buffer), 0.0);
|
||||
index = 0;
|
||||
validMean = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -46,6 +47,7 @@ public:
|
|||
*/
|
||||
void push(ValueType value)
|
||||
{
|
||||
validMean = false;
|
||||
if (size > 0) {
|
||||
buffer[index] = value;
|
||||
if (++index == size)
|
||||
|
|
@ -60,11 +62,18 @@ public:
|
|||
*/
|
||||
ValueType getAverage() const
|
||||
{
|
||||
return mean<ValueType>(buffer);
|
||||
if (!validMean) {
|
||||
mean = sfz::mean<ValueType>(buffer);
|
||||
validMean = true;
|
||||
}
|
||||
|
||||
return mean;
|
||||
}
|
||||
private:
|
||||
Buffer<ValueType> buffer;
|
||||
size_t size { 0 };
|
||||
size_t index { 0 };
|
||||
mutable bool validMean { true };
|
||||
mutable ValueType mean { 0.0 };
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -340,6 +340,66 @@ bool isValidAudio(absl::Span<Type> span)
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Finds the minimum size of 2 spans
|
||||
*
|
||||
* @tparam T
|
||||
* @tparam U
|
||||
* @param span1
|
||||
* @param span2
|
||||
* @return constexpr size_t
|
||||
*/
|
||||
template <class T, class U>
|
||||
constexpr size_t minSpanSize(absl::Span<T>& span1, absl::Span<U>& span2)
|
||||
{
|
||||
return min(span1.size(), span2.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Finds the minimum size of a list of spans.
|
||||
*
|
||||
* @tparam T
|
||||
* @tparam Others
|
||||
* @param first
|
||||
* @param others
|
||||
* @return constexpr size_t
|
||||
*/
|
||||
template <class T, class... Others>
|
||||
constexpr size_t minSpanSize(absl::Span<T>& first, Others... others)
|
||||
{
|
||||
return min(first.size(), minSpanSize(others...));
|
||||
}
|
||||
|
||||
template <class T>
|
||||
constexpr bool _checkSpanSizes(size_t size, absl::Span<T>& span1)
|
||||
{
|
||||
return span1.size() == size;
|
||||
}
|
||||
|
||||
template <class T, class... Others>
|
||||
constexpr bool _checkSpanSizes(size_t size, absl::Span<T>& span1, Others... others)
|
||||
{
|
||||
return span1.size() == size && _checkSpanSizes(size, others...);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check that all spans of a compile time list have the same size
|
||||
*
|
||||
* @tparam T
|
||||
* @tparam Others
|
||||
* @param first
|
||||
* @param others
|
||||
* @return constexpr size_t
|
||||
*/
|
||||
template <class T, class... Others>
|
||||
constexpr bool checkSpanSizes(const absl::Span<T>& span1, Others... others)
|
||||
{
|
||||
return _checkSpanSizes(span1.size(), others...);
|
||||
}
|
||||
|
||||
#define CHECK_SPAN_SIZES(...) ASSERT(checkSpanSizes(__VA_ARGS__))
|
||||
|
||||
|
||||
class ScopedRoundingMode {
|
||||
public:
|
||||
ScopedRoundingMode() = delete;
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@
|
|||
|
||||
#pragma once
|
||||
#include "Config.h"
|
||||
#include "Debug.h"
|
||||
#include "MathHelpers.h"
|
||||
#include "Macros.h"
|
||||
#include <absl/types/span.h>
|
||||
#include <cmath>
|
||||
|
||||
namespace sfz
|
||||
{
|
||||
namespace sfz {
|
||||
|
||||
/**
|
||||
* @brief An implementation of a one pole filter. This is a scalar
|
||||
* implementation.
|
||||
|
|
@ -22,108 +24,90 @@ template <class Type = float>
|
|||
class OnePoleFilter {
|
||||
public:
|
||||
OnePoleFilter() = default;
|
||||
// Normalized cutoff with respect to the sampling rate
|
||||
template <class C>
|
||||
static Type normalizedGain(Type cutoff, C sampleRate)
|
||||
{
|
||||
return std::tan(cutoff / static_cast<Type>(sampleRate) * pi<float>());
|
||||
}
|
||||
|
||||
OnePoleFilter(Type gain)
|
||||
{
|
||||
setGain(gain);
|
||||
}
|
||||
|
||||
void setGain(Type gain)
|
||||
{
|
||||
this->gain = gain;
|
||||
G = gain / (1 + gain);
|
||||
}
|
||||
|
||||
Type getGain() const { return gain; }
|
||||
|
||||
size_t processLowpass(absl::Span<const Type> input, absl::Span<Type> lowpass)
|
||||
void processLowpass(absl::Span<const Type> input, absl::Span<Type> output)
|
||||
{
|
||||
auto in = input.begin();
|
||||
auto out = lowpass.begin();
|
||||
auto size = std::min(input.size(), lowpass.size());
|
||||
auto sentinel = in + size;
|
||||
while (in < sentinel) {
|
||||
oneLowpass(in, out);
|
||||
in++;
|
||||
out++;
|
||||
}
|
||||
return size;
|
||||
CHECK_SPAN_SIZES(input, output);
|
||||
processLowpass(input.data(), output.data(), minSpanSize(input, output));
|
||||
}
|
||||
|
||||
size_t processHighpass(absl::Span<const Type> input, absl::Span<Type> highpass)
|
||||
void processHighpass(absl::Span<const Type> input, absl::Span<Type> output)
|
||||
{
|
||||
auto in = input.begin();
|
||||
auto out = highpass.begin();
|
||||
auto size = std::min(input.size(), highpass.size());
|
||||
auto sentinel = in + size;
|
||||
while (in < sentinel) {
|
||||
oneHighpass(in, out);
|
||||
in++;
|
||||
out++;
|
||||
}
|
||||
return size;
|
||||
CHECK_SPAN_SIZES(input, output);
|
||||
processHighpass(input.data(), output.data(), minSpanSize(input, output));
|
||||
}
|
||||
|
||||
size_t processLowpassVariableGain(absl::Span<const Type> input, absl::Span<Type> lowpass, absl::Span<const Type> gain)
|
||||
void processLowpass(absl::Span<const Type> input, absl::Span<Type> output, absl::Span<const Type> gain)
|
||||
{
|
||||
auto in = input.begin();
|
||||
auto out = lowpass.begin();
|
||||
auto g = gain.begin();
|
||||
auto size = min(input.size(), lowpass.size(), gain.size());
|
||||
auto sentinel = in + size;
|
||||
while (in < sentinel) {
|
||||
setGain(*g);
|
||||
oneLowpass(in, out);
|
||||
in++;
|
||||
out++;
|
||||
g++;
|
||||
}
|
||||
return size;
|
||||
CHECK_SPAN_SIZES(input, output, gain);
|
||||
processLowpass(input.data(), output.data(), gain.data(), minSpanSize(input, output, gain));
|
||||
}
|
||||
|
||||
size_t processHighpassVariableGain(absl::Span<const Type> input, absl::Span<Type> highpass, absl::Span<const Type> gain)
|
||||
void processHighpass(absl::Span<const Type> input, absl::Span<Type> output, absl::Span<const Type> gain)
|
||||
{
|
||||
auto in = input.begin();
|
||||
auto out = highpass.begin();
|
||||
auto g = gain.begin();
|
||||
auto size = min(input.size(), highpass.size(), gain.size());
|
||||
auto sentinel = in + size;
|
||||
while (in < sentinel) {
|
||||
setGain(*g);
|
||||
oneHighpass(in, out);
|
||||
in++;
|
||||
out++;
|
||||
g++;
|
||||
}
|
||||
return size;
|
||||
CHECK_SPAN_SIZES(input, output, gain);
|
||||
processHighpass(input.data(), output.data(), gain.data(), minSpanSize(input, output, gain));
|
||||
}
|
||||
|
||||
void reset() { state = 0.0; }
|
||||
void processLowpass(const Type* input, Type* output, unsigned size)
|
||||
{
|
||||
for (unsigned i = 0; i < size; ++i) {
|
||||
output[i] = tickLowpass(input[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void processHighpass(const Type* input, Type* output, unsigned size)
|
||||
{
|
||||
for (unsigned i = 0; i < size; ++i) {
|
||||
output[i] = tickHighpass(input[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void processLowpass(const Type* input, Type* output, const Type* gain, unsigned size)
|
||||
{
|
||||
for (unsigned i = 0; i < size; ++i) {
|
||||
setGain(gain[i]);
|
||||
output[i] = tickLowpass(input[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void processHighpass(const Type* input, Type* output, const Type* gain, unsigned size)
|
||||
{
|
||||
for (unsigned i = 0; i < size; ++i) {
|
||||
setGain(gain[i]);
|
||||
output[i] = tickHighpass(input[i]);
|
||||
}
|
||||
}
|
||||
|
||||
Type tickHighpass(const Type& input)
|
||||
{
|
||||
const Type intermediate = G * (input - state);
|
||||
const Type output = input - intermediate - state;
|
||||
state += 2 * intermediate;
|
||||
return output;
|
||||
}
|
||||
|
||||
Type tickLowpass(const Type& input)
|
||||
{
|
||||
const Type intermediate = G * (input - state);
|
||||
const Type output = intermediate + state;
|
||||
state = output + intermediate;
|
||||
return output;
|
||||
}
|
||||
|
||||
void reset(Type value = 0.0)
|
||||
{
|
||||
state = value;
|
||||
}
|
||||
|
||||
private:
|
||||
Type state { 0.0 };
|
||||
Type gain { 0.25 };
|
||||
Type intermediate { 0.0 };
|
||||
Type G { gain / (1 + gain) };
|
||||
|
||||
inline void oneLowpass(const Type* in, Type* out)
|
||||
{
|
||||
intermediate = G * (*in - state);
|
||||
*out = intermediate + state;
|
||||
state = *out + intermediate;
|
||||
}
|
||||
|
||||
inline void oneHighpass(const Type* in, Type* out)
|
||||
{
|
||||
intermediate = G * (*in - state);
|
||||
*out = *in - intermediate - state;
|
||||
state += 2 * intermediate;
|
||||
}
|
||||
Type G { 0.5 };
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -204,6 +204,12 @@ namespace literals {
|
|||
*/
|
||||
absl::optional<uint8_t> readNoteValue(const absl::string_view& value);
|
||||
|
||||
template <class Type>
|
||||
inline CXX14_CONSTEXPR Type vaGain(Type cutoff, Type sampleRate)
|
||||
{
|
||||
return std::tan(cutoff / sampleRate * pi<Type>());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief From a source view, find the next sfz header and its members and
|
||||
* return them, while updating the source by removing this header
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ void sfz::Synth::clear()
|
|||
effectBuses[0]->setGainToMain(1.0);
|
||||
effectBuses[0]->setSamplesPerBlock(samplesPerBlock);
|
||||
effectBuses[0]->setSampleRate(sampleRate);
|
||||
effectBuses[0]->clearInputs(samplesPerBlock);
|
||||
resources.clear();
|
||||
numGroups = 0;
|
||||
numMasters = 0;
|
||||
|
|
@ -248,6 +249,7 @@ void sfz::Synth::handleEffectOpcodes(const std::vector<Opcode>& rawMembers)
|
|||
bus.reset(new EffectBus);
|
||||
bus->setSampleRate(sampleRate);
|
||||
bus->setSamplesPerBlock(samplesPerBlock);
|
||||
bus->clearInputs(samplesPerBlock);
|
||||
}
|
||||
return *bus;
|
||||
};
|
||||
|
|
@ -413,15 +415,14 @@ void sfz::Synth::finalizeSfzLoad()
|
|||
// TODO: adjust with LFO targets
|
||||
const auto maxOffset = [region]() {
|
||||
uint64_t sumOffsetCC = region->offset + region->offsetRandom;
|
||||
for (const auto& offsets: region->offsetCC)
|
||||
for (const auto& offsets : region->offsetCC)
|
||||
sumOffsetCC += offsets.data;
|
||||
return Default::offsetCCRange.clamp(sumOffsetCC);
|
||||
}();
|
||||
|
||||
if (!resources.filePool.preloadFile(region->sampleId, maxOffset))
|
||||
removeCurrentRegion();
|
||||
}
|
||||
else if (region->oscillator && !region->isGenerator()) {
|
||||
} else if (region->oscillator && !region->isGenerator()) {
|
||||
if (!resources.filePool.checkSampleId(region->sampleId)) {
|
||||
removeCurrentRegion();
|
||||
continue;
|
||||
|
|
@ -436,7 +437,6 @@ void sfz::Synth::finalizeSfzLoad()
|
|||
if (region->keyswitchLabel && region->keyswitch)
|
||||
keyswitchLabels.push_back({ *region->keyswitch, *region->keyswitchLabel });
|
||||
|
||||
|
||||
// Some regions had group number but no "group-level" opcodes handled the polyphony
|
||||
while (groupMaxPolyphony.size() <= region->group)
|
||||
groupMaxPolyphony.push_back(config::maxVoices);
|
||||
|
|
@ -497,25 +497,66 @@ void sfz::Synth::finalizeSfzLoad()
|
|||
|
||||
sfz::Voice* sfz::Synth::findFreeVoice() noexcept
|
||||
{
|
||||
auto freeVoice = absl::c_find_if(voices, [](const std::unique_ptr<Voice>& voice) { return voice->isFree(); });
|
||||
auto freeVoice = absl::c_find_if(voices, [](const std::unique_ptr<Voice>& voice) {
|
||||
return voice->isFree();
|
||||
});
|
||||
if (freeVoice != voices.end())
|
||||
return freeVoice->get();
|
||||
|
||||
// Find voices that can be stolen
|
||||
voiceViewArray.clear();
|
||||
for (auto& voice : voices)
|
||||
if (voice->canBeStolen())
|
||||
voiceViewArray.push_back(voice.get());
|
||||
absl::c_sort(voiceViewArray, [](Voice* lhs, Voice* rhs) { return lhs->getSourcePosition() > rhs->getSourcePosition(); });
|
||||
// Start of the voice stealing algorithm
|
||||
absl::c_sort(voiceViewArray, voiceOrdering);
|
||||
|
||||
for (auto* voice : voiceViewArray) {
|
||||
if (voice->getMeanSquaredAverage() < config::voiceStealingThreshold) {
|
||||
voice->reset();
|
||||
return voice;
|
||||
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;
|
||||
|
||||
auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock);
|
||||
const auto killVoice = [&] (Voice* v) {
|
||||
renderVoiceToOutputs(*v, *tempSpan);
|
||||
v->reset();
|
||||
};
|
||||
|
||||
Voice* returnedVoice = voiceViewArray.front();
|
||||
unsigned idx = 0;
|
||||
while (idx < voiceViewArray.size()) {
|
||||
const auto refIdx = idx;
|
||||
const auto ref = voiceViewArray[idx];
|
||||
idx++;
|
||||
|
||||
if (ref->getAge() < ageThreshold) {
|
||||
unsigned killIdx = 1;
|
||||
while (killIdx < voiceViewArray.size()
|
||||
&& sisterVoices(returnedVoice, voiceViewArray[killIdx])) {
|
||||
killVoice(voiceViewArray[killIdx]);
|
||||
killIdx++;
|
||||
}
|
||||
// std::cout << "Went too far, picking the oldest voice and killing "
|
||||
// << killIdx << " voices" << '\n';
|
||||
killVoice(returnedVoice);
|
||||
break;
|
||||
}
|
||||
|
||||
float maxEnvelope = ref->getAverageEnvelope();
|
||||
while (idx < voiceViewArray.size()
|
||||
&& sisterVoices(ref, voiceViewArray[idx])) {
|
||||
maxEnvelope = max(maxEnvelope, voiceViewArray[idx]->getAverageEnvelope());
|
||||
idx++;
|
||||
}
|
||||
|
||||
if (maxEnvelope < envThreshold) {
|
||||
returnedVoice = ref;
|
||||
// std::cout << "Killing " << idx - refIdx << " voices" << '\n';
|
||||
for (unsigned j = refIdx; j < idx; j++) {
|
||||
killVoice(voiceViewArray[j]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
assert(returnedVoice->isFree());
|
||||
return returnedVoice;
|
||||
}
|
||||
|
||||
int sfz::Synth::getNumActiveVoices() const noexcept
|
||||
|
|
@ -566,6 +607,19 @@ void sfz::Synth::setSampleRate(float sampleRate) noexcept
|
|||
}
|
||||
}
|
||||
|
||||
void sfz::Synth::renderVoiceToOutputs(Voice& voice, AudioSpan<float>& tempSpan) noexcept
|
||||
{
|
||||
const Region* region = voice.getRegion();
|
||||
voice.renderBlock(tempSpan);
|
||||
for (size_t i = 0, n = effectBuses.size(); i < n; ++i) {
|
||||
if (auto& bus = effectBuses[i]) {
|
||||
float addGain = region->getGainToEffectBus(i);
|
||||
bus->addToInputs(tempSpan, addGain, tempSpan.getNumFrames());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
||||
{
|
||||
ScopedFTZ ftz;
|
||||
|
|
@ -583,23 +637,15 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
|||
if (!lock.owns_lock())
|
||||
return;
|
||||
|
||||
|
||||
size_t numFrames = buffer.getNumFrames();
|
||||
auto tempSpan = resources.bufferPool.getStereoBuffer(numFrames);
|
||||
auto tempMixSpan = resources.bufferPool.getStereoBuffer(numFrames);
|
||||
if (!tempSpan || !tempMixSpan) {
|
||||
auto rampSpan = resources.bufferPool.getBuffer(numFrames);
|
||||
if (!tempSpan || !tempMixSpan || !rampSpan) {
|
||||
DBG("[sfizz] Could not get a temporary buffer; exiting callback... ");
|
||||
return;
|
||||
}
|
||||
|
||||
{ // Prepare the effect inputs. They are mixes of per-region outputs.
|
||||
ScopedTiming logger { callbackBreakdown.effects };
|
||||
for (auto& bus : effectBuses) {
|
||||
if (bus)
|
||||
bus->clearInputs(numFrames);
|
||||
}
|
||||
}
|
||||
|
||||
int numActiveVoices { 0 };
|
||||
{ // Main render block
|
||||
ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration };
|
||||
|
|
@ -607,25 +653,20 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
|||
tempMixSpan->fill(0.0f);
|
||||
resources.filePool.cleanupPromises();
|
||||
|
||||
// Ramp out whatever is in the buffer at this point; should only be killed voice data
|
||||
linearRamp<float>(*rampSpan, 1.0f, -1.0f / static_cast<float>(numFrames));
|
||||
for (size_t i = 0, n = effectBuses.size(); i < n; ++i) {
|
||||
if (auto& bus = effectBuses[i]) {
|
||||
bus->applyGain(rampSpan->data(), numFrames);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& voice : voices) {
|
||||
if (voice->isFree())
|
||||
continue;
|
||||
|
||||
const Region* region = voice->getRegion();
|
||||
|
||||
numActiveVoices++;
|
||||
voice->renderBlock(*tempSpan);
|
||||
|
||||
{ // Add the output into the effects linked to this region
|
||||
ScopedTiming logger { callbackBreakdown.effects, ScopedTiming::Operation::addToDuration };
|
||||
for (size_t i = 0, n = effectBuses.size(); i < n; ++i) {
|
||||
if (auto& bus = effectBuses[i]) {
|
||||
float addGain = region->getGainToEffectBus(i);
|
||||
bus->addToInputs(*tempSpan, addGain, numFrames);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderVoiceToOutputs(*voice, *tempSpan);
|
||||
callbackBreakdown.data += voice->getLastDataDuration();
|
||||
callbackBreakdown.amplitude += voice->getLastAmplitudeDuration();
|
||||
callbackBreakdown.filters += voice->getLastFilterDuration();
|
||||
|
|
@ -666,6 +707,14 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
|||
// Reset the dispatch counter
|
||||
dispatchDuration = Duration(0);
|
||||
|
||||
{ // Clear for the next run
|
||||
ScopedTiming logger { callbackBreakdown.effects };
|
||||
for (auto& bus : effectBuses) {
|
||||
if (bus)
|
||||
bus->clearInputs(numFrames);
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT(!hasNanInf(buffer.getConstSpan(0)));
|
||||
ASSERT(!hasNanInf(buffer.getConstSpan(1)));
|
||||
ASSERT(isValidAudio(buffer.getConstSpan(0)));
|
||||
|
|
@ -1014,12 +1063,13 @@ void sfz::Synth::resetVoices(int numVoices)
|
|||
for (int i = 0; i < numVoices; ++i)
|
||||
voices.push_back(absl::make_unique<Voice>(resources));
|
||||
|
||||
voiceViewArray.clear();
|
||||
for (auto& voice : voices) {
|
||||
voice->setSampleRate(this->sampleRate);
|
||||
voice->setSamplesPerBlock(this->samplesPerBlock);
|
||||
voiceViewArray.push_back(voice.get());
|
||||
}
|
||||
|
||||
voiceViewArray.reserve(numVoices);
|
||||
this->numVoices = numVoices;
|
||||
}
|
||||
|
||||
|
|
@ -1090,7 +1140,7 @@ void sfz::Synth::resetAllControllers(int delay) noexcept
|
|||
fs::file_time_type sfz::Synth::checkModificationTime()
|
||||
{
|
||||
auto returnedTime = modificationTime;
|
||||
for (const auto& file: parser.getIncludedFiles()) {
|
||||
for (const auto& file : parser.getIncludedFiles()) {
|
||||
std::error_code ec;
|
||||
const auto fileTime = fs::last_write_time(file, ec);
|
||||
if (!ec && returnedTime < fileTime)
|
||||
|
|
|
|||
|
|
@ -510,11 +510,21 @@ private:
|
|||
*/
|
||||
void resetVoices(int numVoices);
|
||||
|
||||
/**
|
||||
* @brief Render the voice to its designated outputs and effect busses.
|
||||
*
|
||||
* @param voice
|
||||
* @param tempSpan a temporary span used for rendering
|
||||
*/
|
||||
void renderVoiceToOutputs(Voice& voice, AudioSpan<float>& tempSpan) noexcept;
|
||||
|
||||
fs::file_time_type checkModificationTime();
|
||||
|
||||
void noteOnDispatch(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
|
||||
// will integrate opcodes from the group, master and global block
|
||||
std::vector<Opcode> globalOpcodes;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ sfz::Voice::Voice(sfz::Resources& resources)
|
|||
|
||||
for (WavetableOscillator& osc : waveOscillators)
|
||||
osc.init(sampleRate);
|
||||
|
||||
for (auto & filter : channelEnvelopeFilters)
|
||||
filter.setGain(vaGain(config::filteredEnvelopeCutoff, sampleRate));
|
||||
}
|
||||
|
||||
void sfz::Voice::startVoice(Region* region, int delay, int number, float value, sfz::Voice::TriggerType triggerType) noexcept
|
||||
|
|
@ -192,6 +195,9 @@ void sfz::Voice::setSampleRate(float sampleRate) noexcept
|
|||
{
|
||||
this->sampleRate = sampleRate;
|
||||
|
||||
for (auto & filter : channelEnvelopeFilters)
|
||||
filter.setGain(vaGain(config::filteredEnvelopeCutoff, sampleRate));
|
||||
|
||||
for (WavetableOscillator& osc : waveOscillators)
|
||||
osc.init(sampleRate);
|
||||
}
|
||||
|
|
@ -207,11 +213,6 @@ void sfz::Voice::renderBlock(AudioSpan<float> buffer) noexcept
|
|||
ASSERT(static_cast<int>(buffer.getNumFrames()) <= samplesPerBlock);
|
||||
buffer.fill(0.0f);
|
||||
|
||||
if (state == State::idle || region == nullptr) {
|
||||
powerHistory.push(0.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto delay = min(static_cast<size_t>(initialDelay), buffer.getNumFrames());
|
||||
auto delayed_buffer = buffer.subspan(delay);
|
||||
initialDelay -= static_cast<int>(delay);
|
||||
|
|
@ -237,8 +238,15 @@ void sfz::Voice::renderBlock(AudioSpan<float> buffer) noexcept
|
|||
if (!egEnvelope.isSmoothing())
|
||||
reset();
|
||||
|
||||
powerHistory.push(buffer.meanSquared());
|
||||
this->triggerDelay = absl::nullopt;
|
||||
updateChannelPowers(buffer);
|
||||
|
||||
age += buffer.getNumFrames();
|
||||
if (triggerDelay) {
|
||||
// Should be OK but just in case;
|
||||
age = min(age - *triggerDelay, 0);
|
||||
triggerDelay = absl::nullopt;
|
||||
}
|
||||
|
||||
#if 0
|
||||
ASSERT(!hasNanInf(buffer.getConstSpan(0)));
|
||||
ASSERT(!hasNanInf(buffer.getConstSpan(1)));
|
||||
|
|
@ -608,39 +616,32 @@ bool sfz::Voice::checkOffGroup(int delay, uint32_t group) noexcept
|
|||
return false;
|
||||
}
|
||||
|
||||
int sfz::Voice::getTriggerNumber() const noexcept
|
||||
{
|
||||
return triggerNumber;
|
||||
}
|
||||
|
||||
float sfz::Voice::getTriggerValue() const noexcept
|
||||
{
|
||||
return triggerValue;
|
||||
}
|
||||
|
||||
sfz::Voice::TriggerType sfz::Voice::getTriggerType() const noexcept
|
||||
{
|
||||
return triggerType;
|
||||
}
|
||||
|
||||
void sfz::Voice::reset() noexcept
|
||||
{
|
||||
state = State::idle;
|
||||
region = nullptr;
|
||||
currentPromise.reset();
|
||||
sourcePosition = 0;
|
||||
age = 0;
|
||||
floatPositionOffset = 0.0f;
|
||||
noteIsOff = false;
|
||||
|
||||
for (auto& f : channelEnvelopeFilters)
|
||||
f.reset();
|
||||
|
||||
for (auto& p : smoothedChannelEnvelopes)
|
||||
p = 0.0f;
|
||||
|
||||
filters.clear();
|
||||
equalizers.clear();
|
||||
}
|
||||
|
||||
float sfz::Voice::getMeanSquaredAverage() const noexcept
|
||||
float sfz::Voice::getAverageEnvelope() const noexcept
|
||||
{
|
||||
return powerHistory.getAverage();
|
||||
return max(smoothedChannelEnvelopes[0], smoothedChannelEnvelopes[1]);
|
||||
}
|
||||
|
||||
bool sfz::Voice::canBeStolen() const noexcept
|
||||
bool sfz::Voice::releasedOrFree() const noexcept
|
||||
{
|
||||
return state == State::idle || egEnvelope.isReleased();
|
||||
}
|
||||
|
|
@ -720,3 +721,19 @@ void sfz::Voice::setupOscillatorUnison()
|
|||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
void sfz::Voice::updateChannelPowers(AudioSpan<float> buffer)
|
||||
{
|
||||
assert(smoothedChannelEnvelopes.size() == channelEnvelopeFilters.size());
|
||||
assert(buffer.getNumFrames() <= channelEnvelopeFilters.size());
|
||||
if (buffer.getNumFrames() == 0)
|
||||
return;
|
||||
|
||||
for (unsigned i = 0; i < smoothedChannelEnvelopes.size(); ++i) {
|
||||
const auto input = buffer.getConstSpan(i);
|
||||
for (unsigned s = 0; s < buffer.getNumFrames(); ++s)
|
||||
smoothedChannelEnvelopes[i] =
|
||||
channelEnvelopeFilters[i].tickLowpass(std::abs(input[s]));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
#include "Resources.h"
|
||||
#include "AudioSpan.h"
|
||||
#include "LeakDetector.h"
|
||||
#include "OnePoleFilter.h"
|
||||
#include "absl/types/span.h"
|
||||
#include <memory>
|
||||
#include <random>
|
||||
|
|
@ -144,30 +145,30 @@ public:
|
|||
*/
|
||||
bool isFree() const noexcept;
|
||||
/**
|
||||
* @brief Can the voice be "stolen" and reused (i.e. is it releasing)
|
||||
* @brief Can the voice be reused (i.e. is it releasing or free)
|
||||
*
|
||||
* @return true
|
||||
* @return false
|
||||
*/
|
||||
bool canBeStolen() const noexcept;
|
||||
bool releasedOrFree() const noexcept;
|
||||
/**
|
||||
* @brief Get the number that triggered the voice (note number or cc number)
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
int getTriggerNumber() const noexcept;
|
||||
int getTriggerNumber() const noexcept { return triggerNumber; }
|
||||
/**
|
||||
* @brief Get the value that triggered the voice (note velocity or cc value)
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
float getTriggerValue() const noexcept;
|
||||
float getTriggerValue() const noexcept { return triggerValue; }
|
||||
/**
|
||||
* @brief Get the type of trigger
|
||||
*
|
||||
* @return TriggerType
|
||||
*/
|
||||
TriggerType getTriggerType() const noexcept;
|
||||
TriggerType getTriggerType() const noexcept { return triggerType; }
|
||||
|
||||
/**
|
||||
* @brief Reset the voice to its initial values
|
||||
|
|
@ -181,7 +182,7 @@ public:
|
|||
*
|
||||
* @return float
|
||||
*/
|
||||
float getMeanSquaredAverage() const noexcept;
|
||||
float getAverageEnvelope() const noexcept;
|
||||
/**
|
||||
* @brief Get the position of the voice in the source, in samples
|
||||
*
|
||||
|
|
@ -214,6 +215,13 @@ public:
|
|||
*/
|
||||
void release(int delay, bool fastRelease = false) noexcept;
|
||||
|
||||
/**
|
||||
* @brief gets the age of the Voice
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
int getAge() const noexcept { return age; }
|
||||
|
||||
Duration getLastDataDuration() const noexcept { return dataDuration; }
|
||||
Duration getLastAmplitudeDuration() const noexcept { return amplitudeDuration; }
|
||||
Duration getLastFilterDuration() const noexcept { return filterDuration; }
|
||||
|
|
@ -245,6 +253,7 @@ private:
|
|||
* @brief Initialize frequency and gain coefficients for the oscillators.
|
||||
*/
|
||||
void setupOscillatorUnison();
|
||||
void updateChannelPowers(AudioSpan<float> buffer);
|
||||
|
||||
Region* region { nullptr };
|
||||
|
||||
|
|
@ -262,13 +271,14 @@ private:
|
|||
|
||||
float speedRatio { 1.0 };
|
||||
float pitchRatio { 1.0 };
|
||||
float baseVolumedB{ 0.0 };
|
||||
float baseVolumedB { 0.0 };
|
||||
float baseGain { 1.0 };
|
||||
float baseFrequency { 440.0 };
|
||||
|
||||
float floatPositionOffset { 0.0f };
|
||||
int sourcePosition { 0 };
|
||||
int initialDelay { 0 };
|
||||
int age { 0 };
|
||||
|
||||
FilePromisePtr currentPromise { nullptr };
|
||||
|
||||
|
|
@ -288,9 +298,9 @@ private:
|
|||
|
||||
// unison of oscillators
|
||||
unsigned waveUnisonSize { 0 };
|
||||
float waveDetuneRatio[config::oscillatorsPerVoice] { };
|
||||
float waveLeftGain[config::oscillatorsPerVoice] { };
|
||||
float waveRightGain[config::oscillatorsPerVoice] { };
|
||||
float waveDetuneRatio[config::oscillatorsPerVoice] {};
|
||||
float waveLeftGain[config::oscillatorsPerVoice] {};
|
||||
float waveRightGain[config::oscillatorsPerVoice] {};
|
||||
|
||||
Duration dataDuration;
|
||||
Duration amplitudeDuration;
|
||||
|
|
@ -299,8 +309,40 @@ private:
|
|||
|
||||
std::normal_distribution<float> noiseDist { 0, config::noiseVariance };
|
||||
|
||||
HistoricalBuffer<float> powerHistory { config::powerHistoryLength };
|
||||
std::array<OnePoleFilter<float>, 2> channelEnvelopeFilters;
|
||||
std::array<float, 2> smoothedChannelEnvelopes;
|
||||
LEAK_DETECTOR(Voice);
|
||||
};
|
||||
|
||||
inline bool sisterVoices(const Voice* lhs, const Voice* rhs)
|
||||
{
|
||||
return lhs->getAge() == rhs->getAge()
|
||||
&& lhs->getTriggerNumber() == rhs->getTriggerNumber()
|
||||
&& lhs->getTriggerValue() == rhs->getTriggerValue()
|
||||
&& lhs->getTriggerType() == rhs->getTriggerType();
|
||||
}
|
||||
|
||||
inline bool voiceOrdering(const Voice* lhs, const Voice* rhs)
|
||||
{
|
||||
if (lhs->getAge() > rhs->getAge())
|
||||
return true;
|
||||
if (lhs->getAge() < rhs->getAge())
|
||||
return false;
|
||||
|
||||
if (lhs->getTriggerNumber() > rhs->getTriggerNumber())
|
||||
return true;
|
||||
if (lhs->getTriggerNumber() < rhs->getTriggerNumber())
|
||||
return false;
|
||||
|
||||
if (lhs->getTriggerValue() > rhs->getTriggerValue())
|
||||
return true;
|
||||
if (lhs->getTriggerValue() < rhs->getTriggerValue())
|
||||
return false;
|
||||
|
||||
if (lhs->getTriggerType() > rhs->getTriggerType())
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace sfz
|
||||
|
|
|
|||
|
|
@ -455,7 +455,7 @@ TEST_CASE("[Files] Off by with different delays")
|
|||
REQUIRE( group1Voice->getRegion()->offBy == 2ul );
|
||||
synth.noteOn(100, 64, 63);
|
||||
synth.renderBlock(buffer);
|
||||
REQUIRE( group1Voice->canBeStolen() );
|
||||
REQUIRE(group1Voice->releasedOrFree());
|
||||
}
|
||||
|
||||
TEST_CASE("[Files] Off by with the same delays")
|
||||
|
|
@ -470,7 +470,7 @@ TEST_CASE("[Files] Off by with the same delays")
|
|||
REQUIRE( group1Voice->getRegion()->group == 1ul );
|
||||
REQUIRE( group1Voice->getRegion()->offBy == 2ul );
|
||||
synth.noteOn(0, 64, 63);
|
||||
REQUIRE( !group1Voice->canBeStolen() );
|
||||
REQUIRE(!group1Voice->releasedOrFree());
|
||||
}
|
||||
|
||||
TEST_CASE("[Files] Off by with the same notes at the same time")
|
||||
|
|
|
|||
|
|
@ -40,12 +40,13 @@ void testFilter(const std::array<Type, N>& input, const std::array<Type, N>& exp
|
|||
std::array<Type, N> gains;
|
||||
std::fill(gains.begin(), gains.end(), gain);
|
||||
|
||||
sfz::OnePoleFilter<Type> filter { gain };
|
||||
sfz::OnePoleFilter<Type> filter;
|
||||
filter.setGain(gain);
|
||||
filter.processLowpass(input, outputSpan);
|
||||
REQUIRE(approxEqual(output, expectedLow));
|
||||
|
||||
filter.reset();
|
||||
filter.processLowpassVariableGain(input, outputSpan, gains);
|
||||
filter.processLowpass(input, outputSpan, gains);
|
||||
REQUIRE(approxEqual(output, expectedLow));
|
||||
|
||||
filter.reset();
|
||||
|
|
@ -53,7 +54,7 @@ void testFilter(const std::array<Type, N>& input, const std::array<Type, N>& exp
|
|||
REQUIRE(approxEqual(output, expectedHigh));
|
||||
|
||||
filter.reset();
|
||||
filter.processHighpassVariableGain(input, outputSpan, gains);
|
||||
filter.processHighpass(input, outputSpan, gains);
|
||||
REQUIRE(approxEqual(output, expectedHigh));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ TEST_CASE("[Synth] Trigger=release and an envelope properly kills the voice at t
|
|||
synth.renderBlock(buffer); // Decay (0.02)
|
||||
synth.renderBlock(buffer);
|
||||
synth.renderBlock(buffer); // Release (0.1)
|
||||
REQUIRE( synth.getVoiceView(0)->canBeStolen() );
|
||||
REQUIRE(synth.getVoiceView(0)->releasedOrFree());
|
||||
// Release is 0.1s
|
||||
for (int i = 0; i < 10; ++i)
|
||||
synth.renderBlock(buffer);
|
||||
|
|
@ -218,7 +218,7 @@ TEST_CASE("[Synth] Trigger=release_key and an envelope properly kills the voice
|
|||
synth.renderBlock(buffer); // Decay (0.02)
|
||||
synth.renderBlock(buffer);
|
||||
synth.renderBlock(buffer); // Release (0.1)
|
||||
REQUIRE( synth.getVoiceView(0)->canBeStolen() );
|
||||
REQUIRE(synth.getVoiceView(0)->releasedOrFree());
|
||||
// Release is 0.1s
|
||||
for (int i = 0; i < 10; ++i)
|
||||
synth.renderBlock(buffer);
|
||||
|
|
@ -241,7 +241,7 @@ TEST_CASE("[Synth] loopmode=one_shot and an envelope properly kills the voice at
|
|||
synth.renderBlock(buffer); // Decay (0.02)
|
||||
synth.renderBlock(buffer);
|
||||
synth.renderBlock(buffer); // Release (0.1)
|
||||
REQUIRE( synth.getVoiceView(0)->canBeStolen() );
|
||||
REQUIRE(synth.getVoiceView(0)->releasedOrFree());
|
||||
// Release is 0.1s
|
||||
for (int i = 0; i < 10; ++i)
|
||||
synth.renderBlock(buffer);
|
||||
|
|
@ -376,11 +376,11 @@ TEST_CASE("[Synth] Self-masking")
|
|||
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)->canBeStolen());
|
||||
REQUIRE(!synth.getVoiceView(0)->releasedOrFree());
|
||||
REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm);
|
||||
REQUIRE(synth.getVoiceView(1)->canBeStolen()); // The lowest velocity voice is the masking candidate
|
||||
REQUIRE(synth.getVoiceView(1)->releasedOrFree()); // The lowest velocity voice is the masking candidate
|
||||
REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm);
|
||||
REQUIRE(!synth.getVoiceView(2)->canBeStolen());
|
||||
REQUIRE(!synth.getVoiceView(2)->releasedOrFree());
|
||||
}
|
||||
|
||||
TEST_CASE("[Synth] Not self-masking")
|
||||
|
|
@ -392,11 +392,11 @@ TEST_CASE("[Synth] Not self-masking")
|
|||
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)->canBeStolen()); // The first encountered voice is the masking candidate
|
||||
REQUIRE(synth.getVoiceView(0)->releasedOrFree()); // The first encountered voice is the masking candidate
|
||||
REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm);
|
||||
REQUIRE(!synth.getVoiceView(1)->canBeStolen());
|
||||
REQUIRE(!synth.getVoiceView(1)->releasedOrFree());
|
||||
REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm);
|
||||
REQUIRE(!synth.getVoiceView(2)->canBeStolen());
|
||||
REQUIRE(!synth.getVoiceView(2)->releasedOrFree());
|
||||
}
|
||||
|
||||
TEST_CASE("[Synth] Polyphony in master")
|
||||
|
|
|
|||
|
|
@ -80,6 +80,6 @@ struct SfizzParameterRange {
|
|||
};
|
||||
|
||||
static constexpr SfizzParameterRange kParamVolumeRange(0.0, -60.0, +6.0);
|
||||
static constexpr SfizzParameterRange kParamNumVoicesRange(64.0, 1.0, 256.0);
|
||||
static constexpr SfizzParameterRange kParamNumVoicesRange(256.0, 1.0, 512.0);
|
||||
static constexpr SfizzParameterRange kParamOversamplingRange(0.0, 0.0, 3.0);
|
||||
static constexpr SfizzParameterRange kParamPreloadSizeRange(8192.0, 1024.0, 65536.0);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue