HISE Logo Forum
    • Categories
    • Register
    • Login

    Random, Cycle, Synthetic, Hybrid Round Robin

    Scheduled Pinned Locked Moved Presets / Scripts / Ideas
    13 Posts 2 Posters 3.1k Views
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • d.healeyD
      d.healey
      last edited by d.healey

      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>
      

      Libre Wave - Freedom respecting instruments and effects
      My Patreon - HISE tutorials
      YouTube Channel - Public HISE tutorials

      1 Reply Last reply Reply Quote 1
      • Christoph HartC
        Christoph Hart
        last edited by Christoph Hart

        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:
            // ...
        
        
        1 Reply Last reply Reply Quote 1
        • d.healeyD
          d.healey
          last edited by d.healey

          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>```

          Libre Wave - Freedom respecting instruments and effects
          My Patreon - HISE tutorials
          YouTube Channel - Public HISE tutorials

          1 Reply Last reply Reply Quote 0
          • d.healeyD
            d.healey
            last edited by

            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>
            

            Libre Wave - Freedom respecting instruments and effects
            My Patreon - HISE tutorials
            YouTube Channel - Public HISE tutorials

            1 Reply Last reply Reply Quote 0
            • d.healeyD
              d.healey
              last edited by

              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>
              

              Libre Wave - Freedom respecting instruments and effects
              My Patreon - HISE tutorials
              YouTube Channel - Public HISE tutorials

              1 Reply Last reply Reply Quote 0
              • Christoph HartC
                Christoph Hart
                last edited by

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

                1 Reply Last reply Reply Quote 0
                • d.healeyD
                  d.healey
                  last edited by d.healey

                  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.

                  Libre Wave - Freedom respecting instruments and effects
                  My Patreon - HISE tutorials
                  YouTube Channel - Public HISE tutorials

                  1 Reply Last reply Reply Quote 0
                  • d.healeyD
                    d.healey
                    last edited by

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

                    Libre Wave - Freedom respecting instruments and effects
                    My Patreon - HISE tutorials
                    YouTube Channel - Public HISE tutorials

                    1 Reply Last reply Reply Quote 0
                    • d.healeyD
                      d.healey
                      last edited by d.healey

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

                      Libre Wave - Freedom respecting instruments and effects
                      My Patreon - HISE tutorials
                      YouTube Channel - Public HISE tutorials

                      1 Reply Last reply Reply Quote 0
                      • Christoph HartC
                        Christoph Hart
                        last edited by

                        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).

                        1 Reply Last reply Reply Quote 0
                        • d.healeyD
                          d.healey
                          last edited by

                          Thanks I'll explore your code - and thanks for the CPU tip, I didn't know you could profile it like that!

                          Libre Wave - Freedom respecting instruments and effects
                          My Patreon - HISE tutorials
                          YouTube Channel - Public HISE tutorials

                          1 Reply Last reply Reply Quote 0
                          • Christoph HartC
                            Christoph Hart
                            last edited by

                            Yes its pretty hard to check the script performance of a single callback by looking on the CPU meter :)

                            1 Reply Last reply Reply Quote 0
                            • d.healeyD
                              d.healey
                              last edited by d.healey

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

                              Libre Wave - Freedom respecting instruments and effects
                              My Patreon - HISE tutorials
                              YouTube Channel - Public HISE tutorials

                              1 Reply Last reply Reply Quote 2
                              • First post
                                Last post

                              56

                              Online

                              1.7k

                              Users

                              11.7k

                              Topics

                              101.8k

                              Posts