Random, Cycle, Synthetic, Hybrid Round Robin
-
I've made a multi-purpose round robin script.
You can select between several types of round robin: real RR (this is for multiple groups), synthetic/sample borrowed RR (this uses the played sample and one either side of it, retuned, to create 3 repetitions), hybrid (combines real and synthetic).
You can also choose the mode: Cycle (provides normal 1, 2, 3, 4 etc. round robin), Random is just random, Random no repeat makes sure that the same RR won't be played twice in a row, Random full cycle means that each RR will only play once, until all of the RRs have been played - the order will still be random and there won't be any repeats.
The RR counters are unique to each note.
You can set a reset timer, after that time (in seconds) has passed the RR counter will be reset, internally there is one timer per note.
You need to set the lowest and highest playable key when using the synthetic RR so it doesn't try to use a sample that doesn't exist beyond that range.
There is a microtuning knob you can use to add a little bit of random variation.
The script usesMessage.setTransposeAmount()
andMessage.setCoarseDetune()
for implementing the synthetic RR so you'll need to use the correspondingget
functions if you apply any pitch/tuning changes in your other scripts in the same chain.You can use this script at the sampler level or at the container level - it will use all samplers in the container, except for samplers that have "Release" in their name. There's an array at the top of the script where you can add other sampler names to exclude or if you want release groups included then just remove it from the array.
Let me know if you find any bugs or have suggestions. The script is released under the GPLv3 license (same as HISE).
<Processor Type="ScriptProcessor" ID="Multi Round Robin2" Bypassed="0" Script="/** * Multi-Round Robin script v2.0.js * Author: David Healey * Date: 07/01/2017 * License: GPLv3 - https://www.gnu.org/licenses/gpl-3.0.en.html */ //Includes Content.setHeight(150); //Init reg excludeSamplers = ["Release"]; //Samples with these words in their ID won't be affected reg samplerNames = Synth.getIdList("Sampler"); //Get the ids of all child samplers reg samplers = []; reg group = Engine.createMidiList(); //For group based RR reg groupValue; //Value taken from the group MIDI list reg offset = Engine.createMidiList(); //For synthetic RR reg offsetValue; //Value taken from the offset MIDI list reg lastGroup = Engine.createMidiList(); reg lastOffset = Engine.createMidiList(); reg groupCounter = Engine.createMidiList(); reg offsetCounter = Engine.createMidiList(); groupCounter.fill(0); offsetCounter.fill(0); reg groupHistory = []; reg offsetHistory = []; reg numGroups; //Script assumes all samplers have the same number of groups reg numOffsets = 3; //Number of adjacent samples used by the synthetic RR (including 0) - currently anything other than 3 won't work as expected reg timer = Engine.createMidiList(); //Get child samplers for (i = 0; i < samplerNames.length; i++) { 	for (j = 0; j < excludeSamplers.length; j++) 	{ 		if (samplerNames[i].indexOf(excludeSamplers[j]) !== -1) continue; //Skip exluded IDs 	} 	samplers.push(Synth.getSampler(samplerNames[i])); //Add sampler to array } for (i = 0; i < samplers.length; i++) { 	samplers[i].enableRoundRobin(false); //Disable default RR behaviour 	samplers[i].refreshRRMap(); } //Create offset history MIDI lists - same is done for groupHistory in on control callback for (i = 0; i < numOffsets; i++) //3 possible offset states for synthetic RR { 	offsetHistory[i] = Engine.createMidiList(); 	offsetHistory[i].fill(-1); } //GUI const var cmbType = Content.addComboBox("Type", 0, 10); cmbType.set("items", ["Off", "Real", "Synthetic", "Hybrid"].join("\n")); const var cmbMode = Content.addComboBox("Mode", 150, 10); cmbMode.set("items", ["Cycle", "Random", "Random No Repeat", "Random Full Cycle"].join("\n")); //Playable Range controls const var knbLowNote = Content.addKnob("Low Note", 300, 0); const var knbHighNote = Content.addKnob("High Note", 450, 0); knbLowNote.setRange(0, 127, 1); knbHighNote.setRange(0, 127, 1); //Reset timeout knob const var knbReset = Content.addKnob("Reset Time", 600, 0); knbReset.setRange(0, 60, 1); //Microtuning knob const var knbMicroTuning = Content.addKnob("Microtuning", 0, 50); knbMicroTuning.setRange(0, 100, 0.1); //Functions //Callbacks function onNoteOn() { 	if (cmbType.getValue() > 1 && Message.getNoteNumber() >= knbLowNote.getValue() && Message.getNoteNumber() <= knbHighNote.getValue()) //RR is not off 	{ 		//Group based RR 		if (cmbType.getValue() == 2 || cmbType.getValue() == 4) //Real or Hybrid RR 		{ 			if (numGroups != 1) 			{ 				switch (cmbMode.getValue()) 				{ 					case 1: //Cycle 						group.setValue(Message.getNoteNumber(), (group.getValue(Message.getNoteNumber()) + 1) % numGroups); 					break; 					case 2: //True random 						group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups)); 					break; 					case 3: //Random no repeat 						group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups)); 						while (group.getValue(Message.getNoteNumber()) == lastGroup.getValue(Message.getNoteNumber())) 						{ 							group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups)); 						} 					break; 					case 4: //Random full cycle 				 						//Reset counter and history if all RRs have been played 						if (groupCounter.getValue(Message.getNoteNumber()) >= numGroups) 						{ 							groupCounter.setValue(Message.getNoteNumber(), 0); //Reset groupCounter for this note 							for (i = 0; i < numGroups; i++) 							{ 								groupHistory[i].setValue(Message.getNoteNumber(), -1); 							} 						} 						//Get random group number that isn't the last group and hasn't been marked as played in groupHistory for this note 						group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups)); 						while (group.getValue(Message.getNoteNumber()) == lastGroup.getValue(Message.getNoteNumber()) || groupHistory[group.getValue(Message.getNoteNumber())].getValue(Message.getNoteNumber()) != -1) 						{ 							group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups)); 						} 					break; 				} 			} 			else 			{ 				group.setValue(Message.getNoteNumber(), 0); 			} 			//Reset group if timer expired 			if (Engine.getUptime() - timer.getValue(Message.getNoteNumber()) > knbReset.getValue()) 			{ 				group.setValue(Message.getNoteNumber(), 0); 			} 			groupValue = group.getValue(Message.getNoteNumber()); //Current group value for this note 			//Set active group for each samper 			for (i = 0; i < samplers.length; i++) 			{ 				samplers[i].setActiveGroup(1+groupValue); 			} 			groupCounter.setValue(Message.getNoteNumber(), groupCounter.getValue(Message.getNoteNumber()) + 1); //Increment counter 			groupHistory[groupValue].setValue(Message.getNoteNumber(), 1); //Mark RR as used 			lastGroup.setValue(Message.getNoteNumber(), groupValue); 		} 		 		//Synthetic sample borrowed RR 		if (cmbType.getValue() == 3 || cmbType.getValue() == 4) //Synthetic or Hybrid RR 		{ 			switch (cmbMode.getValue()) 			{ 				case 1: //Cycle 					offset.setValue(Message.getNoteNumber(), (offset.getValue(Message.getNoteNumber()) + 1) % numOffsets); 				break; 				case 2: //True random 					offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets)); 					while ((offset.getValue(Message.getNoteNumber())-1) + Message.getNoteNumber() > knbHighNote.getValue() && (offset.getValue(Message.getNoteNumber())-1) + Message.getNoteNumber() < knbLowNote.getValue()) 					{ 						offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets)); 					} 				break; 				case 3: //Random no repeat 					offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets)); 					while (offset.getValue(Message.getNoteNumber()) == lastOffset.getValue(Message.getNoteNumber()) || ((offset.getValue(Message.getNoteNumber())-1) + Message.getNoteNumber() > knbHighNote.getValue() && (offset.getValue(Message.getNoteNumber())-1) + Message.getNoteNumber() < knbLowNote.getValue())) 					{ 						offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets)); 					} 				break; 				case 4: //Random full cycle 					 					//Reset counter and history if all RRs have been played 					if (offsetCounter.getValue(Message.getNoteNumber()) >= numOffsets) 					{ 						offsetCounter.setValue(Message.getNoteNumber(), 0); //Reset offsetCounter for this note 						for (i = 0; i < numOffsets; i++) 						{ 							offsetHistory[i].setValue(Message.getNoteNumber(), -1); 						} 					} 					//Get random offset number that isn't the last offset and hasn't been marked as played in offsetHistory for this note 					offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets)); 					while (offset.getValue(Message.getNoteNumber()) == lastOffset.getValue(Message.getNoteNumber()) || offsetHistory[offset.getValue(Message.getNoteNumber())].getValue(Message.getNoteNumber()) != -1) 					{ 						offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets));	 					} 				break; 			} 			//Reset offset if timer expired 			if (Engine.getUptime() - timer.getValue(Message.getNoteNumber()) > knbReset.getValue()) 			{ 				offset.setValue(Message.getNoteNumber(), 0); 			} 			//Make sure offset + note is not outside playable range 			if (offset.getValue(Message.getNoteNumber())-1 + Message.getNoteNumber() < knbLowNote.getValue()) 			{ 				offset.setValue(Message.getNoteNumber(), 1); 			} 			else if (offset.getValue(Message.getNoteNumber())-1 + Message.getNoteNumber() > knbHighNote.getValue()) 			{ 				offset.setValue(Message.getNoteNumber(), 0); 			} 			offsetValue = offset.getValue(Message.getNoteNumber()); 			//Apply offset and coarse tuning to message 			Message.setTransposeAmount(offsetValue-1); //Use -1 to make offset range -1 to +1 			Message.setCoarseDetune(-(offsetValue-1)); //Use coarse detune so setting can be picked up by later scripts 			offsetCounter.setValue(Message.getNoteNumber(), offsetCounter.getValue(Message.getNoteNumber()) + 1); //Increment counter 			offsetHistory[offsetValue].setValue(Message.getNoteNumber(), 1); //Mark RR as used 			lastOffset.setValue(Message.getNoteNumber(), offsetValue); 		} 		//Apply random microtuning, if any 		if (knbMicroTuning.getValue() != 0) 		{ 			Message.setFineDetune(Math.random() * (knbMicroTuning.getValue() - -knbMicroTuning.getValue() + 1) + -knbMicroTuning.getValue()); 		} 		timer.setValue(Message.getNoteNumber(), Engine.getUptime()); 	} } function onNoteOff() { 	 } function onController() { 	 } function onTimer() { 	 } function onControl(number, value) { 	switch (number) 	{ 		case knbHighNote: 			 			//Get number of groups - assumes all samplers have the same number of groups and samples are mapped over velocity 64 			numGroups = samplers[0].getRRGroupsForMessage(value, 64); 			groupHistory = []; 			//Create group history MIDI lists 			for (i = 0; i < numGroups; i++) //One MIDI list per group 			{ 				groupHistory[i] = Engine.createMidiList(); 				groupHistory[i].fill(-1); 			} 		break; 	} }"> <EditorStates BodyShown="1" Visible="1" Solo="0" contentShown="1" onInitOpen="0" onNoteOnOpen="1" onNoteOffOpen="0" onControllerOpen="0" onTimerOpen="0" onControlOpen="0" Folded="0"/> <ChildProcessors/> <Content> <Control type="ScriptComboBox" id="Type" value="1"/> <Control type="ScriptComboBox" id="Mode" value="1"/> <Control type="ScriptSlider" id="Low Note" value="0"/> <Control type="ScriptSlider" id="High Note" value="127"/> <Control type="ScriptSlider" id="Reset Time" value="5"/> <Control type="ScriptSlider" id="Microtuning" value="0"/> </Content> </Processor>
-
Nice!
I took a quick look at the script code and it appears to be pretty well done. I have only a few remarks:
Use
const var
instead ofreg
for MidiListsYou actually need
reg
only for temporary variables that hold numbers / strings. If you store a object like the MIDI list (or every UI widget) in a const var, the interpreter can speed things up, because it doesn't have to look up twice (usually it first resolves the variable name and then the function name, but when it's aconst var
it knows it won't change during execution and resolves the function call at compile time:reg i = 0; reg slowMidi = Engine.createMidiList(); const var fastMidi = Engine.createMidiList(); Console.start(); while(++i < 200000) slowMidi.setValue(5, 2.0); Console.stop(); i = 0; Console.start(); while(++i < 200000) fastMidi.setValue(5, 2.0); Console.stop();
The second version is about 40% faster. You may know this already, but the
const
word is a bit misleading, it is not a read-only storage type (so you can change it after creation). You can even declare an array asconst var
and insert elements into it. You just can't reassign it to something else.Script Error in the
onControl
callbackIf there is no sampler, the script throws an error in the
onControl
callback (because the array is empty). If you change this linenumGroups = samplers[0].getRRGroupsForMessage(value, 64);
to
numGroups = samplers.length != 0 ? samplers[0].getRRGroupsForMessage(value, 64) : 0;
it handles this case more gracefully.
Loops in the
onNote
callbackYou are creating a non-local variable in the
onNoteOn
callback every time you run thefor
loop. Just putlocal i = 0;
at the top of the callback and it will reuse this variable without allocation. Also, use the
for ... in
loop wherever you can (it's faster and more clear). You only need the other loop when you need access to the current index (i
).//Set active group for each samper for (i = 0; i < samplers.length; i++) { samplers[i].setActiveGroup(1+groupValue); } // better: for (sampler in samplers) sampler.setActiveGroup(1+groupValue);
Enumerations vs. Magic numbers
This is more a general advice and won't affect neither the performance nor the functionality, but you might want to change the "Magic numbers" in the switch statement with enumeration variables (this way you get self-documenting code):
namespace RRModes { const var CYCLE = 1; const var TRUE_RANDOM = 2; const var RANDOM_NO_REPEAT = 3; const var RANDOM_FULL_CYCLE = 4; };
and
switch (cmbMode.getValue()) { case RRModes.CYCLE: group.setValue(Message.getNoteNumber(), (group.getValue(Message.getNoteNumber()) + 1) % numGroups); break; case RRModes.TRUE_RANDOM: // ...
-
Thanks for the tips, especially the efficiency related ones, I need to go through some of my other scripts and incorporate these ideas too :) I didn't know that about
const
or thatfor in
could be used in that context, I shall definitely make good use of this.EDIT: I've implemented those suggestions now, here is the updated version:
<Processor Type="ScriptProcessor" ID="Multi Round Robin" Bypassed="0" Script="/** * Multi-Round Robin script v2.0.js * Author: David Healey * Date: 07/01/2017 * License: GPLv3 - https://www.gnu.org/licenses/gpl-3.0.en.html */ //Includes Content.setHeight(100); //Init reg excludeSamplers = ["Release"]; //Samples with these words in their ID won't be affected const var samplerNames = Synth.getIdList("Sampler"); //Get the ids of all child samplers reg samplers = []; const var group = Engine.createMidiList(); //For group based RR reg groupValue; //Value taken from the group MIDI list const var offset = Engine.createMidiList(); //For synthetic RR reg offsetValue; //Value taken from the offset MIDI list const var lastGroup = Engine.createMidiList(); const var lastOffset = Engine.createMidiList(); const var groupCounter = Engine.createMidiList(); const var offsetCounter = Engine.createMidiList(); groupCounter.fill(0); offsetCounter.fill(0); reg groupHistory = []; reg offsetHistory = []; reg numGroups; //Script assumes all samplers have the same number of groups reg numOffsets = 3; //Number of adjacent samples used by the synthetic RR (including 0) - currently anything other than 3 won't work as expected const var timer = Engine.createMidiList(); namespace RRModes { 	const var CYCLE = 1; 	const var TRUE_RANDOM = 2; 	const var RANDOM_NO_REPEAT = 3; 	const var RANDOM_FULL_CYCLE = 4; }; //Get child samplers for (i = 0; i < samplerNames.length; i++) { 	for (j = 0; j < excludeSamplers.length; j++) 	{ 		if (samplerNames[i].indexOf(excludeSamplers[j]) !== -1) continue; //Skip exluded IDs 	} 	samplers.push(Synth.getSampler(samplerNames[i])); //Add sampler to array } for (sampler in samplers) { 	sampler.enableRoundRobin(false); //Disable default RR behaviour 	sampler.refreshRRMap(); } //Create offset history MIDI lists - same is done for groupHistory in on control callback for (i = 0; i < numOffsets; i++) //3 possible offset states for synthetic RR { 	offsetHistory[i] = Engine.createMidiList(); 	offsetHistory[i].fill(-1); } //GUI //RR Type and Mode Combo Boxes const var cmbType = Content.addComboBox("Type", 0, 10); cmbType.set("items", ["Off", "Real", "Synthetic", "Hybrid"].join("\n")); const var cmbMode = Content.addComboBox("Mode", 150, 10); cmbMode.set("items", ["Cycle", "Random", "Random No Repeat", "Random Full Cycle"].join("\n")); //Playable Range controls const var knbLowNote = Content.addKnob("Low Note", 300, 0); const var knbHighNote = Content.addKnob("High Note", 450, 0); knbLowNote.setRange(0, 127, 1); knbHighNote.setRange(0, 127, 1); //Reset timeout knob const var knbReset = Content.addKnob("Reset Time", 600, 0); knbReset.setRange(0, 60, 1); //Microtuning knob const var knbMicroTuning = Content.addKnob("Microtuning", 0, 50); knbMicroTuning.setRange(0, 100, 0.1); //Functions //Callbacks function onNoteOn() { 	local i; 	if (cmbType.getValue() > 1 && Message.getNoteNumber() >= knbLowNote.getValue() && Message.getNoteNumber() <= knbHighNote.getValue()) //RR is not off 	{ 		//Group based RR 		if (cmbType.getValue() == 2 || cmbType.getValue() == 4) //Real or Hybrid RR 		{ 			if (numGroups != 1) 			{ 				switch (cmbMode.getValue()) 				{ 					case RRModes.CYCLE: 						group.setValue(Message.getNoteNumber(), (group.getValue(Message.getNoteNumber()) + 1) % numGroups); 					break; 					case RRModes.TRUE_RANDOM: 						group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups)); 					break; 					case RRModes.RANDOM_NO_REPEAT: 						group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups)); 						while (group.getValue(Message.getNoteNumber()) == lastGroup.getValue(Message.getNoteNumber())) 						{ 							group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups)); 						} 					break; 					case RRModes.RANDOM_FULL_CYCLE: 				 						//Reset counter and history if all RRs have been played 						if (groupCounter.getValue(Message.getNoteNumber()) >= numGroups) 						{ 							groupCounter.setValue(Message.getNoteNumber(), 0); //Reset groupCounter for this note 							for (i = 0; i < numGroups; i++) 							{ 								groupHistory[i].setValue(Message.getNoteNumber(), -1); 							} 						} 						//Get random group number that isn't the last group and hasn't been marked as played in groupHistory for this note 						group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups)); 						while (group.getValue(Message.getNoteNumber()) == lastGroup.getValue(Message.getNoteNumber()) || groupHistory[group.getValue(Message.getNoteNumber())].getValue(Message.getNoteNumber()) != -1) 						{ 							group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups)); 						} 					break; 				} 			} 			else 			{ 				group.setValue(Message.getNoteNumber(), 0); 			} 			//Reset group if timer expired 			if (Engine.getUptime() - timer.getValue(Message.getNoteNumber()) > knbReset.getValue()) 			{ 				group.setValue(Message.getNoteNumber(), 0); 			} 			groupValue = group.getValue(Message.getNoteNumber()); //Current group value for this note 			//Set active group for each samper 			for (sampler in samplers) 			{ 				sampler.setActiveGroup(1+groupValue); 			} 			groupCounter.setValue(Message.getNoteNumber(), groupCounter.getValue(Message.getNoteNumber()) + 1); //Increment counter 			groupHistory[groupValue].setValue(Message.getNoteNumber(), 1); //Mark RR as used 			lastGroup.setValue(Message.getNoteNumber(), groupValue); 		} 		 		//Synthetic sample borrowed RR 		if (cmbType.getValue() == 3 || cmbType.getValue() == 4) //Synthetic or Hybrid RR 		{ 			switch (cmbMode.getValue()) 			{ 				case RRModes.CYCLE: 					offset.setValue(Message.getNoteNumber(), (offset.getValue(Message.getNoteNumber()) + 1) % numOffsets); 				break; 				case RRModes.TRUE_RANDOM: 					offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets)); 					while ((offset.getValue(Message.getNoteNumber())-1) + Message.getNoteNumber() > knbHighNote.getValue() && (offset.getValue(Message.getNoteNumber())-1) + Message.getNoteNumber() < knbLowNote.getValue()) 					{ 						offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets)); 					} 				break; 				case RRModes.RANDOM_NO_REPEAT: 					offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets)); 					while (offset.getValue(Message.getNoteNumber()) == lastOffset.getValue(Message.getNoteNumber()) || ((offset.getValue(Message.getNoteNumber())-1) + Message.getNoteNumber() > knbHighNote.getValue() && (offset.getValue(Message.getNoteNumber())-1) + Message.getNoteNumber() < knbLowNote.getValue())) 					{ 						offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets)); 					} 				break; 				case RRModes.RANDOM_FULL_CYCLE: 					 					//Reset counter and history if all RRs have been played 					if (offsetCounter.getValue(Message.getNoteNumber()) >= numOffsets) 					{ 						offsetCounter.setValue(Message.getNoteNumber(), 0); //Reset offsetCounter for this note 						for (i = 0; i < numOffsets; i++) 						{ 							offsetHistory[i].setValue(Message.getNoteNumber(), -1); 						} 					} 					//Get random offset number that isn't the last offset and hasn't been marked as played in offsetHistory for this note 					offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets)); 					while (offset.getValue(Message.getNoteNumber()) == lastOffset.getValue(Message.getNoteNumber()) || offsetHistory[offset.getValue(Message.getNoteNumber())].getValue(Message.getNoteNumber()) != -1) 					{ 						offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets));	 					} 				break; 			} 			//Reset offset if timer expired 			if (Engine.getUptime() - timer.getValue(Message.getNoteNumber()) > knbReset.getValue()) 			{ 				offset.setValue(Message.getNoteNumber(), 0); 			} 			//Make sure offset + note is not outside playable range 			if (offset.getValue(Message.getNoteNumber())-1 + Message.getNoteNumber() < knbLowNote.getValue()) 			{ 				offset.setValue(Message.getNoteNumber(), 1); 			} 			else if (offset.getValue(Message.getNoteNumber())-1 + Message.getNoteNumber() > knbHighNote.getValue()) 			{ 				offset.setValue(Message.getNoteNumber(), 0); 			} 			offsetValue = offset.getValue(Message.getNoteNumber()); 			//Apply offset and coarse tuning to message 			Message.setTransposeAmount(offsetValue-1); //Use -1 to make offset range -1 to +1 			Message.setCoarseDetune(-(offsetValue-1)); //Use coarse detune so setting can be picked up by later scripts 			offsetCounter.setValue(Message.getNoteNumber(), offsetCounter.getValue(Message.getNoteNumber()) + 1); //Increment counter 			offsetHistory[offsetValue].setValue(Message.getNoteNumber(), 1); //Mark RR as used 			lastOffset.setValue(Message.getNoteNumber(), offsetValue); 		} 		//Apply random microtuning, if any 		if (knbMicroTuning.getValue() != 0) 		{ 			Message.setFineDetune(Math.random() * (knbMicroTuning.getValue() - -knbMicroTuning.getValue() + 1) + -knbMicroTuning.getValue()); 		} 		timer.setValue(Message.getNoteNumber(), Engine.getUptime()); 	} } function onNoteOff() { 	 } function onController() { 	 } function onTimer() { 	 } function onControl(number, value) { 	switch (number) 	{ 		case knbHighNote: 			 			//Get number of groups - assumes all samplers have the same number of groups and samples are mapped over velocity 64 			numGroups = samplers.length != 0 ? samplers[0].getRRGroupsForMessage(value, 64) : 0; 			groupHistory = []; 			//Create group history MIDI lists 			for (i = 0; i < numGroups; i++) //One MIDI list per group 			{ 				groupHistory[i] = Engine.createMidiList(); 				groupHistory[i].fill(-1); 			} 		break; 	} }"> <EditorStates BodyShown="1" Visible="1" Solo="0" contentShown="1" onInitOpen="0" onNoteOnOpen="0" onNoteOffOpen="0" onControllerOpen="0" onTimerOpen="0" onControlOpen="0" Folded="0"/> <ChildProcessors/> <Content> <Control type="ScriptComboBox" id="Type" value="1"/> <Control type="ScriptComboBox" id="Mode" value="3"/> <Control type="ScriptSlider" id="Low Note" value="60"/> <Control type="ScriptSlider" id="High Note" value="96"/> <Control type="ScriptSlider" id="Reset Time" value="5"/> <Control type="ScriptSlider" id="Microtuning" value="0"/> </Content> </Processor>```
-
Small Update: Couple of bug fixes and improved readability.
<?xml version="1.0" encoding="UTF-8"?> <Processor Type="ScriptProcessor" ID="Multi Round Robin" Bypassed="0" Script="/** * Multi-Round Robin script v2.2 * Author: David Healey * Date: 07/01/2017 * Modified: 09/01/2017 * License: GPLv3 - https://www.gnu.org/licenses/gpl-3.0.en.html */ //Includes Content.setHeight(100); //Init reg excludeSamplers = ["Release"]; //Samples with these words in their ID won't be affected const var samplerNames = Synth.getIdList("Sampler"); //Get the ids of all child samplers reg samplers = []; reg group; //Group RR counter reg offset; //Synthetic RR counter const var lastGroup = Engine.createMidiList(); const var lastOffset = Engine.createMidiList(); const var groupCounter = Engine.createMidiList(); const var offsetCounter = Engine.createMidiList(); groupCounter.fill(0); offsetCounter.fill(0); reg groupHistory = []; reg offsetHistory = []; reg numGroups; //Script assumes all samplers have the same number of groups reg numOffsets = 3; //Number of adjacent samples used by the synthetic RR (including 0) - currently anything other than 3 won't work as expected reg lowNote; reg highNote; reg noteNumber; const var timer = Engine.createMidiList(); namespace RRModes { 	const var CYCLE = 1; 	const var TRUE_RANDOM = 2; 	const var RANDOM_NO_REPEAT = 3; 	const var RANDOM_FULL_CYCLE = 4; }; //Get child samplers for (i = 0; i < samplerNames.length; i++) { 	for (j = 0; j < excludeSamplers.length; j++) 	{ 		if (samplerNames[i].indexOf(excludeSamplers[j]) !== -1) continue; //Skip exluded IDs 	} 	samplers.push(Synth.getSampler(samplerNames[i])); //Add sampler to array } for (sampler in samplers) { 	sampler.enableRoundRobin(false); //Disable default RR behaviour 	sampler.refreshRRMap(); } //Create offset history MIDI lists - same is done for groupHistory in on control callback for (i = 0; i < numOffsets; i++) //3 possible offset states for synthetic RR { 	offsetHistory[i] = Engine.createMidiList(); 	offsetHistory[i].fill(-1); } //GUI //RR Type and Mode Combo Boxes const var cmbType = Content.addComboBox("Type", 0, 10); cmbType.set("items", ["Off", "Real", "Synthetic", "Hybrid"].join("\n")); const var cmbMode = Content.addComboBox("Mode", 150, 10); cmbMode.set("items", ["Cycle", "Random", "Random No Repeat", "Random Full Cycle"].join("\n")); //Playable Range controls const var knbLowNote = Content.addKnob("Low Note", 300, 0); const var knbHighNote = Content.addKnob("High Note", 450, 0); knbLowNote.setRange(0, 127, 1); knbHighNote.setRange(0, 127, 1); //Reset timeout knob const var knbReset = Content.addKnob("Reset Time", 600, 0); knbReset.setRange(0, 60, 1); //Microtuning knob const var knbMicroTuning = Content.addKnob("Microtuning", 0, 50); knbMicroTuning.setRange(0, 100, 0.1); //Functions //Callbacks function onNoteOn() { 	local i; 	noteNumber = Message.getNoteNumber(); 	if (cmbType.getValue() > 1 && noteNumber >= lowNote && noteNumber <= highNote) //RR is not off 	{ 		//Group based RR 		if (cmbType.getValue() == 2 || cmbType.getValue() == 4) //Real or Hybrid RR 		{ 			if (numGroups != 1) 			{ 				switch (cmbMode.getValue()) 				{ 					case RRModes.CYCLE: 						group = (lastGroup.getValue(Message.getNoteNumber()) + 1) % numGroups; 					break; 					case RRModes.TRUE_RANDOM: 						group = Math.floor(Math.random() * numGroups); 					break; 					case RRModes.RANDOM_NO_REPEAT: 						group = -1; 						while (group == -1 || group == lastGroup.getValue(noteNumber)) 						{ 							group = Math.floor(Math.random() * numGroups); 						} 						 					break; 					case RRModes.RANDOM_FULL_CYCLE: 				 						//Reset counter and history if all RRs have been played 						if (groupCounter.getValue(noteNumber) >= numGroups) 						{ 							groupCounter.setValue(noteNumber, 0); //Reset groupCounter for this note 							for (gHistory in groupHistory) 							{ 								gHistory.setValue(noteNumber, -1); 							} 						} 						//Get random group number that isn't the last group and hasn't been marked as played in groupHistory for this note 						group = -1; 						while (group == -1 || group == lastGroup.getValue(noteNumber) || groupHistory[group].getValue(noteNumber) != -1) 						{ 							group = Math.floor(Math.random() * numGroups); 						} 					break; 				} 			} 			else 			{ 				group = 0; 			} 			//Reset group if timer expired 			if (Engine.getUptime() - timer.getValue(noteNumber) > knbReset.getValue()) 			{ 				group = 0; 			} 			 			//Set active group for each samper 			for (sampler in samplers) 			{ 				sampler.setActiveGroup(1+group); 			} 			groupCounter.setValue(noteNumber, groupCounter.getValue(noteNumber) + 1); //Increment counter 			groupHistory[group].setValue(noteNumber, 1); //Mark RR as used 			lastGroup.setValue(noteNumber, group); 		} 		 		//Synthetic sample borrowed RR 		if (cmbType.getValue() == 3 || cmbType.getValue() == 4) //Synthetic or Hybrid RR 		{ 			offset = -1; 			switch (cmbMode.getValue()) 			{ 				case RRModes.CYCLE: 					offset = (lastOffset.getValue(noteNumber) + 1) % numOffsets; 				break; 				case RRModes.TRUE_RANDOM: 					offset = Math.floor(Math.random() * numOffsets); 				break; 				case RRModes.RANDOM_NO_REPEAT: 					while (offset == -1 || offset == lastOffset.getValue(noteNumber)) 					{ 						offset = Math.floor(Math.random() * numOffsets); 					} 				break; 				case RRModes.RANDOM_FULL_CYCLE: 					 					//Reset counter and history if all RRs have been played 					if (offsetCounter.getValue(noteNumber) >= numOffsets) 					{ 						offsetCounter.setValue(noteNumber, 0); //Reset offsetCounter for this note 						for (oHistory in offsetHistory) 						{ 							oHistory.setValue(noteNumber, -1); 						} 					} 					//Get random offset number that isn't the last offset and hasn't been marked as played in offsetHistory for this note 					while (offset == -1 || offset == lastOffset.getValue(noteNumber) || offsetHistory[offset].getValue(noteNumber) != -1) 					{ 						offset = Math.floor(Math.random() * numOffsets); 					} 				break; 			} 			//Reset offset if timer expired 			if (Engine.getUptime() - timer.getValue(noteNumber) > knbReset.getValue()) 			{ 				offset = 0; 			} 			 			if (noteNumber + offset - 1 < lowNote) offset = 1; 			if (noteNumber + offset - 1 > highNote) offset = 0; 			//Apply offset and coarse tuning to message 			Message.setTransposeAmount(offset-1); //Use -1 to make offset range -1 to +1 			Message.setCoarseDetune(-(offset-1)); //Use coarse detune so setting can be picked up by later scripts 			offsetCounter.setValue(noteNumber, offsetCounter.getValue(noteNumber) + 1); //Increment counter 			offsetHistory[offset].setValue(noteNumber, 1); //Mark RR as used 			lastOffset.setValue(noteNumber, offset); 		} 		//Apply random microtuning, if any 		if (knbMicroTuning.getValue() != 0) 		{ 			Message.setFineDetune(Math.random() * (knbMicroTuning.getValue() - -knbMicroTuning.getValue() + 1) + -knbMicroTuning.getValue()); 		} 		timer.setValue(noteNumber, Engine.getUptime()); 	} } function onNoteOff() { 	 } function onController() { 	 } function onTimer() { 	 } function onControl(number, value) { 	switch (number) 	{ 		case knbLowNote: 			lowNote = value; 		break; 		case knbHighNote: 			 			highNote = value; 			//Get number of groups - assumes all samplers have the same number of groups and samples are mapped over velocity 64 			numGroups = samplers.length != 0 ? samplers[0].getRRGroupsForMessage(value, 64) : 0; 			groupHistory = []; 			//Create group history MIDI lists 			for (i = 0; i < numGroups; i++) //One MIDI list per group 			{ 				groupHistory[i] = Engine.createMidiList(); 				groupHistory[i].fill(-1); 			} 		break; 	} }"> <EditorStates BodyShown="1" Visible="1" Solo="0" contentShown="1" onInitOpen="0" onNoteOnOpen="1" onNoteOffOpen="0" onControllerOpen="0" onTimerOpen="0" onControlOpen="0" Folded="0"/> <ChildProcessors/> <Content> <Control type="ScriptComboBox" id="Type" value="2"/> <Control type="ScriptComboBox" id="Mode" value="1"/> <Control type="ScriptSlider" id="Low Note" value="60"/> <Control type="ScriptSlider" id="High Note" value="96"/> <Control type="ScriptSlider" id="Reset Time" value="5"/> <Control type="ScriptSlider" id="Microtuning" value="0"/> </Content> </Processor>
-
Version 2.3: Some code improvements. The exclude sampler part now uses a regex string instead of indexOf() and an array of strings.
<?xml version="1.0" encoding="UTF-8"?> <Processor Type="ScriptProcessor" ID="Multi Round Robin" Bypassed="0" Script="/** * Multi-Round Robin script v2.3 * Author: David Healey * Date: 07/01/2017 * Modified: 13/01/2017 * License: GPLv3 - https://www.gnu.org/licenses/gpl-3.0.en.html */ //Includes Content.setHeight(100); //Init const var excludedIds = "(elease)"; //Regex string of words in sampler IDs to exclude RR from const var samplerNames = Synth.getIdList("Sampler"); //Get the ids of all child samplers const var samplers = []; reg group; //Group RR counter reg offset; //Synthetic RR counter const var lastGroup = Engine.createMidiList(); const var lastOffset = Engine.createMidiList(); const var groupCounter = Engine.createMidiList(); const var offsetCounter = Engine.createMidiList(); groupCounter.fill(0); offsetCounter.fill(0); const var groupHistory = []; const var offsetHistory = []; reg numGroups; //Script assumes all samplers have the same number of groups reg numOffsets = 3; //Number of adjacent samples used by the synthetic RR (including 0) - currently anything other than 3 won't work as expected reg lowNote; reg highNote; reg noteNumber; const var timer = Engine.createMidiList(); namespace RRModes { 	const var CYCLE = 1; 	const var TRUE_RANDOM = 2; 	const var RANDOM_NO_REPEAT = 3; 	const var RANDOM_FULL_CYCLE = 4; }; //Get child samplers for (samplerName in samplerNames) { 	if (Engine.matchesRegex(samplerName, excludedIds) == true) continue; //Skip excluded IDs 	samplers.push(Synth.getSampler(samplerName)); //Add sampler to array 	samplers[samplers.length-1].enableRoundRobin(false); //Disable default RR behaviour 	samplers[samplers.length-1].refreshRRMap(); } //Create offset history MIDI lists - same is done for groupHistory in on control callback for (i = 0; i < numOffsets; i++) //3 possible offset states for synthetic RR { 	offsetHistory[i] = Engine.createMidiList(); 	offsetHistory[i].fill(-1); } //GUI //RR Type and Mode Combo Boxes const var cmbType = Content.addComboBox("Type", 0, 10); cmbType.set("items", ["Off", "Real", "Synthetic", "Hybrid"].join("\n")); const var cmbMode = Content.addComboBox("Mode", 150, 10); cmbMode.set("items", ["Cycle", "Random", "Random No Repeat", "Random Full Cycle"].join("\n")); //Playable Range controls const var knbLowNote = Content.addKnob("Low Note", 300, 0); const var knbHighNote = Content.addKnob("High Note", 450, 0); knbLowNote.setRange(0, 127, 1); knbHighNote.setRange(0, 127, 1); //Reset timeout knob const var knbReset = Content.addKnob("Reset Time", 600, 0); knbReset.setRange(0, 60, 1); //Microtuning knob const var knbMicroTuning = Content.addKnob("Microtuning", 0, 50); knbMicroTuning.setRange(0, 100, 0.1); const var postInit = Content.addButton("postInit", 0, 0); //Hidden button to trigger last control callback on init postInit.set("visible", false); //Functions //Callbacks function onNoteOn() { 	local i; 	noteNumber = Message.getNoteNumber(); 	if (cmbType.getValue() > 1 && noteNumber >= lowNote && noteNumber <= highNote) //RR is not off 	{ 		//Group based RR 		if (cmbType.getValue() == 2 || cmbType.getValue() == 4) //Real or Hybrid RR 		{ 			if (numGroups != 1) 			{ 				switch (cmbMode.getValue()) 				{ 					case RRModes.CYCLE: 						group = (lastGroup.getValue(Message.getNoteNumber()) + 1) % numGroups; 					break; 					case RRModes.TRUE_RANDOM: 						group = Math.floor(Math.random() * numGroups); 					break; 					case RRModes.RANDOM_NO_REPEAT: 						group = -1; 						while (group == -1 || group == lastGroup.getValue(noteNumber)) 						{ 							group = Math.floor(Math.random() * numGroups); 						} 						 					break; 					case RRModes.RANDOM_FULL_CYCLE: 				 						//Reset counter and history if all RRs have been played 						if (groupCounter.getValue(noteNumber) >= numGroups) 						{ 							groupCounter.setValue(noteNumber, 0); //Reset groupCounter for this note 							for (gHistory in groupHistory) 							{ 								gHistory.setValue(noteNumber, -1); 							} 						} 						//Get random group number that isn't the last group and hasn't been marked as played in groupHistory for this note 						group = -1; 						while (group == -1 || group == lastGroup.getValue(noteNumber) || groupHistory[group].getValue(noteNumber) != -1) 						{ 							group = Math.floor(Math.random() * numGroups); 						} 					break; 				} 			} 			else 			{ 				group = 0; 			} 			//Reset group if timer expired 			if (Engine.getUptime() - timer.getValue(noteNumber) > knbReset.getValue()) 			{ 				group = 0; 			} 			 			//Set active group for each samper 			for (sampler in samplers) 			{ 				sampler.setActiveGroup(1+group); 			} 			groupCounter.setValue(noteNumber, groupCounter.getValue(noteNumber) + 1); //Increment counter 			groupHistory[group].setValue(noteNumber, 1); //Mark RR as used 			lastGroup.setValue(noteNumber, group); 		} 		 		//Synthetic sample borrowed RR 		if (cmbType.getValue() == 3 || cmbType.getValue() == 4) //Synthetic or Hybrid RR 		{ 			offset = -1; 			switch (cmbMode.getValue()) 			{ 				case RRModes.CYCLE: 					offset = (lastOffset.getValue(noteNumber) + 1) % numOffsets; 				break; 				case RRModes.TRUE_RANDOM: 					offset = Math.floor(Math.random() * numOffsets); 				break; 				case RRModes.RANDOM_NO_REPEAT: 					while (offset == -1 || offset == lastOffset.getValue(noteNumber)) 					{ 						offset = Math.floor(Math.random() * numOffsets); 					} 				break; 				case RRModes.RANDOM_FULL_CYCLE: 					 					//Reset counter and history if all RRs have been played 					if (offsetCounter.getValue(noteNumber) >= numOffsets) 					{ 						offsetCounter.setValue(noteNumber, 0); //Reset offsetCounter for this note 						for (oHistory in offsetHistory) 						{ 							oHistory.setValue(noteNumber, -1); 						} 					} 					//Get random offset number that isn't the last offset and hasn't been marked as played in offsetHistory for this note 					while (offset == -1 || offset == lastOffset.getValue(noteNumber) || offsetHistory[offset].getValue(noteNumber) != -1) 					{ 						offset = Math.floor(Math.random() * numOffsets); 					} 				break; 			} 			//Reset offset if timer expired 			if (Engine.getUptime() - timer.getValue(noteNumber) > knbReset.getValue()) 			{ 				offset = 0; 			} 			 			if (noteNumber + offset - 1 < lowNote) offset = 1; 			if (noteNumber + offset - 1 > highNote) offset = 0; 			//Apply offset and coarse tuning to message 			Message.setTransposeAmount(offset-1); //Use -1 to make offset range -1 to +1 			Message.setCoarseDetune(-(offset-1)); //Use coarse detune so setting can be picked up by later scripts 			offsetCounter.setValue(noteNumber, offsetCounter.getValue(noteNumber) + 1); //Increment counter 			offsetHistory[offset].setValue(noteNumber, 1); //Mark RR as used 			lastOffset.setValue(noteNumber, offset); 		} 		//Apply random microtuning, if any 		if (knbMicroTuning.getValue() != 0) 		{ 			Message.setFineDetune(Math.random() * (knbMicroTuning.getValue() - -knbMicroTuning.getValue() + 1) + -knbMicroTuning.getValue()); 		} 		timer.setValue(noteNumber, Engine.getUptime()); 	} } function onNoteOff() { 	 } function onController() { 	 } function onTimer() { 	 } function onControl(number, value) { 	switch (number) 	{ 		case knbLowNote: 			lowNote = value; 		break; 		case knbHighNote: 			highNote = value; 		break; 		case postInit: 			//Get number of groups - assumes all samplers have the same number of groups and samples are mapped over velocity 64 			numGroups = samplers.length != 0 ? samplers[0].getRRGroupsForMessage(lowNote + 1, 64) : 0; 			//Create group history MIDI lists 			for (i = 0; i < numGroups; i++) //One MIDI list per group 			{ 				groupHistory[i] = Engine.createMidiList(); 				groupHistory[i].fill(-1); 			} 		break; 	} }"> <EditorStates BodyShown="1" Visible="1" Solo="0" contentShown="1" onInitOpen="0" onNoteOnOpen="0" onNoteOffOpen="0" onControllerOpen="0" onTimerOpen="0" onControlOpen="0" Folded="1"/> <ChildProcessors/> <Content> <Control type="ScriptComboBox" id="Type" value="2"/> <Control type="ScriptComboBox" id="Mode" value="3"/> <Control type="ScriptSlider" id="Low Note" value="60"/> <Control type="ScriptSlider" id="High Note" value="96"/> <Control type="ScriptSlider" id="Reset Time" value="5"/> <Control type="ScriptSlider" id="Microtuning" value="0"/> <Control type="ScriptButton" id="postInit" value="0"/> </Content> </Processor>
-
The
postInit
-thingie looks like a code smell to me. Why do you need it to happen after theonInit
callback? -
Because it needs the value from
lowNote
and I believe (possibly mistakenly) that the value of the UI knobs isn't recalled until afteronInit
has completed. - now that's you've pointed it out I think I'm mixing it up with Kontakt's way of doing things. -
Version 2.4: I've added a knob for specifying the number of groups since I was having some issues doing it the other way.
/** * Multi-Round Robin script v2.4 * Author: David Healey * Date: 07/01/2017 * Modified: 02/02/2017 * License: GPLv3 - https://www.gnu.org/licenses/gpl-3.0.en.html */ Content.setHeight(100); //Init const var excludedIds = "(elease)"; //Regex string of words in sampler IDs to exclude RR from const var samplerNames = Synth.getIdList("Sampler"); //Get the ids of all child samplers const var samplers = []; reg group; //Group RR counter reg offset; //Synthetic RR counter reg numOffsets = 3; //Number of adjacent samples used by the synthetic RR (including 0) - currently anything other than 3 won't work as expected const var lastGroup = Engine.createMidiList(); const var lastOffset = Engine.createMidiList(); const var groupCounter = Engine.createMidiList(); const var offsetCounter = Engine.createMidiList(); groupCounter.fill(0); offsetCounter.fill(0); var groupHistory = []; const var offsetHistory = []; reg lowNote; reg highNote; reg noteNumber; const var timer = Engine.createMidiList(); namespace RRModes { const var CYCLE = 1; const var TRUE_RANDOM = 2; const var RANDOM_NO_REPEAT = 3; const var RANDOM_FULL_CYCLE = 4; }; //Get child samplers for (samplerName in samplerNames) { if (Engine.matchesRegex(samplerName, excludedIds) == true) continue; //Skip excluded IDs samplers.push(Synth.getSampler(samplerName)); //Add sampler to array samplers[samplers.length-1].enableRoundRobin(false); //Disable default RR behaviour samplers[samplers.length-1].refreshRRMap(); } //Create offset history MIDI lists - same is done for groupHistory in on control callback for (i = 0; i < numOffsets; i++) //3 possible offset states for synthetic RR { offsetHistory[i] = Engine.createMidiList(); offsetHistory[i].fill(-1); } //GUI //RR Type and Mode Combo Boxes const var cmbType = Content.addComboBox("Type", 0, 10); cmbType.set("items", ["Off", "Real", "Synthetic", "Hybrid"].join("\n")); const var cmbMode = Content.addComboBox("Mode", 150, 10); cmbMode.set("items", ["Cycle", "Random", "Random No Repeat", "Random Full Cycle"].join("\n")); //Number of groups knob const var knbNumGroups = Content.addKnob("Num Groups", 300, 0); knbNumGroups.setRange(0, 100, 1); //Playable Range controls const var knbLowNote = Content.addKnob("Low Note", 450, 0); knbLowNote.setRange(0, 127, 1); const var knbHighNote = Content.addKnob("High Note", 600, 0); knbHighNote.setRange(0, 127, 1); //Reset timeout knob const var knbReset = Content.addKnob("Reset Time", 0, 50); knbReset.setRange(0, 60, 1); //Microtuning knob const var knbMicroTuning = Content.addKnob("Microtuning", 150, 50); knbMicroTuning.setRange(0, 100, 0.1); //Functions inline function createGroupHistoryMidiLists() { groupHistory = []; //Reset variable //Create group history MIDI lists for (i = 0; i < knbNumGroups.getValue(); i++) //One MIDI list per group { groupHistory[i] = Engine.createMidiList(); groupHistory[i].clear(); } } //Callbacks function onNoteOn() { local i; noteNumber = Message.getNoteNumber(); if (cmbType.getValue() > 1 && noteNumber >= lowNote && noteNumber <= highNote) //RR is not off { //Group based RR if (groupHistory.length > 0 && (cmbType.getValue() == 2 || cmbType.getValue() == 4)) //Real or Hybrid RR { if (knbNumGroups.getValue() != 1) { switch (cmbMode.getValue()) { case RRModes.CYCLE: group = (lastGroup.getValue(Message.getNoteNumber()) + 1) % parseInt(knbNumGroups.getValue()); break; case RRModes.TRUE_RANDOM: group = Math.floor(Math.random() * knbNumGroups.getValue()); break; case RRModes.RANDOM_NO_REPEAT: group = -1; while (group == -1 || group == lastGroup.getValue(noteNumber)) { group = Math.floor(Math.random() * knbNumGroups.getValue()); } break; case RRModes.RANDOM_FULL_CYCLE: //Reset counter and history if all RRs have been played if (groupCounter.getValue(noteNumber) >= knbNumGroups.getValue()) { groupCounter.setValue(noteNumber, 0); //Reset groupCounter for this note for (gHistory in groupHistory) { gHistory.setValue(noteNumber, -1); } } //Get random group number that isn't the last group and hasn't been marked as played in groupHistory for this note group = -1; while (group == -1 || group == lastGroup.getValue(noteNumber) || groupHistory[group].getValue(noteNumber) != -1) { group = Math.floor(Math.random() * knbNumGroups.getValue()); } break; } } else { group = 0; } //Reset group if timer expired if (Engine.getUptime() - timer.getValue(noteNumber) > knbReset.getValue()) { group = 0; } //Set active group for each samper for (sampler in samplers) { sampler.setActiveGroup(1+group); } groupCounter.setValue(noteNumber, groupCounter.getValue(noteNumber) + 1); //Increment counter groupHistory[group].setValue(noteNumber, 1); //Mark RR as used lastGroup.setValue(noteNumber, group); } //Synthetic sample borrowed RR if (cmbType.getValue() == 3 || cmbType.getValue() == 4) //Synthetic or Hybrid RR { offset = -1; switch (cmbMode.getValue()) { case RRModes.CYCLE: offset = (lastOffset.getValue(noteNumber) + 1) % numOffsets; break; case RRModes.TRUE_RANDOM: offset = Math.floor(Math.random() * numOffsets); break; case RRModes.RANDOM_NO_REPEAT: while (offset == -1 || offset == lastOffset.getValue(noteNumber)) { offset = Math.floor(Math.random() * numOffsets); } break; case RRModes.RANDOM_FULL_CYCLE: //Reset counter and history if all RRs have been played if (offsetCounter.getValue(noteNumber) >= numOffsets) { offsetCounter.setValue(noteNumber, 0); //Reset offsetCounter for this note for (oHistory in offsetHistory) { oHistory.setValue(noteNumber, -1); } } //Get random offset number that isn't the last offset and hasn't been marked as played in offsetHistory for this note while (offset == -1 || offset == lastOffset.getValue(noteNumber) || offsetHistory[offset].getValue(noteNumber) != -1) { offset = Math.floor(Math.random() * numOffsets); } break; } //Reset offset if timer expired if (Engine.getUptime() - timer.getValue(noteNumber) > knbReset.getValue()) { offset = 0; } if (noteNumber + offset - 1 < lowNote) offset = 1; if (noteNumber + offset - 1 > highNote) offset = 0; //Apply offset and coarse tuning to message Message.setTransposeAmount(offset-1); //Use -1 to make offset range -1 to +1 Message.setCoarseDetune(-(offset-1)); //Use coarse detune so setting can be picked up by later scripts offsetCounter.setValue(noteNumber, offsetCounter.getValue(noteNumber) + 1); //Increment counter offsetHistory[offset].setValue(noteNumber, 1); //Mark RR as used lastOffset.setValue(noteNumber, offset); } //Apply random microtuning, if any if (knbMicroTuning.getValue() != 0) { Message.setFineDetune(Math.random() * (knbMicroTuning.getValue() - -knbMicroTuning.getValue() + 1) + -knbMicroTuning.getValue()); } timer.setValue(noteNumber, Engine.getUptime()); } } function onNoteOff() { } function onController() { } function onTimer() { } function onControl(number, value) { switch (number) { case knbLowNote: lowNote = value; createGroupHistoryMidiLists(); break; case knbHighNote: highNote = value; break; } }
-
A few more adjustments, one to correct an issue with setting high and low notes, and a couple of improvements to the mode selection and I set the minimum number of groups to 1 instead of 0.
/** * Multi-Round Robin script v2.5 * Author: David Healey * Date: 07/01/2017 * Modified: 09/02/2017 * License: GPLv3 - https://www.gnu.org/licenses/gpl-3.0.en.html */ Content.setHeight(100); //Init const var excludedIds = "(elease)"; //Regex string of words in sampler IDs to exclude RR from const var samplerNames = Synth.getIdList("Sampler"); //Get the ids of all child samplers const var samplers = []; reg group; //Group RR counter reg offset; //Synthetic RR counter reg numOffsets = 3; //Number of adjacent samples used by the synthetic RR (including 0) - currently anything other than 3 won't work as expected const var lastGroup = Engine.createMidiList(); const var lastOffset = Engine.createMidiList(); const var groupCounter = Engine.createMidiList(); const var offsetCounter = Engine.createMidiList(); groupCounter.fill(0); offsetCounter.fill(0); var groupHistory = []; const var offsetHistory = []; reg lowNote; reg highNote; reg noteNumber; const var timer = Engine.createMidiList(); namespace RRModes { const var CYCLE = 1; const var TRUE_RANDOM = 2; const var RANDOM_NO_REPEAT = 3; const var RANDOM_FULL_CYCLE = 4; }; //Get child samplers for (samplerName in samplerNames) { if (Engine.matchesRegex(samplerName, excludedIds) == true) continue; //Skip excluded IDs samplers.push(Synth.getSampler(samplerName)); //Add sampler to array samplers[samplers.length-1].enableRoundRobin(false); //Disable default RR behaviour samplers[samplers.length-1].refreshRRMap(); } //Create offset history MIDI lists - same is done for groupHistory in on control callback for (i = 0; i < numOffsets; i++) //3 possible offset states for synthetic RR { offsetHistory[i] = Engine.createMidiList(); offsetHistory[i].fill(-1); } //GUI //RR Type and Mode Combo Boxes const var cmbType = Content.addComboBox("Type", 0, 10); cmbType.set("items", ["Off", "Real", "Synthetic", "Hybrid"].join("\n")); const var cmbMode = Content.addComboBox("Mode", 150, 10); cmbMode.set("items", ["Cycle", "Random", "Random No Repeat", "Random Full Cycle"].join("\n")); //Number of groups knob const var knbNumGroups = Content.addKnob("Num Groups", 300, 0); knbNumGroups.setRange(1, 100, 1); //Playable Range controls const var knbLowNote = Content.addKnob("Low Note", 450, 0); knbLowNote.setRange(0, 126, 1); const var knbHighNote = Content.addKnob("High Note", 600, 0); knbHighNote.setRange(1, 127, 1); //Reset timeout knob const var knbReset = Content.addKnob("Reset Time", 0, 50); knbReset.setRange(0, 60, 1); //Microtuning knob const var knbMicroTuning = Content.addKnob("Microtuning", 150, 50); knbMicroTuning.setRange(0, 100, 0.1); //Functions inline function createGroupHistoryMidiLists() { groupHistory = []; //Reset variable //Create group history MIDI lists for (i = 0; i < knbNumGroups.getValue(); i++) //One MIDI list per group { groupHistory[i] = Engine.createMidiList(); groupHistory[i].clear(); } } //Callbacks function onNoteOn() { local i; noteNumber = Message.getNoteNumber(); if (cmbType.getValue() > 1 && noteNumber >= lowNote && noteNumber <= highNote) { //Group based RR if (groupHistory.length > 0 && (cmbType.getValue() == 2 || cmbType.getValue() == 4)) //Real or Hybrid RR { if (knbNumGroups.getValue() != 1) { switch (cmbMode.getValue()) { case RRModes.CYCLE: group = (lastGroup.getValue(Message.getNoteNumber()) + 1) % parseInt(knbNumGroups.getValue()); break; case RRModes.TRUE_RANDOM: group = Math.floor(Math.random() * knbNumGroups.getValue()); break; case RRModes.RANDOM_NO_REPEAT: group = -1; while (group == -1 || group == lastGroup.getValue(noteNumber)) { group = Math.floor(Math.random() * knbNumGroups.getValue()); } break; case RRModes.RANDOM_FULL_CYCLE: //Reset counter and history if all RRs have been played if (groupCounter.getValue(noteNumber) >= knbNumGroups.getValue()) { groupCounter.setValue(noteNumber, 0); //Reset groupCounter for this note for (gHistory in groupHistory) { gHistory.setValue(noteNumber, -1); } } //Get random group number that isn't the last group and hasn't been marked as played in groupHistory for this note group = -1; while (group == -1 || group == lastGroup.getValue(noteNumber) || groupHistory[group].getValue(noteNumber) != -1) { group = Math.floor(Math.random() * knbNumGroups.getValue()); } break; } } else { group = 0; } //Reset group if timer expired if (Engine.getUptime() - timer.getValue(noteNumber) > knbReset.getValue()) { group = 0; } //Set active group for each sampler for (sampler in samplers) { sampler.setActiveGroup(1+group); } groupCounter.setValue(noteNumber, groupCounter.getValue(noteNumber) + 1); //Increment counter groupHistory[group].setValue(noteNumber, 1); //Mark RR as used lastGroup.setValue(noteNumber, group); } //Synthetic sample borrowed RR if (cmbType.getValue() == 3 || cmbType.getValue() == 4) //Synthetic or Hybrid RR { offset = -1; switch (cmbMode.getValue()) { case RRModes.CYCLE: offset = (lastOffset.getValue(noteNumber) + 1) % numOffsets; break; case RRModes.TRUE_RANDOM: offset = Math.floor(Math.random() * numOffsets); break; case RRModes.RANDOM_NO_REPEAT: while (offset == -1 || offset == lastOffset.getValue(noteNumber)) { offset = Math.floor(Math.random() * numOffsets); } break; case RRModes.RANDOM_FULL_CYCLE: //Reset counter and history if all RRs have been played if (offsetCounter.getValue(noteNumber) >= numOffsets) { offsetCounter.setValue(noteNumber, 0); //Reset offsetCounter for this note for (oHistory in offsetHistory) { oHistory.setValue(noteNumber, -1); } } //Get random offset number that isn't the last offset and hasn't been marked as played in offsetHistory for this note while (offset == -1 || offset == lastOffset.getValue(noteNumber) || offsetHistory[offset].getValue(noteNumber) != -1) { offset = Math.floor(Math.random() * numOffsets); } break; } //Reset offset if timer expired if (Engine.getUptime() - timer.getValue(noteNumber) > knbReset.getValue()) { offset = 0; } if (noteNumber + offset - 1 < lowNote) offset = 1; if (noteNumber + offset - 1 > highNote) offset = 0; //Apply offset and coarse tuning to message Message.setTransposeAmount(offset-1); //Use -1 to make offset range -1 to +1 Message.setCoarseDetune(-(offset-1)); //Use coarse detune so setting can be picked up by later scripts offsetCounter.setValue(noteNumber, offsetCounter.getValue(noteNumber) + 1); //Increment counter offsetHistory[offset].setValue(noteNumber, 1); //Mark RR as used lastOffset.setValue(noteNumber, offset); } //Apply random microtuning, if any if (knbMicroTuning.getValue() != 0) { Message.setFineDetune(Math.random() * (knbMicroTuning.getValue() - -knbMicroTuning.getValue() + 1) + -knbMicroTuning.getValue()); } timer.setValue(noteNumber, Engine.getUptime()); } } function onNoteOff() { } function onController() { } function onTimer() { } function onControl(number, value) { switch (number) { case cmbType: if (value == 2 && knbNumGroups.getValue() == 1) //Real but only one group { number.setValue(1); //Set mode to off } break; case knbLowNote: lowNote = value; createGroupHistoryMidiLists(); break; case knbHighNote: highNote = value; break; } }
-
Looks tasty. What's the performance of the
onNoteOn
callback (you can see the CPU amount of a single callback in the Script Watch Table if you enable the DBG button)? There are a lot of loops which might cause some spikes (especially in the RANDOM_FULL_CYCLE branch).Here's another approach for full cycle random:
1.) Create a random order
2.) Cycle through it like normal round robin
3.) When you're through, shuffle the order and reset the indexThis is a quick sketch that demonstrates this idea:
/** Swaps two elements in a list randomly. */ inline function swapRandomly(list) { local firstIndex = Math.randInt(0, list.length); local secondIndex = Math.randInt(0, list.length); local temp = list[firstIndex]; list[firstIndex] = list[secondIndex]; list[secondIndex] = temp; } /** Swaps every element in the list. */ inline function swapEntirely(list) { local i = 0; while(++i < list.length) swapRandomly(list); } reg groupList = [1,2,3,4]; reg groupIndex; function onNoteOn() { Console.print(groupList[groupIndex]); if(++groupIndex >= groupList.length) { groupIndex = 0; Console.print("Shuffle!"); swapEntirely(groupList); } } function onNoteOff(){} function onController(){} function onTimer(){} function onControl(number, value) {}
You'll need to change the shuffle functions to support
MidiLists
, but my gut feeling tells me this is faster (and more predictable). -
Thanks I'll explore your code - and thanks for the CPU tip, I didn't know you could profile it like that!
-
Yes its pretty hard to check the script performance of a single callback by looking on the CPU meter :)
-
Version 3.0: Major revision.
- Incorporated Christoph's randomising system.
- Added namespace for RRTypes
- Removed random no repeat mode - I could see no point in keeping it when we have the random full cycle
- Aimed for simpler code and lower CPU usage so the RR is no longer per note it is for the whole keyboard. Reset timer is still per note though
- CPU usage of on note callback is now around 0.60 on average on my system (before it was running up to 3.5!)
- Added sample range knob for synth and hybrid modes. This is used to set how far from the played note the sample borrowed modes will take samples from. So setting it to 1 means the samples 1 down and one up from the played note will be used, as well as the actual note's sample.
/** * Multi-Round Robin script v3.2 * Author: David Healey, Christoph Hart * Date: 07/01/2017 * Modified: 20/02/2017 * License: GPLv3 - https://www.gnu.org/licenses/gpl-3.0.en.html */ //INIT reg noteNumber; reg groupList = []; reg groupIndex; reg offsetList = []; reg offsetIndex; reg offset; namespace RRTypes { const var OFF = 1; const var REAL = 2; const var SYNTH = 3; const var HYBRID = 4; }; namespace RRModes { const var CYCLE = 1; const var RANDOM = 2; const var RANDOM_CYCLE = 3; }; const var timer = Engine.createMidiList(); const var excludedIds = "(elease)"; //Regex string of words in sampler IDs to exclude RR from const var samplerNames = Synth.getIdList("Sampler"); //Get the ids of all child samplers const var samplers = []; //Get child samplers for (samplerName in samplerNames) { if (Engine.matchesRegex(samplerName, excludedIds) == true) continue; //Skip excluded IDs samplers.push(Synth.getSampler(samplerName)); //Add sampler to array samplers[samplers.length-1].enableRoundRobin(false); //Disable default RR behaviour } //GUI Content.setHeight(100); //RR Type and Mode Combo Boxes const var cmbType = Content.addComboBox("Type", 0, 10); cmbType.set("items", ["Off", "Real", "Synthetic", "Hybrid"].join("\n")); const var cmbMode = Content.addComboBox("Mode", 150, 10); cmbMode.set("items", ["Cycle", "Random", "Random Full Cycle"].join("\n")); //Number of groups knob const var knbNumGroups = Content.addKnob("Num Groups", 300, 0); knbNumGroups.setRange(1, 100, 1); //Playable Range controls const var knbLowNote = Content.addKnob("Low Note", 450, 0); knbLowNote.setRange(0, 126, 1); const var knbHighNote = Content.addKnob("High Note", 600, 0); knbHighNote.setRange(1, 127, 1); //Reset timeout knob const var knbReset = Content.addKnob("Reset Time", 0, 50); knbReset.setRange(1, 60, 1); knbReset.set("suffix", " Seconds"); //Microtuning knob const var knbMicroTuning = Content.addKnob("Microtuning", 150, 50); knbMicroTuning.setRange(0, 100, 0.1); //Borrowed Sample Range const var knbSampleRange = Content.addKnob("Sample Range", 300, 50); knbSampleRange.setRange(1, 12, 1); //----------------- //FUNCTIONS /** Swaps two elements in an array randomly. */ inline function swapRandomly(arr) { local firstIndex = Math.randInt(0, arr.length); local secondIndex = Math.randInt(0, arr.length); local temp = arr[firstIndex]; arr[firstIndex] = arr[secondIndex]; arr[secondIndex] = temp; } /** Swaps every element in the array. */ inline function swapEntirely(arr) { local i = 0; while(++i < arr.length) swapRandomly(arr); } inline function resetGroupList(count) { local i; for (i = 0; i < count; i++) { groupList[i] = i; } } inline function resetOffsetList(count) { local i; //Build array going from -value to +value+1 (always need to account for 0) for (i = 0; i < count*2+1; i++) { offsetList[i] = i-count; } } //CALLBACKS function onNoteOn() { noteNumber = Message.getNoteNumber(); if (cmbType.getValue() != RRTypes.OFF && noteNumber >= knbLowNote.getValue() && noteNumber <= knbHighNote.getValue()) { //Group based RR if (knbNumGroups.getValue() > 1 && (cmbType.getValue() == RRTypes.REAL || cmbType.getValue() == RRTypes.HYBRID)) //Real or Hybrid RR { switch (cmbMode.getValue()) { case RRModes.CYCLE: groupIndex = (groupIndex + 1) % groupList.length; break; case RRModes.RANDOM: groupIndex = Math.floor(Math.random() * groupList.length); break; case RRModes.RANDOM_CYCLE: if (++groupIndex >= groupList.length) { groupIndex = 0; swapEntirely(groupList); } break; } } //Reset groupIndex to 0 if only one group is present or the reset time has elapsed if (knbNumGroups.getValue() == 1 || Engine.getUptime() - timer.getValue(noteNumber) > knbReset.getValue()) { groupIndex = 0; resetGroupList(knbNumGroups.getValue()); } //Set active group for each sampler if (samplers.length > 0) { for (sampler in samplers) { sampler.setActiveGroup(1 + groupList[groupIndex]); } } //Synthetic sample borrowed RR if (cmbType.getValue() == RRTypes.SYNTH || cmbType.getValue() == RRTypes.HYBRID) //Synthetic or Hybrid RR { switch (cmbMode.getValue()) { case RRModes.CYCLE: offsetIndex = (offsetIndex + 1) % offsetList.length; break; case RRModes.RANDOM: offsetIndex = Math.floor(Math.random() * offsetList.length); break; case RRModes.RANDOM_CYCLE: if (++offsetIndex >= offsetList.length) { offsetIndex = 0; swapEntirely(offsetList); } break; } //If reset time has elapsed, reset index if (Engine.getUptime() - timer.getValue(noteNumber) > knbReset.getValue()) { offsetIndex = parseInt(Math.floor(offsetList.length / 2)); resetOffsetList(knbSampleRange.getValue()); } //If the played note + the offset are within the playable range if (noteNumber + offsetList[offsetIndex] > knbLowNote.getValue() && noteNumber + offsetList[offsetIndex] < knbHighNote.getValue()) { Message.setTransposeAmount(offsetList[offsetIndex]); //Transpose the note Message.setCoarseDetune(-offsetList[offsetIndex]); //Use coarse detune so setting can be picked up by later scripts } } //Apply random microtuning, if any if (knbMicroTuning.getValue() > 0) { Message.setFineDetune(Math.random() * (knbMicroTuning.getValue() - -knbMicroTuning.getValue() + 1) + -knbMicroTuning.getValue()); } timer.setValue(noteNumber, Engine.getUptime()); //Record time this note was triggered } } function onNoteOff() { } function onController() { } function onTimer() { } function onControl(number, value) { switch(number) { case knbNumGroups: groupList = []; resetGroupList(value); break; case knbSampleRange: offsetList = []; resetOffsetList(value); break; case cmbType: switch (value) { case RRTypes.REAL: knbSampleRange.set("visible", false); if (knbNumGroups.getValue() == 1) { number.setValue(RRTypes.OFF); } break; case RRTypes.SYNTH: knbSampleRange.set("visible", true); break; case RRTypes.HYBRID: knbSampleRange.set("visible", true); if (knbNumGroups.getValue() == 1) //If there are no RR groups then Hybrid defaults to synth { number.setValue(RRTypes.SYNTH); } break; } break; case cmbMode: resetGroupList(knbNumGroups.getValue()); resetOffsetList(knbSampleRange.getValue()); offsetIndex = 0; groupIndex = 0; break; } }