From eb0a72576c71557c8bb64cfd319620f5ea7ba24c Mon Sep 17 00:00:00 2001
From: TheMarlboroMan <marlborometal@gmail.com>
Date: Sun, 15 Nov 2020 16:50:27 +0100
Subject: Implementation of the voice limiting feature.

---
 src/inputprocessor.cc | 87 +++++++++++++++++++++++++++++++++++++++++++++++++--
 src/inputprocessor.h  |  5 +++
 src/settings.h        | 24 ++++++++++++++
 3 files changed, 113 insertions(+), 3 deletions(-)

(limited to 'src')

diff --git a/src/inputprocessor.cc b/src/inputprocessor.cc
index 2da5dbc..fd6e5b9 100644
--- a/src/inputprocessor.cc
+++ b/src/inputprocessor.cc
@@ -252,7 +252,16 @@ bool InputProcessor::processOnset(event_t& event, std::size_t pos,
 		return false;
 	}
 
-	events_ds.startAddingNewGroup(instrument_id);
+	if(settings.enable_voice_limit.load())
+	{
+		limitVoices(instrument_id,
+		            settings.voice_limit_max.load(),
+		            settings.voice_limit_rampdown.load());
+	}
+
+	//Given that audio files could be invalid, maybe we must add the new
+	//group just before adding the first new sample...
+	bool new_group_added = false;
 	for(Channel& ch: kit.channels)
 	{
 		const auto af = sample->getAudioFile(ch);
@@ -263,8 +272,15 @@ bool InputProcessor::processOnset(event_t& event, std::size_t pos,
 		else
 		{
 			//DEBUG(inputprocessor, "Adding event %d.\n", event.offset);
-			auto& event_sample = events_ds.emplace<SampleEvent>(ch.num, ch.num, 1.0, af,
-				                                                instr->getGroup(), instrument_id);
+			if(!new_group_added)
+			{
+				new_group_added=true;
+				events_ds.startAddingNewGroup(instrument_id);
+			}
+
+			auto& event_sample =
+				events_ds.emplace<SampleEvent>(ch.num, ch.num, 1.0, af,
+				                               instr->getGroup(), instrument_id);
 
 			event_sample.offset = (event.offset + pos) * resample_ratio;
 			if(settings.normalized_samples.load() && sample->getNormalized())
@@ -353,3 +369,68 @@ bool InputProcessor::processStop(event_t& event)
 
 	return true;
 }
+
+void InputProcessor::limitVoices(std::size_t instrument_id,
+                                 std::size_t max_voices,
+                                 float rampdown_time)
+{
+	const auto& group_ids=events_ds.getSampleEventGroupIDsOf(instrument_id);
+
+	if(group_ids.size() <= max_voices)
+	{
+		return;
+	}
+
+	//Filter out ramping events...
+	auto filter_ramping_predicate =
+		[this](EventGroupID group_id) -> bool
+		{
+			const auto& event_ids=events_ds.getEventIDsOf(group_id);
+			//TODO: This should not happen.
+			if(!event_ids.size())
+			{
+				return false;
+			}
+
+			const auto&	sample=events_ds.get<SampleEvent>(event_ids[0]);
+			return !sample.rampdownInProgress();
+		};
+
+	EventGroupIDs non_ramping;
+	std::copy_if(std::begin(group_ids),
+	             std::end(group_ids),
+	             std::back_inserter(non_ramping), filter_ramping_predicate);
+
+	if(!non_ramping.size())
+	{
+		return;
+	}
+
+	//Let us get the eldest...
+	//TODO: where is the playhead? Should we add it to the offset?
+	auto compare_event_offsets =
+		[this](EventGroupID a, EventGroupID b)
+		{
+			const auto& event_ids_a=events_ds.getEventIDsOf(a);
+			const auto& event_ids_b=events_ds.getEventIDsOf(b);
+
+			const auto&	sample_a=events_ds.get<SampleEvent>(event_ids_a[0]);
+			const auto& sample_b=events_ds.get<SampleEvent>(event_ids_b[0]);
+			return sample_a.offset < sample_b.offset;
+		};
+
+	auto it = std::min_element(std::begin(non_ramping),
+	                           std::end(non_ramping),
+	                           compare_event_offsets);
+	if(it == std::end(non_ramping))
+	{
+		return;
+	}
+
+	const auto& event_ids = events_ds.getEventIDsOf(*it);
+	for(const auto& event_id : event_ids)
+	{
+		auto& sample=events_ds.get<SampleEvent>(event_id);
+		applyChoke(settings, sample, rampdown_time, sample.offset);
+	}
+}
diff --git a/src/inputprocessor.h b/src/inputprocessor.h
index 3c2cd5a..971cc85 100644
--- a/src/inputprocessor.h
+++ b/src/inputprocessor.h
@@ -64,6 +64,11 @@ private:
 	bool processChoke(event_t& event, std::size_t pos, double resample_ratio);
 	bool processStop(event_t& event);
 
+	//! Ramps down samples from events_ds is there are more groups playing than
+	//! max_voices for a given instrument.
+	void limitVoices(std::size_t instrument_id, std::size_t max_voices,
+	                 float rampdown_time);
+
 	std::vector<std::unique_ptr<InputFilter>> filters;
 
 	Settings& settings;
diff --git a/src/settings.h b/src/settings.h
index 7749adf..7507827 100644
--- a/src/settings.h
+++ b/src/settings.h
@@ -165,6 +165,15 @@ struct Settings
 
 	// Notify UI about load errors
 	Atomic<std::string> load_status_text;
+
+	// Enables the ramping down of old samples once X groups of the same instrument are playing.
+	Atomic<bool> enable_voice_limit{false};
+	// Max number of voices before old samples are ramped down.
+	static std::size_t constexpr voice_limit_max_default = 15;
+	Atomic<std::size_t> voice_limit_max{voice_limit_max_default};
+	// Time it takes for an old sample to completely fall silent.
+	static float constexpr voice_limit_rampdown_default = 0.5f;
+	Atomic<float> voice_limit_rampdown{voice_limit_rampdown_default};
 };
 
 //! Settings getter class.
@@ -243,6 +252,10 @@ struct SettingsGetter
 
 	SettingRef<std::string> load_status_text;
 
+	SettingRef<bool> enable_voice_limit;
+	SettingRef<std::size_t> voice_limit_max;
+	SettingRef<float> voice_limit_rampdown;
+
 	SettingsGetter(Settings& settings)
 		: drumkit_file(settings.drumkit_file)
 		, drumkit_load_status(settings.drumkit_load_status)
@@ -300,6 +313,9 @@ struct SettingsGetter
 		, audition_instrument{settings.audition_instrument}
 		, audition_velocity{settings.audition_velocity}
 		, load_status_text{settings.load_status_text}
+		, enable_voice_limit{settings.enable_voice_limit}
+		, voice_limit_max{settings.voice_limit_max}
+		, voice_limit_rampdown{settings.voice_limit_rampdown}
 	{
 	}
 };
@@ -379,6 +395,10 @@ public:
 
 	Notifier<std::string> load_status_text;
 
+	Notifier<bool> enable_voice_limit;
+	Notifier<std::size_t> voice_limit_max;
+	Notifier<float> voice_limit_rampdown;
+
 	void evaluate()
 	{
 #define EVAL(x) if(settings.x.hasChanged()) { x(settings.x.getValue()); }
@@ -453,6 +473,10 @@ public:
 		EVAL(audition_velocity);
 
 		EVAL(load_status_text);
+
+		EVAL(enable_voice_limit);
+		EVAL(voice_limit_max);
+		EVAL(voice_limit_rampdown);
 	}
 
 	SettingsNotifier(Settings& settings)
-- 
cgit v1.2.3