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:
essej 2023-08-25 18:16:25 -04:00 committed by Paul Ferrand
parent 276ab2ce68
commit 621b4c2415
13 changed files with 86 additions and 6 deletions

View file

@ -199,6 +199,8 @@ FloatSpec lofiDecim { 0.0f, {0.0f, 100.0f}, 0 };
FloatSpec rectify { 0.0f, {0.0f, 100.0f}, 0 };
UInt32Spec stringsNumber { maxStrings, {0, maxStrings}, 0 };
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<CrossfadeCurve> crossfadeCurve { CrossfadeCurve::power, {CrossfadeCurve::gain, CrossfadeCurve::power}, 0};

View file

@ -317,6 +317,8 @@ namespace Default
extern const OpcodeSpec<FilterType> filter;
extern const OpcodeSpec<EqType> eq;
extern const OpcodeSpec<bool> sustainCancelsRelease;
extern const OpcodeSpec<float> loTimer;
extern const OpcodeSpec<float> hiTimer;
// Default/max count for objects
constexpr int numEQs { 3 };

View file

@ -165,6 +165,12 @@ public:
*/
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
*

View file

@ -15,6 +15,7 @@ void sfz::PolyphonyGroup::registerVoice(Voice* voice) noexcept
{
if (absl::c_find(voices, voice) == voices.end())
voices.push_back(voice);
mostRecentStartStamp_ = voice->getStartTimestampSamples();
}
void sfz::PolyphonyGroup::removeVoice(const Voice* voice) noexcept

View file

@ -62,9 +62,15 @@ public:
* @return std::vector<Voice*>&
*/
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:
unsigned polyphonyLimit { config::maxVoices };
std::vector<Voice*> voices;
unsigned mostRecentStartStamp_ { 0 };
};
}

View file

@ -562,6 +562,15 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode, bool cleanOpcode)
groupVolume = opcode.read(Default::volume);
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
case hash("cutoff&"): // also cutoff
{

View file

@ -335,6 +335,8 @@ struct Region {
CCMap<UncheckedRange<float>> crossfadeCCInRange { Default::crossfadeCCInRange }; // xfin_loccN xfin_hiccN
CCMap<UncheckedRange<float>> crossfadeCCOutRange { Default::crossfadeCCOutRange }; // xfout_loccN xfout_hiccN
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 masterAmplitude { 1.0 }; // master_amplitude

View file

@ -1353,6 +1353,7 @@ void Synth::Impl::noteOnDispatch(int delay, int noteNumber, float velocity) noex
{
const auto randValue = randNoteDistribution_(Random::randomGenerator);
SisterVoiceRingBuilder ring;
MidiState& midiState = resources_.getMidiState();
if (!lastKeyswitchLists_[noteNumber].empty()) {
if (currentSwitch_ && *currentSwitch_ != noteNumber) {
@ -1374,6 +1375,9 @@ void Synth::Impl::noteOnDispatch(int delay, int noteNumber, float velocity) noex
for (Layer* layer : noteActivationLists_[noteNumber]) {
if (layer->registerNoteOn(noteNumber, velocity, randValue)) {
const Region& region = layer->getRegion();
if (region.useTimerRange && !voiceManager_.withinValidTimerRange(&region, midiState.getInternalClock() + delay, sampleRate_))
continue;
checkOffGroups(&region, delay, noteNumber);
TriggerEvent triggerEvent { TriggerEventType::NoteOn, noteNumber, velocity };
startVoice(layer, delay, triggerEvent, ring);
@ -1430,6 +1434,7 @@ void Synth::Impl::ccDispatch(int delay, int ccNumber, float value) noexcept
SisterVoiceRingBuilder ring;
TriggerEvent triggerEvent { TriggerEventType::CC, ccNumber, value };
const auto randValue = randNoteDistribution_(Random::randomGenerator);
MidiState& midiState = resources_.getMidiState();
for (Layer* layer : ccActivationLists_[ccNumber]) {
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 (region.useTimerRange && ! voiceManager_.withinValidTimerRange(&region, midiState.getInternalClock() + delay, sampleRate_))
continue;
checkOffGroups(&region, delay, ccNumber);
startVoice(layer, delay, triggerEvent, ring);
}

View file

@ -730,6 +730,14 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co
}
} 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", "") {
GET_REGION_OR_BREAK(indices[0])
client.receive<'f'>(delay, path, region.position * 100.0f);

View file

@ -262,6 +262,7 @@ struct Voice::Impl
int samplesPerBlock_ { config::defaultSamplesPerBlock };
float sampleRate_ { config::defaultSampleRate };
unsigned startTimestamp_ { 0 };
Resources& resources_;
@ -429,14 +430,18 @@ bool Voice::startVoice(Layer* layer, int delay, const TriggerEvent& event) noexc
return false;
}
impl.switchState(State::playing);
impl.updateExtendedCCValues();
ASSERT(delay >= 0);
if (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()) {
WavetablePool& wavePool = resources.getWavePool();
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.triggerDelay_ = delay;
impl.initialDelay_ = delay + static_cast<int>(regionDelay(region, midiState) * impl.sampleRate_);
impl.baseFrequency_ = tuning.getFrequencyOfKey(impl.triggerEvent_.number);
impl.sampleEnd_ = int(sampleEnd(region, midiState));
impl.sampleSize_ = impl.sampleEnd_- impl.sourcePosition_ - 1;
@ -2079,6 +2082,12 @@ int Voice::getSourcePosition() const noexcept
return impl.sourcePosition_;
}
unsigned Voice::getStartTimestampSamples() const noexcept
{
Impl& impl = *impl_;
return impl.startTimestamp_;
}
LFO* Voice::getLFO(size_t index)
{
Impl& impl = *impl_;

View file

@ -437,6 +437,13 @@ public:
*/
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:
/**
* @brief Check if the voice already belongs to a sister ring

View file

@ -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
{
auto& group = polyphonyGroups_[region->group];

View file

@ -139,6 +139,16 @@ struct VoiceManager final : public Voice::StateListener
*/
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:
int numRequiredVoices_ { config::numVoices };
std::vector<Voice> list_;