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

                            47

                            Online

                            1.7k

                            Users

                            11.7k

                            Topics

                            101.8k

                            Posts