Add support for opcodes lotimer/hitimer
While not widely supported or well documented, is very useful especially when triggering a note with on_locc/on_hicc to prevent a bunch of retriggers. The region will trigger as long as the elapsed time from the note-on of the most recently triggered region in the current polyphony group is between lotimer and hitimer amount of seconds. lotimer default 0, hitimer default is max_float. Example use: to preventing a region from retriggering too quickly, set lotimer=0.1
This commit is contained in:
parent
276ab2ce68
commit
621b4c2415
13 changed files with 86 additions and 6 deletions
|
|
@ -199,6 +199,8 @@ FloatSpec lofiDecim { 0.0f, {0.0f, 100.0f}, 0 };
|
||||||
FloatSpec rectify { 0.0f, {0.0f, 100.0f}, 0 };
|
FloatSpec rectify { 0.0f, {0.0f, 100.0f}, 0 };
|
||||||
UInt32Spec stringsNumber { maxStrings, {0, maxStrings}, 0 };
|
UInt32Spec stringsNumber { maxStrings, {0, maxStrings}, 0 };
|
||||||
BoolSpec sustainCancelsRelease { false, {0, 1}, kEnforceBounds };
|
BoolSpec sustainCancelsRelease { false, {0, 1}, kEnforceBounds };
|
||||||
|
FloatSpec loTimer { 0.0f, {0.0f, float_max}, 0 };
|
||||||
|
FloatSpec hiTimer { float_max, {0.0f, float_max}, 0 };
|
||||||
|
|
||||||
ESpec<Trigger> trigger { Trigger::attack, {Trigger::attack, Trigger::release_key}, 0};
|
ESpec<Trigger> trigger { Trigger::attack, {Trigger::attack, Trigger::release_key}, 0};
|
||||||
ESpec<CrossfadeCurve> crossfadeCurve { CrossfadeCurve::power, {CrossfadeCurve::gain, CrossfadeCurve::power}, 0};
|
ESpec<CrossfadeCurve> crossfadeCurve { CrossfadeCurve::power, {CrossfadeCurve::gain, CrossfadeCurve::power}, 0};
|
||||||
|
|
|
||||||
|
|
@ -317,6 +317,8 @@ namespace Default
|
||||||
extern const OpcodeSpec<FilterType> filter;
|
extern const OpcodeSpec<FilterType> filter;
|
||||||
extern const OpcodeSpec<EqType> eq;
|
extern const OpcodeSpec<EqType> eq;
|
||||||
extern const OpcodeSpec<bool> sustainCancelsRelease;
|
extern const OpcodeSpec<bool> sustainCancelsRelease;
|
||||||
|
extern const OpcodeSpec<float> loTimer;
|
||||||
|
extern const OpcodeSpec<float> hiTimer;
|
||||||
|
|
||||||
// Default/max count for objects
|
// Default/max count for objects
|
||||||
constexpr int numEQs { 3 };
|
constexpr int numEQs { 3 };
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,12 @@ public:
|
||||||
*/
|
*/
|
||||||
void advanceTime(int numSamples) noexcept;
|
void advanceTime(int numSamples) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns current internal sample clock
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
unsigned getInternalClock() const noexcept { return internalClock; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Flush events in all states, keeping only the last one as the "base" state
|
* @brief Flush events in all states, keeping only the last one as the "base" state
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ void sfz::PolyphonyGroup::registerVoice(Voice* voice) noexcept
|
||||||
{
|
{
|
||||||
if (absl::c_find(voices, voice) == voices.end())
|
if (absl::c_find(voices, voice) == voices.end())
|
||||||
voices.push_back(voice);
|
voices.push_back(voice);
|
||||||
|
mostRecentStartStamp_ = voice->getStartTimestampSamples();
|
||||||
}
|
}
|
||||||
|
|
||||||
void sfz::PolyphonyGroup::removeVoice(const Voice* voice) noexcept
|
void sfz::PolyphonyGroup::removeVoice(const Voice* voice) noexcept
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,15 @@ public:
|
||||||
* @return std::vector<Voice*>&
|
* @return std::vector<Voice*>&
|
||||||
*/
|
*/
|
||||||
std::vector<Voice*>& getActiveVoices() noexcept { return voices; }
|
std::vector<Voice*>& getActiveVoices() noexcept { return voices; }
|
||||||
|
/**
|
||||||
|
* @brief Returns the start timestamp in samples of the most recently activated voice
|
||||||
|
*/
|
||||||
|
unsigned getMostRecentStartTimestamp() const noexcept { return mostRecentStartStamp_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
unsigned polyphonyLimit { config::maxVoices };
|
unsigned polyphonyLimit { config::maxVoices };
|
||||||
std::vector<Voice*> voices;
|
std::vector<Voice*> voices;
|
||||||
|
unsigned mostRecentStartStamp_ { 0 };
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -562,6 +562,15 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode, bool cleanOpcode)
|
||||||
groupVolume = opcode.read(Default::volume);
|
groupVolume = opcode.read(Default::volume);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case hash("lotimer"):
|
||||||
|
timerRange.setStart(opcode.read(Default::loTimer));
|
||||||
|
useTimerRange = useTimerRange || timerRange.getStart() != Default::loTimer;
|
||||||
|
break;
|
||||||
|
case hash("hitimer"):
|
||||||
|
timerRange.setEnd(opcode.read(Default::hiTimer));
|
||||||
|
useTimerRange = useTimerRange || timerRange.getEnd() != Default::hiTimer;
|
||||||
|
break;
|
||||||
|
|
||||||
// Performance parameters: filters
|
// Performance parameters: filters
|
||||||
case hash("cutoff&"): // also cutoff
|
case hash("cutoff&"): // also cutoff
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -335,6 +335,8 @@ struct Region {
|
||||||
CCMap<UncheckedRange<float>> crossfadeCCInRange { Default::crossfadeCCInRange }; // xfin_loccN xfin_hiccN
|
CCMap<UncheckedRange<float>> crossfadeCCInRange { Default::crossfadeCCInRange }; // xfin_loccN xfin_hiccN
|
||||||
CCMap<UncheckedRange<float>> crossfadeCCOutRange { Default::crossfadeCCOutRange }; // xfout_loccN xfout_hiccN
|
CCMap<UncheckedRange<float>> crossfadeCCOutRange { Default::crossfadeCCOutRange }; // xfout_loccN xfout_hiccN
|
||||||
float rtDecay { Default::rtDecay }; // rt_decay
|
float rtDecay { Default::rtDecay }; // rt_decay
|
||||||
|
UncheckedRange<float> timerRange { Default::loTimer, Default::hiTimer }; // hitimer and lotimer (seconds)
|
||||||
|
bool useTimerRange { false }; // to optimize having to check the timer range, because this is very rare opcode
|
||||||
|
|
||||||
float globalAmplitude { 1.0 }; // global_amplitude
|
float globalAmplitude { 1.0 }; // global_amplitude
|
||||||
float masterAmplitude { 1.0 }; // master_amplitude
|
float masterAmplitude { 1.0 }; // master_amplitude
|
||||||
|
|
|
||||||
|
|
@ -1353,6 +1353,7 @@ void Synth::Impl::noteOnDispatch(int delay, int noteNumber, float velocity) noex
|
||||||
{
|
{
|
||||||
const auto randValue = randNoteDistribution_(Random::randomGenerator);
|
const auto randValue = randNoteDistribution_(Random::randomGenerator);
|
||||||
SisterVoiceRingBuilder ring;
|
SisterVoiceRingBuilder ring;
|
||||||
|
MidiState& midiState = resources_.getMidiState();
|
||||||
|
|
||||||
if (!lastKeyswitchLists_[noteNumber].empty()) {
|
if (!lastKeyswitchLists_[noteNumber].empty()) {
|
||||||
if (currentSwitch_ && *currentSwitch_ != noteNumber) {
|
if (currentSwitch_ && *currentSwitch_ != noteNumber) {
|
||||||
|
|
@ -1374,6 +1375,9 @@ void Synth::Impl::noteOnDispatch(int delay, int noteNumber, float velocity) noex
|
||||||
for (Layer* layer : noteActivationLists_[noteNumber]) {
|
for (Layer* layer : noteActivationLists_[noteNumber]) {
|
||||||
if (layer->registerNoteOn(noteNumber, velocity, randValue)) {
|
if (layer->registerNoteOn(noteNumber, velocity, randValue)) {
|
||||||
const Region& region = layer->getRegion();
|
const Region& region = layer->getRegion();
|
||||||
|
if (region.useTimerRange && !voiceManager_.withinValidTimerRange(®ion, midiState.getInternalClock() + delay, sampleRate_))
|
||||||
|
continue;
|
||||||
|
|
||||||
checkOffGroups(®ion, delay, noteNumber);
|
checkOffGroups(®ion, delay, noteNumber);
|
||||||
TriggerEvent triggerEvent { TriggerEventType::NoteOn, noteNumber, velocity };
|
TriggerEvent triggerEvent { TriggerEventType::NoteOn, noteNumber, velocity };
|
||||||
startVoice(layer, delay, triggerEvent, ring);
|
startVoice(layer, delay, triggerEvent, ring);
|
||||||
|
|
@ -1430,6 +1434,7 @@ void Synth::Impl::ccDispatch(int delay, int ccNumber, float value) noexcept
|
||||||
SisterVoiceRingBuilder ring;
|
SisterVoiceRingBuilder ring;
|
||||||
TriggerEvent triggerEvent { TriggerEventType::CC, ccNumber, value };
|
TriggerEvent triggerEvent { TriggerEventType::CC, ccNumber, value };
|
||||||
const auto randValue = randNoteDistribution_(Random::randomGenerator);
|
const auto randValue = randNoteDistribution_(Random::randomGenerator);
|
||||||
|
MidiState& midiState = resources_.getMidiState();
|
||||||
for (Layer* layer : ccActivationLists_[ccNumber]) {
|
for (Layer* layer : ccActivationLists_[ccNumber]) {
|
||||||
const Region& region = layer->getRegion();
|
const Region& region = layer->getRegion();
|
||||||
|
|
||||||
|
|
@ -1448,6 +1453,9 @@ void Synth::Impl::ccDispatch(int delay, int ccNumber, float value) noexcept
|
||||||
}
|
}
|
||||||
|
|
||||||
if (layer->registerCC(ccNumber, value, randValue)) {
|
if (layer->registerCC(ccNumber, value, randValue)) {
|
||||||
|
if (region.useTimerRange && ! voiceManager_.withinValidTimerRange(®ion, midiState.getInternalClock() + delay, sampleRate_))
|
||||||
|
continue;
|
||||||
|
|
||||||
checkOffGroups(®ion, delay, ccNumber);
|
checkOffGroups(®ion, delay, ccNumber);
|
||||||
startVoice(layer, delay, triggerEvent, ring);
|
startVoice(layer, delay, triggerEvent, ring);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -730,6 +730,14 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co
|
||||||
}
|
}
|
||||||
} break;
|
} break;
|
||||||
|
|
||||||
|
MATCH("/region&/timer_range", "") {
|
||||||
|
GET_REGION_OR_BREAK(indices[0])
|
||||||
|
sfizz_arg_t args[2];
|
||||||
|
args[0].f = region.timerRange.getStart();
|
||||||
|
args[1].f = region.timerRange.getEnd();
|
||||||
|
client.receive(delay, path, "ff", args);
|
||||||
|
} break;
|
||||||
|
|
||||||
MATCH("/region&/position", "") {
|
MATCH("/region&/position", "") {
|
||||||
GET_REGION_OR_BREAK(indices[0])
|
GET_REGION_OR_BREAK(indices[0])
|
||||||
client.receive<'f'>(delay, path, region.position * 100.0f);
|
client.receive<'f'>(delay, path, region.position * 100.0f);
|
||||||
|
|
|
||||||
|
|
@ -262,6 +262,7 @@ struct Voice::Impl
|
||||||
|
|
||||||
int samplesPerBlock_ { config::defaultSamplesPerBlock };
|
int samplesPerBlock_ { config::defaultSamplesPerBlock };
|
||||||
float sampleRate_ { config::defaultSampleRate };
|
float sampleRate_ { config::defaultSampleRate };
|
||||||
|
unsigned startTimestamp_ { 0 };
|
||||||
|
|
||||||
Resources& resources_;
|
Resources& resources_;
|
||||||
|
|
||||||
|
|
@ -429,14 +430,18 @@ bool Voice::startVoice(Layer* layer, int delay, const TriggerEvent& event) noexc
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl.switchState(State::playing);
|
|
||||||
|
|
||||||
impl.updateExtendedCCValues();
|
|
||||||
|
|
||||||
ASSERT(delay >= 0);
|
ASSERT(delay >= 0);
|
||||||
if (delay < 0)
|
if (delay < 0)
|
||||||
delay = 0;
|
delay = 0;
|
||||||
|
|
||||||
|
impl.triggerDelay_ = delay;
|
||||||
|
impl.initialDelay_ = delay + static_cast<int>(regionDelay(region, midiState) * impl.sampleRate_);
|
||||||
|
impl.startTimestamp_ = midiState.getInternalClock() + impl.initialDelay_; // need to set this before switchState
|
||||||
|
|
||||||
|
impl.switchState(State::playing);
|
||||||
|
|
||||||
|
impl.updateExtendedCCValues();
|
||||||
|
|
||||||
if (region.isOscillator()) {
|
if (region.isOscillator()) {
|
||||||
WavetablePool& wavePool = resources.getWavePool();
|
WavetablePool& wavePool = resources.getWavePool();
|
||||||
const WavetableMulti* wave = nullptr;
|
const WavetableMulti* wave = nullptr;
|
||||||
|
|
@ -510,8 +515,6 @@ bool Voice::startVoice(Layer* layer, int delay, const TriggerEvent& event) noexc
|
||||||
impl.equalizers_[i].setup(region, i, impl.triggerEvent_.value);
|
impl.equalizers_[i].setup(region, i, impl.triggerEvent_.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl.triggerDelay_ = delay;
|
|
||||||
impl.initialDelay_ = delay + static_cast<int>(regionDelay(region, midiState) * impl.sampleRate_);
|
|
||||||
impl.baseFrequency_ = tuning.getFrequencyOfKey(impl.triggerEvent_.number);
|
impl.baseFrequency_ = tuning.getFrequencyOfKey(impl.triggerEvent_.number);
|
||||||
impl.sampleEnd_ = int(sampleEnd(region, midiState));
|
impl.sampleEnd_ = int(sampleEnd(region, midiState));
|
||||||
impl.sampleSize_ = impl.sampleEnd_- impl.sourcePosition_ - 1;
|
impl.sampleSize_ = impl.sampleEnd_- impl.sourcePosition_ - 1;
|
||||||
|
|
@ -2079,6 +2082,12 @@ int Voice::getSourcePosition() const noexcept
|
||||||
return impl.sourcePosition_;
|
return impl.sourcePosition_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsigned Voice::getStartTimestampSamples() const noexcept
|
||||||
|
{
|
||||||
|
Impl& impl = *impl_;
|
||||||
|
return impl.startTimestamp_;
|
||||||
|
}
|
||||||
|
|
||||||
LFO* Voice::getLFO(size_t index)
|
LFO* Voice::getLFO(size_t index)
|
||||||
{
|
{
|
||||||
Impl& impl = *impl_;
|
Impl& impl = *impl_;
|
||||||
|
|
|
||||||
|
|
@ -437,6 +437,13 @@ public:
|
||||||
*/
|
*/
|
||||||
int getSourcePosition() const noexcept;
|
int getSourcePosition() const noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the timestamp in midistate transport sample time when the voice was started, in samples
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
unsigned getStartTimestampSamples() const noexcept;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
* @brief Check if the voice already belongs to a sister ring
|
* @brief Check if the voice already belongs to a sister ring
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,16 @@ void VoiceManager::checkNotePolyphony(const Region* region, int delay, const Tri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool VoiceManager::withinValidTimerRange(const Region* region, unsigned timestampSamples, float sampleRate) const noexcept
|
||||||
|
{
|
||||||
|
auto found = polyphonyGroups_.find(static_cast<int>(region->group));
|
||||||
|
if (found != polyphonyGroups_.end()) {
|
||||||
|
// convert timestamp to seconds
|
||||||
|
return region->timerRange.contains((timestampSamples - found->second.getMostRecentStartTimestamp()) / sampleRate);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void VoiceManager::checkGroupPolyphony(const Region* region, int delay) noexcept
|
void VoiceManager::checkGroupPolyphony(const Region* region, int delay) noexcept
|
||||||
{
|
{
|
||||||
auto& group = polyphonyGroups_[region->group];
|
auto& group = polyphonyGroups_[region->group];
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,16 @@ struct VoiceManager final : public Voice::StateListener
|
||||||
*/
|
*/
|
||||||
void requireNumVoices(int numVoices, Resources& resources);
|
void requireNumVoices(int numVoices, Resources& resources);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Is this timestamp within the lo/hitimer range for this region based on current group activity
|
||||||
|
*
|
||||||
|
* @param region
|
||||||
|
* @param timestampSamples in samples
|
||||||
|
* @param sampleRate
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
bool withinValidTimerRange(const Region* region, unsigned timestampSamples, float sampleRate) const noexcept;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int numRequiredVoices_ { config::numVoices };
|
int numRequiredVoices_ { config::numVoices };
|
||||||
std::vector<Voice> list_;
|
std::vector<Voice> list_;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue