Add voices to a ring upon startup, and remove them when resetting the voice
This commit is contained in:
parent
97b4335209
commit
f4660b1a02
5 changed files with 244 additions and 0 deletions
90
src/sfizz/SisterVoiceRing.h
Normal file
90
src/sfizz/SisterVoiceRing.h
Normal 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 };
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
#include "absl/algorithm/container.h"
|
#include "absl/algorithm/container.h"
|
||||||
#include "absl/memory/memory.h"
|
#include "absl/memory/memory.h"
|
||||||
#include "absl/strings/str_replace.h"
|
#include "absl/strings/str_replace.h"
|
||||||
|
#include "SisterVoiceRing.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <iostream>
|
#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
|
void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noexcept
|
||||||
{
|
{
|
||||||
const auto randValue = randNoteDistribution(Random::randomGenerator);
|
const auto randValue = randNoteDistribution(Random::randomGenerator);
|
||||||
|
SisterVoiceRingBuilder ring;
|
||||||
|
|
||||||
for (auto& region : noteActivationLists[noteNumber]) {
|
for (auto& region : noteActivationLists[noteNumber]) {
|
||||||
if (region->registerNoteOff(noteNumber, velocity, randValue)) {
|
if (region->registerNoteOff(noteNumber, velocity, randValue)) {
|
||||||
auto voice = findFreeVoice();
|
auto voice = findFreeVoice();
|
||||||
|
|
@ -807,6 +810,7 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOff);
|
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
|
void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexcept
|
||||||
{
|
{
|
||||||
const auto randValue = randNoteDistribution(Random::randomGenerator);
|
const auto randValue = randNoteDistribution(Random::randomGenerator);
|
||||||
|
SisterVoiceRingBuilder ring;
|
||||||
|
|
||||||
for (auto& region : noteActivationLists[noteNumber]) {
|
for (auto& region : noteActivationLists[noteNumber]) {
|
||||||
if (region->registerNoteOn(noteNumber, velocity, randValue)) {
|
if (region->registerNoteOn(noteNumber, velocity, randValue)) {
|
||||||
unsigned activeNotesInGroup { 0 };
|
unsigned activeNotesInGroup { 0 };
|
||||||
|
|
@ -865,6 +871,7 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn);
|
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)
|
for (auto& voice : voices)
|
||||||
voice->registerCC(delay, ccNumber, normValue);
|
voice->registerCC(delay, ccNumber, normValue);
|
||||||
|
|
||||||
|
SisterVoiceRingBuilder ring;
|
||||||
|
|
||||||
for (auto& region : ccActivationLists[ccNumber]) {
|
for (auto& region : ccActivationLists[ccNumber]) {
|
||||||
if (region->registerCC(ccNumber, normValue)) {
|
if (region->registerCC(ccNumber, normValue)) {
|
||||||
auto voice = findFreeVoice();
|
auto voice = findFreeVoice();
|
||||||
|
|
@ -902,6 +911,7 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
voice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC);
|
voice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC);
|
||||||
|
ring.addVoiceToRing(voice);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -674,6 +674,30 @@ void sfz::Voice::reset() noexcept
|
||||||
|
|
||||||
filters.clear();
|
filters.clear();
|
||||||
equalizers.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
|
float sfz::Voice::getAverageEnvelope() const noexcept
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,40 @@ public:
|
||||||
*/
|
*/
|
||||||
void reset() noexcept;
|
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
|
* @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.
|
* 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 panStageStereo(AudioSpan<float> buffer) noexcept;
|
||||||
void filterStageMono(AudioSpan<float> buffer) noexcept;
|
void filterStageMono(AudioSpan<float> buffer) noexcept;
|
||||||
void filterStageStereo(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.
|
* @brief Initialize frequency and gain coefficients for the oscillators.
|
||||||
*/
|
*/
|
||||||
|
|
@ -359,6 +399,9 @@ private:
|
||||||
Duration panningDuration;
|
Duration panningDuration;
|
||||||
Duration filterDuration;
|
Duration filterDuration;
|
||||||
|
|
||||||
|
Voice* nextSisterVoice { this };
|
||||||
|
Voice* previousSisterVoice { this };
|
||||||
|
|
||||||
std::normal_distribution<float> noiseDist { 0, config::noiseVariance };
|
std::normal_distribution<float> noiseDist { 0, config::noiseVariance };
|
||||||
|
|
||||||
std::array<OnePoleFilter<float>, 2> channelEnvelopeFilters;
|
std::array<OnePoleFilter<float>, 2> channelEnvelopeFilters;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
|
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
|
||||||
|
|
||||||
#include "sfizz/Synth.h"
|
#include "sfizz/Synth.h"
|
||||||
|
#include "sfizz/SisterVoiceRing.h"
|
||||||
#include "sfizz/SfzHelpers.h"
|
#include "sfizz/SfzHelpers.h"
|
||||||
#include "sfizz/NumericId.h"
|
#include "sfizz/NumericId.h"
|
||||||
#include "catch2/catch.hpp"
|
#include "catch2/catch.hpp"
|
||||||
|
|
@ -540,3 +541,79 @@ TEST_CASE("[Synth] sample quality")
|
||||||
synth.allSoundOff();
|
synth.allSoundOff();
|
||||||
synth.disableFreeWheeling();
|
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 );
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue