Add voices to a ring upon startup, and remove them when resetting the voice

This commit is contained in:
Paul Ferrand 2020-06-14 17:33:04 +02:00
parent 97b4335209
commit f4660b1a02
5 changed files with 244 additions and 0 deletions

View file

@ -0,0 +1,90 @@
// 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 "Voice.h"
#include "absl/meta/type_traits.h"
namespace sfz
{
struct SisterVoiceRing {
template<class F, class T,
absl::enable_if_t<std::is_same<Voice, absl::remove_const_t<T>>::value, int> = 0>
static void applyToRing(T* voice, F&& lambda)
{
auto v = voice->getNextSisterVoice();
while (v != voice) {
const auto next = v->getNextSisterVoice();
lambda(v);
v = next;
}
lambda(voice);
}
static unsigned countSisterVoices(const Voice* start)
{
if (!start)
return 0;
unsigned count = 0;
auto next = start;
do
{
count++;
next = next->getNextSisterVoice();
} while (next != start && count < config::maxVoices);
ASSERT(count < config::maxVoices);
return count;
}
};
/**
* @brief RAII helper to build sister voice rings.
* Closes the doubly-linked list on destruction.
*
*/
class SisterVoiceRingBuilder {
public:
~SisterVoiceRingBuilder() {
if (lastStartedVoice != nullptr) {
ASSERT(firstStartedVoice);
lastStartedVoice->setNextSisterVoice(firstStartedVoice);
firstStartedVoice->setPreviousSisterVoice(lastStartedVoice);
}
}
/**
* @brief Add a voice to the sister ring
*
* @param voice
*/
void addVoiceToRing(Voice* voice) {
if (firstStartedVoice == nullptr)
firstStartedVoice = voice;
if (lastStartedVoice != nullptr) {
voice->setPreviousSisterVoice(lastStartedVoice);
lastStartedVoice->setNextSisterVoice(voice);
}
lastStartedVoice = voice;
}
/**
* @brief Apply a function to the sister ring, including the current voice.
* This function should be safe enough to even reset the sister voices, but
* if you mutate the ring significantly you should probably roll your own
* iterator.
*
* @param lambda the function to apply.
* @param voice the starting voice
*/
private:
Voice* firstStartedVoice { nullptr };
Voice* lastStartedVoice { nullptr };
};
}

View file

@ -15,6 +15,7 @@
#include "absl/algorithm/container.h"
#include "absl/memory/memory.h"
#include "absl/strings/str_replace.h"
#include "SisterVoiceRing.h"
#include <algorithm>
#include <chrono>
#include <iostream>
@ -800,6 +801,8 @@ void sfz::Synth::noteOff(int delay, int noteNumber, uint8_t velocity) noexcept
void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noexcept
{
const auto randValue = randNoteDistribution(Random::randomGenerator);
SisterVoiceRingBuilder ring;
for (auto& region : noteActivationLists[noteNumber]) {
if (region->registerNoteOff(noteNumber, velocity, randValue)) {
auto voice = findFreeVoice();
@ -807,6 +810,7 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex
continue;
voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOff);
ring.addVoiceToRing(voice);
}
}
}
@ -814,6 +818,8 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex
void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexcept
{
const auto randValue = randNoteDistribution(Random::randomGenerator);
SisterVoiceRingBuilder ring;
for (auto& region : noteActivationLists[noteNumber]) {
if (region->registerNoteOn(noteNumber, velocity, randValue)) {
unsigned activeNotesInGroup { 0 };
@ -865,6 +871,7 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc
continue;
voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn);
ring.addVoiceToRing(voice);
}
}
}
@ -895,6 +902,8 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept
for (auto& voice : voices)
voice->registerCC(delay, ccNumber, normValue);
SisterVoiceRingBuilder ring;
for (auto& region : ccActivationLists[ccNumber]) {
if (region->registerCC(ccNumber, normValue)) {
auto voice = findFreeVoice();
@ -902,6 +911,7 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept
continue;
voice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC);
ring.addVoiceToRing(voice);
}
}
}

View file

@ -674,6 +674,30 @@ void sfz::Voice::reset() noexcept
filters.clear();
equalizers.clear();
removeVoiceFromRing();
}
void sfz::Voice::setNextSisterVoice(Voice* voice) noexcept
{
// Should never be null
ASSERT(voice);
nextSisterVoice = voice;
}
void sfz::Voice::setPreviousSisterVoice(Voice* voice) noexcept
{
// Should never be null
ASSERT(voice);
previousSisterVoice = voice;
}
void sfz::Voice::removeVoiceFromRing() noexcept
{
previousSisterVoice->setNextSisterVoice(nextSisterVoice);
nextSisterVoice->setPreviousSisterVoice(previousSisterVoice);
previousSisterVoice = this;
nextSisterVoice = this;
}
float sfz::Voice::getAverageEnvelope() const noexcept

View file

@ -210,6 +210,40 @@ public:
*/
void reset() noexcept;
/**
* @brief Set the next voice in the "sister voice" ring
* The sister voices are voices that started on the same event.
* This has to be set by the synth. A voice will remove itself from
* the ring upon reset.
*
* @param voice
*/
void setNextSisterVoice(Voice* voice) noexcept;
/**
* @brief Set the previous voice in the "sister voice" ring
* The sister voices are voices that started on the same event.
* This has to be set by the synth. A voice will remove itself from
* the ring upon reset.
*
* @param voice
*/
void setPreviousSisterVoice(Voice* voice) noexcept;
/**
* @brief Get the next sister voice in the ring
*
* @return Voice*
*/
Voice* getNextSisterVoice() const noexcept { return nextSisterVoice; };
/**
* @brief Get the previous sister voice in the ring
*
* @return Voice*
*/
Voice* getPreviousSisterVoice() const noexcept { return previousSisterVoice; };
/**
* @brief Get the mean squared power of the last rendered block. This is used
* to determine which voice to steal if there are too many notes flying around.
@ -297,6 +331,12 @@ private:
void panStageStereo(AudioSpan<float> buffer) noexcept;
void filterStageMono(AudioSpan<float> buffer) noexcept;
void filterStageStereo(AudioSpan<float> buffer) noexcept;
/**
* @brief Remove the voice from the sister ring
*
*/
void removeVoiceFromRing() noexcept;
/**
* @brief Initialize frequency and gain coefficients for the oscillators.
*/
@ -359,6 +399,9 @@ private:
Duration panningDuration;
Duration filterDuration;
Voice* nextSisterVoice { this };
Voice* previousSisterVoice { this };
std::normal_distribution<float> noiseDist { 0, config::noiseVariance };
std::array<OnePoleFilter<float>, 2> channelEnvelopeFilters;

View file

@ -5,6 +5,7 @@
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#include "sfizz/Synth.h"
#include "sfizz/SisterVoiceRing.h"
#include "sfizz/SfzHelpers.h"
#include "sfizz/NumericId.h"
#include "catch2/catch.hpp"
@ -540,3 +541,79 @@ TEST_CASE("[Synth] sample quality")
synth.allSoundOff();
synth.disableFreeWheeling();
}
TEST_CASE("[Synth] Sister voices")
{
sfz::Synth synth;
synth.loadSfzString(fs::current_path(), R"(
<region> key=61 sample=*sine
<region> key=62 sample=*sine
<region> key=62 sample=*sine
<region> key=63 sample=*saw
<region> key=63 sample=*saw
<region> key=63 sample=*saw
)");
synth.noteOn(0, 61, 85);
REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(0)) == 1 );
REQUIRE( synth.getVoiceView(0)->getNextSisterVoice() == synth.getVoiceView(0) );
REQUIRE( synth.getVoiceView(0)->getPreviousSisterVoice() == synth.getVoiceView(0) );
synth.noteOn(0, 62, 85);
REQUIRE( synth.getNumActiveVoices() == 3 );
REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(1)) == 2 );
REQUIRE( synth.getVoiceView(1)->getNextSisterVoice() == synth.getVoiceView(2) );
REQUIRE( synth.getVoiceView(1)->getPreviousSisterVoice() == synth.getVoiceView(2) );
REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(2)) == 2 );
REQUIRE( synth.getVoiceView(2)->getNextSisterVoice() == synth.getVoiceView(1) );
REQUIRE( synth.getVoiceView(2)->getPreviousSisterVoice() == synth.getVoiceView(1) );
synth.noteOn(0, 63, 85);
REQUIRE( synth.getNumActiveVoices() == 6 );
REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(3)) == 3 );
REQUIRE( synth.getVoiceView(3)->getNextSisterVoice() == synth.getVoiceView(4) );
REQUIRE( synth.getVoiceView(3)->getPreviousSisterVoice() == synth.getVoiceView(5) );
REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(4)) == 3 );
REQUIRE( synth.getVoiceView(4)->getNextSisterVoice() == synth.getVoiceView(5) );
REQUIRE( synth.getVoiceView(4)->getPreviousSisterVoice() == synth.getVoiceView(3) );
REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(5)) == 3 );
REQUIRE( synth.getVoiceView(5)->getNextSisterVoice() == synth.getVoiceView(3) );
REQUIRE( synth.getVoiceView(5)->getPreviousSisterVoice() == synth.getVoiceView(4) );
}
TEST_CASE("[Synth] Apply function on sisters")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, 256 };
synth.loadSfzString(fs::current_path(), R"(
<region> key=63 sample=*saw
<region> key=63 sample=*saw
<region> key=63 sample=*saw
)");
synth.noteOn(0, 63, 85);
REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(0)) == 3 );
float start = 1.0f;
sfz::SisterVoiceRing::applyToRing(synth.getVoiceView(0), [&](const sfz::Voice* v) {
start += static_cast<float>(v->getTriggerNumber());
});
REQUIRE( start == 1.0f + 3.0f * 63.0f );
}
TEST_CASE("[Synth] Sisters and off-by")
{
sfz::Synth synth;
sfz::AudioBuffer<float> buffer { 2, 256 };
synth.loadSfzString(fs::current_path(), R"(
<region> key=62 sample=*sine
<group> group=1 off_by=2 <region> key=62 sample=*sine
<group> group=2 <region> key=63 sample=*saw
)");
synth.noteOn(0, 62, 85);
REQUIRE( synth.getNumActiveVoices() == 2 );
REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(0)) == 2 );
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 2 );
synth.noteOn(0, 63, 85);
REQUIRE( synth.getNumActiveVoices() == 3 );
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices() == 2 );
REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(0)) == 1 );
}