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 };
|
||||
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};
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(®ion, midiState.getInternalClock() + delay, sampleRate_))
|
||||
continue;
|
||||
|
||||
checkOffGroups(®ion, 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(®ion, midiState.getInternalClock() + delay, sampleRate_))
|
||||
continue;
|
||||
|
||||
checkOffGroups(®ion, delay, ccNumber);
|
||||
startVoice(layer, delay, triggerEvent, ring);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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_;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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_;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue