Random, Cycle, Synthetic, Hybrid Round Robin
-
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; } }