363 lines
10 KiB
C++
363 lines
10 KiB
C++
// 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 "ADSREnvelope.h"
|
|
#include "HistoricalBuffer.h"
|
|
#include "Region.h"
|
|
#include "AudioBuffer.h"
|
|
#include "Resources.h"
|
|
#include "AudioSpan.h"
|
|
#include "LeakDetector.h"
|
|
#include "OnePoleFilter.h"
|
|
#include "absl/types/span.h"
|
|
#include <memory>
|
|
#include <random>
|
|
|
|
namespace sfz {
|
|
enum InterpolatorModel : int;
|
|
/**
|
|
* @brief The SFZ voice are the polyphony holders. They get activated by the synth
|
|
* and tasked to play a given region until the end, stopping on note-offs, off-groups
|
|
* or natural sample decay.
|
|
*
|
|
*/
|
|
class Voice {
|
|
public:
|
|
Voice() = delete;
|
|
/**
|
|
* @brief Construct a new voice with the midistate singleton
|
|
*
|
|
* @param midiState
|
|
*/
|
|
Voice(Resources& resources);
|
|
enum class TriggerType {
|
|
NoteOn,
|
|
NoteOff,
|
|
CC
|
|
};
|
|
/**
|
|
* @brief Change the sample rate of the voice. This is used to compute all
|
|
* pitch related transformations so it needs to be propagated from the synth
|
|
* at all times.
|
|
*
|
|
* @param sampleRate
|
|
*/
|
|
void setSampleRate(float sampleRate) noexcept;
|
|
/**
|
|
* @brief Set the expected block size. If the block size is not fixed, set an
|
|
* upper bound. The voice will adapt at each callback to the actual number of
|
|
* samples requested but this function will allocate temporary buffers that are
|
|
* needed for proper functioning.
|
|
*
|
|
* @param samplesPerBlock
|
|
*/
|
|
void setSamplesPerBlock(int samplesPerBlock) noexcept;
|
|
/**
|
|
* @brief Get the sample rate of the voice.
|
|
*
|
|
* @return float
|
|
*/
|
|
float getSampleRate() const noexcept { return sampleRate; }
|
|
/**
|
|
* @brief Get the expected block size.
|
|
*
|
|
* @return int
|
|
*/
|
|
int getSamplesPerBlock() const noexcept { return samplesPerBlock; }
|
|
|
|
/**
|
|
* @brief Start playing a region after a short delay for different triggers (note on, off, cc)
|
|
*
|
|
* @param region
|
|
* @param delay
|
|
* @param number
|
|
* @param value
|
|
* @param triggerType
|
|
*/
|
|
void startVoice(Region* region, int delay, int number, float value, TriggerType triggerType) noexcept;
|
|
|
|
/**
|
|
* @brief Register a note-off event; this may trigger a release.
|
|
*
|
|
* @param delay
|
|
* @param noteNumber
|
|
* @param velocity
|
|
*/
|
|
void registerNoteOff(int delay, int noteNumber, float velocity) noexcept;
|
|
/**
|
|
* @brief Register a CC event; this may trigger a release. If the voice is playing and its
|
|
* region has CC modifiers, it will use this value to compute the CC envelope to apply to the
|
|
* parameter.
|
|
*
|
|
* @param delay
|
|
* @param ccNumber
|
|
* @param ccValue
|
|
*/
|
|
void registerCC(int delay, int ccNumber, float ccValue) noexcept;
|
|
/**
|
|
* @brief Register a pitch wheel event; for now this does nothing
|
|
*
|
|
* @param delay
|
|
* @param pitch
|
|
*/
|
|
void registerPitchWheel(int delay, float pitch) noexcept;
|
|
/**
|
|
* @brief Register an aftertouch event; for now this does nothing
|
|
*
|
|
* @param delay
|
|
* @param aftertouch
|
|
*/
|
|
void registerAftertouch(int delay, uint8_t aftertouch) noexcept;
|
|
/**
|
|
* @brief Register a tempo event; for now this does nothing
|
|
*
|
|
* @param delay
|
|
* @param pitch
|
|
*/
|
|
void registerTempo(int delay, float secondsPerQuarter) noexcept;
|
|
/**
|
|
* @brief Checks if the voice should be offed by another starting in the group specified.
|
|
* This will trigger the release if true.
|
|
*
|
|
* @param delay
|
|
* @param group
|
|
* @return true
|
|
* @return false
|
|
*/
|
|
bool checkOffGroup(int delay, uint32_t group) noexcept;
|
|
|
|
/**
|
|
* @brief Render a block of data for this voice into the span
|
|
*
|
|
* @param buffer
|
|
*/
|
|
void renderBlock(AudioSpan<float, 2> buffer) noexcept;
|
|
|
|
/**
|
|
* @brief Is the voice free?
|
|
*
|
|
* @return true
|
|
* @return false
|
|
*/
|
|
bool isFree() const noexcept;
|
|
/**
|
|
* @brief Can the voice be reused (i.e. is it releasing or free)
|
|
*
|
|
* @return true
|
|
* @return false
|
|
*/
|
|
bool releasedOrFree() const noexcept;
|
|
/**
|
|
* @brief Get the number that triggered the voice (note number or cc number)
|
|
*
|
|
* @return int
|
|
*/
|
|
int getTriggerNumber() const noexcept { return triggerNumber; }
|
|
/**
|
|
* @brief Get the value that triggered the voice (note velocity or cc value)
|
|
*
|
|
* @return float
|
|
*/
|
|
float getTriggerValue() const noexcept { return triggerValue; }
|
|
/**
|
|
* @brief Get the type of trigger
|
|
*
|
|
* @return TriggerType
|
|
*/
|
|
TriggerType getTriggerType() const noexcept { return triggerType; }
|
|
|
|
/**
|
|
* @brief Reset the voice to its initial values
|
|
*
|
|
*/
|
|
void reset() noexcept;
|
|
|
|
/**
|
|
* @brief Get the mean squared power of the last rendered block. This is used
|
|
* to determine which voice to steal if there are too many notes flying around.
|
|
*
|
|
* @return float
|
|
*/
|
|
float getAverageEnvelope() const noexcept;
|
|
/**
|
|
* @brief Get the position of the voice in the source, in samples
|
|
*
|
|
* @return uint32_t
|
|
*/
|
|
uint32_t getSourcePosition() const noexcept;
|
|
/**
|
|
* Returns the region that is currently playing. May be null if the voice is not active!
|
|
*
|
|
* @return
|
|
*/
|
|
const Region* getRegion() const noexcept { return region; }
|
|
/**
|
|
* @brief Set the max number of filters per voice
|
|
*
|
|
* @param numFilters
|
|
*/
|
|
void setMaxFiltersPerVoice(size_t numFilters);
|
|
/**
|
|
* @brief Set the max number of EQs per voice
|
|
*
|
|
* @param numFilters
|
|
*/
|
|
void setMaxEQsPerVoice(size_t numEQs);
|
|
/**
|
|
* @brief Release the voice after a given delay
|
|
*
|
|
* @param delay
|
|
* @param fastRelease whether to do a normal release or cut the voice abruptly
|
|
*/
|
|
void release(int delay, bool fastRelease = false) noexcept;
|
|
|
|
/**
|
|
* @brief gets the age of the Voice
|
|
*
|
|
* @return
|
|
*/
|
|
int getAge() const noexcept { return age; }
|
|
|
|
Duration getLastDataDuration() const noexcept { return dataDuration; }
|
|
Duration getLastAmplitudeDuration() const noexcept { return amplitudeDuration; }
|
|
Duration getLastFilterDuration() const noexcept { return filterDuration; }
|
|
Duration getLastPanningDuration() const noexcept { return panningDuration; }
|
|
|
|
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 <sfz::InterpolatorModel M>
|
|
static void fillInterpolated(
|
|
const sfz::AudioSpan<const float>& source, sfz::AudioSpan<float>& dest,
|
|
absl::Span<const int> indices, absl::Span<const float> coeffs);
|
|
|
|
void amplitudeEnvelope(absl::Span<float> modulationSpan) noexcept;
|
|
void ampStageMono(AudioSpan<float> buffer) noexcept;
|
|
void ampStageStereo(AudioSpan<float> buffer) noexcept;
|
|
void panStageMono(AudioSpan<float> buffer) noexcept;
|
|
void panStageStereo(AudioSpan<float> buffer) noexcept;
|
|
void filterStageMono(AudioSpan<float> buffer) noexcept;
|
|
void filterStageStereo(AudioSpan<float> buffer) noexcept;
|
|
/**
|
|
* @brief Initialize frequency and gain coefficients for the oscillators.
|
|
*/
|
|
void setupOscillatorUnison();
|
|
void updateChannelPowers(AudioSpan<float> buffer);
|
|
|
|
Region* region { nullptr };
|
|
|
|
enum class State {
|
|
idle,
|
|
playing
|
|
};
|
|
State state { State::idle };
|
|
bool noteIsOff { false };
|
|
|
|
TriggerType triggerType;
|
|
int triggerNumber;
|
|
float triggerValue;
|
|
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 };
|
|
|
|
FilePromisePtr currentPromise { nullptr };
|
|
|
|
int samplesPerBlock { config::defaultSamplesPerBlock };
|
|
int minEnvelopeDelay { config::defaultSamplesPerBlock / 2 };
|
|
float sampleRate { config::defaultSampleRate };
|
|
|
|
Resources& resources;
|
|
|
|
std::vector<FilterHolderPtr> filters;
|
|
std::vector<EQHolderPtr> equalizers;
|
|
|
|
ADSREnvelope<float> egEnvelope;
|
|
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;
|
|
|
|
std::normal_distribution<float> noiseDist { 0, config::noiseVariance };
|
|
|
|
std::array<OnePoleFilter<float>, 2> channelEnvelopeFilters;
|
|
std::array<float, 2> smoothedChannelEnvelopes;
|
|
LEAK_DETECTOR(Voice);
|
|
};
|
|
|
|
inline bool sisterVoices(const Voice* lhs, const Voice* rhs)
|
|
{
|
|
return lhs->getAge() == rhs->getAge()
|
|
&& lhs->getTriggerNumber() == rhs->getTriggerNumber()
|
|
&& lhs->getTriggerValue() == rhs->getTriggerValue()
|
|
&& lhs->getTriggerType() == rhs->getTriggerType();
|
|
}
|
|
|
|
inline bool voiceOrdering(const Voice* lhs, const Voice* rhs)
|
|
{
|
|
if (lhs->getAge() > rhs->getAge())
|
|
return true;
|
|
if (lhs->getAge() < rhs->getAge())
|
|
return false;
|
|
|
|
if (lhs->getTriggerNumber() > rhs->getTriggerNumber())
|
|
return true;
|
|
if (lhs->getTriggerNumber() < rhs->getTriggerNumber())
|
|
return false;
|
|
|
|
if (lhs->getTriggerValue() > rhs->getTriggerValue())
|
|
return true;
|
|
if (lhs->getTriggerValue() < rhs->getTriggerValue())
|
|
return false;
|
|
|
|
if (lhs->getTriggerType() > rhs->getTriggerType())
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
} // namespace sfz
|