diff --git a/src/sfizz/Defaults.cpp b/src/sfizz/Defaults.cpp index 04982371..5df7cb40 100644 --- a/src/sfizz/Defaults.cpp +++ b/src/sfizz/Defaults.cpp @@ -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::attack, {Trigger::attack, Trigger::release_key}, 0}; ESpec crossfadeCurve { CrossfadeCurve::power, {CrossfadeCurve::gain, CrossfadeCurve::power}, 0}; diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index fc4ef546..21868260 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -317,6 +317,8 @@ namespace Default extern const OpcodeSpec filter; extern const OpcodeSpec eq; extern const OpcodeSpec sustainCancelsRelease; + extern const OpcodeSpec loTimer; + extern const OpcodeSpec hiTimer; // Default/max count for objects constexpr int numEQs { 3 }; diff --git a/src/sfizz/MidiState.h b/src/sfizz/MidiState.h index fb187005..76d97841 100644 --- a/src/sfizz/MidiState.h +++ b/src/sfizz/MidiState.h @@ -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 * diff --git a/src/sfizz/PolyphonyGroup.cpp b/src/sfizz/PolyphonyGroup.cpp index 2e8ba56a..ded4fac5 100644 --- a/src/sfizz/PolyphonyGroup.cpp +++ b/src/sfizz/PolyphonyGroup.cpp @@ -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 diff --git a/src/sfizz/PolyphonyGroup.h b/src/sfizz/PolyphonyGroup.h index a3925443..63be170d 100644 --- a/src/sfizz/PolyphonyGroup.h +++ b/src/sfizz/PolyphonyGroup.h @@ -62,9 +62,15 @@ public: * @return std::vector& */ std::vector& 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 voices; + unsigned mostRecentStartStamp_ { 0 }; }; } diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index fe274fe8..8c260e22 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -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 { diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 43165d1e..de7cc37f 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -335,6 +335,8 @@ struct Region { CCMap> crossfadeCCInRange { Default::crossfadeCCInRange }; // xfin_loccN xfin_hiccN CCMap> crossfadeCCOutRange { Default::crossfadeCCOutRange }; // xfout_loccN xfout_hiccN float rtDecay { Default::rtDecay }; // rt_decay + UncheckedRange 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 diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index e1228778..e4b532bd 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -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); } diff --git a/src/sfizz/SynthMessaging.cpp b/src/sfizz/SynthMessaging.cpp index e48ed637..29dae245 100644 --- a/src/sfizz/SynthMessaging.cpp +++ b/src/sfizz/SynthMessaging.cpp @@ -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); diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 0df6d42e..1a0823fa 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -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(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(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_; diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 58774562..3cebdeeb 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -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 diff --git a/src/sfizz/VoiceManager.cpp b/src/sfizz/VoiceManager.cpp index 73e7476b..47dbbefe 100644 --- a/src/sfizz/VoiceManager.cpp +++ b/src/sfizz/VoiceManager.cpp @@ -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(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]; diff --git a/src/sfizz/VoiceManager.h b/src/sfizz/VoiceManager.h index 354976b5..b14e083c 100644 --- a/src/sfizz/VoiceManager.h +++ b/src/sfizz/VoiceManager.h @@ -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 list_;