Merge pull request #533 from paulfd/voice-synth-pimpl

Refactor of Synth and Voice into a pimpl pattern
This commit is contained in:
Paul Ferrand 2020-11-01 10:09:06 +01:00 committed by GitHub
commit 5d90b2a82c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 2532 additions and 2032 deletions

View file

@ -115,6 +115,7 @@ SFIZZ_SOURCES = \
src/sfizz/Tuning.cpp \
src/sfizz/utility/SpinMutex.cpp \
src/sfizz/Voice.cpp \
src/sfizz/VoiceManager.cpp \
src/sfizz/VoiceStealing.cpp \
src/sfizz/Wavetables.cpp

View file

@ -109,6 +109,7 @@ set (SFIZZ_HEADERS
sfizz/SynthConfig.h
sfizz/Tuning.h
sfizz/Voice.h
sfizz/VoiceManager.h
sfizz/VoiceStealing.h
sfizz/Wavetables.h
sfizz.h
@ -137,6 +138,7 @@ set (SFIZZ_SOURCES
sfizz/Tuning.cpp
sfizz/RegionSet.cpp
sfizz/PolyphonyGroup.cpp
sfizz/VoiceManager.cpp
sfizz/VoiceStealing.cpp
sfizz/RTSemaphore.cpp
sfizz/Panning.cpp

View file

@ -143,7 +143,7 @@ void sfz::Oversampler::stream(AudioSpan<float> input, AudioSpan<float> output, s
void sfz::Oversampler::stream(AudioReader& input, AudioSpan<float> output, std::atomic<size_t>* framesReady)
{
ASSERT(output.getNumFrames() >= input.frames() * static_cast<int>(factor));
ASSERT(output.getNumFrames() >= static_cast<size_t>(input.frames() * static_cast<int>(factor)));
ASSERT(output.getNumChannels() == input.channels());
const auto numFrames = static_cast<size_t>(input.frames());

View file

@ -6,6 +6,7 @@
#pragma once
#include "Config.h"
#include "Region.h"
#include "Voice.h"
#include "SwapAndPop.h"

File diff suppressed because it is too large Load diff

View file

@ -5,31 +5,26 @@
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#pragma once
#include "Resources.h"
#include "Parser.h"
#include "Voice.h"
#include "Region.h"
#include "RegionSet.h"
#include "PolyphonyGroup.h"
#include "Effects.h"
#include "LeakDetector.h"
#include "MidiState.h"
#include "AudioSpan.h"
#include "LeakDetector.h"
#include "Resources.h"
#include "utility/NumericId.h"
#include "parser/Parser.h"
#include "VoiceStealing.h"
#include "utility/SpinMutex.h"
#include <absl/types/span.h>
#include <absl/types/optional.h>
#include <ghc/fs_std.hpp>
#include <absl/strings/string_view.h>
#include <random>
#include <set>
#include <memory>
#include <bitset>
#include <string>
#include <vector>
namespace sfz {
class ControllerSource;
class LFOSource;
class FlexEnvelopeSource;
class ADSREnvelopeSource;
// Forward declarations for the introspection methods
class RegionSet;
class PolyphonyGroup;
class EffectBus;
class Region;
class Voice;
/**
* @brief This class is the core of the sfizz library. In C++ it is the main point
@ -68,11 +63,10 @@ class ADSREnvelopeSource;
* 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 Voice::StateListener, public Parser::Listener {
class Synth final {
public:
/**
* @brief Construct a new Synth object with no voices. If you want sound
* you will need to call setNumVoices() before playing.
* @brief Construct a new Synth object with a default number of voices.
*
*/
Synth();
@ -80,13 +74,10 @@ public:
* @brief Destructor
*/
~Synth();
/**
* @brief Construct a new Synth object with a specified number of voices.
*
* @param numVoices
*/
Synth(int numVoices);
Synth(const Synth& other) = delete;
Synth& operator=(const Synth& other) = delete;
Synth(Synth&& other) = delete;
Synth& operator=(Synth&& other) = delete;
/**
* @brief Processing mode
*/
@ -122,11 +113,6 @@ public:
* @true otherwise.
*/
bool loadSfzString(const fs::path& path, absl::string_view text);
/**
* @brief Finalize SFZ loading, following a successful execution of the
* parsing step.
*/
void finalizeSfzLoad();
/**
* @brief Sets the tuning from a Scala file loaded from the file system.
*
@ -208,24 +194,6 @@ public:
* @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 Find the voice which is associated with the given identifier.
*
* @param id
* @return Voice*
*/
Voice* getVoiceById(NumericId<Voice> id) noexcept
{
return const_cast<Voice*>(
const_cast<const Synth*>(this)->getVoiceById(id));
}
/**
* @brief Get a raw view into a specific region. This is mostly used
* for testing.
@ -245,6 +213,7 @@ public:
/**
* @brief Get a raw view into a specific effect bus. This is mostly used
* for testing.
* You'll need to include "Effects.h" to resolve the forward declaration.
*
* @param idx
* @return const EffectBus*
@ -253,6 +222,7 @@ public:
/**
* @brief Get a raw view into a specific set of regions. This is mostly used
* for testing.
* You'll need to include "RegionSet.h" to resolve the forward declaration.
*
* @param idx
* @return const RegionSet*
@ -261,6 +231,7 @@ public:
/**
* @brief Get a raw view into a specific polyphony group. This is mostly used
* for testing.
* You'll need to include "PolyphonyGroup.h" to resolve the forward declaration.
*
* @param idx
* @return const PolyphonyGroup*
@ -370,29 +341,20 @@ public:
* @param normValue the normalized cc value, in domain 0 to 1
*/
void hdcc(int delay, int ccNumber, float normValue) noexcept;
private:
/**
* @brief Set the initial value of a controller and send it to the synth
* @brief Get the current value of a controller under the current instrument
*
* @param ccNumber the cc number
* @param ccValue the cc value
* @return the current value
*/
void initCc(int ccNumber, uint8_t ccValue) noexcept;
float getHdcc(int ccNumber);
/**
* @brief Set the initial value of a controller and send it to the synth
* @brief Get the default value of a controller under the current instrument
*
* @param ccNumber the cc number
* @param normValue the normalized cc value, in domain 0 to 1
* @return the default value
*/
void initHdcc(int ccNumber, float normValue) noexcept;
public:
/**
* @brief Get the initial value of a controller under the current instrument
*
* @param ccNumber the cc number
* @return the initial value
*/
float getHdccInit(int ccNumber);
float getDefaultHdcc(int ccNumber);
/**
* @brief Send a pitch bend event to the synth
*
@ -458,7 +420,7 @@ public:
*
* @return int
*/
int getNumActiveVoices(bool recompute = false) const noexcept;
int getNumActiveVoices() const noexcept;
/**
* @brief Get the total number of voices in the synth (the polyphony)
*
@ -475,15 +437,6 @@ public:
* @param numVoices
*/
void setNumVoices(int numVoices) noexcept;
/**
* @brief Trigger a garbage collection, which removes the samples that are
* loaded by the FilePool after being requested by the voices. This does
* not concern the preloaded samples, only the samples loaded to be played
* fully. This function is run regularly in a background thread so normally
* you should not need to call it explicitely.
*
*/
void garbageCollect() noexcept;
/**
* @brief Set the oversampling factor to a new value.
@ -551,8 +504,8 @@ public:
*/
void disableFreeWheeling() noexcept;
Resources& getResources() noexcept { return resources; }
const Resources& getResources() const noexcept { return resources; }
Resources& getResources() noexcept;
const Resources& getResources() const noexcept;
/**
* @brief Check if the SFZ should be reloaded.
@ -604,26 +557,26 @@ public:
*
* @return A reference to the parser.
*/
Parser& getParser() noexcept { return parser; }
Parser& getParser() noexcept;
/**
* @brief Get the parser.
*
* @return A reference to the parser.
*/
const Parser& getParser() const noexcept { return parser; }
const Parser& getParser() const noexcept;
/**
* @brief Get the key labels, if any
*
* @return const std::vector<NoteNamePair>&
*/
const std::vector<NoteNamePair>& getKeyLabels() const noexcept { return keyLabels; }
const std::vector<NoteNamePair>& getKeyLabels() const noexcept;
/**
* @brief Get the CC labels, if any
*
* @return const std::vector<NoteNamePair>&
*/
const std::vector<CCNamePair>& getCCLabels() const noexcept { return ccLabels; }
const std::vector<CCNamePair>& getCCLabels() const noexcept;
/**
* @brief Get the used CCs
@ -632,332 +585,9 @@ public:
*/
std::bitset<config::numCCs> getUsedCCs() const noexcept;
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
* a new region, group, master, global, curve or control set of opcodes
* appears in the parser
*
* @param header the header for the set of opcodes
* @param members the opcode members
*/
void onParseFullBlock(const std::string& header, const std::vector<Opcode>& members) override;
/**
* @brief The parser callback when an error occurs.
*/
void onParseError(const SourceRange& range, const std::string& message) override;
/**
* @brief The parser callback when a warning occurs.
*/
void onParseWarning(const SourceRange& range, const std::string& message) override;
private:
/**
* @brief change the group maximum polyphony
*
* @param groupIdx the group index
* @param polyphone the max polyphony
*/
void setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept;
/**
* @brief Reset all CCs; to be used on CC 121
*
* @param delay the delay for the controller reset
*
*/
void resetAllControllers(int delay) noexcept;
int numGroups { 0 };
int numMasters { 0 };
/**
* @brief Remove all regions, resets all voices and clears everything
* to bring back the synth in its original state.
*
* The callback mutex should be taken to call this function.
*/
void clear();
/**
* @brief Helper function to dispatch <global> opcodes
*
* @param members the opcodes of the <global> block
*/
void handleGlobalOpcodes(const std::vector<Opcode>& members);
/**
* @brief Helper function to dispatch <master> opcodes
*
* @param members the opcodes of the <master> block
*/
void handleMasterOpcodes(const std::vector<Opcode>& members);
/**
* @brief Helper function to dispatch <group> opcodes
*
* @param members the opcodes of the <group> block
*/
void handleGroupOpcodes(const std::vector<Opcode>& members, const std::vector<Opcode>& masterMembers);
/**
* @brief Helper function to dispatch <control> opcodes
*
* @param members the opcodes of the <control> block
*/
void handleControlOpcodes(const std::vector<Opcode>& members);
/**
* @brief Helper function to dispatch <effect> opcodes
*
* @param members the opcodes of the <effect> block
*/
void handleEffectOpcodes(const std::vector<Opcode>& members);
/**
* @brief Helper function to merge all the currently active opcodes
* as set by the successive callbacks and create a new region to store
* in the synth.
*
* @param regionOpcodes the opcodes that are specific to the region
*/
void buildRegion(const std::vector<Opcode>& regionOpcodes);
/**
* @brief Resets and possibly changes the number of voices (polyphony) in
* the synth.
*
* @param numVoices
*/
void resetVoices(int numVoices);
/**
* @brief Make the stored settings take effect in all the voices
*/
void applySettingsPerVoice();
/**
* @brief Establish all connections of the modulation matrix.
*/
void setupModMatrix();
/**
* @brief Get the modification time of all included sfz files
*
* @return fs::file_time_type
*/
fs::file_time_type checkModificationTime();
/**
* @brief Check all regions and start voices for note on events
*
* @param delay
* @param noteNumber
* @param velocity
*/
void noteOnDispatch(int delay, int noteNumber, float velocity) noexcept;
/**
* @brief Check all regions and start voices for note off events
*
* @param delay
* @param noteNumber
* @param velocity
*/
void noteOffDispatch(int delay, int noteNumber, float velocity) noexcept;
/**
* @brief Check all regions and start voices for cc events
*
* @param delay
* @param ccNumber
* @param value
*/
void ccDispatch(int delay, int ccNumber, float value) noexcept;
template<class T>
static void updateUsedCCsFromCCMap(std::bitset<sfz::config::numCCs>& usedCCs, const CCMap<T> map)
{
for (auto& mod : map)
usedCCs[mod.cc] = true;
}
static void updateUsedCCsFromRegion(std::bitset<sfz::config::numCCs>& usedCCs, const Region& region);
static void updateUsedCCsFromModulations(std::bitset<sfz::config::numCCs>& usedCCs, const ModMatrix& mm);
// 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;
std::vector<Opcode> masterOpcodes;
std::vector<Opcode> groupOpcodes;
/**
* @brief Find a voice that is not currently playing
*
* @return Voice*
*/
Voice* findFreeVoice() noexcept;
// Names for the CC and notes as set by label_cc and label_key
std::vector<CCNamePair> ccLabels;
std::vector<NoteNamePair> keyLabels;
std::vector<NoteNamePair> keyswitchLabels;
// Set as sw_default if present in the file
absl::optional<uint8_t> currentSwitch;
std::vector<std::string> unknownOpcodes;
using RegionViewVector = std::vector<Region*>;
using VoiceViewVector = std::vector<Voice*>;
using VoicePtr = std::unique_ptr<Voice>;
using RegionPtr = std::unique_ptr<Region>;
using RegionSetPtr = std::unique_ptr<RegionSet>;
std::vector<RegionPtr> regions;
std::vector<VoicePtr> voices;
// These are more general "groups" than sfz and encapsulates the full hierarchy
RegionSet* currentSet { nullptr };
std::vector<RegionSetPtr> sets;
// This region set holds the engine set of voices, which tries to respect the required
// engine polyphony
RegionSetPtr engineSet;
// These are the `group=` groups where you can off voices
std::vector<PolyphonyGroup> polyphonyGroups;
// Views to speed up iteration over the regions and voices when events
// occur in the audio callback
VoiceViewVector tempPolyphonyArray;
VoiceViewVector voiceViewArray;
VoiceStealing stealer;
/**
* @brief Check the region polyphony, releasing voices if necessary
*
* @param region
* @param delay
*/
void checkRegionPolyphony(const Region* region, int delay) noexcept;
/**
* @brief Check the note polyphony, releasing voices if necessary
*
* @param region
* @param delay
* @param triggerEvent
*/
void checkNotePolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept;
/**
* @brief Check the group polyphony, releasing voices if necessary
*
* @param region
* @param delay
*/
void checkGroupPolyphony(const Region* region, int delay) noexcept;
/**
* @brief Check the region set polyphony at all levels, releasing voices if necessary
*
* @param region
* @param delay
*/
void checkSetPolyphony(const Region* region, int delay) noexcept;
/**
* @brief Check the engine polyphony, fast releasing voices if necessary
*
* @param delay
*/
void checkEnginePolyphony(int delay) noexcept;
/**
* @brief Start a voice for a specific region.
* This will do the needed polyphony checks and voice stealing.
*
* @param region
* @param delay
* @param triggerEvent
* @param ring
*/
void startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept;
/**
* @brief Start all delayed release voices of the region if necessary
*
* @param region
* @param delay
* @param ring
*/
void startDelayedReleaseVoices(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept;
/**
* @brief Check if a playing voice matches the release region
*
* @param releaseRegion
* @return true
* @return false
*/
bool playingAttackVoice(const Region* releaseRegion) noexcept;
std::array<RegionViewVector, 128> lastKeyswitchLists;
std::array<RegionViewVector, 128> downKeyswitchLists;
std::array<RegionViewVector, 128> upKeyswitchLists;
RegionViewVector previousKeyswitchLists;
std::array<RegionViewVector, 128> noteActivationLists;
std::array<RegionViewVector, config::numCCs> ccActivationLists;
// Effect factory and buses
EffectFactory effectFactory;
typedef std::unique_ptr<EffectBus> EffectBusPtr;
std::vector<EffectBusPtr> effectBuses; // 0 is "main", 1-N are "fx1"-"fxN"
int samplesPerBlock { config::defaultSamplesPerBlock };
float sampleRate { config::defaultSampleRate };
float volume { Default::globalVolume };
int numRequiredVoices { config::numVoices };
int numActualVoices { static_cast<int>(config::numVoices * config::overflowVoiceMultiplier) };
int activeVoices { 0 };
Oversampling oversamplingFactor { config::defaultOversamplingFactor };
// Distribution used to generate random value for the *rand opcodes
std::uniform_real_distribution<float> randNoteDistribution { 0, 1 };
SpinMutex callbackGuard;
// Singletons passed as references to the voices
Resources resources;
// Control opcodes
std::string defaultPath { "" };
int noteOffset { 0 };
int octaveOffset { 0 };
// Modulation source generators
std::unique_ptr<ControllerSource> genController;
std::unique_ptr<LFOSource> genLFO;
std::unique_ptr<FlexEnvelopeSource> genFlexEnvelope;
std::unique_ptr<ADSREnvelopeSource> genADSREnvelope;
// Settings per voice
struct SettingsPerVoice {
size_t maxFilters { 0 };
size_t maxEQs { 0 };
size_t maxLFOs { 0 };
size_t maxFlexEGs { 0 };
bool havePitchEG { false };
bool haveFilterEG { false };
};
SettingsPerVoice settingsPerVoice;
// Controller initial values
std::array<float, config::numCCs> ccInitialValues;
Duration dispatchDuration { 0 };
std::chrono::time_point<std::chrono::high_resolution_clock> lastGarbageCollection;
Parser parser;
fs::file_time_type modificationTime { };
struct Impl;
std::unique_ptr<Impl> impl_;
LEAK_DETECTOR(Synth);
};

File diff suppressed because it is too large Load diff

View file

@ -5,24 +5,14 @@
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#pragma once
#include "TriggerEvent.h"
#include "Config.h"
#include "ADSREnvelope.h"
#include "HistoricalBuffer.h"
#include "TriggerEvent.h"
#include "Region.h"
#include "AudioBuffer.h"
#include "Resources.h"
#include "FilterPool.h"
#include "EQPool.h"
#include "Smoothers.h"
#include "AudioSpan.h"
#include "LeakDetector.h"
#include "OnePoleFilter.h"
#include "PowerFollower.h"
#include "utility/NumericId.h"
#include "absl/types/span.h"
#include <memory>
#include <random>
namespace sfz {
enum InterpolatorModel : int;
@ -44,16 +34,16 @@ public:
* @param midiState
*/
Voice(int voiceNumber, Resources& resources);
~Voice();
Voice(const Voice& other) = delete;
Voice& operator=(const Voice& other) = delete;
Voice(Voice&& other) noexcept;
Voice& operator=(Voice&& other) noexcept;
/**
* @brief Get the unique identifier of this voice in a synth
*/
NumericId<Voice> getId() const noexcept
{
return id;
}
NumericId<Voice> getId() const noexcept;
enum class State {
idle,
@ -63,18 +53,18 @@ public:
class StateListener {
public:
virtual void onVoiceStateChanged(NumericId<Voice> /*id*/, State /*state*/) {}
virtual void onVoiceStateChanging(NumericId<Voice> /*id*/, State /*state*/) {}
};
/**
* @brief Return true if the voice is to be cleaned up (zombie state)
*/
bool toBeCleanedUp() const { return state == State::cleanMeUp; }
bool toBeCleanedUp() const;
/**
* @brief Sets the listener which is called when the voice state changes.
*/
void setStateListener(StateListener *l) noexcept { stateListener = l; }
void setStateListener(StateListener *l) noexcept;
/**
* @brief Change the sample rate of the voice. This is used to compute all
@ -98,13 +88,13 @@ public:
*
* @return float
*/
float getSampleRate() const noexcept { return sampleRate; }
float getSampleRate() const noexcept;
/**
* @brief Get the expected block size.
*
* @return int
*/
int getSamplesPerBlock() const noexcept { return samplesPerBlock; }
int getSamplesPerBlock() const noexcept;
/**
* @brief Start playing a region after a short delay for different triggers (note on, off, cc)
@ -199,7 +189,7 @@ public:
*
* @return int
*/
const TriggerEvent& getTriggerEvent() const noexcept { return triggerEvent; }
const TriggerEvent& getTriggerEvent() const noexcept;
/**
* @brief Reset the voice to its initial values
@ -232,14 +222,14 @@ public:
*
* @return Voice*
*/
Voice* getNextSisterVoice() const noexcept { return nextSisterVoice; };
Voice* getNextSisterVoice() const noexcept { return nextSisterVoice_; };
/**
* @brief Get the previous sister voice in the ring
*
* @return Voice*
*/
Voice* getPreviousSisterVoice() const noexcept { return previousSisterVoice; };
Voice* getPreviousSisterVoice() const noexcept { return previousSisterVoice_; };
/**
* @brief Get the mean squared power of the last rendered block. This is used
@ -266,19 +256,19 @@ public:
*
* @return
*/
const Region* getRegion() const noexcept { return region; }
const Region* getRegion() const noexcept;
/**
* @brief Get the LFO designated by the given index
*
* @param index
*/
LFO* getLFO(size_t index) { return lfos[index].get(); }
LFO* getLFO(size_t index);
/**
* @brief Get the Flex EG designated by the given index
*
* @param index
*/
FlexEnvelope* getFlexEG(size_t index) { return flexEGs[index].get(); }
FlexEnvelope* getFlexEG(size_t index);
/**
* @brief Set the max number of filters per voice
*
@ -337,250 +327,42 @@ public:
*
* @return
*/
int getAge() const noexcept { return age; }
int getAge() const noexcept;
Duration getLastDataDuration() const noexcept { return dataDuration; }
Duration getLastAmplitudeDuration() const noexcept { return amplitudeDuration; }
Duration getLastFilterDuration() const noexcept { return filterDuration; }
Duration getLastPanningDuration() const noexcept { return panningDuration; }
Duration getLastDataDuration() const noexcept;
Duration getLastAmplitudeDuration() const noexcept;
Duration getLastFilterDuration() const noexcept;
Duration getLastPanningDuration() const noexcept;
/**
* @brief Get the SFZv1 amplitude EG, if existing
*/
ADSREnvelope<float>* getAmplitudeEG() { return &egAmplitude; }
ADSREnvelope<float>* getAmplitudeEG();
/**
* @brief Get the SFZv1 pitch EG, if existing
*/
ADSREnvelope<float>* getPitchEG() { return egPitch.get(); }
ADSREnvelope<float>* getPitchEG();
/**
* @brief Get the SFZv1 filter EG, if existing
*/
ADSREnvelope<float>* getFilterEG() { return egFilter.get(); }
ADSREnvelope<float>* getFilterEG();
/**
* @brief Get the trigger event
*/
const TriggerEvent& getTriggerEvent() { return triggerEvent; }
const TriggerEvent& getTriggerEvent();
private:
/**
* @brief Fill a span with data from a file source. This is the first step
* in rendering each block of data.
*
* @param buffer
*/
void fillWithData(AudioSpan<float> buffer) noexcept;
/**
* @brief Fill a span with data from a generator source. This is the first step
* in rendering each block of data.
*
* @param buffer
*/
void fillWithGenerator(AudioSpan<float> buffer) noexcept;
/**
* @brief Fill a destination with an interpolated source.
*
* @param source the source sample
* @param dest the destination buffer
* @param indices the integral parts of the source positions
* @param coeffs the fractional parts of the source positions
*/
template <InterpolatorModel M, bool Adding>
static void fillInterpolated(
const AudioSpan<const float>& source, const AudioSpan<float>& dest,
absl::Span<const int> indices, absl::Span<const float> coeffs,
absl::Span<const float> addingGains);
/**
* @brief Fill a destination with an interpolated source, selecting
* interpolation type dynamically by quality level.
*
* @param source the source sample
* @param dest the destination buffer
* @param indices the integral parts of the source positions
* @param coeffs the fractional parts of the source positions
* @param quality the quality level 1-10
*/
template <bool Adding>
static void fillInterpolatedWithQuality(
const AudioSpan<const float>& source, const AudioSpan<float>& dest,
absl::Span<const int> indices, absl::Span<const float> coeffs,
absl::Span<const float> addingGains, int quality);
/**
* @brief Get a S-shaped curve that is applicable to loop crossfading.
*/
static const Curve& getSCurve();
/**
* @brief Compute the amplitude envelope, applied as a gain to a mono
* or stereo buffer
*
* @param modulationSpan
*/
void amplitudeEnvelope(absl::Span<float> modulationSpan) noexcept;
/**
* @brief Apply the crossfade envelope to a span.
*
* @param modulationSpan
*/
void applyCrossfades(absl::Span<float> modulationSpan) noexcept;
void resetCrossfades() noexcept;
/**
* @brief Amplitude stage for a mono source
*
* @param buffer
*/
void ampStageMono(AudioSpan<float> buffer) noexcept;
/**
* @brief Amplitude stage for a stereo source
*
* @param buffer
*/
void ampStageStereo(AudioSpan<float> buffer) noexcept;
/**
* @brief Amplitude stage for a mono source
*
* @param buffer
*/
void panStageMono(AudioSpan<float> buffer) noexcept;
void panStageStereo(AudioSpan<float> buffer) noexcept;
/**
* @brief Amplitude stage for a mono source
*
* @param buffer
*/
void filterStageMono(AudioSpan<float> buffer) noexcept;
void filterStageStereo(AudioSpan<float> buffer) noexcept;
/**
* @brief Compute the pitch envelope. This envelope is meant to multiply
* the frequency parameter for each sample (which translates to floating
* point intervals for sample-based voices, or phases for generators)
*
* @param pitchSpan
*/
void pitchEnvelope(absl::Span<float> pitchSpan) noexcept;
struct Impl;
std::unique_ptr<Impl> impl_;
/**
* @brief Remove the voice from the sister ring
*
*/
void removeVoiceFromRing() noexcept;
/**
* @brief Initialize frequency and gain coefficients for the oscillators.
*/
void setupOscillatorUnison();
void updateChannelPowers(AudioSpan<float> buffer);
/**
* @brief Modify the voice state and notify any listeners.
*/
void switchState(State s);
/**
* @brief Save the modulation targets to avoid recomputing them in every callback.
* Must be called during startVoice() ideally.
*/
void saveModulationTargets(const Region* region) noexcept;
const NumericId<Voice> id;
StateListener* stateListener = nullptr;
Region* region { nullptr };
State state { State::idle };
bool noteIsOff { false };
TriggerEvent triggerEvent;
absl::optional<int> triggerDelay;
float speedRatio { 1.0 };
float pitchRatio { 1.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 };
struct {
int start { 0 };
int end { 0 };
int size { 0 };
int xfSize { 0 };
int xfOutStart { 0 };
int xfInStart { 0 };
} loop;
/**
* @brief Reset the loop information
*
*/
void resetLoopInformation() noexcept;
/**
* @brief Read the loop information data from the region.
* This requires that the region and promise is properly set.
*
*/
void updateLoopInformation() noexcept;
FileDataHolder currentPromise;
int samplesPerBlock { config::defaultSamplesPerBlock };
float sampleRate { config::defaultSampleRate };
Resources& resources;
std::vector<FilterHolder> filters;
std::vector<EQHolder> equalizers;
std::vector<std::unique_ptr<LFO>> lfos;
std::vector<std::unique_ptr<FlexEnvelope>> flexEGs;
ADSREnvelope<float> egAmplitude;
std::unique_ptr<ADSREnvelope<float>> egPitch;
std::unique_ptr<ADSREnvelope<float>> egFilter;
float bendStepFactor { centsFactor(1) };
WavetableOscillator waveOscillators[config::oscillatorsPerVoice];
// unison of oscillators
unsigned waveUnisonSize { 0 };
float waveDetuneRatio[config::oscillatorsPerVoice] {};
float waveLeftGain[config::oscillatorsPerVoice] {};
float waveRightGain[config::oscillatorsPerVoice] {};
Duration dataDuration;
Duration amplitudeDuration;
Duration panningDuration;
Duration filterDuration;
Voice* nextSisterVoice { this };
Voice* previousSisterVoice { this };
fast_real_distribution<float> uniformNoiseDist { -config::uniformNoiseBounds, config::uniformNoiseBounds };
fast_gaussian_generator<float> gaussianNoiseDist { 0.0f, config::noiseVariance };
Smoother gainSmoother;
Smoother bendSmoother;
Smoother xfadeSmoother;
void resetSmoothers() noexcept;
ModMatrix::TargetId masterAmplitudeTarget;
ModMatrix::TargetId amplitudeTarget;
ModMatrix::TargetId volumeTarget;
ModMatrix::TargetId panTarget;
ModMatrix::TargetId positionTarget;
ModMatrix::TargetId widthTarget;
ModMatrix::TargetId pitchTarget;
ModMatrix::TargetId oscillatorDetuneTarget;
ModMatrix::TargetId oscillatorModDepthTarget;
bool followPower { false };
PowerFollower powerFollower;
Voice* nextSisterVoice_ { this };
Voice* previousSisterVoice_ { this };
LEAK_DETECTOR(Voice);
};

242
src/sfizz/VoiceManager.cpp Normal file
View file

@ -0,0 +1,242 @@
// 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 "VoiceManager.h"
#include "SisterVoiceRing.h"
#include "RegionSet.h"
#include <absl/algorithm/container.h>
namespace sfz {
void VoiceManager::onVoiceStateChanging(NumericId<Voice> id, Voice::State state)
{
(void)id;
if (state == Voice::State::idle) {
auto voice = getVoiceById(id);
RegionSet::removeVoiceFromHierarchy(voice->getRegion(), voice);
swapAndPopFirst(activeVoices_, [voice](const Voice* v) { return v == voice; });
polyphonyGroups_[voice->getRegion()->group].removeVoice(voice);
} else if (state == Voice::State::playing) {
auto voice = getVoiceById(id);
activeVoices_.push_back(voice);
RegionSet::registerVoiceInHierarchy(voice->getRegion(), voice);
polyphonyGroups_[voice->getRegion()->group].registerVoice(voice);
}
}
const Voice* VoiceManager::getVoiceById(NumericId<Voice> id) const noexcept
{
const size_t size = list_.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 && list_[index].getId().number() > id.number())
--index;
return (list_[index].getId() == id) ? &list_[index] : nullptr;
}
Voice* VoiceManager::getVoiceById(NumericId<Voice> id) noexcept
{
return const_cast<Voice*>(
const_cast<const VoiceManager*>(this)->getVoiceById(id));
}
void VoiceManager::reset()
{
for (auto& voice : list_)
voice.reset();
polyphonyGroups_.clear();
polyphonyGroups_.emplace_back();
polyphonyGroups_.back().setPolyphonyLimit(config::maxVoices);
setStealingAlgorithm(StealingAlgorithm::Oldest);
}
bool VoiceManager::playingAttackVoice(const Region* releaseRegion) noexcept
{
const auto compatibleVoice = [releaseRegion](const Voice& v) -> bool {
const TriggerEvent& event = v.getTriggerEvent();
return (
!v.isFree()
&& event.type == TriggerEventType::NoteOn
&& releaseRegion->keyRange.containsWithEnd(event.number)
&& releaseRegion->velocityRange.containsWithEnd(event.value)
);
};
if (absl::c_find_if(list_, compatibleVoice) == list_.end())
return false;
else
return true;
}
void VoiceManager::ensureNumPolyphonyGroups(unsigned groupIdx) noexcept
{
while (polyphonyGroups_.size() <= groupIdx)
polyphonyGroups_.emplace_back();
}
void VoiceManager::setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept
{
ensureNumPolyphonyGroups(groupIdx);
polyphonyGroups_[groupIdx].setPolyphonyLimit(polyphony);
}
const PolyphonyGroup* VoiceManager::getPolyphonyGroupView(int idx) const noexcept
{
return (size_t)idx < polyphonyGroups_.size() ? &polyphonyGroups_[idx] : nullptr;
}
void VoiceManager::clear()
{
reset();
list_.clear();
activeVoices_.clear();
}
void VoiceManager::setStealingAlgorithm(StealingAlgorithm algorithm)
{
switch(algorithm){
case StealingAlgorithm::First: // fallthrough
for (auto& voice : list_)
voice.disablePowerFollower();
stealer_ = absl::make_unique<FirstStealer>();
break;
case StealingAlgorithm::Oldest:
for (auto& voice : list_)
voice.disablePowerFollower();
stealer_ = absl::make_unique<OldestStealer>();
break;
case StealingAlgorithm::EnvelopeAndAge:
for (auto& voice : list_)
voice.enablePowerFollower();
stealer_ = absl::make_unique<EnvelopeAndAgeStealer>();
break;
}
}
void VoiceManager::checkPolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept
{
checkNotePolyphony(region, delay, triggerEvent);
checkRegionPolyphony(region, delay);
checkGroupPolyphony(region, delay);
checkSetPolyphony(region, delay);
checkEnginePolyphony(delay);
}
Voice* VoiceManager::findFreeVoice() noexcept
{
auto freeVoice = absl::c_find_if(list_, [](const Voice& voice) {
return voice.isFree();
});
if (freeVoice != list_.end())
return &*freeVoice;
DBG("Engine hard polyphony reached");
return {};
}
void VoiceManager::requireNumVoices(int numVoices, Resources& resources)
{
numActualVoices_ =
static_cast<int>(config::overflowVoiceMultiplier * numVoices);
numRequiredVoices_ = numVoices;
clear();
list_.reserve(numActualVoices_);
activeVoices_.reserve(numActualVoices_);
for (int i = 0; i < numActualVoices_; ++i) {
list_.emplace_back(i, resources);
Voice& lastVoice = list_.back();
lastVoice.setStateListener(this);
}
}
void VoiceManager::checkRegionPolyphony(const Region* region, int delay) noexcept
{
Voice* candidate = stealer_->checkRegionPolyphony(region, absl::MakeSpan(activeVoices_));
SisterVoiceRing::offAllSisters(candidate, delay);
}
void VoiceManager::checkNotePolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept
{
if (!region->notePolyphony)
return;
unsigned notePolyphonyCounter { 0 };
Voice* selfMaskCandidate { nullptr };
for (Voice* voice : activeVoices_) {
const TriggerEvent& voiceTriggerEvent = voice->getTriggerEvent();
const bool skipVoice =
(triggerEvent.type == TriggerEventType::NoteOn && voice->releasedOrFree())
|| voice->isFree();
if (!skipVoice
&& voice->getRegion()->group == region->group
&& voiceTriggerEvent.number == triggerEvent.number
&& voiceTriggerEvent.type == triggerEvent.type) {
notePolyphonyCounter += 1;
switch (region->selfMask) {
case SfzSelfMask::mask:
if (voiceTriggerEvent.value <= triggerEvent.value) {
if (!selfMaskCandidate
|| selfMaskCandidate->getTriggerEvent().value > voiceTriggerEvent.value) {
selfMaskCandidate = voice;
}
}
break;
case SfzSelfMask::dontMask:
if (!selfMaskCandidate || selfMaskCandidate->getAge() < voice->getAge())
selfMaskCandidate = voice;
break;
}
}
}
if (notePolyphonyCounter >= *region->notePolyphony) {
SisterVoiceRing::offAllSisters(selfMaskCandidate, delay);
}
}
void VoiceManager::checkGroupPolyphony(const Region* region, int delay) noexcept
{
auto& group = polyphonyGroups_[region->group];
Voice* candidate = stealer_->checkPolyphony(
absl::MakeSpan(group.getActiveVoices()), group.getPolyphonyLimit());
SisterVoiceRing::offAllSisters(candidate, delay);
}
void VoiceManager::checkSetPolyphony(const Region* region, int delay) noexcept
{
auto parent = region->parent;
while (parent != nullptr) {
Voice* candidate = stealer_->checkPolyphony(
absl::MakeSpan(parent->getActiveVoices()), parent->getPolyphonyLimit());
SisterVoiceRing::offAllSisters(candidate, delay);
parent = parent->getParent();
}
}
void VoiceManager::checkEnginePolyphony(int delay) noexcept
{
Voice* candidate = stealer_->checkPolyphony(
absl::MakeSpan(activeVoices_), numRequiredVoices_);
SisterVoiceRing::offAllSisters(candidate, delay);
}
} // namespace sfz

193
src/sfizz/VoiceManager.h Normal file
View file

@ -0,0 +1,193 @@
// 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
#include "Config.h"
#include "PolyphonyGroup.h"
#include "Region.h"
#include "Resources.h"
#include "Voice.h"
#include "VoiceStealing.h"
#include <vector>
namespace sfz {
struct VoiceManager final : public Voice::StateListener
{
/**
* @brief The voice callback which is called during a change of state.
*/
void onVoiceStateChanging(NumericId<Voice> id, Voice::State state) final;
/**
* @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 Find the voice which is associated with the given identifier.
*
* @param id
* @return Voice*
*/
Voice* getVoiceById(NumericId<Voice> id) noexcept;
/**
* @brief Reset all voices and clear the polyphony groups
*/
void reset();
/**
* @brief Check if a compatible attack voice is playing for the release region.
*
* @param releaseRegion
* @return true
* @return false
*/
bool playingAttackVoice(const Region* releaseRegion) noexcept;
/**
* @brief Ensures that the polyphony groups are at least this size.
* Call this each time a new `group=N` is given in an sfz file.
*
* @param groupIdx
*/
void ensureNumPolyphonyGroups(unsigned groupIdx) noexcept;
/**
* @brief Set the polyphony for a given group
* If the number of polyphony groups is too small, it will
* be increased.
*
* @param groupIdx
* @param polyphony
*/
void setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept;
/**
* @brief Get a view into a given polyphony group
*
* @param idx
* @return const PolyphonyGroup*
*/
const PolyphonyGroup* getPolyphonyGroupView(int idx) const noexcept;
/**
* @brief Clear all voices and polyphony groups.
* Also resets the stealing algorithm to default.
*/
void clear();
/**
* @brief Set the stealing algorithm
*
* @param algorithm
*/
void setStealingAlgorithm(StealingAlgorithm algorithm);
/**
* @brief Off voices as necessary depending on the trigger event and started region
*
* @param region
* @param delay
* @param triggerEvent
*/
void checkPolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept;
/**
* @brief Get the number of active voices
*
* @return size_t
*/
size_t getNumActiveVoices() const { return activeVoices_.size(); }
/**
* @brief Get the number of polyphony groups
*
* @return size_t
*/
size_t getNumPolyphonyGroups() const noexcept { return polyphonyGroups_.size(); }
/**
* @brief Find a voice that is not currently playing
*
* @return Voice*
*/
Voice* findFreeVoice() noexcept;
/**
* @brief Require a number of voices from this manager.
* In practice, the manager will handle slightly more, in order to
* allow voices to die off upon reaching higher polyphony count.
*
* @param numVoices
* @param resources
*/
void requireNumVoices(int numVoices, Resources& resources);
private:
int numRequiredVoices_ { config::numVoices };
int numActualVoices_ { static_cast<int>(config::numVoices * config::overflowVoiceMultiplier) };
std::vector<Voice> list_;
std::vector<Voice*> activeVoices_;
// These are the `group=` groups where you can off voices
std::vector<PolyphonyGroup> polyphonyGroups_;
std::unique_ptr<VoiceStealer> stealer_ { absl::make_unique<OldestStealer>() };
/**
* @brief Check the region polyphony, releasing voices if necessary
*
* @param region
* @param delay
*/
void checkRegionPolyphony(const Region* region, int delay) noexcept;
/**
* @brief Check the note polyphony, releasing voices if necessary
*
* @param region
* @param delay
* @param triggerEvent
*/
void checkNotePolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept;
/**
* @brief Check the group polyphony, releasing voices if necessary
*
* @param region
* @param delay
*/
void checkGroupPolyphony(const Region* region, int delay) noexcept;
/**
* @brief Check the region set polyphony at all levels, releasing voices if necessary
*
* @param region
* @param delay
*/
void checkSetPolyphony(const Region* region, int delay) noexcept;
/**
* @brief Check the engine polyphony, fast releasing voices if necessary
*
* @param delay
*/
void checkEnginePolyphony(int delay) noexcept;
public:
// Vector shortcuts
typename decltype(list_)::iterator begin() { return list_.begin(); }
typename decltype(list_)::const_iterator cbegin() const { return list_.cbegin(); }
typename decltype(list_)::iterator end() { return list_.end(); }
typename decltype(list_)::const_iterator cend() const { return list_.cend(); }
typename decltype(list_)::reference operator[] (size_t n) { return list_[n]; }
typename decltype(list_)::const_reference operator[] (size_t n) const { return list_[n]; }
};
} // namespace sfz

View file

@ -1,57 +1,102 @@
#include "VoiceStealing.h"
#include "SisterVoiceRing.h"
sfz::VoiceStealing::VoiceStealing()
namespace sfz {
/**
* @brief Generic polyphony checker
* A voice is counted as incrementing the voice count and "stealable" if voiceCond(voice) is true.
* For each stealable voice, the voice becomes the stealing candidate if candidateCont(voice, candidate) is true.
*
* @tparam F
* @tparam G
* @param candidates
* @param voiceCond a functor with signature bool(Voice* voice)
* @param candidateCond a functor with signature bool(Voice* voice, Voice* candidate)
* @return Voice*
*/
template<class F, class G>
Voice* genericPolyphonyCheck(absl::Span<Voice*> candidates, unsigned polyphony, F&& voiceCond, G&& candidateCond)
{
voiceScores.reserve(config::maxVoices);
}
sfz::Voice* sfz::VoiceStealing::steal(absl::Span<sfz::Voice*> voices) noexcept
{
if (voices.empty())
return {};
switch(stealingAlgorithm) {
case StealingAlgorithm::First:
return stealFirst(voices);
case StealingAlgorithm::EnvelopeAndAge:
return stealEnvelopeAndAge(voices);
case StealingAlgorithm::Oldest:
default:
return stealOldest(voices);
Voice* candidate = nullptr;
unsigned numPlaying = 0;
for (const auto& voice : candidates) {
if (voiceCond(voice)) {
if (candidateCond(voice, candidate))
candidate = voice;
numPlaying += 1;
}
}
if (numPlaying >= polyphony)
return candidate;
return {};
}
void sfz::VoiceStealing::setStealingAlgorithm(StealingAlgorithm algorithm) noexcept
/**
* @brief Helper to ignore the voice if released depending on a boolean
*
* @param voice
* @param ignoreReleased
*/
constexpr bool ignoreVoice(const Voice* voice)
{
stealingAlgorithm = algorithm;
return (voice == nullptr || voice->releasedOrFree());
}
sfz::Voice* sfz::VoiceStealing::stealFirst(absl::Span<Voice*> voices) noexcept
Voice* FirstStealer::checkRegionPolyphony(const Region* region, absl::Span<Voice*> candidates)
{
return voices.front();
ASSERT(region);
return genericPolyphonyCheck(candidates, region->polyphony,
[=](const Voice* v) { return (!ignoreVoice(v) && v->getRegion() == region); },
[=](const Voice*, const Voice* c) { return c == nullptr; });
}
sfz::Voice* sfz::VoiceStealing::stealOldest(absl::Span<Voice*> voices) noexcept
Voice* FirstStealer::checkPolyphony(absl::Span<Voice*> candidates, unsigned maxPolyphony)
{
absl::c_sort(voices, voiceOrdering);
return voices.front();
return genericPolyphonyCheck(candidates, maxPolyphony,
[=](const Voice* v) { return (!ignoreVoice(v)); },
[=](const Voice*, const Voice* c) { return c == nullptr; });
}
sfz::Voice* sfz::VoiceStealing::stealEnvelopeAndAge(absl::Span<Voice*> voices) noexcept
Voice* OldestStealer::checkRegionPolyphony(const Region* region, absl::Span<Voice*> candidates)
{
ASSERT(region);
return genericPolyphonyCheck(candidates, region->polyphony,
[=](const Voice* v) { return (!ignoreVoice(v) && v->getRegion() == region); },
[=](const Voice* v, const Voice* c) { return (c == nullptr || v->getAge() > c->getAge()); });
}
Voice* OldestStealer::checkPolyphony(absl::Span<Voice*> candidates, unsigned maxPolyphony)
{
return genericPolyphonyCheck(candidates, maxPolyphony,
[=](const Voice* v) { return (!ignoreVoice(v)); },
[=](const Voice* v, const Voice* c) { return (c == nullptr || v->getAge() > c->getAge()); });
}
/**
* @brief Stealer on envelope and age.
* The stealer checks that the power to try and kill voices with relative low contribution
* to the output compared to the rest.
* The stealer also checks the age so that voices have the time to build up attack
* This is not perfect because pad-type voices will take a long time to output
* their sound, but it's reasonable for sounds with a quick attack and longer
* release.
*
* @param voices
* @return sfz::Voice*
*/
sfz::Voice* stealEnvelopeAndAge(absl::Span<Voice*> voices) noexcept
{
absl::c_sort(voices, voiceOrdering);
const auto sumPower = absl::c_accumulate(voices, 0.0f, [](float sum, const Voice* v) {
return sum + v->getAveragePower();
});
// We are checking the power to try and kill voices with relative low contribution
// to the output compared to the rest.
const auto powerThreshold = sumPower
/ static_cast<float>(voices.size()) * config::stealingPowerCoeff;
// We are checking the age so that voices have the time to build up attack
// This is not perfect because pad-type voices will take a long time to output
// their sound, but it's reasonable for sounds with a quick attack and longer
// release.
const auto ageThreshold =
static_cast<int>(voices.front()->getAge() * config::stealingAgeCoeff);
@ -82,3 +127,37 @@ sfz::Voice* sfz::VoiceStealing::stealEnvelopeAndAge(absl::Span<Voice*> voices) n
return returnedVoice;
}
Voice* EnvelopeAndAgeStealer::checkRegionPolyphony(const Region* region, absl::Span<Voice*> candidates)
{
ASSERT(region);
temp_.clear();
absl::c_copy_if(candidates, std::back_inserter(temp_), [=](Voice* v) {
return (!ignoreVoice(v) && v->getRegion() == region);
});
if (temp_.size() >= region->polyphony)
return stealEnvelopeAndAge(absl::MakeSpan(temp_));
return {};
}
Voice* EnvelopeAndAgeStealer::checkPolyphony(absl::Span<Voice*> candidates, unsigned maxPolyphony)
{
temp_.clear();
absl::c_copy_if(candidates, std::back_inserter(temp_), [=](Voice* v) {
return !ignoreVoice(v);
});
if (temp_.size() >= maxPolyphony)
return stealEnvelopeAndAge(absl::MakeSpan(temp_));
return {};
}
EnvelopeAndAgeStealer::EnvelopeAndAgeStealer()
{
temp_.reserve(config::maxVoices);
}
}

View file

@ -7,6 +7,7 @@
#pragma once
#include "Config.h"
#include "Region.h"
#include "Voice.h"
#include "SisterVoiceRing.h"
#include <vector>
@ -14,64 +15,57 @@
namespace sfz
{
class VoiceStealing
enum class StealingAlgorithm {
First,
Oldest,
EnvelopeAndAge
};
class VoiceStealer
{
public:
enum class StealingAlgorithm {
First,
Oldest,
EnvelopeAndAge
};
VoiceStealing();
virtual ~VoiceStealer() {}
/**
* @brief Get the current stealing algorithm
* @brief Check that the region polyphony is respected.
*
* @return StealingAlgorithm
* @param region
* @param candidates
* @return Voice* a non-null voice if the region polyphony is not respected
*/
StealingAlgorithm getStealingAlgorithm() const noexcept { return stealingAlgorithm; }
virtual Voice* checkRegionPolyphony(const Region* region, absl::Span<Voice*> candidates) = 0;
/**
* @brief Set a default stealing algorithm
* @brief Check that then polyphony is respected.
*
* @param algorithm
* @param region
* @param candidates
* @return Voice* a non-null voice if the region polyphony is not respected
*/
void setStealingAlgorithm(StealingAlgorithm algorithm) noexcept;
/**
* @brief Propose a voice to steal from a set of voices
*
* @param voices
* @return Voice*
*/
Voice* steal(absl::Span<Voice*> voices) noexcept;
private:
StealingAlgorithm stealingAlgorithm { StealingAlgorithm::Oldest };
Voice* stealFirst(absl::Span<Voice*> voices) noexcept;
Voice* stealOldest(absl::Span<Voice*> voices) noexcept;
Voice* stealEnvelopeAndAge(absl::Span<Voice*> voices) noexcept;
struct VoiceScore
{
Voice* voice;
double score;
};
struct VoiceScoreComparator
{
bool operator()(const VoiceScore& voiceScore, const double& score)
{
return (voiceScore.score < score);
}
bool operator()(const double& score, const VoiceScore& voiceScore)
{
return (score < voiceScore.score);
}
bool operator()(const VoiceScore& lhs, const VoiceScore& rhs)
{
return (lhs.score < rhs.score);
}
};
std::vector<VoiceScore> voiceScores;
virtual Voice* checkPolyphony(absl::Span<Voice*> candidates, unsigned maxPolyphony) = 0;
};
class FirstStealer final : public VoiceStealer
{
public:
Voice* checkRegionPolyphony(const Region* region, absl::Span<Voice*> candidates) final;
Voice* checkPolyphony(absl::Span<Voice*> candidates, unsigned maxPolyphony) final;
};
class OldestStealer final : public VoiceStealer
{
public:
Voice* checkRegionPolyphony(const Region* region, absl::Span<Voice*> candidates) final;
Voice* checkPolyphony(absl::Span<Voice*> candidates, unsigned maxPolyphony) final;
};
class EnvelopeAndAgeStealer final : public VoiceStealer
{
public:
EnvelopeAndAgeStealer();
Voice* checkRegionPolyphony(const Region* region, absl::Span<Voice*> candidates) final;
Voice* checkPolyphony(absl::Span<Voice*> candidates, unsigned maxPolyphony) final;
private:
std::vector<Voice*> temp_;
};
}

View file

@ -15,16 +15,14 @@
namespace sfz {
ADSREnvelopeSource::ADSREnvelopeSource(Synth &synth)
: synth_(&synth)
ADSREnvelopeSource::ADSREnvelopeSource(VoiceManager& manager, MidiState& state)
: voiceManager_(manager), midiState_(state)
{
}
void ADSREnvelopeSource::init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay)
{
Synth& synth = *synth_;
Voice* voice = synth.getVoiceById(voiceId);
Voice* voice = voiceManager_.getVoiceById(voiceId);
if (!voice) {
ASSERTFALSE;
return;
@ -55,17 +53,14 @@ void ADSREnvelopeSource::init(const ModKey& sourceKey, NumericId<Voice> voiceId,
return;
}
Resources& resources = synth.getResources();
const TriggerEvent& triggerEvent = voice->getTriggerEvent();
const float sampleRate = voice->getSampleRate();
eg->reset(*desc, *region, resources.midiState, delay, triggerEvent.value, sampleRate);
eg->reset(*desc, *region, midiState_, delay, triggerEvent.value, sampleRate);
}
void ADSREnvelopeSource::release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay)
{
Synth& synth = *synth_;
Voice* voice = synth.getVoiceById(voiceId);
Voice* voice = voiceManager_.getVoiceById(voiceId);
if (!voice) {
ASSERTFALSE;
return;
@ -96,9 +91,7 @@ void ADSREnvelopeSource::release(const ModKey& sourceKey, NumericId<Voice> voice
void ADSREnvelopeSource::generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer)
{
Synth& synth = *synth_;
Voice* voice = synth.getVoiceById(voiceId);
Voice* voice = voiceManager_.getVoiceById(voiceId);
if (!voice) {
ASSERTFALSE;
return;

View file

@ -6,19 +6,22 @@
#pragma once
#include "../ModGenerator.h"
#include "../../VoiceManager.h"
#include "../../MidiState.h"
namespace sfz {
class Synth;
class ADSREnvelopeSource : public ModGenerator {
public:
explicit ADSREnvelopeSource(Synth &synth);
explicit ADSREnvelopeSource(VoiceManager &manager, MidiState& state);
void init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
void release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
void generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) override;
private:
Synth* synth_ = nullptr;
VoiceManager& voiceManager_;
MidiState& midiState_;
};
} // namespace sfz

View file

@ -14,17 +14,16 @@
namespace sfz {
FlexEnvelopeSource::FlexEnvelopeSource(Synth &synth)
: synth_(&synth)
FlexEnvelopeSource::FlexEnvelopeSource(VoiceManager& manager)
: voiceManager_(manager)
{
}
void FlexEnvelopeSource::init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay)
{
Synth& synth = *synth_;
unsigned egIndex = sourceKey.parameters().N;
Voice* voice = synth.getVoiceById(voiceId);
Voice* voice = voiceManager_.getVoiceById(voiceId);
if (!voice) {
ASSERTFALSE;
return;
@ -49,10 +48,9 @@ void FlexEnvelopeSource::init(const ModKey& sourceKey, NumericId<Voice> voiceId,
void FlexEnvelopeSource::release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay)
{
Synth& synth = *synth_;
unsigned egIndex = sourceKey.parameters().N;
Voice* voice = synth.getVoiceById(voiceId);
Voice* voice = voiceManager_.getVoiceById(voiceId);
if (!voice) {
ASSERTFALSE;
return;
@ -70,10 +68,9 @@ void FlexEnvelopeSource::release(const ModKey& sourceKey, NumericId<Voice> voice
void FlexEnvelopeSource::generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer)
{
Synth& synth = *synth_;
unsigned egIndex = sourceKey.parameters().N;
Voice* voice = synth.getVoiceById(voiceId);
Voice* voice = voiceManager_.getVoiceById(voiceId);
if (!voice) {
ASSERTFALSE;
return;

View file

@ -6,19 +6,20 @@
#pragma once
#include "../ModGenerator.h"
#include "../../VoiceManager.h"
namespace sfz {
class Synth;
class FlexEnvelopeSource : public ModGenerator {
public:
explicit FlexEnvelopeSource(Synth &synth);
explicit FlexEnvelopeSource(VoiceManager& manager);
void init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
void release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
void generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) override;
private:
Synth* synth_ = nullptr;
VoiceManager& voiceManager_;
};
} // namespace sfz

View file

@ -14,17 +14,16 @@
namespace sfz {
LFOSource::LFOSource(Synth &synth)
: synth_(&synth)
LFOSource::LFOSource(VoiceManager& manager)
: voiceManager_(manager)
{
}
void LFOSource::init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay)
{
Synth& synth = *synth_;
unsigned lfoIndex = sourceKey.parameters().N;
Voice* voice = synth.getVoiceById(voiceId);
Voice* voice = voiceManager_.getVoiceById(voiceId);
if (!voice) {
ASSERTFALSE;
return;
@ -43,10 +42,9 @@ void LFOSource::init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned
void LFOSource::generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer)
{
Synth& synth = *synth_;
const unsigned lfoIndex = sourceKey.parameters().N;
Voice* voice = synth.getVoiceById(voiceId);
Voice* voice = voiceManager_.getVoiceById(voiceId);
if (!voice) {
ASSERTFALSE;
fill(buffer, 0.0f);

View file

@ -6,18 +6,18 @@
#pragma once
#include "../ModGenerator.h"
#include "../../VoiceManager.h"
namespace sfz {
class Synth;
class LFOSource : public ModGenerator {
public:
explicit LFOSource(Synth &synth);
explicit LFOSource(VoiceManager &manager);
void init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
void generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) override;
private:
Synth* synth_ = nullptr;
VoiceManager& voiceManager_;
};
} // namespace sfz

View file

@ -6,10 +6,10 @@
#pragma once
#include "../Opcode.h"
#include "ghc/fs_std.hpp"
#include "absl/types/optional.h"
#include "absl/container/flat_hash_map.h"
#include "absl/container/flat_hash_set.h"
#include <ghc/fs_std.hpp>
#include <absl/types/optional.h>
#include <absl/container/flat_hash_map.h>
#include <absl/container/flat_hash_set.h>
#include <string>
#include <memory>

View file

@ -6,6 +6,7 @@
#include "TestHelpers.h"
#include "sfizz/Synth.h"
#include "sfizz/Voice.h"
#include "sfizz/SfzHelpers.h"
#include "sfizz/modulations/ModId.h"
#include "sfizz/modulations/ModKey.h"
@ -489,7 +490,7 @@ TEST_CASE("[Files] Off modes")
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/off_mode.sfz");
REQUIRE( synth.getNumRegions() == 3 );
synth.noteOn(0, 64, 63);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
REQUIRE( synth.getNumActiveVoices() == 2 );
const auto* fastVoice =
synth.getVoiceView(0)->getRegion()->offMode == SfzOffMode::fast ?
synth.getVoiceView(0) :
@ -499,12 +500,12 @@ TEST_CASE("[Files] Off modes")
synth.getVoiceView(1) :
synth.getVoiceView(0) ;
synth.noteOn(100, 63, 63);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
REQUIRE( synth.getNumActiveVoices() == 3 );
REQUIRE( numPlayingVoices(synth) == 1 );
AudioBuffer<float> buffer { 2, 256 };
for (unsigned i = 0; i < 10; ++i) // Not enough for the "normal" voice to die
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
REQUIRE( synth.getNumActiveVoices() == 2 );
REQUIRE( fastVoice->isFree() );
REQUIRE( !normalVoice->isFree() );
}

View file

@ -394,25 +394,25 @@ TEST_CASE("[FlexEG] Free-running flex AmpEG (no sustain)")
synth.noteOn(0, 60, 0);
sfz::AudioBuffer<float> buffer { 2, 256 };
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
for (unsigned i = 0; i < 100; ++i)
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
synth.noteOn(0, 62, 0);
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
for (unsigned i = 0; i < 100; ++i)
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
synth.noteOn(0, 64, 0);
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
for (unsigned i = 0; i < 100; ++i)
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
synth.noteOff(0, 64, 0); // the release stage is 0 duration
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
}

View file

@ -7,6 +7,7 @@
#include "DataHelpers.h"
#include "sfizz/Synth.h"
#include "sfizz/LFO.h"
#include "sfizz/Region.h"
#include "catch2/catch.hpp"
static bool computeLFO(DataPoints& dp, const fs::path& sfzPath, double sampleRate, size_t numFrames)

View file

@ -17,6 +17,7 @@
#include "sfizz/Synth.h"
#include "sfizz/LFO.h"
#include "sfizz/Region.h"
#include "sfizz/LFODescription.h"
#include "sfizz/MathHelpers.h"
#include "cxxopts.hpp"

View file

@ -10,6 +10,10 @@
#include <absl/algorithm/container.h>
#include "catch2/catch.hpp"
// Need these for the introspection of Synth
#include "sfizz/PolyphonyGroup.h"
#include "sfizz/RegionSet.h"
using namespace Catch::literals;
using namespace sfz::literals;
@ -80,7 +84,7 @@ TEST_CASE("[Polyphony] group polyphony limits")
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
REQUIRE( synth.getNumActiveVoices() == 3 );
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing
}
@ -96,7 +100,7 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits")
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
REQUIRE( synth.getNumActiveVoices() == 3 );
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing
}
@ -112,7 +116,7 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (group)")
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
REQUIRE( synth.getNumActiveVoices() == 3 );
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing
}
@ -129,7 +133,7 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (master)")
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
REQUIRE( synth.getNumActiveVoices() == 3 );
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing
}
@ -151,7 +155,7 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (limit in another master)")
synth.noteOn(0, 66, 64);
synth.noteOn(0, 66, 64);
synth.noteOn(0, 66, 64);
REQUIRE( synth.getNumActiveVoices(true) == 6);
REQUIRE( synth.getNumActiveVoices() == 6);
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 5); // One is releasing
}
@ -168,7 +172,7 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (global)")
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
REQUIRE( synth.getNumActiveVoices() == 3 );
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing
}
@ -190,25 +194,25 @@ TEST_CASE("[Polyphony] Polyphony in master")
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
synth.noteOn(0, 65, 64);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
REQUIRE( synth.getNumActiveVoices() == 3 );
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing
synth.allSoundOff();
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 0);
REQUIRE( synth.getNumActiveVoices() == 0);
synth.noteOn(0, 63, 64);
synth.noteOn(0, 63, 64);
synth.noteOn(0, 63, 64);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
REQUIRE( synth.getNumActiveVoices() == 3 );
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 2 ); // One is releasing
synth.allSoundOff();
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 0);
REQUIRE( synth.getNumActiveVoices() == 0);
synth.noteOn(0, 61, 64);
synth.noteOn(0, 61, 64);
synth.noteOn(0, 61, 64);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
REQUIRE( synth.getNumActiveVoices() == 3 );
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 3 );
}
@ -224,7 +228,7 @@ TEST_CASE("[Polyphony] Self-masking")
synth.noteOn(0, 64, 63 );
synth.noteOn(0, 64, 62 );
synth.noteOn(0, 64, 64);
REQUIRE( synth.getNumActiveVoices(true) == 3 ); // One of these is releasing
REQUIRE( synth.getNumActiveVoices() == 3 ); // One of these is releasing
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 2 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
@ -245,7 +249,7 @@ TEST_CASE("[Polyphony] Not self-masking")
synth.noteOn(0, 66, 63 );
synth.noteOn(0, 66, 62 );
synth.noteOn(0, 66, 64);
REQUIRE( synth.getNumActiveVoices(true) == 3 ); // One of these is releasing
REQUIRE( synth.getNumActiveVoices() == 3 ); // One of these is releasing
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 2 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
@ -266,7 +270,7 @@ TEST_CASE("[Polyphony] Self-masking with the exact same velocity")
synth.noteOn(0, 64, 64);
synth.noteOn(0, 64, 63 );
synth.noteOn(0, 64, 63 );
REQUIRE( synth.getNumActiveVoices(true) == 3 ); // One of these is releasing
REQUIRE( synth.getNumActiveVoices() == 3 ); // One of these is releasing
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 2 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 64_norm);
@ -285,7 +289,7 @@ TEST_CASE("[Polyphony] Self-masking only works from low to high")
)");
synth.noteOn(0, 64, 63 );
synth.noteOn(0, 64, 62 );
REQUIRE( synth.getNumActiveVoices(true) == 2 ); // Both notes are playing
REQUIRE( synth.getNumActiveVoices() == 2 ); // Both notes are playing
REQUIRE( numPlayingVoices(synth) == 2 ); // id
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
REQUIRE(!synth.getVoiceView(0)->releasedOrFree());
@ -303,7 +307,7 @@ TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same po
)");
synth.noteOn(0, 64, 62 );
synth.noteOn(0, 64, 63 );
REQUIRE( synth.getNumActiveVoices(true) == 4);
REQUIRE( synth.getNumActiveVoices() == 4);
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 1 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 62_norm);
@ -326,12 +330,12 @@ TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same po
<region> sw_last=37 key=48 transpose=12 note_polyphony=1 sample=*tri
)");
synth.noteOn(0, 48, 63 );
REQUIRE( synth.getNumActiveVoices(true) == 1);
REQUIRE( synth.getNumActiveVoices() == 1);
synth.cc(0, 64, 127);
synth.noteOn(0, 37, 127);
synth.noteOff(0, 37, 0);
synth.noteOn(0, 48, 64);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
REQUIRE( synth.getNumActiveVoices() == 2 );
synth.renderBlock(buffer);
REQUIRE( numPlayingVoices(synth) == 1 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
@ -351,7 +355,7 @@ TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups")
)");
synth.noteOn(0, 64, 62 );
synth.noteOn(0, 64, 63 );
REQUIRE( synth.getNumActiveVoices(true) == 4); // Both notes are playing
REQUIRE( synth.getNumActiveVoices() == 4); // Both notes are playing
synth.renderBlock(buffer);
REQUIRE(numPlayingVoices(synth) == 2 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 62_norm);
@ -374,12 +378,12 @@ TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups (wi
<region> group=2 sw_last=37 key=48 transpose=12 note_polyphony=1 sample=*tri
)");
synth.noteOn(0, 48, 63 );
REQUIRE( synth.getNumActiveVoices(true) == 1);
REQUIRE( synth.getNumActiveVoices() == 1);
synth.cc(0, 64, 127);
synth.noteOn(0, 37, 127);
synth.noteOff(0, 37, 0);
synth.noteOn(0, 48, 64);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
REQUIRE( synth.getNumActiveVoices() == 2 );
synth.renderBlock(buffer);
REQUIRE(numPlayingVoices(synth) == 2 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
@ -397,10 +401,10 @@ TEST_CASE("[Polyphony] Note polyphony operates on release voices")
)");
synth.noteOn(0, 48, 63 );
synth.noteOff(10, 48, 0 );
REQUIRE( synth.getNumActiveVoices(true) == 1);
REQUIRE( synth.getNumActiveVoices() == 1);
synth.noteOn(20, 48, 65 );
synth.noteOff(30, 48, 10 );
REQUIRE( synth.getNumActiveVoices(true) == 2 );
REQUIRE( synth.getNumActiveVoices() == 2 );
synth.renderBlock(buffer);
REQUIRE(numPlayingVoices(synth) == 1 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
@ -418,11 +422,11 @@ TEST_CASE("[Polyphony] Note polyphony operates on release voices (masking works
)");
synth.noteOn(0, 48, 63 );
synth.noteOff(10, 48, 0 );
REQUIRE( synth.getNumActiveVoices(true) == 1);
REQUIRE( synth.getNumActiveVoices() == 1);
REQUIRE( numPlayingVoices(synth) == 1 );
synth.noteOn(20, 48, 61 );
synth.noteOff(30, 48, 10 );
REQUIRE( synth.getNumActiveVoices(true) == 2 );
REQUIRE( synth.getNumActiveVoices() == 2 );
REQUIRE( numPlayingVoices(synth) == 2 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
REQUIRE(!synth.getVoiceView(0)->releasedOrFree());
@ -445,10 +449,10 @@ TEST_CASE("[Polyphony] Note polyphony operates on release voices and sustain ped
synth.noteOff(3, 48, 0 );
synth.noteOn(4, 48, 63 );
synth.noteOff(5, 48, 0 );
REQUIRE( synth.getNumActiveVoices(true) == 3);
REQUIRE( synth.getNumActiveVoices() == 3);
REQUIRE( numPlayingVoices(synth) == 3 );
synth.cc(20, 64, 0);
REQUIRE( synth.getNumActiveVoices(true) == 6 );
REQUIRE( synth.getNumActiveVoices() == 6 );
REQUIRE( numPlayingVoices(synth) == 1 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 61_norm);
REQUIRE( synth.getVoiceView(0)->releasedOrFree());
@ -479,10 +483,10 @@ TEST_CASE("[Polyphony] Note polyphony operates on release voices and sustain ped
synth.noteOff(3, 48, 0 );
synth.noteOn(4, 48, 61 );
synth.noteOff(5, 48, 0 );
REQUIRE( synth.getNumActiveVoices(true) == 3);
REQUIRE( synth.getNumActiveVoices() == 3);
REQUIRE( numPlayingVoices(synth) == 3 );
synth.cc(20, 64, 0);
REQUIRE( synth.getNumActiveVoices(true) == 6 );
REQUIRE( synth.getNumActiveVoices() == 6 );
REQUIRE( numPlayingVoices(synth) == 3 );
REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm);
REQUIRE( synth.getVoiceView(0)->releasedOrFree());

View file

@ -186,14 +186,14 @@ TEST_CASE("[Keyswitches] Normal lastKeyswitch range")
<region> sw_last=41 key=62 sample=*saw
)");
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 62, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 41, 64);
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 62, 64);
REQUIRE(synth.getNumActiveVoices(true) == 2);
REQUIRE(synth.getNumActiveVoices() == 2);
}
TEST_CASE("[Keyswitches] No lastKeyswitch range")
@ -204,19 +204,19 @@ TEST_CASE("[Keyswitches] No lastKeyswitch range")
<region> sw_last=41 key=62 sample=*saw
)");
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 0);
REQUIRE(synth.getNumActiveVoices() == 0);
synth.noteOn(0, 62, 64);
REQUIRE(synth.getNumActiveVoices(true) == 0);
REQUIRE(synth.getNumActiveVoices() == 0);
synth.noteOn(0, 40, 64);
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 62, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 41, 64);
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 62, 64);
REQUIRE(synth.getNumActiveVoices(true) == 2);
REQUIRE(synth.getNumActiveVoices() == 2);
}
TEST_CASE("[Keyswitches] Out of lastKeyswitch range")
@ -228,14 +228,14 @@ TEST_CASE("[Keyswitches] Out of lastKeyswitch range")
<region> sw_last=43 key=62 sample=*saw
)");
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 62, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 43, 64);
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 62, 64);
REQUIRE(synth.getNumActiveVoices(true) == 2);
REQUIRE(synth.getNumActiveVoices() == 2);
}
TEST_CASE("[Keyswitches] Overlapping key and lastKeyswitch range")
@ -247,19 +247,19 @@ TEST_CASE("[Keyswitches] Overlapping key and lastKeyswitch range")
<region> sw_last=41 key=62 sample=*saw
)");
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 62, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 41, 64);
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 62, 64);
REQUIRE(synth.getNumActiveVoices(true) == 2);
REQUIRE(synth.getNumActiveVoices() == 2);
synth.noteOn(0, 43, 64);
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 2);
REQUIRE(synth.getNumActiveVoices() == 2);
synth.noteOn(0, 62, 64);
REQUIRE(synth.getNumActiveVoices(true) == 3);
REQUIRE(synth.getNumActiveVoices() == 3);
}
TEST_CASE("[Keyswitches] sw_down, in range")
@ -270,13 +270,13 @@ TEST_CASE("[Keyswitches] sw_down, in range")
<region> sw_down=40 key=60 sample=*sine
)");
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 0);
REQUIRE(synth.getNumActiveVoices() == 0);
synth.noteOn(0, 40, 64);
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOff(0, 40, 64);
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
}
TEST_CASE("[Keyswitches] sw_down, out of range")
@ -287,13 +287,13 @@ TEST_CASE("[Keyswitches] sw_down, out of range")
<region> sw_down=40 key=60 sample=*sine
)");
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 0);
REQUIRE(synth.getNumActiveVoices() == 0);
synth.noteOn(0, 40, 64);
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOff(0, 40, 64);
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
}
TEST_CASE("[Keyswitches] sw_up, in range")
@ -304,13 +304,13 @@ TEST_CASE("[Keyswitches] sw_up, in range")
<region> sw_up=40 key=60 sample=*sine
)");
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 40, 64);
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOff(0, 40, 64);
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 2);
REQUIRE(synth.getNumActiveVoices() == 2);
}
TEST_CASE("[Keyswitches] sw_up, out of range")
@ -321,13 +321,13 @@ TEST_CASE("[Keyswitches] sw_up, out of range")
<region> sw_up=40 key=60 sample=*sine
)");
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOn(0, 40, 64);
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
synth.noteOff(0, 40, 64);
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 2);
REQUIRE(synth.getNumActiveVoices() == 2);
}
TEST_CASE("[Keyswitches] sw_default")
@ -387,18 +387,18 @@ TEST_CASE("[Keyswitches] sw_previous in range")
// the test assumes that sw_previous regions are disabled by default
REQUIRE(!synth.getRegionView(0)->isSwitchedOn());
synth.noteOn(0, 51, 64);
REQUIRE(synth.getNumActiveVoices(true) == 0);
REQUIRE(synth.getNumActiveVoices() == 0);
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 0);
REQUIRE(synth.getNumActiveVoices() == 0);
REQUIRE(synth.getRegionView(0)->isSwitchedOn());
synth.noteOn(0, 51, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
REQUIRE(!synth.getRegionView(0)->isSwitchedOn());
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
REQUIRE(synth.getRegionView(0)->isSwitchedOn());
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 2);
REQUIRE(synth.getNumActiveVoices() == 2);
REQUIRE(synth.getRegionView(0)->isSwitchedOn());
}
@ -411,15 +411,15 @@ TEST_CASE("[Keyswitches] sw_previous out of range")
)");
REQUIRE(!synth.getRegionView(0)->isSwitchedOn());
synth.noteOn(0, 51, 64);
REQUIRE(synth.getNumActiveVoices(true) == 0);
REQUIRE(synth.getNumActiveVoices() == 0);
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 0);
REQUIRE(synth.getNumActiveVoices() == 0);
REQUIRE(synth.getRegionView(0)->isSwitchedOn());
synth.noteOn(0, 51, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
REQUIRE(!synth.getRegionView(0)->isSwitchedOn());
synth.noteOn(0, 60, 64);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
REQUIRE(synth.getRegionView(0)->isSwitchedOn());
synth.noteOn(0, 61, 64);
REQUIRE(!synth.getRegionView(0)->isSwitchedOn());

View file

@ -19,7 +19,6 @@ using namespace Catch::literals;
template <class T, std::size_t A = sfz::config::defaultAlignment>
using aligned_vector = std::vector<T, jsl::aligned_allocator<T, A>>;
constexpr int smallBufferSize { 3 };
constexpr int bigBufferSize { 4095 };
constexpr int medBufferSize { 127 };
constexpr float fillValue { 1.3f };

View file

@ -14,6 +14,9 @@
using namespace Catch::literals;
using namespace sfz::literals;
// Need these for the introspection of Synth
#include "sfizz/Effects.h"
constexpr int blockSize { 256 };
TEST_CASE("[Synth] Play and check active voices")
@ -25,11 +28,11 @@ TEST_CASE("[Synth] Play and check active voices")
synth.noteOn(0, 36, 24);
synth.noteOn(0, 36, 89);
REQUIRE(synth.getNumActiveVoices(true) == 2);
REQUIRE(synth.getNumActiveVoices() == 2);
// Render for a while
for (int i = 0; i < 200; ++i)
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices(true) == 0);
REQUIRE(synth.getNumActiveVoices() == 0);
}
TEST_CASE("[Synth] All sound off")
@ -38,9 +41,9 @@ TEST_CASE("[Synth] All sound off")
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/groups_avl.sfz");
synth.noteOn(0, 36, 24);
synth.noteOn(0, 36, 89);
REQUIRE(synth.getNumActiveVoices(true) == 2);
REQUIRE(synth.getNumActiveVoices() == 2);
synth.allSoundOff();
REQUIRE(synth.getNumActiveVoices(true) == 0);
REQUIRE(synth.getNumActiveVoices() == 0);
}
TEST_CASE("[Synth] Change the number of voice while playing")
@ -53,9 +56,9 @@ TEST_CASE("[Synth] Change the number of voice while playing")
synth.noteOn(0, 36, 24);
synth.noteOn(0, 36, 89);
synth.renderBlock(buffer);
REQUIRE(synth.getNumActiveVoices(true) == 2);
REQUIRE(synth.getNumActiveVoices() == 2);
synth.setNumVoices(8);
REQUIRE(synth.getNumActiveVoices(true) == 0);
REQUIRE(synth.getNumActiveVoices() == 0);
REQUIRE(synth.getNumVoices() == 8);
}
@ -133,15 +136,15 @@ TEST_CASE("[Synth] All notes offs/all sounds off")
)");
synth.noteOn(0, 60, 63);
synth.noteOn(0, 62, 63);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
REQUIRE( synth.getNumActiveVoices() == 2 );
synth.cc(0, 120, 63);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
synth.noteOn(0, 62, 63);
synth.noteOn(0, 60, 63);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
REQUIRE( synth.getNumActiveVoices() == 2 );
synth.cc(0, 123, 63);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
}
TEST_CASE("[Synth] Reset all controllers")
@ -567,14 +570,14 @@ TEST_CASE("[Synth] sample quality")
// default sample quality
synth.noteOn(0, 60, 100);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
REQUIRE(synth.getVoiceView(0)->getCurrentSampleQuality() == sfz::Default::sampleQuality);
synth.allSoundOff();
// default sample quality, freewheeling
synth.enableFreeWheeling();
synth.noteOn(0, 60, 100);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
REQUIRE(synth.getVoiceView(0)->getCurrentSampleQuality() == sfz::Default::sampleQualityInFreewheelingMode);
synth.allSoundOff();
synth.disableFreeWheeling();
@ -582,7 +585,7 @@ TEST_CASE("[Synth] sample quality")
// user-defined sample quality
synth.setSampleQuality(sfz::Synth::ProcessLive, 3);
synth.noteOn(0, 60, 100);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
REQUIRE(synth.getVoiceView(0)->getCurrentSampleQuality() == 3);
synth.allSoundOff();
@ -590,21 +593,21 @@ TEST_CASE("[Synth] sample quality")
synth.enableFreeWheeling();
synth.setSampleQuality(sfz::Synth::ProcessFreewheeling, 8);
synth.noteOn(0, 60, 100);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
REQUIRE(synth.getVoiceView(0)->getCurrentSampleQuality() == 8);
synth.allSoundOff();
synth.disableFreeWheeling();
// region sample quality
synth.noteOn(0, 61, 100);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
REQUIRE(synth.getVoiceView(0)->getCurrentSampleQuality() == 5);
synth.allSoundOff();
// region sample quality, freewheeling
synth.enableFreeWheeling();
synth.noteOn(0, 61, 100);
REQUIRE(synth.getNumActiveVoices(true) == 1);
REQUIRE(synth.getNumActiveVoices() == 1);
REQUIRE(synth.getVoiceView(0)->getCurrentSampleQuality() == 5);
synth.allSoundOff();
synth.disableFreeWheeling();
@ -627,7 +630,7 @@ TEST_CASE("[Synth] Sister voices")
REQUIRE( synth.getVoiceView(0)->getNextSisterVoice() == synth.getVoiceView(0) );
REQUIRE( synth.getVoiceView(0)->getPreviousSisterVoice() == synth.getVoiceView(0) );
synth.noteOn(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
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) );
@ -635,7 +638,7 @@ TEST_CASE("[Synth] Sister voices")
REQUIRE( synth.getVoiceView(2)->getNextSisterVoice() == synth.getVoiceView(1) );
REQUIRE( synth.getVoiceView(2)->getPreviousSisterVoice() == synth.getVoiceView(1) );
synth.noteOn(0, 63, 85);
REQUIRE( synth.getNumActiveVoices(true) == 6 );
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) );
@ -675,15 +678,15 @@ TEST_CASE("[Synth] Sisters and off-by")
<group> group=2 <region> key=63 sample=*saw
)");
synth.noteOn(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
REQUIRE( synth.getNumActiveVoices() == 2 );
REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(0)) == 2 );
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
REQUIRE( synth.getNumActiveVoices() == 2 );
synth.noteOn(0, 63, 85);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
REQUIRE( synth.getNumActiveVoices() == 3 );
for (unsigned i = 0; i < 100; ++i)
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
REQUIRE( synth.getNumActiveVoices() == 2 );
REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(0)) == 1 );
}
@ -747,9 +750,9 @@ TEST_CASE("[Synth] Release")
synth.noteOn(0, 62, 85);
synth.cc(0, 64, 127);
synth.noteOff(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
synth.cc(0, 64, 0);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
REQUIRE( synth.getNumActiveVoices() == 2 );
}
TEST_CASE("[Synth] Release (pedal was already down)")
@ -762,9 +765,9 @@ TEST_CASE("[Synth] Release (pedal was already down)")
synth.cc(0, 64, 127);
synth.noteOn(0, 62, 85);
synth.noteOff(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
synth.cc(0, 64, 0);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
REQUIRE( synth.getNumActiveVoices() == 2 );
}
@ -777,12 +780,12 @@ TEST_CASE("[Synth] Release samples don't play unless there is another playing re
)");
synth.noteOn(0, 62, 85);
synth.noteOff(0, 62, 0);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
synth.cc(0, 64, 127);
synth.noteOn(0, 62, 85);
synth.noteOff(0, 62, 0);
synth.cc(0, 64, 0);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
}
TEST_CASE("[Synth] Release key (Different sustain CC)")
@ -795,7 +798,7 @@ TEST_CASE("[Synth] Release key (Different sustain CC)")
synth.noteOn(0, 62, 85);
synth.cc(0, 54, 127);
synth.noteOff(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
}
TEST_CASE("[Synth] Release (Different sustain CC)")
@ -809,9 +812,9 @@ TEST_CASE("[Synth] Release (Different sustain CC)")
synth.noteOn(0, 62, 85);
synth.cc(0, 54, 127);
synth.noteOff(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
synth.cc(0, 54, 0);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
REQUIRE( synth.getNumActiveVoices() == 2 );
}
TEST_CASE("[Synth] Sustain threshold default")
@ -823,7 +826,7 @@ TEST_CASE("[Synth] Sustain threshold default")
synth.noteOn(0, 62, 85);
synth.cc(0, 64, 1);
synth.noteOff(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
}
TEST_CASE("[Synth] Sustain threshold")
@ -837,15 +840,15 @@ TEST_CASE("[Synth] Sustain threshold")
synth.noteOn(0, 62, 85);
synth.cc(0, 64, 1);
synth.noteOff(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
REQUIRE( synth.getNumActiveVoices() == 2 );
synth.noteOn(0, 62, 85);
synth.noteOff(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 4 );
REQUIRE( synth.getNumActiveVoices() == 4 );
synth.noteOn(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 5 );
REQUIRE( synth.getNumActiveVoices() == 5 );
synth.cc(0, 64, 64);
synth.noteOff(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 5 );
REQUIRE( synth.getNumActiveVoices() == 5 );
}
TEST_CASE("[Synth] Release (Multiple notes, release_key ignores the pedal)")
@ -861,7 +864,7 @@ TEST_CASE("[Synth] Release (Multiple notes, release_key ignores the pedal)")
synth.noteOff(0, 64, 0);
synth.noteOff(0, 63, 2);
synth.noteOff(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
REQUIRE( synth.getNumActiveVoices() == 3 );
std::vector<float> requiredVelocities { 34_norm, 78_norm, 85_norm};
std::vector<float> actualVelocities;
@ -887,9 +890,9 @@ TEST_CASE("[Synth] Release (Multiple notes, release, cleared the delayed voices
synth.noteOff(0, 64, 0);
synth.noteOff(0, 63, 2);
synth.noteOff(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
REQUIRE( synth.getNumActiveVoices() == 3 );
synth.cc(0, 64, 0);
REQUIRE( synth.getNumActiveVoices(true) == 6 );
REQUIRE( synth.getNumActiveVoices() == 6 );
std::vector<float> requiredVelocities { 34_norm, 78_norm, 85_norm, 34_norm, 78_norm, 85_norm };
std::vector<float> actualVelocities;
@ -917,9 +920,9 @@ TEST_CASE("[Synth] Release (Multiple notes after pedal is down, release, cleared
synth.noteOff(2, 64, 0);
synth.noteOff(2, 63, 2);
synth.noteOff(2, 62, 3);
REQUIRE( synth.getNumActiveVoices(true) == 3 );
REQUIRE( synth.getNumActiveVoices() == 3 );
synth.cc(3, 64, 0);
REQUIRE( synth.getNumActiveVoices(true) == 6 );
REQUIRE( synth.getNumActiveVoices() == 6 );
std::vector<float> requiredVelocities { 34_norm, 78_norm, 85_norm, 34_norm, 78_norm, 85_norm };
std::vector<float> actualVelocities;
@ -945,9 +948,9 @@ TEST_CASE("[Synth] Release (Multiple note ons during pedal down)")
synth.noteOff(0, 62, 0);
synth.noteOn(0, 62, 78);
synth.noteOff(0, 62, 2);
REQUIRE( synth.getNumActiveVoices(true) == 2 );
REQUIRE( synth.getNumActiveVoices() == 2 );
synth.cc(0, 64, 0);
REQUIRE( synth.getNumActiveVoices(true) == 4 );
REQUIRE( synth.getNumActiveVoices() == 4 );
std::vector<float> requiredVelocities { 78_norm, 85_norm, 78_norm, 85_norm };
std::vector<float> actualVelocities;
@ -971,25 +974,25 @@ TEST_CASE("[Synth] No release sample after the main sample stopped sounding by d
loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1
)");
synth.noteOn(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
for (unsigned i = 0; i < 100; ++i) {
synth.renderBlock(buffer);
}
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
synth.noteOff(0, 62, 0);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
synth.noteOn(0, 62, 85);
synth.cc(0, 64, 127);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
for (unsigned i = 0; i < 100; ++i) {
synth.renderBlock(buffer);
}
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
synth.noteOff(0, 62, 0);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
synth.cc(0, 64, 0);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
REQUIRE( synth.getRegionView(1)->delayedReleases.empty() );
}
@ -1006,25 +1009,25 @@ TEST_CASE("[Synth] If rt_dead is active the release sample can sound after the a
loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1
)");
synth.noteOn(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
for (unsigned i = 0; i < 100; ++i) {
synth.renderBlock(buffer);
}
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
synth.noteOff(0, 62, 0);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
synth.noteOn(0, 62, 85);
synth.cc(0, 64, 127);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
for (unsigned i = 0; i < 100; ++i) {
synth.renderBlock(buffer);
}
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
synth.noteOff(0, 62, 0);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
synth.cc(0, 64, 0);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
REQUIRE( synth.getRegionView(1)->delayedReleases.empty() );
}
@ -1038,9 +1041,9 @@ TEST_CASE("[Synth] sw_default works at a global level")
<region> sw_last=37 key=63 sample=*sine
)");
synth.noteOn(0, 63, 85);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
synth.noteOn(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
}
TEST_CASE("[Synth] sw_default works at a master level")
@ -1052,9 +1055,9 @@ TEST_CASE("[Synth] sw_default works at a master level")
<region> sw_last=37 key=63 sample=*sine
)");
synth.noteOn(0, 63, 85);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
synth.noteOn(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
}
TEST_CASE("[Synth] sw_default works at a group level")
@ -1066,9 +1069,9 @@ TEST_CASE("[Synth] sw_default works at a group level")
<region> sw_last=37 key=63 sample=*sine
)");
synth.noteOn(0, 63, 85);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
synth.noteOn(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
}
TEST_CASE("[Synth] Used CCs")
@ -1166,10 +1169,10 @@ TEST_CASE("[Synth] Activate also on the sustain CC")
<region> locc64=64 key=53 sample=*sine
)");
synth.noteOn(0, 53, 127);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
synth.cc(1, 64, 127);
synth.noteOn(2, 53, 127);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
}
TEST_CASE("[Synth] Trigger also on the sustain CC")
@ -1179,7 +1182,7 @@ TEST_CASE("[Synth] Trigger also on the sustain CC")
<region> on_locc64=64 sample=*sine
)");
synth.cc(0, 64, 127);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
}
TEST_CASE("[Synth] end=-1 voices are immediately killed after triggering but they kill other voices")
@ -1194,11 +1197,11 @@ TEST_CASE("[Synth] end=-1 voices are immediately killed after triggering but the
<region> key=63 end=-1 sample=*saw group=2
)");
synth.noteOn(0, 60, 85);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
synth.noteOn(0, 61, 85);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
synth.noteOn(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
REQUIRE( numPlayingVoices(synth) == 1 );
synth.noteOn(1, 63, 85);
synth.renderBlock(buffer);
@ -1217,11 +1220,11 @@ TEST_CASE("[Synth] end=0 voices are immediately killed after triggering but they
<region> key=63 end=0 sample=*saw group=2
)");
synth.noteOn(0, 60, 85);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
synth.noteOn(0, 61, 85);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
synth.noteOn(0, 62, 85);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
REQUIRE( numPlayingVoices(synth) == 1 );
synth.noteOn(1, 63, 85);
synth.renderBlock(buffer);
@ -1239,19 +1242,19 @@ TEST_CASE("[Synth] ampeg_sustain = 0 puts the ampeg envelope in free-running mod
)");
synth.noteOn(0, 60, 85);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
synth.noteOn(0, 61, 85);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
// Render a bit; this does not kill the voice
for (unsigned i = 0; i < 5; ++i)
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 1 );
REQUIRE( synth.getNumActiveVoices() == 1 );
// Render about half a second
for (unsigned i = 0; i < 100; ++i)
synth.renderBlock(buffer);
REQUIRE( synth.getNumActiveVoices(true) == 0 );
REQUIRE( synth.getNumActiveVoices() == 0 );
}
TEST_CASE("[Synth] Off by standard")
@ -1357,17 +1360,28 @@ TEST_CASE("[Synth] Initial values of CC")
<region> sample=*sine
)");
REQUIRE(synth.getHdccInit(111) == 0.0f);
REQUIRE(synth.getHdccInit(7) == Approx(100.0f / 127)); // default volume
REQUIRE(synth.getHdccInit(10) == 0.5f); // default pan
REQUIRE(synth.getHdcc(111) == 0.0f);
REQUIRE(synth.getDefaultHdcc(111) == 0.0f);
REQUIRE(synth.getHdcc(7) == Approx(100.0f / 127)); // default volume
REQUIRE(synth.getDefaultHdcc(7) == Approx(100.0f / 127));
REQUIRE(synth.getHdcc(10) == 0.5f); // default pan
REQUIRE(synth.getDefaultHdcc(10) == 0.5f);
REQUIRE(synth.getHdcc(11) == 1.0f); // default expression
REQUIRE(synth.getDefaultHdcc(11) == 1.0f);
synth.hdcc(0, 10, 0.7f);
REQUIRE(synth.getHdcc(10) == 0.7f);
REQUIRE(synth.getDefaultHdcc(10) == 0.5f);
synth.loadSfzString(fs::current_path() / "init_cc.sfz", R"(
<control> set_hdcc111=0.1234 set_cc112=77
<region> sample=*sine
)");
REQUIRE(synth.getHdccInit(111) == Approx(0.1234f));
REQUIRE(synth.getHdccInit(112) == Approx(77.0f / 127));
REQUIRE(synth.getHdcc(111) == Approx(0.1234f));
REQUIRE(synth.getDefaultHdcc(111) == Approx(0.1234f));
REQUIRE(synth.getHdcc(112) == Approx(77.0f / 127));
REQUIRE(synth.getDefaultHdcc(112) == Approx(77.0f / 127));
}
TEST_CASE("[Synth] Default ampeg_release")

View file

@ -7,6 +7,7 @@
#pragma once
#include "sfizz/Synth.h"
#include "sfizz/Region.h"
#include "sfizz/Voice.h"
#include "sfizz/modulations/ModKey.h"
class RegionCCView {