Merge pull request #246 from jpcima/voice-lifecycle

Voice lifecycle
This commit is contained in:
Paul Ferrand 2020-06-13 23:40:57 +02:00 committed by GitHub
commit 35837c501f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 231 additions and 53 deletions

View file

@ -35,7 +35,7 @@ public:
}
sfz::MidiState midiState;
sfz::Region region{midiState};
sfz::Region region{0, midiState};
sfz::ADSREnvelope<float> envelope;
std::vector<float> output;
};

41
src/sfizz/NumericId.h Normal file
View file

@ -0,0 +1,41 @@
// SPDX-License-Identifier: BSD-2-Clause
// This code is part of the sfizz library and is licensed under a BSD 2-clause
// license. You should have receive a LICENSE.md file along with the code.
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#pragma once
/**
* @brief Numeric identifier
*
* It is a generic numeric identifier. The template wrapper serves to enforce a
* stronger compile-time check, such that one kind of identifier can't be
* mistaken for another kind, or for an unrelated integer such as an index.
*/
template <class T>
struct NumericId {
constexpr NumericId() = default;
explicit constexpr NumericId(int number)
: number(number)
{
}
constexpr bool valid() const noexcept
{
return number != -1;
}
constexpr bool operator==(NumericId other) const noexcept
{
return number == other.number;
}
constexpr bool operator!=(NumericId other) const noexcept
{
return number != other.number;
}
const int number = -1;
};

View file

@ -16,6 +16,7 @@
#include "AudioBuffer.h"
#include "MidiState.h"
#include "FileId.h"
#include "NumericId.h"
#include "absl/types/optional.h"
#include <bitset>
#include <string>
@ -35,8 +36,8 @@ namespace sfz {
*
*/
struct Region {
Region(const MidiState& midiState, absl::string_view defaultPath = "")
: midiState(midiState), defaultPath(std::move(defaultPath))
Region(int regionNumber, const MidiState& midiState, absl::string_view defaultPath = "")
: id{regionNumber}, midiState(midiState), defaultPath(std::move(defaultPath))
{
ccSwitched.set();
@ -46,6 +47,14 @@ struct Region {
Region(const Region&) = default;
~Region() = default;
/**
* @brief Get the number which identifies this region
*/
NumericId<Region> getId() const noexcept
{
return id;
}
/**
* @brief Triggers on release?
*
@ -238,6 +247,8 @@ struct Region {
*/
float getGainToEffectBus(unsigned number) const noexcept;
const NumericId<Region> id;
// Sound source: sample playback
FileId sampleId {}; // Sample
int sampleQuality { Default::sampleQuality };

View file

@ -44,6 +44,13 @@ sfz::Synth::~Synth()
resources.filePool.emptyFileLoadingQueues();
}
void sfz::Synth::onVoiceStateChanged(NumericId<Voice> id, Voice::State state)
{
(void)id;
(void)state;
DBG("Voice " << id.number << ": state " << static_cast<int>(state));
}
void sfz::Synth::onParseFullBlock(const std::string& header, const std::vector<Opcode>& members)
{
switch (hash(header)) {
@ -95,7 +102,8 @@ void sfz::Synth::onParseWarning(const SourceRange& range, const std::string& mes
void sfz::Synth::buildRegion(const std::vector<Opcode>& regionOpcodes)
{
auto lastRegion = absl::make_unique<Region>(resources.midiState, defaultPath);
int regionNumber = static_cast<int>(regions.size());
auto lastRegion = absl::make_unique<Region>(regionNumber, resources.midiState, defaultPath);
auto parseOpcodes = [&](const std::vector<Opcode>& opcodes) {
for (auto& opcode : opcodes) {
@ -353,22 +361,20 @@ void sfz::Synth::finalizeSfzLoad()
{
resources.filePool.setRootDirectory(parser.originalDirectory());
auto currentRegion = regions.begin();
auto lastRegion = regions.rbegin();
auto removeCurrentRegion = [&currentRegion, &lastRegion]() {
if (currentRegion->get() == nullptr)
return;
size_t currentRegionIndex = 0;
size_t currentRegionCount = regions.size();
DBG("Removing the region with sample " << currentRegion->get()->sampleId);
std::iter_swap(currentRegion, lastRegion);
++lastRegion;
auto removeCurrentRegion = [this, &currentRegionIndex, &currentRegionCount]() {
DBG("Removing the region with sample " << regions[currentRegionIndex]->sampleId);
regions.erase(regions.begin() + currentRegionIndex);
--currentRegionCount;
};
size_t maxFilters { 0 };
size_t maxEQs { 0 };
while (currentRegion < lastRegion.base()) {
auto region = currentRegion->get();
while (currentRegionIndex < currentRegionCount) {
auto region = regions[currentRegionIndex].get();
if (!region->oscillator && !region->isGenerator()) {
if (!resources.filePool.checkSampleId(region->sampleId)) {
@ -473,11 +479,10 @@ void sfz::Synth::finalizeSfzLoad()
maxFilters = max(maxFilters, region->filters.size());
maxEQs = max(maxEQs, region->equalizers.size());
++currentRegion;
++currentRegionIndex;
}
const auto remainingRegions = std::distance(regions.begin(), lastRegion.base());
DBG("Removing " << (regions.size() - remainingRegions) << " out of " << regions.size() << " regions");
regions.resize(remainingRegions);
DBG("Removing " << (regions.size() - currentRegionCount) << " out of " << regions.size() << " regions");
regions.resize(currentRegionCount);
modificationTime = checkModificationTime();
for (auto& voice : voices) {
@ -1059,6 +1064,40 @@ const sfz::EffectBus* sfz::Synth::getEffectBusView(int idx) const noexcept
return (size_t)idx < effectBuses.size() ? effectBuses[idx].get() : nullptr;
}
const sfz::Region* sfz::Synth::getRegionById(NumericId<Region> id) const noexcept
{
const size_t size = regions.size();
if (size == 0 || !id.valid())
return nullptr;
// search a sequence of ordered identifiers with potential gaps
size_t index = static_cast<size_t>(id.number);
index = std::min(index, size - 1);
while (index > 0 && regions[index]->getId().number > id.number)
--index;
return (regions[index]->getId() == id) ? regions[index].get() : nullptr;
}
const sfz::Voice* sfz::Synth::getVoiceById(NumericId<Voice> id) const noexcept
{
const size_t size = voices.size();
if (size == 0 || !id.valid())
return nullptr;
// search a sequence of ordered identifiers with potential gaps
size_t index = static_cast<size_t>(id.number);
index = std::min(index, size - 1);
while (index > 0 && voices[index]->getId().number > id.number)
--index;
return (voices[index]->getId() == id) ? voices[index].get() : nullptr;
}
const sfz::Voice* sfz::Synth::getVoiceView(int idx) const noexcept
{
return (size_t)idx < voices.size() ? voices[idx].get() : nullptr;
@ -1104,8 +1143,11 @@ void sfz::Synth::resetVoices(int numVoices)
voices.clear();
voices.reserve(numVoices);
for (int i = 0; i < numVoices; ++i)
voices.push_back(absl::make_unique<Voice>(resources));
for (int i = 0; i < numVoices; ++i) {
auto voice = absl::make_unique<Voice>(i, resources);
voice->setStateListener(this);
voices.emplace_back(std::move(voice));
}
voiceViewArray.clear();
voiceViewArray.reserve(numVoices);

View file

@ -60,7 +60,7 @@ namespace sfz {
* The jack_client.cpp file contains examples of the most classical usage of the
* synth and can be used as a reference.
*/
class Synth final : public Parser::Listener {
class Synth final : public Voice::StateListener, public Parser::Listener {
public:
/**
* @brief Construct a new Synth object with no voices. If you want sound
@ -184,6 +184,20 @@ public:
* @brief Export a MIDI Name document describing the loaded instrument
*/
std::string exportMidnam(absl::string_view model = {}) const;
/**
* @brief Find the region which is associated with the given identifier.
*
* @param id
* @return const Region*
*/
const Region* getRegionById(NumericId<Region> id) const noexcept;
/**
* @brief Find the voice which is associated with the given identifier.
*
* @param id
* @return const Voice*
*/
const Voice* getVoiceById(NumericId<Voice> id) const noexcept;
/**
* @brief Get a raw view into a specific region. This is mostly used
* for testing.
@ -499,6 +513,12 @@ public:
*/
const std::vector<CCNamePair>& getCCLabels() const noexcept { return ccLabels; }
protected:
/**
* @brief The voice callback which is called during a change of state.
*/
void onVoiceStateChanged(NumericId<Voice> idNumber, Voice::State state) override;
protected:
/**
* @brief The parser callback; this is called by the parent object each time

View file

@ -14,8 +14,8 @@
#include "Interpolators.h"
#include "absl/algorithm/container.h"
sfz::Voice::Voice(sfz::Resources& resources)
: resources(resources)
sfz::Voice::Voice(int voiceNumber, sfz::Resources& resources)
: id{voiceNumber}, stateListener(nullptr), resources(resources)
{
filters.reserve(config::filtersPerVoice);
equalizers.reserve(config::eqsPerVoice);
@ -36,7 +36,7 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value,
triggerValue = value;
this->region = region;
state = State::playing;
switchState(State::playing);
ASSERT(delay >= 0);
if (delay < 0)
@ -652,7 +652,7 @@ bool sfz::Voice::checkOffGroup(int delay, uint32_t group) noexcept
void sfz::Voice::reset() noexcept
{
state = State::idle;
switchState(State::idle);
region = nullptr;
currentPromise.reset();
sourcePosition = 0;
@ -771,3 +771,13 @@ void sfz::Voice::updateChannelPowers(AudioSpan<float> buffer)
channelEnvelopeFilters[i].tickLowpass(std::abs(input[s]));
}
}
void sfz::Voice::switchState(State s)
{
if (s != state) {
state = s;
if (stateListener)
stateListener->onVoiceStateChanged(id, s);
}
}

View file

@ -14,6 +14,7 @@
#include "AudioSpan.h"
#include "LeakDetector.h"
#include "OnePoleFilter.h"
#include "NumericId.h"
#include "absl/types/span.h"
#include <memory>
#include <random>
@ -32,14 +33,39 @@ public:
/**
* @brief Construct a new voice with the midistate singleton
*
* @param voiceNumber
* @param midiState
*/
Voice(Resources& resources);
Voice(int voiceNumber, Resources& resources);
enum class TriggerType {
NoteOn,
NoteOff,
CC
};
/**
* @brief Get the unique identifier of this voice in a synth
*/
NumericId<Voice> getId() const noexcept
{
return id;
}
enum class State {
idle,
playing
};
class StateListener {
public:
virtual void onVoiceStateChanged(NumericId<Voice> /*id*/, State /*state*/) {}
};
/**
* @brief Sets the listener which is called when the voice state changes.
*/
void setStateListener(StateListener *l) noexcept { stateListener = l; }
/**
* @brief Change the sample rate of the voice. This is used to compute all
* pitch related transformations so it needs to be propagated from the synth
@ -270,12 +296,16 @@ private:
void setupOscillatorUnison();
void updateChannelPowers(AudioSpan<float> buffer);
/**
* @brief Modify the voice state and notify any listeners.
*/
void switchState(State s);
const NumericId<Voice> id;
StateListener* stateListener = nullptr;
Region* region { nullptr };
enum class State {
idle,
playing
};
State state { State::idle };
bool noteIsOff { false };

View file

@ -13,7 +13,7 @@ using namespace sfz::literals;
TEST_CASE("Region activation", "Region tests")
{
sfz::MidiState midiState;
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" });
SECTION("Basic state")

View file

@ -14,7 +14,7 @@ using namespace sfz::literals;
TEST_CASE("[Region] Parsing opcodes")
{
sfz::MidiState midiState;
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
SECTION("sample")
{
@ -1672,7 +1672,7 @@ TEST_CASE("[Region] Parsing opcodes")
TEST_CASE("[Region] Non-conforming floating point values in integer opcodes")
{
sfz::MidiState midiState;
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "offset", "2014.5" });
REQUIRE(region.offset == 2014);
region.parseOpcode({ "pitch_keytrack", "-2.1" });

View file

@ -13,7 +13,7 @@ using namespace sfz::literals;
TEST_CASE("Basic triggers", "Region triggers")
{
sfz::MidiState midiState;
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" });
SECTION("key")
@ -130,7 +130,7 @@ TEST_CASE("Basic triggers", "Region triggers")
TEST_CASE("Legato triggers", "Region triggers")
{
sfz::MidiState midiState;
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" });
SECTION("First note playing")
{

View file

@ -17,7 +17,7 @@ constexpr int numRandomTests { 64 };
TEST_CASE("[Region] Crossfade in on key")
{
sfz::MidiState midiState;
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfin_lokey", "1" });
region.parseOpcode({ "xfin_hikey", "3" });
@ -29,7 +29,7 @@ TEST_CASE("[Region] Crossfade in on key")
TEST_CASE("[Region] Crossfade in on key - 2")
{
sfz::MidiState midiState;
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfin_lokey", "1" });
region.parseOpcode({ "xfin_hikey", "5" });
@ -44,7 +44,7 @@ TEST_CASE("[Region] Crossfade in on key - 2")
TEST_CASE("[Region] Crossfade in on key - gain")
{
sfz::MidiState midiState;
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfin_lokey", "1" });
region.parseOpcode({ "xfin_hikey", "5" });
@ -59,7 +59,7 @@ TEST_CASE("[Region] Crossfade in on key - gain")
TEST_CASE("[Region] Crossfade out on key")
{
sfz::MidiState midiState;
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfout_lokey", "51" });
region.parseOpcode({ "xfout_hikey", "55" });
@ -75,7 +75,7 @@ TEST_CASE("[Region] Crossfade out on key")
TEST_CASE("[Region] Crossfade out on key - gain")
{
sfz::MidiState midiState;
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfout_lokey", "51" });
region.parseOpcode({ "xfout_hikey", "55" });
@ -92,7 +92,7 @@ TEST_CASE("[Region] Crossfade out on key - gain")
TEST_CASE("[Region] Crossfade in on velocity")
{
sfz::MidiState midiState;
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfin_lovel", "20" });
region.parseOpcode({ "xfin_hivel", "24" });
@ -109,7 +109,7 @@ TEST_CASE("[Region] Crossfade in on velocity")
TEST_CASE("[Region] Crossfade in on vel - gain")
{
sfz::MidiState midiState;
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfin_lovel", "20" });
region.parseOpcode({ "xfin_hivel", "24" });
@ -127,7 +127,7 @@ TEST_CASE("[Region] Crossfade in on vel - gain")
TEST_CASE("[Region] Crossfade out on vel")
{
sfz::MidiState midiState;
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfout_lovel", "51" });
region.parseOpcode({ "xfout_hivel", "55" });
@ -144,7 +144,7 @@ TEST_CASE("[Region] Crossfade out on vel")
TEST_CASE("[Region] Crossfade out on vel - gain")
{
sfz::MidiState midiState;
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfout_lovel", "51" });
region.parseOpcode({ "xfout_hivel", "55" });
@ -162,7 +162,7 @@ TEST_CASE("[Region] Crossfade out on vel - gain")
TEST_CASE("[Region] Crossfade in on CC")
{
sfz::MidiState midiState;
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfin_locc24", "20" });
region.parseOpcode({ "xfin_hicc24", "24" });
@ -186,7 +186,7 @@ TEST_CASE("[Region] Crossfade in on CC")
TEST_CASE("[Region] Crossfade in on CC - gain")
{
sfz::MidiState midiState;
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfin_locc24", "20" });
region.parseOpcode({ "xfin_hicc24", "24" });
@ -210,7 +210,7 @@ TEST_CASE("[Region] Crossfade in on CC - gain")
TEST_CASE("[Region] Crossfade out on CC")
{
sfz::MidiState midiState;
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfout_locc24", "20" });
region.parseOpcode({ "xfout_hicc24", "24" });
@ -234,7 +234,7 @@ TEST_CASE("[Region] Crossfade out on CC")
TEST_CASE("[Region] Crossfade out on CC - gain")
{
sfz::MidiState midiState;
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfout_locc24", "20" });
region.parseOpcode({ "xfout_hicc24", "24" });
@ -259,7 +259,7 @@ TEST_CASE("[Region] Crossfade out on CC - gain")
TEST_CASE("[Region] Velocity bug for extreme values - veltrack at 0")
{
sfz::MidiState midiState;
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "amp_veltrack", "0" });
REQUIRE(region.getNoteGain(64, 127_norm) == 1.0_a);
@ -270,7 +270,7 @@ TEST_CASE("[Region] Velocity bug for extreme values - veltrack at 0")
TEST_CASE("[Region] Velocity bug for extreme values - positive veltrack")
{
sfz::MidiState midiState;
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "amp_veltrack", "100" });
REQUIRE(region.getNoteGain(64, 127_norm) == 1.0_a);
@ -280,7 +280,7 @@ TEST_CASE("[Region] Velocity bug for extreme values - positive veltrack")
TEST_CASE("[Region] Velocity bug for extreme values - negative veltrack")
{
sfz::MidiState midiState;
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "amp_veltrack", "-100" });
REQUIRE(region.getNoteGain(64, 127_norm) == Approx(0.0).margin(0.0001));
@ -291,7 +291,7 @@ TEST_CASE("[Region] rt_decay")
{
sfz::MidiState midiState;
midiState.setSampleRate(1000);
sfz::Region region { midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "trigger", "release" });
region.parseOpcode({ "rt_decay", "10" });
@ -311,7 +311,7 @@ TEST_CASE("[Region] rt_decay")
TEST_CASE("[Region] Base delay")
{
sfz::MidiState midiState;
sfz::Region region{ midiState };
sfz::Region region { 0, midiState };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "delay", "10" });
REQUIRE( region.getDelay() == 10.0f );

View file

@ -6,6 +6,7 @@
#include "sfizz/Synth.h"
#include "sfizz/SfzHelpers.h"
#include "sfizz/NumericId.h"
#include "catch2/catch.hpp"
using namespace Catch::literals;
using namespace sfz::literals;
@ -462,3 +463,26 @@ TEST_CASE("[Synth] velcurve")
REQUIRE( synth.getRegionView(1)->velocityCurve(96_norm) == Approx(0.5f).margin(1e-2) );
REQUIRE( synth.getRegionView(1)->velocityCurve(127_norm) == 0.0_a );
}
TEST_CASE("[Synth] Region by identifier")
{
sfz::Synth synth;
synth.loadSfzString("regionByIdentifier.sfz", R"(
<region>sample=*sine
<region>sample=*sine
<region>sample=doesNotExist.wav
<region>sample=*sine
<region>sample=doesNotExist.wav
<region>sample=*sine
)");
REQUIRE(synth.getNumRegions() == 4);
REQUIRE(synth.getRegionView(0) == synth.getRegionById(NumericId<sfz::Region>{0}));
REQUIRE(synth.getRegionView(1) == synth.getRegionById(NumericId<sfz::Region>{1}));
REQUIRE(nullptr == synth.getRegionById(NumericId<sfz::Region>{2}));
REQUIRE(synth.getRegionView(2) == synth.getRegionById(NumericId<sfz::Region>{3}));
REQUIRE(nullptr == synth.getRegionById(NumericId<sfz::Region>{4}));
REQUIRE(synth.getRegionView(3) == synth.getRegionById(NumericId<sfz::Region>{5}));
REQUIRE(nullptr == synth.getRegionById(NumericId<sfz::Region>{6}));
REQUIRE(nullptr == synth.getRegionById(NumericId<sfz::Region>{}));
}