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 uses Message.setTransposeAmount() and Message.setCoarseDetune() for implementing the synthetic RR so you'll need to use the corresponding get 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="/**&#10; * Multi-Round Robin script v2.0.js&#10; * Author: David Healey&#10; * Date: 07/01/2017&#10; * License: GPLv3 - https://www.gnu.org/licenses/gpl-3.0.en.html&#10; */&#10;&#10;//Includes &#10;&#10;Content.setHeight(150);&#10;&#10;//Init&#10;reg excludeSamplers = [&quot;Release&quot;]; //Samples with these words in their ID won't be affected&#10;reg samplerNames = Synth.getIdList(&quot;Sampler&quot;); //Get the ids of all child samplers&#10;reg samplers = [];&#10;&#10;reg group = Engine.createMidiList(); //For group based RR&#10;reg groupValue; //Value taken from the group MIDI list&#10;reg offset = Engine.createMidiList(); //For synthetic RR&#10;reg offsetValue; //Value taken from the offset MIDI list&#10;reg lastGroup = Engine.createMidiList();&#10;reg lastOffset = Engine.createMidiList();&#10;&#10;reg groupCounter = Engine.createMidiList();&#10;reg offsetCounter = Engine.createMidiList();&#10;groupCounter.fill(0);&#10;offsetCounter.fill(0);&#10;&#10;reg groupHistory = [];&#10;reg offsetHistory = [];&#10;&#10;reg numGroups; //Script assumes all samplers have the same number of groups&#10;reg numOffsets = 3; //Number of adjacent samples used by the synthetic RR (including 0) - currently anything other than 3 won't work as expected&#10;&#10;reg timer = Engine.createMidiList();&#10;&#10;//Get child samplers&#10;for (i = 0; i &lt; samplerNames.length; i++)&#10;{&#10;&#9;for (j = 0; j &lt; excludeSamplers.length; j++)&#10;&#9;{&#10;&#9;&#9;if (samplerNames[i].indexOf(excludeSamplers[j]) !== -1) continue; //Skip exluded IDs&#10;&#9;}&#10;&#10;&#9;samplers.push(Synth.getSampler(samplerNames[i])); //Add sampler to array&#10;}&#10;&#10;for (i = 0; i &lt; samplers.length; i++)&#10;{&#10;&#9;samplers[i].enableRoundRobin(false); //Disable default RR behaviour&#10;&#9;samplers[i].refreshRRMap();&#10;}&#10;&#10;//Create offset history MIDI lists - same is done for groupHistory in on control callback&#10;for (i = 0; i &lt; numOffsets; i++) //3 possible offset states for synthetic RR&#10;{&#10;&#9;offsetHistory[i] = Engine.createMidiList();&#10;&#9;offsetHistory[i].fill(-1);&#10;}&#10;&#10;//GUI&#10;const var cmbType = Content.addComboBox(&quot;Type&quot;, 0, 10);&#10;cmbType.set(&quot;items&quot;, [&quot;Off&quot;, &quot;Real&quot;, &quot;Synthetic&quot;, &quot;Hybrid&quot;].join(&quot;\n&quot;));&#10;&#10;const var cmbMode = Content.addComboBox(&quot;Mode&quot;, 150, 10);&#10;cmbMode.set(&quot;items&quot;, [&quot;Cycle&quot;, &quot;Random&quot;, &quot;Random No Repeat&quot;, &quot;Random Full Cycle&quot;].join(&quot;\n&quot;));&#10;&#10;//Playable Range controls&#10;const var knbLowNote = Content.addKnob(&quot;Low Note&quot;, 300, 0);&#10;const var knbHighNote = Content.addKnob(&quot;High Note&quot;, 450, 0);&#10;knbLowNote.setRange(0, 127, 1);&#10;knbHighNote.setRange(0, 127, 1);&#10;&#10;//Reset timeout knob&#10;const var knbReset = Content.addKnob(&quot;Reset Time&quot;, 600, 0);&#10;knbReset.setRange(0, 60, 1);&#10;&#10;//Microtuning knob&#10;const var knbMicroTuning = Content.addKnob(&quot;Microtuning&quot;, 0, 50);&#10;knbMicroTuning.setRange(0, 100, 0.1);&#10;&#10;//Functions&#10;&#10;//Callbacks&#10;function onNoteOn()&#10;{&#10;&#9;if (cmbType.getValue() &gt; 1 &amp;&amp; Message.getNoteNumber() &gt;= knbLowNote.getValue() &amp;&amp; Message.getNoteNumber() &lt;= knbHighNote.getValue()) //RR is not off&#10;&#9;{&#10;&#9;&#9;//Group based RR&#10;&#9;&#9;if (cmbType.getValue() == 2 || cmbType.getValue() == 4) //Real or Hybrid RR&#10;&#9;&#9;{&#10;&#9;&#9;&#9;if (numGroups != 1)&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;switch (cmbMode.getValue())&#10;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;case 1: //Cycle&#10;&#9;&#9;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), (group.getValue(Message.getNoteNumber()) + 1) % numGroups);&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;&#9;case 2: //True random &#10;&#9;&#9;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups));&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;&#9;case 3: //Random no repeat&#10;&#9;&#9;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups));&#10;&#9;&#9;&#9;&#9;&#9;&#9;while (group.getValue(Message.getNoteNumber()) == lastGroup.getValue(Message.getNoteNumber()))&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups));&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;&#9;case 4: //Random full cycle&#10;&#9;&#9;&#9;&#9;&#10;&#9;&#9;&#9;&#9;&#9;&#9;//Reset counter and history if all RRs have been played&#10;&#9;&#9;&#9;&#9;&#9;&#9;if (groupCounter.getValue(Message.getNoteNumber()) &gt;= numGroups)&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;groupCounter.setValue(Message.getNoteNumber(), 0); //Reset groupCounter for this note&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;for (i = 0; i &lt; numGroups; i++)&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;groupHistory[i].setValue(Message.getNoteNumber(), -1);&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;//Get random group number that isn't the last group and hasn't been marked as played in groupHistory for this note&#10;&#9;&#9;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups));&#10;&#9;&#9;&#9;&#9;&#9;&#9;while (group.getValue(Message.getNoteNumber()) == lastGroup.getValue(Message.getNoteNumber()) || groupHistory[group.getValue(Message.getNoteNumber())].getValue(Message.getNoteNumber()) != -1)&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups));&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;else &#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), 0);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;//Reset group if timer expired&#10;&#9;&#9;&#9;if (Engine.getUptime() - timer.getValue(Message.getNoteNumber()) &gt; knbReset.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), 0);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;groupValue = group.getValue(Message.getNoteNumber()); //Current group value for this note&#10;&#10;&#9;&#9;&#9;//Set active group for each samper&#10;&#9;&#9;&#9;for (i = 0; i &lt; samplers.length; i++)&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;samplers[i].setActiveGroup(1+groupValue);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;groupCounter.setValue(Message.getNoteNumber(), groupCounter.getValue(Message.getNoteNumber()) + 1); //Increment counter&#10;&#9;&#9;&#9;groupHistory[groupValue].setValue(Message.getNoteNumber(), 1); //Mark RR as used&#10;&#9;&#9;&#9;lastGroup.setValue(Message.getNoteNumber(), groupValue);&#10;&#9;&#9;}&#10;&#9;&#9;&#10;&#9;&#9;//Synthetic sample borrowed RR&#10;&#9;&#9;if (cmbType.getValue() == 3 || cmbType.getValue() == 4) //Synthetic or Hybrid RR&#10;&#9;&#9;{&#10;&#9;&#9;&#9;switch (cmbMode.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;case 1: //Cycle&#10;&#9;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), (offset.getValue(Message.getNoteNumber()) + 1) % numOffsets);&#10;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;case 2: //True random &#10;&#9;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets));&#10;&#9;&#9;&#9;&#9;&#9;while ((offset.getValue(Message.getNoteNumber())-1) + Message.getNoteNumber() &gt; knbHighNote.getValue() &amp;&amp; (offset.getValue(Message.getNoteNumber())-1) + Message.getNoteNumber() &lt; knbLowNote.getValue())&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets));&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;case 3: //Random no repeat&#10;&#9;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets));&#10;&#9;&#9;&#9;&#9;&#9;while (offset.getValue(Message.getNoteNumber()) == lastOffset.getValue(Message.getNoteNumber()) || ((offset.getValue(Message.getNoteNumber())-1) + Message.getNoteNumber() &gt; knbHighNote.getValue() &amp;&amp; (offset.getValue(Message.getNoteNumber())-1) + Message.getNoteNumber() &lt; knbLowNote.getValue()))&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets));&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;case 4: //Random full cycle&#10;&#9;&#9;&#9;&#9;&#9;&#10;&#9;&#9;&#9;&#9;&#9;//Reset counter and history if all RRs have been played&#10;&#9;&#9;&#9;&#9;&#9;if (offsetCounter.getValue(Message.getNoteNumber()) &gt;= numOffsets)&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offsetCounter.setValue(Message.getNoteNumber(), 0); //Reset offsetCounter for this note&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;for (i = 0; i &lt; numOffsets; i++)&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;offsetHistory[i].setValue(Message.getNoteNumber(), -1);&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;&#9;//Get random offset number that isn't the last offset and hasn't been marked as played in offsetHistory for this note&#10;&#9;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets));&#10;&#9;&#9;&#9;&#9;&#9;while (offset.getValue(Message.getNoteNumber()) == lastOffset.getValue(Message.getNoteNumber()) || offsetHistory[offset.getValue(Message.getNoteNumber())].getValue(Message.getNoteNumber()) != -1)&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets));&#9;&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;break;&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;//Reset offset if timer expired&#10;&#9;&#9;&#9;if (Engine.getUptime() - timer.getValue(Message.getNoteNumber()) &gt; knbReset.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), 0);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;//Make sure offset + note is not outside playable range&#10;&#9;&#9;&#9;if (offset.getValue(Message.getNoteNumber())-1 + Message.getNoteNumber() &lt; knbLowNote.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), 1);&#10;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;else if (offset.getValue(Message.getNoteNumber())-1 + Message.getNoteNumber() &gt; knbHighNote.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), 0);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;offsetValue = offset.getValue(Message.getNoteNumber());&#10;&#10;&#9;&#9;&#9;//Apply offset and coarse tuning to message&#10;&#9;&#9;&#9;Message.setTransposeAmount(offsetValue-1); //Use -1 to make offset range -1 to +1&#10;&#9;&#9;&#9;Message.setCoarseDetune(-(offsetValue-1)); //Use coarse detune so setting can be picked up by later scripts&#10;&#10;&#9;&#9;&#9;offsetCounter.setValue(Message.getNoteNumber(), offsetCounter.getValue(Message.getNoteNumber()) + 1); //Increment counter&#10;&#9;&#9;&#9;offsetHistory[offsetValue].setValue(Message.getNoteNumber(), 1); //Mark RR as used&#10;&#9;&#9;&#9;lastOffset.setValue(Message.getNoteNumber(), offsetValue);&#10;&#9;&#9;}&#10;&#10;&#9;&#9;//Apply random microtuning, if any&#10;&#9;&#9;if (knbMicroTuning.getValue() != 0)&#10;&#9;&#9;{&#10;&#9;&#9;&#9;Message.setFineDetune(Math.random() * (knbMicroTuning.getValue() - -knbMicroTuning.getValue() + 1) + -knbMicroTuning.getValue());&#10;&#9;&#9;}&#10;&#10;&#9;&#9;timer.setValue(Message.getNoteNumber(), Engine.getUptime());&#10;&#9;}&#10;}&#10;&#10;function onNoteOff()&#10;{&#10;&#9;&#10;}&#10;function onController()&#10;{&#10;&#9;&#10;}&#10;function onTimer()&#10;{&#10;&#9;&#10;}&#10;function onControl(number, value)&#10;{&#10;&#9;switch (number)&#10;&#9;{&#10;&#9;&#9;case knbHighNote:&#10;&#9;&#9;&#9;&#10;&#9;&#9;&#9;//Get number of groups - assumes all samplers have the same number of groups and samples are mapped over velocity 64&#10;&#9;&#9;&#9;numGroups = samplers[0].getRRGroupsForMessage(value, 64);&#10;&#10;&#9;&#9;&#9;groupHistory = [];&#10;&#9;&#9;&#9;//Create group history MIDI lists&#10;&#9;&#9;&#9;for (i = 0; i &lt; numGroups; i++) //One MIDI list per group&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;groupHistory[i] = Engine.createMidiList();&#10;&#9;&#9;&#9;&#9;groupHistory[i].fill(-1);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;break;&#10;&#9;}&#10;}">
      <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>
    

  • administrators

    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 of reg for MidiLists

    You 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 a const 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 as const var and insert elements into it. You just can't reassign it to something else.

    Script Error in the onControl callback

    If there is no sampler, the script throws an error in the onControl callback (because the array is empty). If you change this line

    numGroups = 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 callback

    You are creating a non-local variable in the onNoteOn callback every time you run the for loop. Just put

    local 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 that for 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="/**&#10; * Multi-Round Robin script v2.0.js&#10; * Author: David Healey&#10; * Date: 07/01/2017&#10; * License: GPLv3 - https://www.gnu.org/licenses/gpl-3.0.en.html&#10; */&#10;&#10;//Includes &#10;&#10;Content.setHeight(100);&#10;&#10;//Init&#10;reg excludeSamplers = [&quot;Release&quot;]; //Samples with these words in their ID won't be affected&#10;const var samplerNames = Synth.getIdList(&quot;Sampler&quot;); //Get the ids of all child samplers&#10;reg samplers = [];&#10;&#10;const var group = Engine.createMidiList(); //For group based RR&#10;reg groupValue; //Value taken from the group MIDI list&#10;const var offset = Engine.createMidiList(); //For synthetic RR&#10;reg offsetValue; //Value taken from the offset MIDI list&#10;const var lastGroup = Engine.createMidiList();&#10;const var lastOffset = Engine.createMidiList();&#10;&#10;const var groupCounter = Engine.createMidiList();&#10;const var offsetCounter = Engine.createMidiList();&#10;groupCounter.fill(0);&#10;offsetCounter.fill(0);&#10;&#10;reg groupHistory = [];&#10;reg offsetHistory = [];&#10;&#10;reg numGroups; //Script assumes all samplers have the same number of groups&#10;reg numOffsets = 3; //Number of adjacent samples used by the synthetic RR (including 0) - currently anything other than 3 won't work as expected&#10;&#10;const var timer = Engine.createMidiList();&#10;&#10;namespace RRModes&#10;{&#10;&#9;const var CYCLE = 1;&#10;&#9;const var TRUE_RANDOM = 2;&#10;&#9;const var RANDOM_NO_REPEAT = 3;&#10;&#9;const var RANDOM_FULL_CYCLE = 4;&#10;};&#10;&#10;//Get child samplers&#10;for (i = 0; i &lt; samplerNames.length; i++)&#10;{&#10;&#9;for (j = 0; j &lt; excludeSamplers.length; j++)&#10;&#9;{&#10;&#9;&#9;if (samplerNames[i].indexOf(excludeSamplers[j]) !== -1) continue; //Skip exluded IDs&#10;&#9;}&#10;&#10;&#9;samplers.push(Synth.getSampler(samplerNames[i])); //Add sampler to array&#10;}&#10;&#10;for (sampler in samplers)&#10;{&#10;&#9;sampler.enableRoundRobin(false); //Disable default RR behaviour&#10;&#9;sampler.refreshRRMap();&#10;}&#10;&#10;//Create offset history MIDI lists - same is done for groupHistory in on control callback&#10;for (i = 0; i &lt; numOffsets; i++) //3 possible offset states for synthetic RR&#10;{&#10;&#9;offsetHistory[i] = Engine.createMidiList();&#10;&#9;offsetHistory[i].fill(-1);&#10;}&#10;&#10;//GUI&#10;//RR Type and Mode Combo Boxes&#10;const var cmbType = Content.addComboBox(&quot;Type&quot;, 0, 10);&#10;cmbType.set(&quot;items&quot;, [&quot;Off&quot;, &quot;Real&quot;, &quot;Synthetic&quot;, &quot;Hybrid&quot;].join(&quot;\n&quot;));&#10;&#10;const var cmbMode = Content.addComboBox(&quot;Mode&quot;, 150, 10);&#10;cmbMode.set(&quot;items&quot;, [&quot;Cycle&quot;, &quot;Random&quot;, &quot;Random No Repeat&quot;, &quot;Random Full Cycle&quot;].join(&quot;\n&quot;));&#10;&#10;//Playable Range controls&#10;const var knbLowNote = Content.addKnob(&quot;Low Note&quot;, 300, 0);&#10;const var knbHighNote = Content.addKnob(&quot;High Note&quot;, 450, 0);&#10;knbLowNote.setRange(0, 127, 1);&#10;knbHighNote.setRange(0, 127, 1);&#10;&#10;//Reset timeout knob&#10;const var knbReset = Content.addKnob(&quot;Reset Time&quot;, 600, 0);&#10;knbReset.setRange(0, 60, 1);&#10;&#10;//Microtuning knob&#10;const var knbMicroTuning = Content.addKnob(&quot;Microtuning&quot;, 0, 50);&#10;knbMicroTuning.setRange(0, 100, 0.1);&#10;&#10;//Functions&#10;&#10;//Callbacks&#10;function onNoteOn()&#10;{&#10;&#9;local i; &#10;&#10;&#9;if (cmbType.getValue() &gt; 1 &amp;&amp; Message.getNoteNumber() &gt;= knbLowNote.getValue() &amp;&amp; Message.getNoteNumber() &lt;= knbHighNote.getValue()) //RR is not off&#10;&#9;{&#10;&#9;&#9;//Group based RR&#10;&#9;&#9;if (cmbType.getValue() == 2 || cmbType.getValue() == 4) //Real or Hybrid RR&#10;&#9;&#9;{&#10;&#9;&#9;&#9;if (numGroups != 1)&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;switch (cmbMode.getValue())&#10;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.CYCLE:&#10;&#9;&#9;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), (group.getValue(Message.getNoteNumber()) + 1) % numGroups);&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.TRUE_RANDOM:&#10;&#9;&#9;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups));&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.RANDOM_NO_REPEAT:&#10;&#9;&#9;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups));&#10;&#9;&#9;&#9;&#9;&#9;&#9;while (group.getValue(Message.getNoteNumber()) == lastGroup.getValue(Message.getNoteNumber()))&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups));&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.RANDOM_FULL_CYCLE:&#10;&#9;&#9;&#9;&#9;&#10;&#9;&#9;&#9;&#9;&#9;&#9;//Reset counter and history if all RRs have been played&#10;&#9;&#9;&#9;&#9;&#9;&#9;if (groupCounter.getValue(Message.getNoteNumber()) &gt;= numGroups)&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;groupCounter.setValue(Message.getNoteNumber(), 0); //Reset groupCounter for this note&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;for (i = 0; i &lt; numGroups; i++)&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;groupHistory[i].setValue(Message.getNoteNumber(), -1);&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;//Get random group number that isn't the last group and hasn't been marked as played in groupHistory for this note&#10;&#9;&#9;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups));&#10;&#9;&#9;&#9;&#9;&#9;&#9;while (group.getValue(Message.getNoteNumber()) == lastGroup.getValue(Message.getNoteNumber()) || groupHistory[group.getValue(Message.getNoteNumber())].getValue(Message.getNoteNumber()) != -1)&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups));&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;else &#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), 0);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;//Reset group if timer expired&#10;&#9;&#9;&#9;if (Engine.getUptime() - timer.getValue(Message.getNoteNumber()) &gt; knbReset.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), 0);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;groupValue = group.getValue(Message.getNoteNumber()); //Current group value for this note&#10;&#10;&#9;&#9;&#9;//Set active group for each samper&#10;&#9;&#9;&#9;for (sampler in samplers)&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;sampler.setActiveGroup(1+groupValue);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;groupCounter.setValue(Message.getNoteNumber(), groupCounter.getValue(Message.getNoteNumber()) + 1); //Increment counter&#10;&#9;&#9;&#9;groupHistory[groupValue].setValue(Message.getNoteNumber(), 1); //Mark RR as used&#10;&#9;&#9;&#9;lastGroup.setValue(Message.getNoteNumber(), groupValue);&#10;&#9;&#9;}&#10;&#9;&#9;&#10;&#9;&#9;//Synthetic sample borrowed RR&#10;&#9;&#9;if (cmbType.getValue() == 3 || cmbType.getValue() == 4) //Synthetic or Hybrid RR&#10;&#9;&#9;{&#10;&#9;&#9;&#9;switch (cmbMode.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;case RRModes.CYCLE:&#10;&#9;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), (offset.getValue(Message.getNoteNumber()) + 1) % numOffsets);&#10;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;case RRModes.TRUE_RANDOM:&#10;&#9;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets));&#10;&#9;&#9;&#9;&#9;&#9;while ((offset.getValue(Message.getNoteNumber())-1) + Message.getNoteNumber() &gt; knbHighNote.getValue() &amp;&amp; (offset.getValue(Message.getNoteNumber())-1) + Message.getNoteNumber() &lt; knbLowNote.getValue())&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets));&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;case RRModes.RANDOM_NO_REPEAT:&#10;&#9;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets));&#10;&#9;&#9;&#9;&#9;&#9;while (offset.getValue(Message.getNoteNumber()) == lastOffset.getValue(Message.getNoteNumber()) || ((offset.getValue(Message.getNoteNumber())-1) + Message.getNoteNumber() &gt; knbHighNote.getValue() &amp;&amp; (offset.getValue(Message.getNoteNumber())-1) + Message.getNoteNumber() &lt; knbLowNote.getValue()))&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets));&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;case RRModes.RANDOM_FULL_CYCLE:&#10;&#9;&#9;&#9;&#9;&#9;&#10;&#9;&#9;&#9;&#9;&#9;//Reset counter and history if all RRs have been played&#10;&#9;&#9;&#9;&#9;&#9;if (offsetCounter.getValue(Message.getNoteNumber()) &gt;= numOffsets)&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offsetCounter.setValue(Message.getNoteNumber(), 0); //Reset offsetCounter for this note&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;for (i = 0; i &lt; numOffsets; i++)&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;offsetHistory[i].setValue(Message.getNoteNumber(), -1);&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;&#9;//Get random offset number that isn't the last offset and hasn't been marked as played in offsetHistory for this note&#10;&#9;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets));&#10;&#9;&#9;&#9;&#9;&#9;while (offset.getValue(Message.getNoteNumber()) == lastOffset.getValue(Message.getNoteNumber()) || offsetHistory[offset.getValue(Message.getNoteNumber())].getValue(Message.getNoteNumber()) != -1)&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets));&#9;&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;break;&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;//Reset offset if timer expired&#10;&#9;&#9;&#9;if (Engine.getUptime() - timer.getValue(Message.getNoteNumber()) &gt; knbReset.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), 0);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;//Make sure offset + note is not outside playable range&#10;&#9;&#9;&#9;if (offset.getValue(Message.getNoteNumber())-1 + Message.getNoteNumber() &lt; knbLowNote.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), 1);&#10;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;else if (offset.getValue(Message.getNoteNumber())-1 + Message.getNoteNumber() &gt; knbHighNote.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), 0);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;offsetValue = offset.getValue(Message.getNoteNumber());&#10;&#10;&#9;&#9;&#9;//Apply offset and coarse tuning to message&#10;&#9;&#9;&#9;Message.setTransposeAmount(offsetValue-1); //Use -1 to make offset range -1 to +1&#10;&#9;&#9;&#9;Message.setCoarseDetune(-(offsetValue-1)); //Use coarse detune so setting can be picked up by later scripts&#10;&#10;&#9;&#9;&#9;offsetCounter.setValue(Message.getNoteNumber(), offsetCounter.getValue(Message.getNoteNumber()) + 1); //Increment counter&#10;&#9;&#9;&#9;offsetHistory[offsetValue].setValue(Message.getNoteNumber(), 1); //Mark RR as used&#10;&#9;&#9;&#9;lastOffset.setValue(Message.getNoteNumber(), offsetValue);&#10;&#9;&#9;}&#10;&#10;&#9;&#9;//Apply random microtuning, if any&#10;&#9;&#9;if (knbMicroTuning.getValue() != 0)&#10;&#9;&#9;{&#10;&#9;&#9;&#9;Message.setFineDetune(Math.random() * (knbMicroTuning.getValue() - -knbMicroTuning.getValue() + 1) + -knbMicroTuning.getValue());&#10;&#9;&#9;}&#10;&#10;&#9;&#9;timer.setValue(Message.getNoteNumber(), Engine.getUptime());&#10;&#9;}&#10;}&#10;&#10;function onNoteOff()&#10;{&#10;&#9;&#10;}&#10;function onController()&#10;{&#10;&#9;&#10;}&#10;function onTimer()&#10;{&#10;&#9;&#10;}&#10;function onControl(number, value)&#10;{&#10;&#9;switch (number)&#10;&#9;{&#10;&#9;&#9;case knbHighNote:&#10;&#9;&#9;&#9;&#10;&#9;&#9;&#9;//Get number of groups - assumes all samplers have the same number of groups and samples are mapped over velocity 64&#10;&#9;&#9;&#9;numGroups = samplers.length != 0 ? samplers[0].getRRGroupsForMessage(value, 64) : 0;&#10;&#10;&#9;&#9;&#9;groupHistory = [];&#10;&#9;&#9;&#9;//Create group history MIDI lists&#10;&#9;&#9;&#9;for (i = 0; i &lt; numGroups; i++) //One MIDI list per group&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;groupHistory[i] = Engine.createMidiList();&#10;&#9;&#9;&#9;&#9;groupHistory[i].fill(-1);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;break;&#10;&#9;}&#10;}">
      <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="/**&#10; * Multi-Round Robin script v2.2&#10; * Author: David Healey&#10; * Date: 07/01/2017&#10; * Modified: 09/01/2017&#10; * License: GPLv3 - https://www.gnu.org/licenses/gpl-3.0.en.html&#10; */&#10;&#10;//Includes &#10;&#10;Content.setHeight(100);&#10;&#10;//Init&#10;reg excludeSamplers = [&quot;Release&quot;]; //Samples with these words in their ID won't be affected&#10;const var samplerNames = Synth.getIdList(&quot;Sampler&quot;); //Get the ids of all child samplers&#10;reg samplers = [];&#10;&#10;reg group; //Group RR counter&#10;reg offset; //Synthetic RR counter&#10;&#10;const var lastGroup = Engine.createMidiList();&#10;const var lastOffset = Engine.createMidiList();&#10;&#10;const var groupCounter = Engine.createMidiList();&#10;const var offsetCounter = Engine.createMidiList();&#10;groupCounter.fill(0);&#10;offsetCounter.fill(0);&#10;&#10;reg groupHistory = [];&#10;reg offsetHistory = [];&#10;&#10;reg numGroups; //Script assumes all samplers have the same number of groups&#10;reg numOffsets = 3; //Number of adjacent samples used by the synthetic RR (including 0) - currently anything other than 3 won't work as expected&#10;&#10;reg lowNote;&#10;reg highNote;&#10;reg noteNumber;&#10;&#10;const var timer = Engine.createMidiList();&#10;&#10;namespace RRModes&#10;{&#10;&#9;const var CYCLE = 1;&#10;&#9;const var TRUE_RANDOM = 2;&#10;&#9;const var RANDOM_NO_REPEAT = 3;&#10;&#9;const var RANDOM_FULL_CYCLE = 4;&#10;};&#10;&#10;//Get child samplers&#10;for (i = 0; i &lt; samplerNames.length; i++)&#10;{&#10;&#9;for (j = 0; j &lt; excludeSamplers.length; j++)&#10;&#9;{&#10;&#9;&#9;if (samplerNames[i].indexOf(excludeSamplers[j]) !== -1) continue; //Skip exluded IDs&#10;&#9;}&#10;&#10;&#9;samplers.push(Synth.getSampler(samplerNames[i])); //Add sampler to array&#10;}&#10;&#10;for (sampler in samplers)&#10;{&#10;&#9;sampler.enableRoundRobin(false); //Disable default RR behaviour&#10;&#9;sampler.refreshRRMap();&#10;}&#10;&#10;//Create offset history MIDI lists - same is done for groupHistory in on control callback&#10;for (i = 0; i &lt; numOffsets; i++) //3 possible offset states for synthetic RR&#10;{&#10;&#9;offsetHistory[i] = Engine.createMidiList();&#10;&#9;offsetHistory[i].fill(-1);&#10;}&#10;&#10;//GUI&#10;//RR Type and Mode Combo Boxes&#10;const var cmbType = Content.addComboBox(&quot;Type&quot;, 0, 10);&#10;cmbType.set(&quot;items&quot;, [&quot;Off&quot;, &quot;Real&quot;, &quot;Synthetic&quot;, &quot;Hybrid&quot;].join(&quot;\n&quot;));&#10;&#10;const var cmbMode = Content.addComboBox(&quot;Mode&quot;, 150, 10);&#10;cmbMode.set(&quot;items&quot;, [&quot;Cycle&quot;, &quot;Random&quot;, &quot;Random No Repeat&quot;, &quot;Random Full Cycle&quot;].join(&quot;\n&quot;));&#10;&#10;//Playable Range controls&#10;const var knbLowNote = Content.addKnob(&quot;Low Note&quot;, 300, 0);&#10;const var knbHighNote = Content.addKnob(&quot;High Note&quot;, 450, 0);&#10;knbLowNote.setRange(0, 127, 1);&#10;knbHighNote.setRange(0, 127, 1);&#10;&#10;//Reset timeout knob&#10;const var knbReset = Content.addKnob(&quot;Reset Time&quot;, 600, 0);&#10;knbReset.setRange(0, 60, 1);&#10;&#10;//Microtuning knob&#10;const var knbMicroTuning = Content.addKnob(&quot;Microtuning&quot;, 0, 50);&#10;knbMicroTuning.setRange(0, 100, 0.1);&#10;&#10;//Functions&#10;&#10;//Callbacks&#10;function onNoteOn()&#10;{&#10;&#9;local i;&#10;&#9;noteNumber = Message.getNoteNumber();&#10;&#10;&#9;if (cmbType.getValue() &gt; 1 &amp;&amp; noteNumber &gt;= lowNote &amp;&amp; noteNumber &lt;= highNote) //RR is not off&#10;&#9;{&#10;&#9;&#9;//Group based RR&#10;&#9;&#9;if (cmbType.getValue() == 2 || cmbType.getValue() == 4) //Real or Hybrid RR&#10;&#9;&#9;{&#10;&#9;&#9;&#9;if (numGroups != 1)&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;switch (cmbMode.getValue())&#10;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.CYCLE:&#10;&#9;&#9;&#9;&#9;&#9;&#9;group = (lastGroup.getValue(Message.getNoteNumber()) + 1) % numGroups;&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.TRUE_RANDOM:&#10;&#9;&#9;&#9;&#9;&#9;&#9;group = Math.floor(Math.random() * numGroups);&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.RANDOM_NO_REPEAT:&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;group = -1;&#10;&#9;&#9;&#9;&#9;&#9;&#9;while (group == -1 || group == lastGroup.getValue(noteNumber))&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;group = Math.floor(Math.random() * numGroups);&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.RANDOM_FULL_CYCLE:&#10;&#9;&#9;&#9;&#9;&#10;&#9;&#9;&#9;&#9;&#9;&#9;//Reset counter and history if all RRs have been played&#10;&#9;&#9;&#9;&#9;&#9;&#9;if (groupCounter.getValue(noteNumber) &gt;= numGroups)&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;groupCounter.setValue(noteNumber, 0); //Reset groupCounter for this note&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;for (gHistory in groupHistory)&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;gHistory.setValue(noteNumber, -1);&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;//Get random group number that isn't the last group and hasn't been marked as played in groupHistory for this note&#10;&#9;&#9;&#9;&#9;&#9;&#9;group = -1;&#10;&#9;&#9;&#9;&#9;&#9;&#9;while (group == -1 || group == lastGroup.getValue(noteNumber) || groupHistory[group].getValue(noteNumber) != -1)&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;group = Math.floor(Math.random() * numGroups);&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;else &#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;group = 0;&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;//Reset group if timer expired&#10;&#9;&#9;&#9;if (Engine.getUptime() - timer.getValue(noteNumber) &gt; knbReset.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;group = 0;&#10;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#10;&#9;&#9;&#9;//Set active group for each samper&#10;&#9;&#9;&#9;for (sampler in samplers)&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;sampler.setActiveGroup(1+group);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;groupCounter.setValue(noteNumber, groupCounter.getValue(noteNumber) + 1); //Increment counter&#10;&#9;&#9;&#9;groupHistory[group].setValue(noteNumber, 1); //Mark RR as used&#10;&#9;&#9;&#9;lastGroup.setValue(noteNumber, group);&#10;&#9;&#9;}&#10;&#9;&#9;&#10;&#9;&#9;//Synthetic sample borrowed RR&#10;&#9;&#9;if (cmbType.getValue() == 3 || cmbType.getValue() == 4) //Synthetic or Hybrid RR&#10;&#9;&#9;{&#10;&#9;&#9;&#9;offset = -1;&#10;&#10;&#9;&#9;&#9;switch (cmbMode.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;case RRModes.CYCLE:&#10;&#9;&#9;&#9;&#9;&#9;offset = (lastOffset.getValue(noteNumber) + 1) % numOffsets;&#10;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;case RRModes.TRUE_RANDOM:&#10;&#9;&#9;&#9;&#9;&#9;offset = Math.floor(Math.random() * numOffsets);&#10;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;case RRModes.RANDOM_NO_REPEAT:&#10;&#10;&#9;&#9;&#9;&#9;&#9;while (offset == -1 || offset == lastOffset.getValue(noteNumber))&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offset = Math.floor(Math.random() * numOffsets);&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;case RRModes.RANDOM_FULL_CYCLE:&#10;&#9;&#9;&#9;&#9;&#9;&#10;&#9;&#9;&#9;&#9;&#9;//Reset counter and history if all RRs have been played&#10;&#9;&#9;&#9;&#9;&#9;if (offsetCounter.getValue(noteNumber) &gt;= numOffsets)&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offsetCounter.setValue(noteNumber, 0); //Reset offsetCounter for this note&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;for (oHistory in offsetHistory)&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;oHistory.setValue(noteNumber, -1);&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;&#9;//Get random offset number that isn't the last offset and hasn't been marked as played in offsetHistory for this note&#10;&#9;&#9;&#9;&#9;&#9;while (offset == -1 || offset == lastOffset.getValue(noteNumber) || offsetHistory[offset].getValue(noteNumber) != -1)&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offset = Math.floor(Math.random() * numOffsets);&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;break;&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;//Reset offset if timer expired&#10;&#9;&#9;&#9;if (Engine.getUptime() - timer.getValue(noteNumber) &gt; knbReset.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;offset = 0;&#10;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#10;&#9;&#9;&#9;if (noteNumber + offset - 1 &lt; lowNote) offset = 1;&#10;&#9;&#9;&#9;if (noteNumber + offset - 1 &gt; highNote) offset = 0;&#10;&#10;&#9;&#9;&#9;//Apply offset and coarse tuning to message&#10;&#9;&#9;&#9;Message.setTransposeAmount(offset-1); //Use -1 to make offset range -1 to +1&#10;&#9;&#9;&#9;Message.setCoarseDetune(-(offset-1)); //Use coarse detune so setting can be picked up by later scripts&#10;&#10;&#9;&#9;&#9;offsetCounter.setValue(noteNumber, offsetCounter.getValue(noteNumber) + 1); //Increment counter&#10;&#9;&#9;&#9;offsetHistory[offset].setValue(noteNumber, 1); //Mark RR as used&#10;&#9;&#9;&#9;lastOffset.setValue(noteNumber, offset);&#10;&#9;&#9;}&#10;&#10;&#9;&#9;//Apply random microtuning, if any&#10;&#9;&#9;if (knbMicroTuning.getValue() != 0)&#10;&#9;&#9;{&#10;&#9;&#9;&#9;Message.setFineDetune(Math.random() * (knbMicroTuning.getValue() - -knbMicroTuning.getValue() + 1) + -knbMicroTuning.getValue());&#10;&#9;&#9;}&#10;&#10;&#9;&#9;timer.setValue(noteNumber, Engine.getUptime());&#10;&#9;}&#10;}&#10;&#10;function onNoteOff()&#10;{&#10;&#9;&#10;}&#10;function onController()&#10;{&#10;&#9;&#10;}&#10;function onTimer()&#10;{&#10;&#9;&#10;}&#10;function onControl(number, value)&#10;{&#10;&#9;switch (number)&#10;&#9;{&#10;&#9;&#9;case knbLowNote:&#10;&#9;&#9;&#9;lowNote = value;&#10;&#9;&#9;break;&#10;&#10;&#9;&#9;case knbHighNote:&#10;&#9;&#9;&#9;&#10;&#9;&#9;&#9;highNote = value;&#10;&#10;&#9;&#9;&#9;//Get number of groups - assumes all samplers have the same number of groups and samples are mapped over velocity 64&#10;&#9;&#9;&#9;numGroups = samplers.length != 0 ? samplers[0].getRRGroupsForMessage(value, 64) : 0;&#10;&#10;&#9;&#9;&#9;groupHistory = [];&#10;&#9;&#9;&#9;//Create group history MIDI lists&#10;&#9;&#9;&#9;for (i = 0; i &lt; numGroups; i++) //One MIDI list per group&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;groupHistory[i] = Engine.createMidiList();&#10;&#9;&#9;&#9;&#9;groupHistory[i].fill(-1);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;break;&#10;&#9;}&#10;}">
      <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="/**&#10; * Multi-Round Robin script v2.3&#10; * Author: David Healey&#10; * Date: 07/01/2017&#10; * Modified: 13/01/2017&#10; * License: GPLv3 - https://www.gnu.org/licenses/gpl-3.0.en.html&#10; */&#10;&#10;//Includes &#10;&#10;Content.setHeight(100);&#10;&#10;//Init&#10;const var excludedIds = &quot;(elease)&quot;; //Regex string of words in sampler IDs to exclude RR from&#10;const var samplerNames = Synth.getIdList(&quot;Sampler&quot;); //Get the ids of all child samplers&#10;const var samplers = [];&#10;&#10;reg group; //Group RR counter&#10;reg offset; //Synthetic RR counter&#10;&#10;const var lastGroup = Engine.createMidiList();&#10;const var lastOffset = Engine.createMidiList();&#10;&#10;const var groupCounter = Engine.createMidiList();&#10;const var offsetCounter = Engine.createMidiList();&#10;groupCounter.fill(0);&#10;offsetCounter.fill(0);&#10;&#10;const var groupHistory = [];&#10;const var offsetHistory = [];&#10;&#10;reg numGroups; //Script assumes all samplers have the same number of groups&#10;reg numOffsets = 3; //Number of adjacent samples used by the synthetic RR (including 0) - currently anything other than 3 won't work as expected&#10;&#10;reg lowNote;&#10;reg highNote;&#10;reg noteNumber;&#10;&#10;const var timer = Engine.createMidiList();&#10;&#10;namespace RRModes&#10;{&#10;&#9;const var CYCLE = 1;&#10;&#9;const var TRUE_RANDOM = 2;&#10;&#9;const var RANDOM_NO_REPEAT = 3;&#10;&#9;const var RANDOM_FULL_CYCLE = 4;&#10;};&#10;&#10;//Get child samplers&#10;for (samplerName in samplerNames)&#10;{&#10;&#9;if (Engine.matchesRegex(samplerName, excludedIds) == true) continue; //Skip excluded IDs&#10;&#10;&#9;samplers.push(Synth.getSampler(samplerName)); //Add sampler to array&#10;&#9;samplers[samplers.length-1].enableRoundRobin(false); //Disable default RR behaviour&#10;&#9;samplers[samplers.length-1].refreshRRMap();&#10;}&#10;&#10;//Create offset history MIDI lists - same is done for groupHistory in on control callback&#10;for (i = 0; i &lt; numOffsets; i++) //3 possible offset states for synthetic RR&#10;{&#10;&#9;offsetHistory[i] = Engine.createMidiList();&#10;&#9;offsetHistory[i].fill(-1);&#10;}&#10;&#10;//GUI&#10;//RR Type and Mode Combo Boxes&#10;const var cmbType = Content.addComboBox(&quot;Type&quot;, 0, 10);&#10;cmbType.set(&quot;items&quot;, [&quot;Off&quot;, &quot;Real&quot;, &quot;Synthetic&quot;, &quot;Hybrid&quot;].join(&quot;\n&quot;));&#10;&#10;const var cmbMode = Content.addComboBox(&quot;Mode&quot;, 150, 10);&#10;cmbMode.set(&quot;items&quot;, [&quot;Cycle&quot;, &quot;Random&quot;, &quot;Random No Repeat&quot;, &quot;Random Full Cycle&quot;].join(&quot;\n&quot;));&#10;&#10;//Playable Range controls&#10;const var knbLowNote = Content.addKnob(&quot;Low Note&quot;, 300, 0);&#10;const var knbHighNote = Content.addKnob(&quot;High Note&quot;, 450, 0);&#10;knbLowNote.setRange(0, 127, 1);&#10;knbHighNote.setRange(0, 127, 1);&#10;&#10;//Reset timeout knob&#10;const var knbReset = Content.addKnob(&quot;Reset Time&quot;, 600, 0);&#10;knbReset.setRange(0, 60, 1);&#10;&#10;//Microtuning knob&#10;const var knbMicroTuning = Content.addKnob(&quot;Microtuning&quot;, 0, 50);&#10;knbMicroTuning.setRange(0, 100, 0.1);&#10;&#10;const var postInit = Content.addButton(&quot;postInit&quot;, 0, 0); //Hidden button to trigger last control callback on init&#10;postInit.set(&quot;visible&quot;, false);&#10;&#10;//Functions&#10;&#10;//Callbacks&#10;function onNoteOn()&#10;{&#10;&#9;local i;&#10;&#9;noteNumber = Message.getNoteNumber();&#10;&#10;&#9;if (cmbType.getValue() &gt; 1 &amp;&amp; noteNumber &gt;= lowNote &amp;&amp; noteNumber &lt;= highNote) //RR is not off&#10;&#9;{&#10;&#9;&#9;//Group based RR&#10;&#9;&#9;if (cmbType.getValue() == 2 || cmbType.getValue() == 4) //Real or Hybrid RR&#10;&#9;&#9;{&#10;&#9;&#9;&#9;if (numGroups != 1)&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;switch (cmbMode.getValue())&#10;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.CYCLE:&#10;&#9;&#9;&#9;&#9;&#9;&#9;group = (lastGroup.getValue(Message.getNoteNumber()) + 1) % numGroups;&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.TRUE_RANDOM:&#10;&#9;&#9;&#9;&#9;&#9;&#9;group = Math.floor(Math.random() * numGroups);&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.RANDOM_NO_REPEAT:&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;group = -1;&#10;&#9;&#9;&#9;&#9;&#9;&#9;while (group == -1 || group == lastGroup.getValue(noteNumber))&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;group = Math.floor(Math.random() * numGroups);&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.RANDOM_FULL_CYCLE:&#10;&#9;&#9;&#9;&#9;&#10;&#9;&#9;&#9;&#9;&#9;&#9;//Reset counter and history if all RRs have been played&#10;&#9;&#9;&#9;&#9;&#9;&#9;if (groupCounter.getValue(noteNumber) &gt;= numGroups)&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;groupCounter.setValue(noteNumber, 0); //Reset groupCounter for this note&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;for (gHistory in groupHistory)&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;gHistory.setValue(noteNumber, -1);&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;//Get random group number that isn't the last group and hasn't been marked as played in groupHistory for this note&#10;&#9;&#9;&#9;&#9;&#9;&#9;group = -1;&#10;&#9;&#9;&#9;&#9;&#9;&#9;while (group == -1 || group == lastGroup.getValue(noteNumber) || groupHistory[group].getValue(noteNumber) != -1)&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;group = Math.floor(Math.random() * numGroups);&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;else &#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;group = 0;&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;//Reset group if timer expired&#10;&#9;&#9;&#9;if (Engine.getUptime() - timer.getValue(noteNumber) &gt; knbReset.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;group = 0;&#10;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#10;&#9;&#9;&#9;//Set active group for each samper&#10;&#9;&#9;&#9;for (sampler in samplers)&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;sampler.setActiveGroup(1+group);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;groupCounter.setValue(noteNumber, groupCounter.getValue(noteNumber) + 1); //Increment counter&#10;&#9;&#9;&#9;groupHistory[group].setValue(noteNumber, 1); //Mark RR as used&#10;&#9;&#9;&#9;lastGroup.setValue(noteNumber, group);&#10;&#9;&#9;}&#10;&#9;&#9;&#10;&#9;&#9;//Synthetic sample borrowed RR&#10;&#9;&#9;if (cmbType.getValue() == 3 || cmbType.getValue() == 4) //Synthetic or Hybrid RR&#10;&#9;&#9;{&#10;&#9;&#9;&#9;offset = -1;&#10;&#10;&#9;&#9;&#9;switch (cmbMode.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;case RRModes.CYCLE:&#10;&#9;&#9;&#9;&#9;&#9;offset = (lastOffset.getValue(noteNumber) + 1) % numOffsets;&#10;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;case RRModes.TRUE_RANDOM:&#10;&#9;&#9;&#9;&#9;&#9;offset = Math.floor(Math.random() * numOffsets);&#10;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;case RRModes.RANDOM_NO_REPEAT:&#10;&#10;&#9;&#9;&#9;&#9;&#9;while (offset == -1 || offset == lastOffset.getValue(noteNumber))&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offset = Math.floor(Math.random() * numOffsets);&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;case RRModes.RANDOM_FULL_CYCLE:&#10;&#9;&#9;&#9;&#9;&#9;&#10;&#9;&#9;&#9;&#9;&#9;//Reset counter and history if all RRs have been played&#10;&#9;&#9;&#9;&#9;&#9;if (offsetCounter.getValue(noteNumber) &gt;= numOffsets)&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offsetCounter.setValue(noteNumber, 0); //Reset offsetCounter for this note&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;for (oHistory in offsetHistory)&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;oHistory.setValue(noteNumber, -1);&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;&#9;//Get random offset number that isn't the last offset and hasn't been marked as played in offsetHistory for this note&#10;&#9;&#9;&#9;&#9;&#9;while (offset == -1 || offset == lastOffset.getValue(noteNumber) || offsetHistory[offset].getValue(noteNumber) != -1)&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offset = Math.floor(Math.random() * numOffsets);&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;break;&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;//Reset offset if timer expired&#10;&#9;&#9;&#9;if (Engine.getUptime() - timer.getValue(noteNumber) &gt; knbReset.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;offset = 0;&#10;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#10;&#9;&#9;&#9;if (noteNumber + offset - 1 &lt; lowNote) offset = 1;&#10;&#9;&#9;&#9;if (noteNumber + offset - 1 &gt; highNote) offset = 0;&#10;&#10;&#9;&#9;&#9;//Apply offset and coarse tuning to message&#10;&#9;&#9;&#9;Message.setTransposeAmount(offset-1); //Use -1 to make offset range -1 to +1&#10;&#9;&#9;&#9;Message.setCoarseDetune(-(offset-1)); //Use coarse detune so setting can be picked up by later scripts&#10;&#10;&#9;&#9;&#9;offsetCounter.setValue(noteNumber, offsetCounter.getValue(noteNumber) + 1); //Increment counter&#10;&#9;&#9;&#9;offsetHistory[offset].setValue(noteNumber, 1); //Mark RR as used&#10;&#9;&#9;&#9;lastOffset.setValue(noteNumber, offset);&#10;&#9;&#9;}&#10;&#10;&#9;&#9;//Apply random microtuning, if any&#10;&#9;&#9;if (knbMicroTuning.getValue() != 0)&#10;&#9;&#9;{&#10;&#9;&#9;&#9;Message.setFineDetune(Math.random() * (knbMicroTuning.getValue() - -knbMicroTuning.getValue() + 1) + -knbMicroTuning.getValue());&#10;&#9;&#9;}&#10;&#10;&#9;&#9;timer.setValue(noteNumber, Engine.getUptime());&#10;&#9;}&#10;}&#10;&#10;function onNoteOff()&#10;{&#10;&#9;&#10;}&#10;function onController()&#10;{&#10;&#9;&#10;}&#10;function onTimer()&#10;{&#10;&#9;&#10;}&#10;function onControl(number, value)&#10;{&#10;&#9;switch (number)&#10;&#9;{&#10;&#9;&#9;case knbLowNote:&#10;&#9;&#9;&#9;lowNote = value;&#10;&#9;&#9;break;&#10;&#10;&#9;&#9;case knbHighNote:&#10;&#9;&#9;&#9;highNote = value;&#10;&#9;&#9;break;&#10;&#10;&#9;&#9;case postInit:&#10;&#9;&#9;&#9;//Get number of groups - assumes all samplers have the same number of groups and samples are mapped over velocity 64&#10;&#9;&#9;&#9;numGroups = samplers.length != 0 ? samplers[0].getRRGroupsForMessage(lowNote + 1, 64) : 0;&#10;&#10;&#9;&#9;&#9;//Create group history MIDI lists&#10;&#9;&#9;&#9;for (i = 0; i &lt; numGroups; i++) //One MIDI list per group&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;groupHistory[i] = Engine.createMidiList();&#10;&#9;&#9;&#9;&#9;groupHistory[i].fill(-1);&#10;&#9;&#9;&#9;}&#10;&#9;&#9;break;&#10;&#9;}&#10;}">
      <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>
    

  • administrators

    The postInit-thingie looks like a code smell to me. Why do you need it to happen after the onInit callback?



  • Because it needs the value from lowNote and I believe (possibly mistakenly) that the value of the UI knobs isn't recalled until after onInit 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;
    	}
    }
    

  • administrators

    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 index

    This 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!


  • administrators

    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;
    	}
    }
    

Log in to reply
 

2
Online

343
Users

1.1k
Topics

7.1k
Posts

Looks like your connection to Forum was lost, please wait while we try to reconnect.