Merge pull request #533 from paulfd/voice-synth-pimpl
Refactor of Synth and Voice into a pimpl pattern
This commit is contained in:
commit
5d90b2a82c
28 changed files with 2532 additions and 2032 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include "Config.h"
|
||||
#include "Region.h"
|
||||
#include "Voice.h"
|
||||
#include "SwapAndPop.h"
|
||||
|
|
|
|||
1550
src/sfizz/Synth.cpp
1550
src/sfizz/Synth.cpp
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
1197
src/sfizz/Voice.cpp
1197
src/sfizz/Voice.cpp
File diff suppressed because it is too large
Load diff
|
|
@ -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
242
src/sfizz/VoiceManager.cpp
Normal 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
193
src/sfizz/VoiceManager.h
Normal 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
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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_;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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() );
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
178
tests/SynthT.cpp
178
tests/SynthT.cpp
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue