diff --git a/benchmarks/BM_filterModulation.cpp b/benchmarks/BM_filterModulation.cpp index 9326e21f9..f97a9b54b 100644 --- a/benchmarks/BM_filterModulation.cpp +++ b/benchmarks/BM_filterModulation.cpp @@ -8,6 +8,7 @@ #include "OnePoleFilter.h" #include "SfzFilter.h" #include "ScopedFTZ.h" +#include "SfzHelpers.h" #include #include #include @@ -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::normalizedGain(*cutoffPtr, sampleRate); + const auto gain = sfz::vaGain(*cutoffPtr, sampleRate); filter.setGain(gain); filter.processLowpass({ inputPtr, step }, { outputPtr, step } ); cutoffPtr += step; diff --git a/lv2/sfizz.c b/lv2/sfizz.c index 67501d85b..104f713d2 100644 --- a/lv2/sfizz.c +++ b/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; } diff --git a/lv2/sfizz.ttl.in b/lv2/sfizz.ttl.in index 3ebbc2e78..3b07c0cfc 100644 --- a/lv2/sfizz.ttl.in +++ b/lv2/sfizz.ttl.in @@ -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 ; diff --git a/src/sfizz/AudioBuffer.h b/src/sfizz/AudioBuffer.h index 931402dee..51500d0a9 100644 --- a/src/sfizz/AudioBuffer.h +++ b/src/sfizz/AudioBuffer.h @@ -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 @@ -259,6 +260,15 @@ class AudioBuffer { numChannels = 0; } + /** + * Writes zeros in the buffer + */ + void clear() + { + for (size_t i = 0; i < numChannels; ++i) + fill(getSpan(i), Type{ 0.0 }); + } + /** * @brief Add a positive number of channels to the buffer * @@ -271,6 +281,22 @@ class AudioBuffer { 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; using buffer_ptr = std::unique_ptr; diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 48aeb70f6..b8f9d4ffe 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -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 }; diff --git a/src/sfizz/Effects.cpp b/src/sfizz/Effects.cpp index e6dd469d8..de6e96920 100644 --- a/src/sfizz/Effects.cpp +++ b/src/sfizz/Effects.cpp @@ -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 gainSpan { gain, nframes }; + for (unsigned c = 0; c < EffectChannels; ++c) { + sfz::applyGain(gainSpan, _inputs.getSpan(c)); + } +} + void EffectBus::setSampleRate(double sampleRate) { for (const auto& effectPtr : _effects) diff --git a/src/sfizz/Effects.h b/src/sfizz/Effects.h index 1c1edfebf..2056cac2a 100644 --- a/src/sfizz/Effects.h +++ b/src/sfizz/Effects.h @@ -136,6 +136,11 @@ class EffectBus { */ 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. */ diff --git a/src/sfizz/HistoricalBuffer.h b/src/sfizz/HistoricalBuffer.h index 1ed04ebe3..e2b3b8e52 100644 --- a/src/sfizz/HistoricalBuffer.h +++ b/src/sfizz/HistoricalBuffer.h @@ -37,6 +37,7 @@ class HistoricalBuffer { buffer.resize(size); fill(absl::MakeSpan(buffer), 0.0); index = 0; + validMean = false; } /** @@ -46,6 +47,7 @@ class HistoricalBuffer { */ void push(ValueType value) { + validMean = false; if (size > 0) { buffer[index] = value; if (++index == size) @@ -60,11 +62,18 @@ class HistoricalBuffer { */ ValueType getAverage() const { - return mean(buffer); + if (!validMean) { + mean = sfz::mean(buffer); + validMean = true; + } + + return mean; } private: Buffer buffer; size_t size { 0 }; size_t index { 0 }; + mutable bool validMean { true }; + mutable ValueType mean { 0.0 }; }; } diff --git a/src/sfizz/MathHelpers.h b/src/sfizz/MathHelpers.h index a70446ca5..6567d5b5b 100644 --- a/src/sfizz/MathHelpers.h +++ b/src/sfizz/MathHelpers.h @@ -340,6 +340,66 @@ bool isValidAudio(absl::Span span) return true; } +/** + * @brief Finds the minimum size of 2 spans + * + * @tparam T + * @tparam U + * @param span1 + * @param span2 + * @return constexpr size_t + */ +template +constexpr size_t minSpanSize(absl::Span& span1, absl::Span& 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 +constexpr size_t minSpanSize(absl::Span& first, Others... others) +{ + return min(first.size(), minSpanSize(others...)); +} + +template +constexpr bool _checkSpanSizes(size_t size, absl::Span& span1) +{ + return span1.size() == size; +} + +template +constexpr bool _checkSpanSizes(size_t size, absl::Span& 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 +constexpr bool checkSpanSizes(const absl::Span& span1, Others... others) +{ + return _checkSpanSizes(span1.size(), others...); +} + +#define CHECK_SPAN_SIZES(...) ASSERT(checkSpanSizes(__VA_ARGS__)) + + class ScopedRoundingMode { public: ScopedRoundingMode() = delete; diff --git a/src/sfizz/OnePoleFilter.h b/src/sfizz/OnePoleFilter.h index 99ddc7541..9781b39e5 100644 --- a/src/sfizz/OnePoleFilter.h +++ b/src/sfizz/OnePoleFilter.h @@ -6,12 +6,14 @@ #pragma once #include "Config.h" +#include "Debug.h" #include "MathHelpers.h" +#include "Macros.h" #include #include -namespace sfz -{ +namespace sfz { + /** * @brief An implementation of a one pole filter. This is a scalar * implementation. @@ -22,108 +24,90 @@ template class OnePoleFilter { public: OnePoleFilter() = default; - // Normalized cutoff with respect to the sampling rate - template - static Type normalizedGain(Type cutoff, C sampleRate) + + void setGain(Type gain) { - return std::tan(cutoff / static_cast(sampleRate) * pi()); + G = gain / (1 + gain); } - OnePoleFilter(Type gain) + void processLowpass(absl::Span input, absl::Span output) { - setGain(gain); + CHECK_SPAN_SIZES(input, output); + processLowpass(input.data(), output.data(), minSpanSize(input, output)); } - void setGain(Type gain) + void processHighpass(absl::Span input, absl::Span output) { - this->gain = gain; - G = gain / (1 + gain); + CHECK_SPAN_SIZES(input, output); + processHighpass(input.data(), output.data(), minSpanSize(input, output)); } - Type getGain() const { return gain; } + void processLowpass(absl::Span input, absl::Span output, absl::Span gain) + { + CHECK_SPAN_SIZES(input, output, gain); + processLowpass(input.data(), output.data(), gain.data(), minSpanSize(input, output, gain)); + } - size_t processLowpass(absl::Span input, absl::Span lowpass) + void processHighpass(absl::Span input, absl::Span output, absl::Span gain) { - 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, gain); + processHighpass(input.data(), output.data(), gain.data(), minSpanSize(input, output, gain)); } - size_t processHighpass(absl::Span input, absl::Span highpass) + void processLowpass(const Type* input, Type* output, unsigned size) { - 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++; + for (unsigned i = 0; i < size; ++i) { + output[i] = tickLowpass(input[i]); } - return size; } - size_t processLowpassVariableGain(absl::Span input, absl::Span lowpass, absl::Span gain) + void processHighpass(const Type* input, Type* output, unsigned size) { - 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++; + for (unsigned i = 0; i < size; ++i) { + output[i] = tickHighpass(input[i]); } - return size; } - size_t processHighpassVariableGain(absl::Span input, absl::Span highpass, absl::Span gain) + void processLowpass(const Type* input, Type* output, const Type* gain, unsigned size) { - 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++; + for (unsigned i = 0; i < size; ++i) { + setGain(gain[i]); + output[i] = tickLowpass(input[i]); } - return size; } - void reset() { state = 0.0; } + 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]); + } + } -private: - Type state { 0.0 }; - Type gain { 0.25 }; - Type intermediate { 0.0 }; - Type G { gain / (1 + gain) }; + Type tickHighpass(const Type& input) + { + const Type intermediate = G * (input - state); + const Type output = input - intermediate - state; + state += 2 * intermediate; + return output; + } - inline void oneLowpass(const Type* in, Type* out) + Type tickLowpass(const Type& input) { - intermediate = G * (*in - state); - *out = intermediate + state; - state = *out + intermediate; + const Type intermediate = G * (input - state); + const Type output = intermediate + state; + state = output + intermediate; + return output; } - inline void oneHighpass(const Type* in, Type* out) + void reset(Type value = 0.0) { - intermediate = G * (*in - state); - *out = *in - intermediate - state; - state += 2 * intermediate; + state = value; } + +private: + Type state { 0.0 }; + Type G { 0.5 }; }; + } diff --git a/src/sfizz/SfzHelpers.h b/src/sfizz/SfzHelpers.h index 5d581a743..5cd49aad1 100644 --- a/src/sfizz/SfzHelpers.h +++ b/src/sfizz/SfzHelpers.h @@ -204,6 +204,12 @@ namespace literals { */ absl::optional readNoteValue(const absl::string_view& value); +template +inline CXX14_CONSTEXPR Type vaGain(Type cutoff, Type sampleRate) +{ + return std::tan(cutoff / sampleRate * pi()); +} + /** * @brief From a source view, find the next sfz header and its members and * return them, while updating the source by removing this header diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index ba747d54f..f3c9e2cf8 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -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& 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) { return voice->isFree(); }); + auto freeVoice = absl::c_find_if(voices, [](const std::unique_ptr& 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(); }); - - for (auto* voice : voiceViewArray) { - if (voice->getMeanSquaredAverage() < config::voiceStealingThreshold) { - voice->reset(); - return voice; + // Start of the voice stealing algorithm + 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(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++; } - } - return {}; + if (maxEnvelope < envThreshold) { + returnedVoice = ref; + // std::cout << "Killing " << idx - refIdx << " voices" << '\n'; + for (unsigned j = refIdx; j < idx; j++) { + killVoice(voiceViewArray[j]); + } + break; + } + } + 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& 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 buffer) noexcept { ScopedFTZ ftz; @@ -583,23 +637,15 @@ void sfz::Synth::renderBlock(AudioSpan 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 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(*rampSpan, 1.0f, -1.0f / static_cast(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 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(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) diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index bfda4f425..bc73dc468 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -510,11 +510,21 @@ class Synth final : public Parser::Listener { */ 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& 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 globalOpcodes; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 82054c2e1..b5db36349 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -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 buffer) noexcept ASSERT(static_cast(buffer.getNumFrames()) <= samplesPerBlock); buffer.fill(0.0f); - if (state == State::idle || region == nullptr) { - powerHistory.push(0.0f); - return; - } - const auto delay = min(static_cast(initialDelay), buffer.getNumFrames()); auto delayed_buffer = buffer.subspan(delay); initialDelay -= static_cast(delay); @@ -237,8 +238,15 @@ void sfz::Voice::renderBlock(AudioSpan 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 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])); + } +} diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index bc1e537c2..46b3a3f0b 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -13,6 +13,7 @@ #include "Resources.h" #include "AudioSpan.h" #include "LeakDetector.h" +#include "OnePoleFilter.h" #include "absl/types/span.h" #include #include @@ -144,30 +145,30 @@ class Voice { */ 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 @@ class Voice { * * @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 @@ class Voice { */ 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 @@ class Voice { * @brief Initialize frequency and gain coefficients for the oscillators. */ void setupOscillatorUnison(); + void updateChannelPowers(AudioSpan buffer); Region* region { nullptr }; @@ -262,13 +271,14 @@ class Voice { 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 @@ class Voice { // 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 @@ class Voice { std::normal_distribution noiseDist { 0, config::noiseVariance }; - HistoricalBuffer powerHistory { config::powerHistoryLength }; + std::array, 2> channelEnvelopeFilters; + std::array 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 diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index 2f22dea5e..79dc839cd 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -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") diff --git a/tests/OnePoleFilterT.cpp b/tests/OnePoleFilterT.cpp index 3350fa7d3..f44e65cc7 100644 --- a/tests/OnePoleFilterT.cpp +++ b/tests/OnePoleFilterT.cpp @@ -40,12 +40,13 @@ void testFilter(const std::array& input, const std::array& exp std::array gains; std::fill(gains.begin(), gains.end(), gain); - sfz::OnePoleFilter filter { gain }; + sfz::OnePoleFilter 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& input, const std::array& exp REQUIRE(approxEqual(output, expectedHigh)); filter.reset(); - filter.processHighpassVariableGain(input, outputSpan, gains); + filter.processHighpass(input, outputSpan, gains); REQUIRE(approxEqual(output, expectedHigh)); } diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 6d7022baf..e89ef65e6 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -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") diff --git a/vst/SfizzVstState.h b/vst/SfizzVstState.h index eed212e8b..020e0fe8f 100644 --- a/vst/SfizzVstState.h +++ b/vst/SfizzVstState.h @@ -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);